Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3233b4b9c | ||
|
|
c4fad3cd20 | ||
|
|
9b520b1941 | ||
|
|
bcf4a479ac | ||
|
|
abc923ade4 | ||
|
|
3aa72c8afb | ||
|
|
249017a4fa | ||
|
|
84f432830c | ||
|
|
e6ccd42f89 | ||
|
|
12d46d33d9 | ||
|
|
8d9f301aea | ||
|
|
0ff3f0f426 | ||
|
|
b2f5853b12 | ||
|
|
f8adcbb691 | ||
|
|
a0728b07d0 | ||
|
|
e3b895755d | ||
|
|
e33e4ac4ac | ||
|
|
2f515b725b | ||
|
|
a1d7dc5c28 | ||
|
|
1c5d1e24f7 | ||
|
|
8df45d150d | ||
|
|
a59d20bd0f | ||
|
|
e3be7595e2 | ||
|
|
e33a7a3f08 | ||
|
|
8c02ffb1cb | ||
|
|
7367e96d2b | ||
|
|
2d80187faa | ||
|
|
1d3a768dfe | ||
|
|
c09e1ed203 | ||
|
|
99e50bafc4 | ||
|
|
155cf04536 | ||
|
|
ebe7cd7ba4 | ||
|
|
2d470e0ce1 | ||
|
|
87c0b15f5f | ||
|
|
02d2c2bbf8 | ||
|
|
6996e813b5 | ||
|
|
840f4d2010 | ||
|
|
3cda0a0f32 | ||
|
|
a6931b4936 | ||
|
|
9536b2c322 | ||
|
|
099dbf95e7 | ||
|
|
23d5463213 | ||
|
|
a2ceeb678f | ||
|
|
092329c9e6 | ||
|
|
de989783e8 | ||
|
|
1d7dcce98d | ||
|
|
551828f8cf | ||
|
|
d26964ab08 | ||
|
|
0fe12fe598 | ||
|
|
b0e8024f08 | ||
|
|
bc1c097271 | ||
|
|
f98cb3d8fb | ||
|
|
488005962f | ||
|
|
6d6f8c1b1f | ||
|
|
c4a38087d9 | ||
|
|
f468b9e6e3 | ||
|
|
53a16c5a6f | ||
|
|
108caa0898 | ||
|
|
a0b776cbd1 | ||
|
|
4719797aff | ||
|
|
60cc0c9395 | ||
|
|
ded4cb7a19 | ||
|
|
f5f16a3dd2 | ||
|
|
c5f2323c8a | ||
|
|
ea89fe07d8 | ||
|
|
5d86bb0ba0 | ||
|
|
05032aed3c | ||
|
|
7e46f9c0b1 | ||
|
|
5e15cac598 | ||
|
|
d0551274dc | ||
|
|
99c05650a1 | ||
|
|
617ee3d781 | ||
|
|
47aa3254c7 | ||
|
|
0717b9493c | ||
|
|
824ebc4d76 | ||
|
|
cbaa61bbb7 | ||
|
|
dbac685985 | ||
|
|
b25c024b66 | ||
|
|
bc65180c32 | ||
|
|
10c66b6e63 | ||
|
|
c29b1696f7 | ||
|
|
c974cd048e | ||
|
|
24e9bf2728 | ||
|
|
ff46af9c49 | ||
|
|
0a7345186d | ||
|
|
cb0d9ec95c |
@@ -89,8 +89,9 @@ Refer to the Wiki for additional installation examples: [manual installation](ht
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.5+
|
||||
- Python 3.7+
|
||||
- [Imagemagick](https://imagemagick.org/script/download.php) for cover extraction from EPUBs (Windows users may need to install [Ghostscript](https://ghostscript.com/releases/gsdnld.html) for PDF cover extraction)
|
||||
- Windows users need to install [libmagic for 32bit python](https://gnuwin32.sourceforge.net/downlinks/file.php) or [libmagic for 64bit python](https://github.com/nscaife/file-windows/releases/tag/20170108), depending on the python version; The files need to be installed in path (e.g. script folder of your Calibre-Web venv, or in the root folder of Calibre-Web
|
||||
- Optional: [Calibre desktop program](https://calibre-ebook.com/download) for on-the-fly conversion and metadata editing (set "calibre's converter tool" path on the setup page)
|
||||
- Optional: [Kepubify tool](https://github.com/pgaskin/kepubify/releases/latest) for Kobo device support (place the binary in `/opt/kepubify` on Linux or `C:\Program Files\kepubify` on Windows)
|
||||
|
||||
@@ -107,8 +108,8 @@ Pre-built Docker images are available in the following Docker Hub repositories (
|
||||
|
||||
Both the Calibre-Web and Calibre-Mod images are automatically rebuilt on new releases and updates.
|
||||
|
||||
- Set "path to convertertool" to `/usr/bin/ebook-convert`
|
||||
- Set "path to unrar" to `/usr/bin/unrar`
|
||||
- Set "Path to Calibre Binaries" to `/usr/bin`
|
||||
- Set "Path to Unrar" to `/usr/bin/unrar`
|
||||
|
||||
## Contributor Recognition
|
||||
|
||||
|
||||
+40
-35
@@ -10,41 +10,46 @@ To receive fixes for security vulnerabilities it is required to always upgrade t
|
||||
|
||||
## History
|
||||
|
||||
| Fixed in | Description |CVE number |
|
||||
|---------------|--------------------------------------------------------------------------------------------------------------------|---------|
|
||||
| 3rd July 2018 | Guest access acts as a backdoor ||
|
||||
| V 0.6.7 | Hardcoded secret key for sessions |CVE-2020-12627 |
|
||||
| V 0.6.13 | Calibre-Web Metadata cross site scripting |CVE-2021-25964|
|
||||
| V 0.6.13 | Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo ||
|
||||
| V 0.6.13 | JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource) ||
|
||||
| V 0.6.13 | JavaScript could get executed in a custom column of type "comment" field ||
|
||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a title containing javascript code ||
|
||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a username containing javascript code ||
|
||||
| V 0.6.13 | JavaScript could get executed in the description series, categories or publishers title ||
|
||||
| V 0.6.13 | JavaScript could get executed in the shelf title ||
|
||||
| V 0.6.13 | Login with the old session cookie after logout. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.14 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
|
||||
| V 0.6.14 | Migrated some routes to POST-requests (CSRF protection). Thanks to @scara31 |CVE-2021-4164|
|
||||
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 |CVE-2021-4170|
|
||||
| V 0.6.15 | Cross-Site Scripting vulnerability on uploaded cover file names. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.15 | Creating public shelfs is now denied if user is missing the edit public shelf right. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.15 | Changed error message in case of trying to delete a shelf unauthorized. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.16 | JavaScript could get executed on authors page. Thanks to @alicaz |CVE-2022-0352|
|
||||
| V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 |CVE-2022-0339|
|
||||
| V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @nhiephon |CVE-2022-0273|
|
||||
| V 0.6.16 | It's prevented to get the name of a private shelfs. Thanks to @nhiephon |CVE-2022-0405|
|
||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via an HTTP redirect. Thanks to @416e6e61 |CVE-2022-0767|
|
||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH |CVE-2022-0766|
|
||||
| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) |CVE-2022-30765|
|
||||
| V 0.6.18 | The SSRF protection no longer can be bypassed by IPV6/IPV4 embedding. Thanks to @416e6e61 |CVE-2022-0939|
|
||||
| V 0.6.18 | The SSRF protection no longer can be bypassed to connect to other servers in the local network. Thanks to @michaellrowley |CVE-2022-0990|
|
||||
| V 0.6.20 | Credentials for emails are now stored encrypted ||
|
||||
| V 0.6.20 | Login is rate limited ||
|
||||
| V 0.6.20 | Passwordstrength can be forced ||
|
||||
| V 0.6.21 | SMTP server credentials are no longer returned to client ||
|
||||
| V 0.6.21 | Cross-site scripting (XSS) stored in href bypasses filter using data wrapper no longer possible ||
|
||||
| V 0.6.21 | Cross-site scripting (XSS) is no longer possible via pathchooser ||
|
||||
| V 0.6.21 | Error Handling at non existent rating, language, and user downloaded books was fixed ||
|
||||
| Fixed in | Description |CVE number |
|
||||
|---------------|--------------------------------------------------------------------------------------------------------------------------------|---------|
|
||||
| 3rd July 2018 | Guest access acts as a backdoor ||
|
||||
| V 0.6.7 | Hardcoded secret key for sessions |CVE-2020-12627 |
|
||||
| V 0.6.13 | Calibre-Web Metadata cross site scripting |CVE-2021-25964|
|
||||
| V 0.6.13 | Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo ||
|
||||
| V 0.6.13 | JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource) ||
|
||||
| V 0.6.13 | JavaScript could get executed in a custom column of type "comment" field ||
|
||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a title containing javascript code ||
|
||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a username containing javascript code ||
|
||||
| V 0.6.13 | JavaScript could get executed in the description series, categories or publishers title ||
|
||||
| V 0.6.13 | JavaScript could get executed in the shelf title ||
|
||||
| V 0.6.13 | Login with the old session cookie after logout. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.14 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
|
||||
| V 0.6.14 | Migrated some routes to POST-requests (CSRF protection). Thanks to @scara31 |CVE-2021-4164|
|
||||
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 |CVE-2021-4170|
|
||||
| V 0.6.15 | Cross-Site Scripting vulnerability on uploaded cover file names. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.15 | Creating public shelfs is now denied if user is missing the edit public shelf right. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.15 | Changed error message in case of trying to delete a shelf unauthorized. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.16 | JavaScript could get executed on authors page. Thanks to @alicaz |CVE-2022-0352|
|
||||
| V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 |CVE-2022-0339|
|
||||
| V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @nhiephon |CVE-2022-0273|
|
||||
| V 0.6.16 | It's prevented to get the name of a private shelfs. Thanks to @nhiephon |CVE-2022-0405|
|
||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via an HTTP redirect. Thanks to @416e6e61 |CVE-2022-0767|
|
||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH |CVE-2022-0766|
|
||||
| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) |CVE-2022-30765|
|
||||
| V 0.6.18 | The SSRF protection no longer can be bypassed by IPV6/IPV4 embedding. Thanks to @416e6e61 |CVE-2022-0939|
|
||||
| V 0.6.18 | The SSRF protection no longer can be bypassed to connect to other servers in the local network. Thanks to @michaellrowley |CVE-2022-0990|
|
||||
| V 0.6.20 | Credentials for emails are now stored encrypted ||
|
||||
| V 0.6.20 | Login is rate limited ||
|
||||
| V 0.6.20 | Passwordstrength can be forced ||
|
||||
| V 0.6.21 | SMTP server credentials are no longer returned to client ||
|
||||
| V 0.6.21 | Cross-site scripting (XSS) stored in href bypasses filter using data wrapper no longer possible ||
|
||||
| V 0.6.21 | Cross-site scripting (XSS) is no longer possible via pathchooser ||
|
||||
| V 0.6.21 | Error Handling at non existent rating, language, and user downloaded books was fixed ||
|
||||
| V 0.6.22 | Upload mimetype is checked to prevent malicious file content in the books library ||
|
||||
| V 0.6.22 | Cross-site scripting (XSS) stored in comments section is prevented better (switching from lxml to bleach for sanitizing strings) ||
|
||||
| V 0.6.23 | Cookies are no longer stored for opds basic authentication and proxy authentication ||
|
||||
|
||||
|
||||
|
||||
|
||||
## Statement regarding Log4j (CVE-2021-44228 and related)
|
||||
|
||||
@@ -30,19 +30,15 @@ from cps.main import main
|
||||
|
||||
def hide_console_windows():
|
||||
import ctypes
|
||||
import os
|
||||
|
||||
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
|
||||
if hwnd != 0:
|
||||
try:
|
||||
import win32process
|
||||
except ImportError:
|
||||
print("To hide console window install 'pywin32' using 'pip install pywin32'")
|
||||
return
|
||||
ctypes.windll.user32.ShowWindow(hwnd, 0)
|
||||
ctypes.windll.kernel32.CloseHandle(hwnd)
|
||||
_, pid = win32process.GetWindowThreadProcessId(hwnd)
|
||||
os.system('taskkill /PID ' + str(pid) + ' /f')
|
||||
kernel32 = ctypes.WinDLL('kernel32')
|
||||
user32 = ctypes.WinDLL('user32')
|
||||
|
||||
SW_HIDE = 0
|
||||
|
||||
hWnd = kernel32.GetConsoleWindow()
|
||||
if hWnd:
|
||||
user32.ShowWindow(hWnd, SW_HIDE)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+4
-20
@@ -20,11 +20,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask_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
|
||||
from .cw_login import LoginManager
|
||||
from flask import session
|
||||
|
||||
|
||||
class MyLoginManager(LoginManager):
|
||||
@@ -36,18 +33,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
|
||||
|
||||
|
||||
|
||||
Executable → Regular
+4
-4
@@ -70,8 +70,8 @@ mimetypes.add_type('image/vnd.djv', '.djv')
|
||||
mimetypes.add_type('image/vnd.djv', '.djvu')
|
||||
mimetypes.add_type('application/mpeg', '.mpeg')
|
||||
mimetypes.add_type('audio/mpeg', '.mp3')
|
||||
mimetypes.add_type('application/mp4', '.m4a')
|
||||
mimetypes.add_type('application/mp4', '.m4b')
|
||||
mimetypes.add_type('audio/x-m4a', '.m4a')
|
||||
mimetypes.add_type('audio/x-m4a', '.m4b')
|
||||
mimetypes.add_type('audio/ogg', '.ogg')
|
||||
mimetypes.add_type('application/ogg', '.oga')
|
||||
mimetypes.add_type('text/css', '.css')
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
Executable → Regular
+61
-61
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ def clean_string(unsafe_text, book_id=0):
|
||||
try:
|
||||
if bleach:
|
||||
allowed_tags = list(ALLOWED_TAGS)
|
||||
allowed_tags.extend(['p', 'span', 'div', 'pre'])
|
||||
allowed_tags.extend(["p", "span", "div", "pre", "br", "h1", "h2", "h3", "h4", "h5", "h6"])
|
||||
safe_text = clean_html(unsafe_text, tags=set(allowed_tags))
|
||||
else:
|
||||
safe_text = clean_html(unsafe_text)
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, d
|
||||
'series_id, languages, publisher, pubdate, identifiers')
|
||||
|
||||
# python build process likes to have x.y.zbw -> b for beta and w a counting number
|
||||
STABLE_VERSION = {'version': '0.6.22'}
|
||||
STABLE_VERSION = {'version': '0.6.23'}
|
||||
|
||||
NIGHTLY_VERSION = dict()
|
||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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=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=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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
|
||||
+26
-24
@@ -23,7 +23,7 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
import json
|
||||
from shutil import copyfile, move
|
||||
from shutil import copyfile
|
||||
from uuid import uuid4
|
||||
from markupsafe import escape, Markup # dependency of flask
|
||||
from functools import wraps
|
||||
@@ -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', ""))
|
||||
|
||||
@@ -133,8 +134,9 @@ def edit_book(book_id):
|
||||
# handle upload other formats from local disk
|
||||
meta = upload_single_file(request, book, book_id)
|
||||
# only merge metadata if file was uploaded and no error occurred (meta equals not false or none)
|
||||
upload_format = False
|
||||
if meta:
|
||||
merge_metadata(to_save, meta)
|
||||
upload_format = merge_metadata(to_save, meta)
|
||||
# handle upload covers from local disk
|
||||
cover_upload_success = upload_cover(request, book)
|
||||
if cover_upload_success:
|
||||
@@ -178,7 +180,7 @@ def edit_book(book_id):
|
||||
modify_date |= edit_book_publisher(to_save['publisher'], book)
|
||||
# handle book languages
|
||||
try:
|
||||
modify_date |= edit_book_languages(to_save['languages'], book)
|
||||
modify_date |= edit_book_languages(to_save['languages'], book, upload_format)
|
||||
except ValueError as e:
|
||||
flash(str(e), category="error")
|
||||
edit_error = True
|
||||
@@ -331,7 +333,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 +457,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 +474,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 +490,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 +528,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')
|
||||
@@ -574,6 +576,10 @@ def merge_metadata(to_save, meta):
|
||||
to_save['author_name'] = ''
|
||||
if to_save.get('book_title', "") == _('Unknown'):
|
||||
to_save['book_title'] = ''
|
||||
if not to_save["languages"] and meta.languages:
|
||||
upload_language = True
|
||||
else:
|
||||
upload_language = False
|
||||
for s_field, m_field in [
|
||||
('tags', 'tags'), ('author_name', 'author'), ('series', 'series'),
|
||||
('series_index', 'series_id'), ('languages', 'languages'),
|
||||
@@ -581,7 +587,7 @@ def merge_metadata(to_save, meta):
|
||||
to_save[s_field] = to_save[s_field] or getattr(meta, m_field, '')
|
||||
to_save["description"] = to_save["description"] or Markup(
|
||||
getattr(meta, 'description', '')).unescape()
|
||||
|
||||
return upload_language
|
||||
|
||||
def identifier_list(to_save, book):
|
||||
"""Generate a list of Identifiers from form information"""
|
||||
@@ -1288,7 +1294,6 @@ def handle_author_on_edit(book, author_name, update_stored=True):
|
||||
# handle author(s)
|
||||
input_authors = prepare_authors(author_name, config.get_book_path(), config.config_use_google_drive)
|
||||
|
||||
# change |= modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
||||
# Search for each author if author is in database, if not, author name and sorted author name is generated new
|
||||
# everything then is assembled for sorted author field in database
|
||||
sort_authors_list = list()
|
||||
@@ -1368,11 +1373,9 @@ def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
|
||||
for add_element in add_elements:
|
||||
# check if an element with that name exists
|
||||
changed = True
|
||||
# db_session.query(db.Tags).filter((func.lower(db.Tags.name).ilike("GênOt"))).all()
|
||||
db_element = db_session.query(db_object).filter((func.lower(db_filter).ilike(add_element))).first()
|
||||
# db_element = db_session.query(db_object).filter(func.lower(db_filter) == add_element.lower()).first()
|
||||
db_element = db_session.query(db_object).filter((func.lower(db_filter).ilike(add_element))).all()
|
||||
# if no element is found add it
|
||||
if db_element is None:
|
||||
if not db_element:
|
||||
if db_type == 'author':
|
||||
new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')))
|
||||
elif db_type == 'series':
|
||||
@@ -1386,12 +1389,11 @@ def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
|
||||
db_session.add(new_element)
|
||||
db_book_object.append(new_element)
|
||||
else:
|
||||
db_no_case = db_session.query(db_object).filter(db_filter == add_element).first()
|
||||
if db_no_case:
|
||||
# check for new case of element
|
||||
db_element = create_objects_for_addition(db_element, add_element, db_type)
|
||||
#else:
|
||||
# db_element = create_objects_for_addition(db_element, add_element, db_type)
|
||||
if len(db_element) == 1:
|
||||
db_element = create_objects_for_addition(db_element[0], add_element, db_type)
|
||||
else:
|
||||
db_el = db_session.query(db_object).filter(db_filter == add_element).first()
|
||||
db_element = db_element[0] if not db_el else db_el
|
||||
# add element to book
|
||||
db_book_object.append(db_element)
|
||||
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ log = logger.create()
|
||||
|
||||
def do_calibre_export(book_id, book_format):
|
||||
try:
|
||||
quotes = [3, 5, 7, 9]
|
||||
quotes = [4, 6]
|
||||
tmp_dir = get_temp_dir()
|
||||
calibredb_binarypath = get_calibre_binarypath("calibredb")
|
||||
temp_file_name = str(uuid4())
|
||||
|
||||
+12
-9
@@ -21,17 +21,18 @@ import os
|
||||
import shutil
|
||||
import zipfile
|
||||
import mimetypes
|
||||
import copy
|
||||
from io import BytesIO
|
||||
try:
|
||||
import magic
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from . import logger
|
||||
|
||||
log = logger.create()
|
||||
|
||||
try:
|
||||
import magic
|
||||
error = None
|
||||
except ImportError as e:
|
||||
error = "Cannot import python-magic, checking uploaded file metadata will not work: {}".format(e)
|
||||
|
||||
|
||||
def get_temp_dir():
|
||||
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
|
||||
@@ -46,12 +47,15 @@ def del_temp_dir():
|
||||
|
||||
|
||||
def validate_mime_type(file_buffer, allowed_extensions):
|
||||
if error:
|
||||
log.error(error)
|
||||
return False
|
||||
mime = magic.Magic(mime=True)
|
||||
allowed_mimetypes =list()
|
||||
allowed_mimetypes = list()
|
||||
for x in allowed_extensions:
|
||||
try:
|
||||
allowed_mimetypes.append(mimetypes.types_map["." + x])
|
||||
except KeyError as e:
|
||||
except KeyError:
|
||||
log.error("Unkown mimetype for Extension: {}".format(x))
|
||||
tmp_mime_type = mime.from_buffer(file_buffer.read())
|
||||
file_buffer.seek(0)
|
||||
@@ -66,6 +70,5 @@ def validate_mime_type(file_buffer, allowed_extensions):
|
||||
return True
|
||||
except:
|
||||
file_buffer.seek(0)
|
||||
pass
|
||||
|
||||
log.error("Mimetype '{}' not found in allowed types".format(tmp_mime_type))
|
||||
return False
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+23
-1
@@ -16,7 +16,7 @@
|
||||
# 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 datetime import datetime
|
||||
from gevent.pywsgi import WSGIHandler
|
||||
|
||||
|
||||
@@ -27,4 +27,26 @@ class MyWSGIHandler(WSGIHandler):
|
||||
env['RAW_URI'] = path
|
||||
return env
|
||||
|
||||
def format_request(self):
|
||||
now = datetime.now().replace(microsecond=0)
|
||||
length = self.response_length or '-'
|
||||
if self.time_finish:
|
||||
delta = '%.6f' % (self.time_finish - self.time_start)
|
||||
else:
|
||||
delta = '-'
|
||||
forwarded = self.environ.get('HTTP_X_FORWARDED_FOR', None)
|
||||
if forwarded:
|
||||
client_address = forwarded
|
||||
else:
|
||||
client_address = self.client_address[0] if isinstance(self.client_address, tuple) else self.client_address
|
||||
return '%s - - [%s] "%s" %s %s %s' % (
|
||||
client_address or '-',
|
||||
now,
|
||||
self.requestline or '',
|
||||
# Use the native string version of the status, saved so we don't have to
|
||||
# decode. But fallback to the encoded 'status' in case of subclasses
|
||||
# (Is that really necessary? At least there's no overhead.)
|
||||
(self._orig_status or self.status or '000').split()[0],
|
||||
length,
|
||||
delta)
|
||||
|
||||
|
||||
+31
-53
@@ -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
|
||||
@@ -117,9 +117,11 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
|
||||
|
||||
# Texts are not lazy translated as they are supposed to get send out as is
|
||||
def send_test_mail(ereader_mail, user_name):
|
||||
WorkerThread.add(user_name, TaskEmail(_('Calibre-Web Test Email'), None, None,
|
||||
config.get_mail_settings(), ereader_mail, N_("Test Email"),
|
||||
_('This Email has been sent via Calibre-Web.')))
|
||||
for email in ereader_mail.split(','):
|
||||
email = email.strip()
|
||||
WorkerThread.add(user_name, TaskEmail(_('Calibre-Web Test Email'), None, None,
|
||||
config.get_mail_settings(), email, N_("Test Email"),
|
||||
_('This Email has been sent via Calibre-Web.')))
|
||||
return
|
||||
|
||||
|
||||
@@ -225,9 +227,11 @@ def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id)
|
||||
converted_file_name = entry.name + '.' + book_format.lower()
|
||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
|
||||
email_text = N_("%(book)s send to eReader", book=link)
|
||||
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
|
||||
config.get_mail_settings(), ereader_mail,
|
||||
email_text, _('This Email has been sent via Calibre-Web.'), book.id))
|
||||
for email in ereader_mail.split(','):
|
||||
email = email.strip()
|
||||
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
|
||||
config.get_mail_settings(), email,
|
||||
email_text, _('This Email has been sent via Calibre-Web.'), book.id))
|
||||
return
|
||||
return _("The requested file could not be read. Maybe wrong permissions?")
|
||||
|
||||
@@ -409,36 +413,6 @@ def rename_all_files_on_change(one_book, new_path, old_path, all_new_name, gdriv
|
||||
file_format.name = all_new_name
|
||||
|
||||
|
||||
'''def rename_all_authors(first_author, renamed_author, calibre_path="", localbook=None, gdrive=False):
|
||||
# Create new_author_dir from parameter or from database
|
||||
# Create new title_dir from database and add id
|
||||
if first_author:
|
||||
new_authordir = get_valid_filename(first_author, chars=96)
|
||||
for r in renamed_author:
|
||||
new_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == r).first()
|
||||
old_author_dir = get_valid_filename(r, chars=96)
|
||||
new_author_rename_dir = get_valid_filename(new_author.name, chars=96)
|
||||
if gdrive:
|
||||
g_file = gd.getFileFromEbooksFolder(None, old_author_dir)
|
||||
if g_file:
|
||||
gd.moveGdriveFolderRemote(g_file, new_author_rename_dir)
|
||||
gd.updateDatabaseOnEdit(g_file['id'], new_author_rename_dir)
|
||||
else:
|
||||
if os.path.isdir(os.path.join(calibre_path, old_author_dir)):
|
||||
old_author_path = os.path.join(calibre_path, old_author_dir)
|
||||
new_author_path = os.path.join(calibre_path, new_author_rename_dir)
|
||||
try:
|
||||
shutil.move(os.path.normcase(old_author_path), os.path.normcase(new_author_path))
|
||||
except OSError as ex:
|
||||
log.error("Rename author from: %s to %s: %s", old_author_path, new_author_path, ex)
|
||||
log.debug(ex, exc_info=True)
|
||||
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||
src=old_author_path, dest=new_author_path, error=str(ex))
|
||||
else:
|
||||
new_authordir = get_valid_filename(localbook.authors[0].name, chars=96)
|
||||
return new_authordir'''
|
||||
|
||||
|
||||
def rename_author_path(first_author, old_author_dir, renamed_author, calibre_path="", gdrive=False):
|
||||
# Create new_author_dir from parameter or from database
|
||||
# Create new title_dir from database and add id
|
||||
@@ -455,12 +429,15 @@ def rename_author_path(first_author, old_author_dir, renamed_author, calibre_pat
|
||||
old_author_path = os.path.join(calibre_path, old_author_dir)
|
||||
new_author_path = os.path.join(calibre_path, new_author_rename_dir)
|
||||
try:
|
||||
shutil.move(old_author_path, new_author_path)
|
||||
except OSError as ex:
|
||||
log.error("Rename author from: %s to %s: %s", old_author_path, new_author_path, ex)
|
||||
log.debug(ex, exc_info=True)
|
||||
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||
src=old_author_path, dest=new_author_path, error=str(ex))
|
||||
os.rename(old_author_path, new_author_path)
|
||||
except OSError:
|
||||
try:
|
||||
shutil.move(old_author_path, new_author_path)
|
||||
except OSError as ex:
|
||||
log.error("Rename author from: %s to %s: %s", old_author_path, new_author_path, ex)
|
||||
log.error_or_exception(ex)
|
||||
raise Exception(_("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
|
||||
src=old_author_path, dest=new_author_path, error=str(ex)))
|
||||
return new_authordir
|
||||
|
||||
# Moves files in file storage during author/title rename, or from temp dir to file storage
|
||||
@@ -558,7 +535,7 @@ def move_files_on_change(calibre_path, new_author_dir, new_titledir, localbook,
|
||||
if not os.path.isdir(new_path):
|
||||
os.makedirs(new_path)
|
||||
shutil.move(original_filepath, os.path.join(new_path, db_filename))
|
||||
log.debug("Moving title: %s to %s/%s", original_filepath, new_path)
|
||||
log.debug("Moving title: %s to %s", original_filepath, new_path)
|
||||
else:
|
||||
# Check new path is not valid path
|
||||
if not os.path.exists(new_path):
|
||||
@@ -691,15 +668,16 @@ def check_username(username):
|
||||
return username
|
||||
|
||||
|
||||
def valid_email(email):
|
||||
email = email.strip()
|
||||
# if email is not deleted
|
||||
if email:
|
||||
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
|
||||
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
|
||||
email):
|
||||
log.error("Invalid Email address format")
|
||||
raise Exception(_("Invalid Email address format"))
|
||||
def valid_email(emails):
|
||||
for email in emails.split(','):
|
||||
email = email.strip()
|
||||
# if email is not deleted
|
||||
if email:
|
||||
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
|
||||
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
|
||||
email):
|
||||
log.error("Invalid Email address format")
|
||||
raise Exception(_("Invalid Email address format"))
|
||||
return email
|
||||
|
||||
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
+5
-3
@@ -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,
|
||||
|
||||
+1
-1
@@ -20,7 +20,6 @@ import sys
|
||||
|
||||
from . import create_app, limiter
|
||||
from .jinjia import jinjia
|
||||
from .remotelogin import remotelogin
|
||||
from flask import request
|
||||
|
||||
|
||||
@@ -42,6 +41,7 @@ def main():
|
||||
from .shelf import shelf
|
||||
from .tasks_status import tasks
|
||||
from .error_handler import init_errorhandler
|
||||
from .remotelogin import remotelogin
|
||||
try:
|
||||
from .kobo import kobo, get_kobo_activated
|
||||
from .kobo_auth import kobo_auth
|
||||
|
||||
@@ -137,7 +137,7 @@ class LubimyCzytac(Metadata):
|
||||
|
||||
def _prepare_query(self, title: str) -> str:
|
||||
query = ""
|
||||
characters_to_remove = "\?()\/"
|
||||
characters_to_remove = r"\?()\/"
|
||||
pattern = "[" + characters_to_remove + "]"
|
||||
title = re.sub(pattern, "", title)
|
||||
title = title.replace("_", " ")
|
||||
|
||||
+4
-3
@@ -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'])
|
||||
|
||||
+27
-26
@@ -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)
|
||||
|
||||
Executable → Regular
+3
-2
@@ -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):
|
||||
|
||||
+3
-3
@@ -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):
|
||||
|
||||
+39
-13
@@ -19,17 +19,19 @@ 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
|
||||
from sqlalchemy.sql.functions import coalesce
|
||||
from sqlalchemy import exists
|
||||
|
||||
from . import logger, db, calibre_db, config, ub
|
||||
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()
|
||||
@@ -80,16 +82,27 @@ def adv_search_custom_columns(cc, term, q):
|
||||
if custom_end:
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
func.datetime(db.cc_classes[c.id].value) <= func.datetime(custom_end)))
|
||||
elif c.datatype in ["int", "float"]:
|
||||
custom_low = term.get('custom_column_' + str(c.id) + '_low')
|
||||
custom_high = term.get('custom_column_' + str(c.id) + '_high')
|
||||
if custom_low:
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
db.cc_classes[c.id].value >= custom_low))
|
||||
if custom_high:
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
db.cc_classes[c.id].value <= custom_high))
|
||||
else:
|
||||
custom_query = term.get('custom_column_' + str(c.id))
|
||||
if custom_query != '' and custom_query is not None:
|
||||
if c.datatype == 'bool':
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
db.cc_classes[c.id].value == (custom_query == "True")))
|
||||
elif c.datatype == 'int' or c.datatype == 'float':
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
db.cc_classes[c.id].value == custom_query))
|
||||
elif c.datatype == 'rating':
|
||||
if c.datatype == 'bool':
|
||||
if custom_query != "Any":
|
||||
if custom_query == "":
|
||||
q = q.filter(~getattr(db.Books, 'custom_column_' + str(c.id)).
|
||||
any(db.cc_classes[c.id].value >= 0))
|
||||
else:
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
db.cc_classes[c.id].value == bool(custom_query == "True")))
|
||||
elif custom_query != '' and custom_query is not None:
|
||||
if c.datatype == 'rating':
|
||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||
db.cc_classes[c.id].value == int(float(custom_query) * 2)))
|
||||
else:
|
||||
@@ -128,10 +141,10 @@ def adv_search_read_status(read_status):
|
||||
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
|
||||
else:
|
||||
try:
|
||||
if read_status == "True":
|
||||
db_filter = db.cc_classes[config.config_read_column].value == True
|
||||
if read_status == "":
|
||||
db_filter = coalesce(db.cc_classes[config.config_read_column].value, 2) == 2
|
||||
else:
|
||||
db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True
|
||||
db_filter = db.cc_classes[config.config_read_column].value == bool(read_status == "True")
|
||||
except (KeyError, AttributeError, IndexError):
|
||||
log.error("Custom Column No.{} does not exist in calibre database".format(config.config_read_column))
|
||||
flash(_("Custom Column No.%(column)d does not exist in calibre database",
|
||||
@@ -274,10 +287,23 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
||||
cc_present = True
|
||||
if column_end:
|
||||
search_term.extend(["{} <= {}".format(c.name,
|
||||
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
|
||||
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
|
||||
format='medium')
|
||||
)])
|
||||
cc_present = True
|
||||
if c.datatype in ["int", "float"]:
|
||||
column_low = term.get('custom_column_' + str(c.id) + '_low')
|
||||
column_high = term.get('custom_column_' + str(c.id) + '_high')
|
||||
if column_low:
|
||||
search_term.extend(["{} >= {}".format(c.name, column_low)])
|
||||
cc_present = True
|
||||
if column_high:
|
||||
search_term.extend(["{} <= {}".format(c.name,column_high)])
|
||||
cc_present = True
|
||||
elif c.datatype == "bool":
|
||||
if term.get('custom_column_' + str(c.id)) != "Any":
|
||||
search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
|
||||
cc_present = True
|
||||
elif term.get('custom_column_' + str(c.id)):
|
||||
search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
|
||||
cc_present = True
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -21,7 +21,6 @@ import os
|
||||
import errno
|
||||
import signal
|
||||
import socket
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
from gevent.pywsgi import WSGIServer
|
||||
|
||||
+9
-9
@@ -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):
|
||||
|
||||
Vendored
+15
@@ -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;
|
||||
|
||||
@@ -551,7 +551,7 @@ input:-moz-placeholder { color: #454545; }
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
margin: 0 auto;
|
||||
height: 320px;
|
||||
/* height: 320px; */
|
||||
}
|
||||
|
||||
.md-content > div {
|
||||
@@ -826,3 +826,10 @@ input:-moz-placeholder { color: #454545; }
|
||||
.icon-columns::before { content: '\e810'; } /* '' */
|
||||
.icon-list::before { content: '\e800'; } /* '' */
|
||||
.icon-resize-small::before { content: '\e808'; } /* '' */
|
||||
|
||||
#progress{
|
||||
right: 4rem;
|
||||
bottom: 4rem;
|
||||
width: fit-content;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@@ -27,3 +27,21 @@
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
font-family: Open Sans;
|
||||
padding-right: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.item~button {
|
||||
display: inline-block;
|
||||
border: none;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
margin-top: 5%;
|
||||
margin-right: 1%;
|
||||
font-size: 16px;
|
||||
}
|
||||
Vendored
+464
-207
File diff suppressed because it is too large
Load Diff
Vendored
+382
-202
File diff suppressed because one or more lines are too long
Vendored
+186
-118
@@ -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);
|
||||
}
|
||||
@@ -11549,26 +11611,26 @@ class SecondaryToolbar {
|
||||
eventDetails
|
||||
} of buttons) {
|
||||
if ( element !== null ) {
|
||||
element.addEventListener("click", evt => {
|
||||
if (eventName !== null) {
|
||||
eventBus.dispatch(eventName, {
|
||||
source: this,
|
||||
...eventDetails
|
||||
});
|
||||
element.addEventListener("click", evt => {
|
||||
if (eventName !== null) {
|
||||
eventBus.dispatch(eventName, {
|
||||
source: this,
|
||||
...eventDetails
|
||||
});
|
||||
}
|
||||
if (close) {
|
||||
this.close();
|
||||
}
|
||||
eventBus.dispatch("reporttelemetry", {
|
||||
source: this,
|
||||
details: {
|
||||
type: "buttons",
|
||||
data: {
|
||||
id: element.id
|
||||
}
|
||||
if (close) {
|
||||
this.close();
|
||||
}
|
||||
eventBus.dispatch("reporttelemetry", {
|
||||
source: this,
|
||||
details: {
|
||||
type: "buttons",
|
||||
data: {
|
||||
id: element.id
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
eventBus._on("cursortoolchanged", this.#cursorToolChanged.bind(this));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -12378,7 +12448,7 @@ const PDFViewerApplication = {
|
||||
const params = parseQueryString(queryString);
|
||||
file = params.get("file") ?? AppOptions.get("defaultUrl");
|
||||
validateFileURL(file);
|
||||
/* const fileInput = this._openFileInput = document.createElement("input");
|
||||
/*const fileInput = this._openFileInput = document.createElement("input");
|
||||
fileInput.id = "fileInput";
|
||||
fileInput.hidden = true;
|
||||
fileInput.type = "file";
|
||||
@@ -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";
|
||||
for (const item of evt.dataTransfer.items) {
|
||||
if (item.type === "application/pdf") {
|
||||
evt.dataTransfer.dropEffect = evt.dataTransfer.effectAllowed === "copy" ? "copy" : "move";
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
appConfig.mainContainer.addEventListener("drop", function (evt) {
|
||||
evt.preventDefault();
|
||||
const {
|
||||
files
|
||||
} = evt.dataTransfer;
|
||||
if (!files || files.length === 0) {
|
||||
if (evt.dataTransfer.files?.[0].type !== "application/pdf") {
|
||||
return;
|
||||
}
|
||||
/*eventBus.dispatch("fileinputchange", {
|
||||
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,
|
||||
|
||||
@@ -160,15 +160,21 @@ $(document).ready(function() {
|
||||
|
||||
$(".session").click(function() {
|
||||
window.sessionStorage.setItem("back", window.location.pathname);
|
||||
window.sessionStorage.setItem("search", window.location.search);
|
||||
});
|
||||
|
||||
$("#back").click(function() {
|
||||
var loc = sessionStorage.getItem("back");
|
||||
var param = sessionStorage.getItem("search");
|
||||
if (!loc) {
|
||||
loc = $(this).data("back");
|
||||
}
|
||||
sessionStorage.removeItem("back");
|
||||
window.location.href = loc;
|
||||
sessionStorage.removeItem("search");
|
||||
if (param === null) {
|
||||
param = "";
|
||||
}
|
||||
window.location.href = loc + param;
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* waits until queue is finished, meaning the book is done loading
|
||||
* @param callback
|
||||
*/
|
||||
function qFinished(callback){
|
||||
let timeout=setInterval(()=>{
|
||||
if(reader.rendition.q.running===undefined)
|
||||
clearInterval(timeout);
|
||||
callback();
|
||||
},300
|
||||
)
|
||||
}
|
||||
|
||||
function calculateProgress(){
|
||||
let data=reader.rendition.location.end;
|
||||
return Math.round(epub.locations.percentageFromCfi(data.cfi)*100);
|
||||
}
|
||||
|
||||
// register new event emitter locationchange that fires on urlchange
|
||||
// source: https://stackoverflow.com/a/52809105/21941129
|
||||
(() => {
|
||||
let oldPushState = history.pushState;
|
||||
history.pushState = function pushState() {
|
||||
let ret = oldPushState.apply(this, arguments);
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
return ret;
|
||||
};
|
||||
|
||||
let oldReplaceState = history.replaceState;
|
||||
history.replaceState = function replaceState() {
|
||||
let ret = oldReplaceState.apply(this, arguments);
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
return ret;
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
window.dispatchEvent(new Event('locationchange'));
|
||||
});
|
||||
})();
|
||||
|
||||
window.addEventListener('locationchange',()=>{
|
||||
let newPos=calculateProgress();
|
||||
progressDiv.textContent=newPos+"%";
|
||||
});
|
||||
|
||||
var epub=ePub(calibre.bookUrl)
|
||||
|
||||
let progressDiv=document.getElementById("progress");
|
||||
|
||||
qFinished(()=>{
|
||||
epub.locations.generate().then(()=> {
|
||||
window.dispatchEvent(new Event('locationchange'))
|
||||
});
|
||||
})
|
||||
@@ -13,10 +13,9 @@ var reader;
|
||||
bookmarks: calibre.bookmark ? [calibre.bookmark] : []
|
||||
});
|
||||
|
||||
reader.rendition.themes.register("lightTheme", "/static/css/epub_themes.css");
|
||||
reader.rendition.themes.register("darkTheme", "/static/css/epub_themes.css");
|
||||
reader.rendition.themes.register("sepiaTheme", "/static/css/epub_themes.css");
|
||||
reader.rendition.themes.register("blackTheme", "/static/css/epub_themes.css");
|
||||
Object.keys(themes).forEach(function (theme) {
|
||||
reader.rendition.themes.register(theme, themes[theme].css_path);
|
||||
});
|
||||
|
||||
if (calibre.useBookmarks) {
|
||||
reader.on("reader:bookmarked", updateBookmark.bind(reader, "add"));
|
||||
@@ -78,6 +77,8 @@ var reader;
|
||||
alert(error);
|
||||
});
|
||||
}
|
||||
|
||||
// Default settings load
|
||||
const theme = localStorage.getItem("calibre.reader.theme") ?? "lightTheme";
|
||||
selectTheme(theme);
|
||||
})();
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
+26
-16
@@ -105,17 +105,19 @@ class TaskConvert(CalibreTask):
|
||||
# if we're sending to E-Reader after converting, create a one-off task and run it immediately
|
||||
# todo: figure out how to incorporate this into the progress
|
||||
try:
|
||||
EmailText = N_("%(book)s send to E-Reader", book=escape(self.title))
|
||||
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
||||
self.results["path"],
|
||||
filename,
|
||||
self.settings,
|
||||
self.ereader_mail,
|
||||
EmailText,
|
||||
self.settings['body'],
|
||||
id=self.book_id,
|
||||
internal=True)
|
||||
)
|
||||
EmailText = N_(u"%(book)s send to E-Reader", book=escape(self.title))
|
||||
for email in self.ereader_mail.split(','):
|
||||
email = email.strip()
|
||||
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
||||
self.results["path"],
|
||||
filename,
|
||||
self.settings,
|
||||
email,
|
||||
EmailText,
|
||||
self.settings['body'],
|
||||
id=self.book_id,
|
||||
internal=True)
|
||||
)
|
||||
except Exception as ex:
|
||||
return self._handleError(str(ex))
|
||||
|
||||
@@ -253,7 +255,7 @@ class TaskConvert(CalibreTask):
|
||||
try:
|
||||
# path_tmp_opf = self._embed_metadata()
|
||||
if config.config_embed_metadata:
|
||||
quotes = [3, 5]
|
||||
quotes = [5]
|
||||
tmp_dir = get_temp_dir()
|
||||
calibredb_binarypath = os.path.join(config.config_binariesdir, SUPPORTED_CALIBRE_BINARIES["calibredb"])
|
||||
my_env = os.environ.copy()
|
||||
@@ -267,10 +269,18 @@ class TaskConvert(CalibreTask):
|
||||
'--with-library', library_path]
|
||||
p = process_open(opf_command, quotes, my_env)
|
||||
p.wait()
|
||||
path_tmp_opf = os.path.join(tmp_dir, "metadata_" + str(uuid4()) + ".opf")
|
||||
with open(path_tmp_opf, 'w') as fd:
|
||||
copyfileobj(p.stdout, fd)
|
||||
|
||||
check = p.returncode
|
||||
calibre_traceback = p.stderr.readlines()
|
||||
if check == 0:
|
||||
path_tmp_opf = os.path.join(tmp_dir, "metadata_" + str(uuid4()) + ".opf")
|
||||
with open(path_tmp_opf, 'w') as fd:
|
||||
copyfileobj(p.stdout, fd)
|
||||
else:
|
||||
error_message = ""
|
||||
for ele in calibre_traceback:
|
||||
if not ele.startswith('Traceback') and not ele.startswith(' File'):
|
||||
error_message = N_("Calibre failed with error: %(error)s", error=ele)
|
||||
return check, error_message
|
||||
quotes = [1, 2, 4, 6]
|
||||
command = [config.config_converterpath, (file_path + format_old_ext),
|
||||
(file_path + format_new_ext)]
|
||||
|
||||
+5
-13
@@ -16,11 +16,9 @@
|
||||
# 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
|
||||
|
||||
from flask_babel import lazy_gettext as N_
|
||||
|
||||
from cps import config, logger
|
||||
from cps import config, logger, db, ub
|
||||
from cps.services.worker import CalibreTask
|
||||
|
||||
|
||||
@@ -28,18 +26,12 @@ class TaskReconnectDatabase(CalibreTask):
|
||||
def __init__(self, task_message=N_('Reconnecting Calibre database')):
|
||||
super(TaskReconnectDatabase, self).__init__(task_message)
|
||||
self.log = logger.create()
|
||||
self.listen_address = config.get_config_ipaddress()
|
||||
self.listen_port = config.config_port
|
||||
self.calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
|
||||
def run(self, worker_thread):
|
||||
address = self.listen_address if self.listen_address else 'localhost'
|
||||
port = self.listen_port if self.listen_port else 8083
|
||||
|
||||
try:
|
||||
urlopen('http://' + address + ':' + str(port) + '/reconnect')
|
||||
self._handleSuccess()
|
||||
except Exception as ex:
|
||||
self._handleError('Unable to reconnect Calibre database: ' + str(ex))
|
||||
self.calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
self.calibre_db.session.close()
|
||||
self._handleSuccess()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -70,7 +70,6 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
||||
self.log = logger.create()
|
||||
self.book_id = book_id
|
||||
self.app_db_session = ub.get_new_session_instance()
|
||||
# self.calibre_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||
self.cache = fs.FileSystem()
|
||||
self.resolutions = [
|
||||
constants.COVER_THUMBNAIL_SMALL,
|
||||
|
||||
+5
-3
@@ -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")
|
||||
@@ -81,6 +82,7 @@ def render_task_status(tasklist):
|
||||
ret['task_id'] = task.id
|
||||
ret['stat'] = task.stat
|
||||
ret['is_cancellable'] = task.is_cancellable
|
||||
ret['error'] = task.error
|
||||
|
||||
rendered_tasklist.append(ret)
|
||||
|
||||
|
||||
@@ -62,13 +62,13 @@
|
||||
{% if g.current_theme == 1 %}
|
||||
<li class="dropdown"><a href="#" class="dropdown-toggle profileDrop" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span></a>
|
||||
<ul class="dropdown-menu profileDropli">
|
||||
<li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{current_user.name}}</span></a></li>
|
||||
<li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('web.profile')}}" ><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{current_user.name}}</span></a></li>
|
||||
{% if g.allow_registration and not current_user.is_authenticated %}
|
||||
<li><a id="login" href="{{url_for('web.login')}}"><span class="glyphicon glyphicon-log-in"></span> {{_('Login')}}</a></li>
|
||||
<li><a id="register" href="{{url_for('web.register')}}"><span class="glyphicon glyphicon-user"></span> {{_('Register')}}</a></li>
|
||||
{% endif %}
|
||||
{% if not current_user.is_anonymous %}
|
||||
<li><a id="logout" href="{{url_for('web.logout')}}"><span class="glyphicon glyphicon-log-out"></span> <span class="hidden-sm">{{_('Logout')}}</span></a></li>
|
||||
<li><a id="logout" href="{{url_for('web.logout', next=(request.path + "?" + request.query_string.decode("utf-8")).rstrip("?"))}}"><span class="glyphicon glyphicon-log-out"></span> <span class="hidden-sm">{{_('Logout')}}</span></a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
@@ -91,9 +91,9 @@
|
||||
<li><a id="top_admin" data-text="{{_('Settings')}}" href="{{url_for('admin.admin')}}"><span class="glyphicon glyphicon-dashboard"></span> <span class="hidden-sm">{{_('Admin')}}</span></a></li>
|
||||
{% endif %}
|
||||
{% if g.current_theme == 0 %}
|
||||
<li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{current_user.name}}</span></a></li>
|
||||
<li><a id="top_user" data-text="{{_('Account')}}" href="{% if not current_user.is_anonymous %}{{url_for('web.profile')}}{% else %}{{url_for('web.login', next=(request.path + "?" + request.query_string.decode("utf-8")).rstrip("?"))}}{% endif %}"><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{current_user.name}}</span></a></li>
|
||||
{% if not current_user.is_anonymous %}
|
||||
<li><a id="logout" href="{{url_for('web.logout')}}"><span class="glyphicon glyphicon-log-out"></span> <span class="hidden-sm">{{_('Logout')}}</span></a></li>
|
||||
<li><a id="logout" href="{{url_for('web.logout', next=(request.path + "?" + request.query_string.decode("utf-8")).rstrip("?"))}}"><span class="glyphicon glyphicon-log-out"></span> <span class="hidden-sm">{{_('Logout')}}</span></a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
+166
-105
@@ -1,33 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>{{_('epub Reader')}} | {{title}}</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
{% if g.google_site_verification|length > 0 %}
|
||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||
{% endif %}
|
||||
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/popup.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/reader.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div id="sidebar">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>{{_('epub Reader')}} | {{title}}</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
{% if g.google_site_verification|length > 0 %}
|
||||
<meta name="google-site-verification" content="{{g.google_site_verification}}">
|
||||
{% endif %}
|
||||
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/popup.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/reader.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="sidebar">
|
||||
<div id="panels">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<!--input id="searchBox" placeholder="search" type="search"-->
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<!--input id="searchBox" placeholder="search" type="search"-->
|
||||
|
||||
<!--a id="show-Search" class="show_view icon-search" data-view="Search">Search</a-->
|
||||
<a id="show-Toc" class="show_view icon-list-1 active" data-view="Toc">TOC</a>
|
||||
<a id="show-Bookmarks" class="show_view icon-bookmark" data-view="Bookmarks">Bookmarks</a>
|
||||
<!--a id="show-Notes" class="show_view icon-edit" data-view="Notes">Notes</a-->
|
||||
<!--a id="show-Search" class="show_view icon-search" data-view="Search">Search</a-->
|
||||
<a id="show-Toc" class="show_view icon-list-1 active" data-view="Toc">TOC</a>
|
||||
<a id="show-Bookmarks" class="show_view icon-bookmark" data-view="Bookmarks">Bookmarks</a>
|
||||
<!--a id="show-Notes" class="show_view icon-edit" data-view="Notes">Notes</a-->
|
||||
|
||||
</div>
|
||||
<div id="tocView" class="view">
|
||||
@@ -36,7 +38,7 @@
|
||||
<ul id="searchResults"></ul>
|
||||
</div-->
|
||||
<div id="bookmarksView" class="view">
|
||||
<ul id="bookmarks"></ul>
|
||||
<ul id="bookmarks"></ul>
|
||||
</div>
|
||||
<!--div id="notesView" class="view">
|
||||
<div id="new-note">
|
||||
@@ -45,110 +47,169 @@
|
||||
</div>
|
||||
<ol id="notes"></ol>
|
||||
</div-->
|
||||
</div>
|
||||
<div id="main">
|
||||
</div>
|
||||
<div id="main">
|
||||
|
||||
<div id="titlebar">
|
||||
<div id="opener">
|
||||
<a id="slider" class="icon-menu">Menu</a>
|
||||
</div>
|
||||
<div id="metainfo">
|
||||
<span id="book-title"></span>
|
||||
<span id="title-seperator"> – </span>
|
||||
<span id="chapter-title"></span>
|
||||
</div>
|
||||
<div id="title-controls">
|
||||
<a id="bookmark" class="icon-bookmark-empty">Bookmark</a>
|
||||
<a id="setting" class="icon-cog">Settings</a>
|
||||
<a id="fullscreen" class="icon-resize-full">Fullscreen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="opener">
|
||||
<a id="slider" class="icon-menu">Menu</a>
|
||||
</div>
|
||||
<div id="metainfo">
|
||||
<span id="book-title"></span>
|
||||
<span id="title-seperator"> – </span>
|
||||
<span id="chapter-title"></span>
|
||||
</div>
|
||||
<div id="title-controls">
|
||||
<a id="bookmark" class="icon-bookmark-empty">Bookmark</a>
|
||||
<a id="setting" class="icon-cog">Settings</a>
|
||||
<a id="fullscreen" class="icon-resize-full">Fullscreen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="divider"></div>
|
||||
<div id="prev" class="arrow">‹</div>
|
||||
<div id="viewer"></div>
|
||||
<div id="next" class="arrow">›</div>
|
||||
<div id="divider"></div>
|
||||
<div id="prev" class="arrow">‹</div>
|
||||
<div id="viewer"></div>
|
||||
<div id="next" class="arrow">›</div>
|
||||
<div id="progress">0%</div>
|
||||
<div id="loader"><img src="{{ url_for('static', filename='img/loader.gif') }}"></div>
|
||||
</div>
|
||||
<div class="modal md-effect-1" id="settings-modal">
|
||||
<div class="md-content">
|
||||
<h3>{{_('Settings')}}</h3>
|
||||
<div class="form-group themes" id="themes">
|
||||
Choose a theme below: <br />
|
||||
|
||||
<div id="loader"><img src="{{ url_for('static', filename='img/loader.gif') }}"></div>
|
||||
</div>
|
||||
<div class="modal md-effect-1" id="settings-modal">
|
||||
<div class="md-content">
|
||||
<h3>{{_('Settings')}}</h3>
|
||||
<div class="themes" id="themes">
|
||||
Choose a theme below: <br />
|
||||
|
||||
<!-- Hardcoded a tick in the light theme button because it is the "default" theme. Need to find a way to do this dynamically on startup-->
|
||||
<button type="button" id="lightTheme" class="lightTheme" onclick="selectTheme(this.id)"><span id="lightSelected">✓</span>{{_('Light')}}</button>
|
||||
<button type="button" id="darkTheme" class="darkTheme" onclick="selectTheme(this.id)"><span id="darkSelected"> </span>{{_('Dark')}}</button>
|
||||
<button type="button" id="sepiaTheme" class="sepiaTheme" onclick="selectTheme(this.id)"><span id="sepiaSelected"> </span>{{_('Sepia')}}</button>
|
||||
<button type="button" id="blackTheme" class="blackTheme" onclick="selectTheme(this.id)"><span id="blackSelected"> </span>{{_('Black')}}</button>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<input type="checkbox" id="sidebarReflow" name="sidebarReflow">{{_('Reflow text when sidebars are open.')}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="fontSizeWrapper">
|
||||
<!-- Hardcoded a tick in the light theme button because it is the "default" theme. Need to find a way to do this dynamically on startup-->
|
||||
<button type="button" id="lightTheme" class="lightTheme" onclick="selectTheme(this.id)"><span
|
||||
id="lightSelected">✓</span>{{_('Light')}}</button>
|
||||
<button type="button" id="darkTheme" class="darkTheme" onclick="selectTheme(this.id)"><span
|
||||
id="darkSelected"> </span>{{_('Dark')}}</button>
|
||||
<button type="button" id="sepiaTheme" class="sepiaTheme" onclick="selectTheme(this.id)"><span
|
||||
id="sepiaSelected"> </span>{{_('Sepia')}}</button>
|
||||
<button type="button" id="blackTheme" class="blackTheme" onclick="selectTheme(this.id)"><span
|
||||
id="blackSelected"> </span>{{_('Black')}}</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p>
|
||||
<input type="checkbox" id="sidebarReflow"
|
||||
name="sidebarReflow">{{_('Reflow text when sidebars are open.')}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group fontSizeWrapper">
|
||||
<div class="slider">
|
||||
<label for="fader">{{ _('Font Sizes') }}</label>
|
||||
<input type="range" min="75" max="200" value="100" id="fontSizeFader" step="25">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font" id="font">
|
||||
<label class="item">{{_('Font')}}:</label>
|
||||
<button type="button" id="default" onclick="selectFont(this.id)"><span>✓</span>{{_('Default')}}</button>
|
||||
<button type="button" id="Yahei" onclick="selectFont(this.id)"><span></span>{{_('Yahei')}}</button>
|
||||
<button type="button" id="SimSun" onclick="selectFont(this.id)"><span></span>{{_('SimSun')}}</button>
|
||||
<button type="button" id="KaiTi" onclick="selectFont(this.id)"><span></span>{{_('KaiTi')}}</button>
|
||||
<button type="button" id="Arial" onclick="selectFont(this.id)"><span></span>{{_('Arial')}}</button>
|
||||
</div>
|
||||
<div class="closer icon-cancel-circled"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay"></div>
|
||||
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/compress/jszip_epub.min.js') }}">
|
||||
</script> <script src="{{ url_for('static', filename='js/libs/epub.min.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
window.calibre = {
|
||||
filePath: "{{ url_for('static', filename='js/libs/') }}",
|
||||
cssPath: "{{ url_for('static', filename='css/') }}",
|
||||
bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=bookid, book_format='EPUB') }}",
|
||||
bookUrl: "{{ url_for('web.serve_book', book_id=bookid, book_format='epub', anyname='file.epub') }}",
|
||||
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
|
||||
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
|
||||
};
|
||||
<div class="layou" id="layout">
|
||||
<label class="item">{{ _('Spread') }}:</label>
|
||||
<button type="button" id="spread" onclick="spread(this.id)"><span>✓</span>{{_('Two columns')}}</button>
|
||||
<button type="button" id="nonespread" onclick="spread(this.id)"><span></span>{{_('One column')}}</button>
|
||||
</div>
|
||||
<div class="closer icon-cancel-circled"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay"></div>
|
||||
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/compress/jszip_epub.min.js') }}">
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/libs/epub.min.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
window.calibre = {
|
||||
filePath: "{{ url_for('static', filename='js/libs/') }}",
|
||||
cssPath: "{{ url_for('static', filename='css/') }}",
|
||||
bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=bookid, book_format='EPUB') }}",
|
||||
bookUrl: "{{ url_for('web.serve_book', book_id=bookid, book_format='epub', anyname='file.epub') }}",
|
||||
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
|
||||
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
|
||||
};
|
||||
|
||||
window.themes = {
|
||||
"darkTheme": {
|
||||
"bgColor": "#202124",
|
||||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}"
|
||||
},
|
||||
"lightTheme": {
|
||||
"bgColor": "white",
|
||||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}"
|
||||
},
|
||||
"sepiaTheme": {
|
||||
"bgColor": "#ece1ca",
|
||||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}"
|
||||
},
|
||||
"blackTheme": {
|
||||
"bgColor": "black",
|
||||
"css_path": "{{ url_for('static', filename='css/epub_themes.css') }}"
|
||||
},
|
||||
};
|
||||
|
||||
function selectTheme (id) {
|
||||
let tickSpans = document.getElementById("themes").querySelectorAll("span");
|
||||
|
||||
function selectTheme(id) {
|
||||
tickSpans = document.getElementById("themes").querySelectorAll("span");
|
||||
|
||||
// Remove tick mark from all theme buttons
|
||||
for(var i = 0; i < tickSpans.length; i++) {
|
||||
document.getElementById(tickSpans[i].id).textContent = "";
|
||||
}
|
||||
tickSpans.forEach(function (tickSpan) {
|
||||
document.getElementById(tickSpan.id).textContent = "";
|
||||
});
|
||||
|
||||
// Add tick mark to the button corresponding to the currently selected theme
|
||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
||||
|
||||
// Saving theme to local storage
|
||||
localStorage.setItem("calibre.reader.theme", id);
|
||||
|
||||
// Apply theme to epubjs iframe
|
||||
reader.rendition.themes.select(id);
|
||||
|
||||
// Apply theme to rest of the page. TODO - Do this smarter
|
||||
if (id == "darkTheme") {
|
||||
document.getElementById("main").style.backgroundColor = "#202124";
|
||||
}
|
||||
else if (id == "lightTheme") {
|
||||
document.getElementById("main").style.backgroundColor = "white";
|
||||
}
|
||||
else if (id == "sepiaTheme") {
|
||||
document.getElementById("main").style.backgroundColor = "#ece1ca";
|
||||
}
|
||||
else if (id == "blackTheme") {
|
||||
document.getElementById("main").style.backgroundColor = "black";
|
||||
}
|
||||
|
||||
// Apply theme to rest of the page.
|
||||
document.getElementById("main").style.backgroundColor = themes[id]["bgColor"];
|
||||
}
|
||||
|
||||
// font size settings logic
|
||||
let fontSizeFader = document.getElementById('fontSizeFader');
|
||||
fontSizeFader.addEventListener ("change", function () {
|
||||
reader.rendition.themes.fontSize(`${this.value}%`)
|
||||
fontSizeFader.addEventListener("change", function () {
|
||||
reader.rendition.themes.fontSize(`${this.value}%`);
|
||||
});
|
||||
|
||||
let defaultFont;
|
||||
|
||||
function selectFont(id) {
|
||||
if (!defaultFont) {
|
||||
defaultFont = reader.rendition.getContents()[0]?.css('font-family');
|
||||
}
|
||||
|
||||
spans = document.getElementById("font").querySelectorAll("span");
|
||||
for(var i = 0; i < spans.length; i++) {
|
||||
spans[i].textContent = "";
|
||||
}
|
||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
||||
|
||||
if (id == 'default') {
|
||||
reader.rendition.themes.font(defaultFont);
|
||||
return;
|
||||
}
|
||||
reader.rendition.themes.font(id);
|
||||
}
|
||||
|
||||
function spread(id) {
|
||||
spans = document.getElementById("layout").querySelectorAll("span");
|
||||
for(var i = 0; i < spans.length; i++) {
|
||||
spans[i].textContent = "";
|
||||
}
|
||||
document.getElementById(id).querySelector("span").textContent = "✓";
|
||||
reader.rendition.spread(id==='spread'?true:'none');
|
||||
}
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/reading/epub.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/reading/epub-progress.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -158,21 +158,41 @@
|
||||
{% if cc|length > 0 %}
|
||||
{% for c in cc %}
|
||||
<div class="form-group">
|
||||
<!--input type="number" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value=""-->
|
||||
<label for="{{ 'custom_column_' ~ c.id }}">{{ c.name }}</label>
|
||||
{% if c.datatype == 'bool' %}
|
||||
<select name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" class="form-control">
|
||||
<option value="" selected></option>
|
||||
<option value="Any" selected>{{_('Any')}}</option>
|
||||
<option value="">{{_('Empty')}}</option>
|
||||
<option value="True" >{{_('Yes')}}</option>
|
||||
<option value="False" >{{_('No')}}</option>
|
||||
</select>
|
||||
{% endif %}
|
||||
|
||||
{% if c.datatype == 'int' %}
|
||||
<input type="number" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value="">
|
||||
<div class="row">
|
||||
<div class="form-group col-sm-6">
|
||||
<label for="{{ 'custom_column_' ~ c.id }}_low">{{_('From:')}}</label>
|
||||
<input type="number" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}_low" id="{{ 'custom_column_' ~ c.id }}_low" value="">
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label for="{{ 'custom_column_' ~ c.id }}_high">{{_('To:')}}</label>
|
||||
<input type="number" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}_high" id="{{ 'custom_column_' ~ c.id }}_high" value="">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if c.datatype == 'float' %}
|
||||
<input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value="">
|
||||
<div class="row">
|
||||
<div class="form-group col-sm-6">
|
||||
<label for="{{ 'custom_column_' ~ c.id }}_low">{{_('From:')}}</label>
|
||||
<input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}_low" id="{{ 'custom_column_' ~ c.id }}_low" value="">
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label for="{{ 'custom_column_' ~ c.id }}_high">{{_('To:')}}</label>
|
||||
<input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}_high" id="{{ 'custom_column_' ~ c.id }}_high" value="">
|
||||
</div>
|
||||
</div>
|
||||
<!--input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value=""-->
|
||||
{% endif %}
|
||||
|
||||
{% if c.datatype == 'datetime' %}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Run Time')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="starttime" data-sortable="true" data-sort-name="id">{{_('Start Time')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="error" data-sortable="true">{{_('Message')}}</th>
|
||||
{% if current_user.role_admin() %}
|
||||
<th data-halign="right" data-align="right" data-formatter="TaskActions" data-switchable="false">{{_('Actions')}}</th>
|
||||
{% endif %}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<label for="kindle_mail">{{_('Send to eReader Email')}}</label>
|
||||
<label for="kindle_mail">{{_('Send to eReader Email Address. Use comma to seperate emails for multiple eReaders')}}</label>
|
||||
<input type="email" class="form-control" name="kindle_mail" id="kindle_mail" value="{{ content.kindle_mail if content.kindle_mail != None }}">
|
||||
</div>
|
||||
{% if not content.role_anonymous() %}
|
||||
|
||||
@@ -22,6 +22,7 @@ import tornado
|
||||
from tornado import escape
|
||||
from tornado import httputil
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.log import access_log
|
||||
|
||||
from typing import List, Tuple, Optional, Callable, Any, Dict, Text
|
||||
from types import TracebackType
|
||||
@@ -96,5 +97,26 @@ class MyWSGIContainer(WSGIContainer):
|
||||
except TypeError as e:
|
||||
environ = WSGIContainer.environ(request)
|
||||
environ['RAW_URI'] = request.path
|
||||
self.env = environ
|
||||
return environ
|
||||
|
||||
def _log(self, status_code: int, request: httputil.HTTPServerRequest) -> None:
|
||||
if status_code < 400:
|
||||
log_method = access_log.info
|
||||
elif status_code < 500:
|
||||
log_method = access_log.warning
|
||||
else:
|
||||
log_method = access_log.error
|
||||
request_time = 1000.0 * request.request_time()
|
||||
assert request.method is not None
|
||||
assert request.uri is not None
|
||||
ip = self.env.get("HTTP_FORWARD_FOR", None) or request.remote_ip
|
||||
summary = (
|
||||
request.method # type: ignore[operator]
|
||||
+ " "
|
||||
+ request.uri
|
||||
+ " ("
|
||||
+ ip
|
||||
+ ")"
|
||||
)
|
||||
log_method("%d %s %.2fms", status_code, summary, request_time)
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user