Merge branch 'master' into Develop

pull/2970/head
Ozzie Isaacs 4 months ago
commit 902fa254b0

@ -65,7 +65,7 @@ Calibre-Web is a web app that offers a clean and intuitive interface for browsin
*Note: Raspberry Pi OS users may encounter issues during installation. If so, please update pip (`./venv/bin/python3 -m pip install --upgrade pip`) and/or install cargo (`sudo apt install cargo`) before retrying the installation.* *Note: Raspberry Pi OS users may encounter issues during installation. If so, please update pip (`./venv/bin/python3 -m pip install --upgrade pip`) and/or install cargo (`sudo apt install cargo`) before retrying the installation.*
Refer to the Wiki for additional installation examples: [manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation), [Linux Mint](https://github.com/janeczku/calibre-web/wiki/How-To:Install-Calibre-Web-in-Linux-Mint-19-or-20), [Cloud Provider](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-on-a-Cloud-Provider). Refer to the Wiki for additional installation examples: [manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation), [Linux Mint](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-in-Linux-Mint-19-or-20), [Cloud Provider](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-on-a-Cloud-Provider).
## Quick Start ## Quick Start

@ -27,8 +27,10 @@ from shutil import copyfile
from uuid import uuid4 from uuid import uuid4
from markupsafe import escape, Markup # dependency of flask from markupsafe import escape, Markup # dependency of flask
from functools import wraps from functools import wraps
from lxml.etree import ParserError
try: try:
# at least bleach 6.0 is needed -> incomplatible change from list arguments to set arguments
from bleach import clean_text as clean_html from bleach import clean_text as clean_html
BLEACH = True BLEACH = True
except ImportError: except ImportError:
@ -1001,10 +1003,14 @@ def edit_book_series_index(series_index, book):
def edit_book_comments(comments, book): def edit_book_comments(comments, book):
modify_date = False modify_date = False
if comments: if comments:
if BLEACH: try:
comments = clean_html(comments, tags=None, attributes=None) if BLEACH:
else: comments = clean_html(comments, tags=set(), attributes=set())
comments = clean_html(comments) else:
comments = clean_html(comments)
except ParserError as e:
log.error("Comments of book {} are corrupted: {}".format(book.id, e))
comments = ""
if len(book.comments): if len(book.comments):
if book.comments[0].text != comments: if book.comments[0].text != comments:
book.comments[0].text = comments book.comments[0].text = comments

@ -103,7 +103,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
elif s == 'date': elif s == 'date':
epub_metadata[s] = tmp[0][:10] epub_metadata[s] = tmp[0][:10]
else: else:
epub_metadata[s] = tmp[0] epub_metadata[s] = tmp[0].strip()
else: else:
epub_metadata[s] = 'Unknown' epub_metadata[s] = 'Unknown'

@ -137,10 +137,13 @@ 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 # @download_required
def HandleSyncRequest(): def HandleSyncRequest():
if not current_user.role_download():
log.info("Users need download permissions for syncing library to Kobo reader")
return abort(403)
sync_token = SyncToken.SyncToken.from_headers(request.headers) sync_token = SyncToken.SyncToken.from_headers(request.headers)
log.info("Kobo library sync request received.") log.info("Kobo library sync request received")
log.debug("SyncToken: {}".format(sync_token)) log.debug("SyncToken: {}".format(sync_token))
log.debug("Download link format {}".format(get_download_url_for_book('[bookid]','[bookformat]'))) log.debug("Download link format {}".format(get_download_url_for_book('[bookid]','[bookformat]')))
if not current_app.wsgi_app.is_proxied: if not current_app.wsgi_app.is_proxied:

@ -21,6 +21,7 @@ import os
import errno import errno
import signal import signal
import socket import socket
import asyncio
try: try:
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
@ -326,4 +327,5 @@ class WebServer(object):
if restart: if restart:
self.wsgiserver.call_later(1.0, self.wsgiserver.stop) self.wsgiserver.call_later(1.0, self.wsgiserver.stop)
else: else:
self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop) self.wsgiserver.asyncio_loop.call_soon_threadsafe(self.wsgiserver.stop)

@ -3296,6 +3296,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
left: 0 !important; left: 0 !important;
} }
#add-to-shelves { #add-to-shelves {
min-height: 48px;
max-height: calc(100% - 120px); max-height: calc(100% - 120px);
overflow-y: auto; overflow-y: auto;
} }
@ -4812,8 +4813,14 @@ body.advsearch:not(.blur) > div.container-fluid > div.row-fluid > div.col-sm-10
z-index: 999999999999999999999999999999999999 z-index: 999999999999999999999999999999999999
} }
.search #shelf-actions, body.login .home-btn { body.search #shelf-actions button#add-to-shelf {
display: none height: 40px;
}
@media screen and (max-width: 767px) {
body.search .discover, body.advsearch .discover {
display: flex;
flex-direction: column;
}
} }
body.read:not(.blur) a[href*=readbooks] { body.read:not(.blur) a[href*=readbooks] {
@ -5134,7 +5141,7 @@ body.login > div.navbar.navbar-default.navbar-static-top > div > div.navbar-head
right: 5px right: 5px
} }
#shelf-actions > .btn-group.open, .downloadBtn.open, .profileDrop[aria-expanded=true] { body:not(.search) #shelf-actions > .btn-group.open, .downloadBtn.open, .profileDrop[aria-expanded=true] {
pointer-events: none pointer-events: none
} }
@ -5151,7 +5158,7 @@ body.login > div.navbar.navbar-default.navbar-static-top > div > div.navbar-head
color: var(--color-primary) color: var(--color-primary)
} }
#shelf-actions, #shelf-actions > .btn-group, #shelf-actions > .btn-group > .empty-ul { body:not(.search) #shelf-actions, body:not(.search) #shelf-actions > .btn-group, body:not(.search) #shelf-actions > .btn-group > .empty-ul {
pointer-events: none pointer-events: none
} }

@ -369,6 +369,13 @@ $("div.comments").readmore({
// End of Global Work // // End of Global Work //
/////////////////////////////// ///////////////////////////////
// Search Results
if($("body.search").length > 0) {
$('div[aria-label="Add to shelves"]').click(function () {
$("#add-to-shelves").toggle();
});
}
// Advanced Search Results // Advanced Search Results
if($("body.advsearch").length > 0) { if($("body.advsearch").length > 0) {
$("#loader + .container-fluid") $("#loader + .container-fluid")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1195,8 +1195,15 @@ def serve_book(book_id, book_format, anyname):
rawdata = open(os.path.join(config.get_book_path(), book.path, data.name + "." + book_format), rawdata = open(os.path.join(config.get_book_path(), book.path, data.name + "." + book_format),
"rb").read() "rb").read()
result = chardet.detect(rawdata) result = chardet.detect(rawdata)
return make_response( try:
rawdata.decode(result['encoding'], 'surrogatepass').encode('utf-8', 'surrogatepass')) text_data = rawdata.decode(result['encoding']).encode('utf-8')
except UnicodeDecodeError as e:
log.error("Encoding error in text file {}: {}".format(book.id, e))
if "surrogate" in e.reason:
text_data = rawdata.decode(result['encoding'], 'surrogatepass').encode('utf-8', 'surrogatepass')
else:
text_data = rawdata.decode(result['encoding'], 'ignore').encode('utf-8', 'ignore')
return make_response(text_data)
except FileNotFoundError: except FileNotFoundError:
log.error("File Not Found") log.error("File Not Found")
return "File Not Found" return "File Not Found"
@ -1347,21 +1354,21 @@ def login():
@limiter.limit("3/minute", key_func=lambda: request.form.get('username', "").strip().lower()) @limiter.limit("3/minute", key_func=lambda: request.form.get('username', "").strip().lower())
def login_post(): def login_post():
form = request.form.to_dict() form = request.form.to_dict()
username = form.get('username', "").strip().lower().replace("\n","\\n").replace("\r","")
try: try:
limiter.check() limiter.check()
except RateLimitExceeded: except RateLimitExceeded:
flash(_(u"Please wait one minute before next login"), category="error") flash(_(u"Please wait one minute before next login"), category="error")
return render_login(form.get("username", ""), form.get("password", "")) 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:
log.error(u"Cannot activate LDAP authentication") log.error(u"Cannot activate LDAP authentication")
flash(_(u"Cannot activate LDAP authentication"), category="error") flash(_(u"Cannot activate LDAP authentication"), category="error")
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == form.get('username', "").strip().lower()) \ user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == username).first()
.first()
remember_me = bool(form.get('remember_me')) remember_me = bool(form.get('remember_me'))
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "": if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "":
login_result, error = services.ldap.bind_user(form['username'], form['password']) login_result, error = services.ldap.bind_user(username, form['password'])
if login_result: if login_result:
log.debug(u"You are now logged in as: '{}'".format(user.name)) log.debug(u"You are now logged in as: '{}'".format(user.name))
return handle_login_user(user, return handle_login_user(user,
@ -1381,7 +1388,7 @@ def login_post():
flash(_(u"Could not login: %(message)s", message=error), category="error") flash(_(u"Could not login: %(message)s", message=error), category="error")
else: else:
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr) ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ip_address) log.warning('LDAP Login failed for user "%s" IP-address: %s', username, ip_address)
flash(_(u"Wrong Username or Password"), category="error") flash(_(u"Wrong Username or Password"), category="error")
else: else:
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr) ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
@ -1390,7 +1397,7 @@ def login_post():
ret, __ = reset_password(user.id) ret, __ = reset_password(user.id)
if ret == 1: if ret == 1:
flash(_(u"New Password was send to your email address"), category="info") flash(_(u"New Password was send to your email address"), category="info")
log.info('Password reset for user "%s" IP-address: %s', form['username'], ip_address) log.info('Password reset for user "%s" IP-address: %s', username, ip_address)
else: else:
log.error(u"An unknown error occurred. Please try again later") log.error(u"An unknown error occurred. Please try again later")
flash(_(u"An unknown error occurred. Please try again later."), category="error") flash(_(u"An unknown error occurred. Please try again later."), category="error")
@ -1406,9 +1413,9 @@ def login_post():
_(u"You are now logged in as: '%(nickname)s'", nickname=user.name), _(u"You are now logged in as: '%(nickname)s'", nickname=user.name),
"success") "success")
else: else:
log.warning('Login failed for user "{}" IP-address: {}'.format(form['username'], ip_address)) log.warning('Login failed for user "{}" IP-address: {}'.format(username, ip_address))
flash(_(u"Wrong Username or Password"), category="error") flash(_(u"Wrong Username or Password"), category="error")
return render_login(form.get("username", ""), form.get("password", "")) return render_login(username, form.get("password", ""))
@web.route('/logout') @web.route('/logout')

File diff suppressed because it is too large Load Diff

@ -1,5 +1,5 @@
# GDrive Integration # GDrive Integration
google-api-python-client>=1.7.11,<2.98.0 google-api-python-client>=1.7.11,<2.108.0
gevent>20.6.0,<24.0.0 gevent>20.6.0,<24.0.0
greenlet>=0.4.17,<2.1.0 greenlet>=0.4.17,<2.1.0
httplib2>=0.9.2,<0.23.0 httplib2>=0.9.2,<0.23.0
@ -13,7 +13,7 @@ rsa>=3.4.2,<4.10.0
# Gmail # Gmail
google-auth-oauthlib>=0.4.3,<1.1.0 google-auth-oauthlib>=0.4.3,<1.1.0
google-api-python-client>=1.7.11,<2.98.0 google-api-python-client>=1.7.11,<2.108.0
# goodreads # goodreads
goodreads>=0.3.2,<0.4.0 goodreads>=0.3.2,<0.4.0

Loading…
Cancel
Save