mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-25 02:57:22 +00:00
Merge branch 'master' of https://github.com/janeczku/calibre-web
This commit is contained in:
commit
6a2868c68d
1
cps.py
1
cps.py
@ -70,7 +70,6 @@ def main():
|
|||||||
app.register_blueprint(shelf)
|
app.register_blueprint(shelf)
|
||||||
app.register_blueprint(admi)
|
app.register_blueprint(admi)
|
||||||
app.register_blueprint(remotelogin)
|
app.register_blueprint(remotelogin)
|
||||||
# if config.config_use_google_drive:
|
|
||||||
app.register_blueprint(gdrive)
|
app.register_blueprint(gdrive)
|
||||||
app.register_blueprint(editbook)
|
app.register_blueprint(editbook)
|
||||||
if kobo_available:
|
if kobo_available:
|
||||||
|
@ -37,6 +37,11 @@ from . import config_sql, logger, cache_buster, cli, ub, db
|
|||||||
from .reverseproxy import ReverseProxied
|
from .reverseproxy import ReverseProxied
|
||||||
from .server import WebServer
|
from .server import WebServer
|
||||||
|
|
||||||
|
try:
|
||||||
|
import lxml
|
||||||
|
lxml_present = True
|
||||||
|
except ImportError:
|
||||||
|
lxml_present = False
|
||||||
|
|
||||||
mimetypes.init()
|
mimetypes.init()
|
||||||
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
||||||
@ -90,6 +95,16 @@ db.CalibreDB.setup_db(config.config_calibre_dir, cli.settingspath)
|
|||||||
calibre_db = db.CalibreDB()
|
calibre_db = db.CalibreDB()
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
log.info(
|
||||||
|
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
||||||
|
print(
|
||||||
|
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
||||||
|
sys.exit(5)
|
||||||
|
if not lxml_present:
|
||||||
|
log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
||||||
|
print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
||||||
|
sys.exit(6)
|
||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
# For python2 convert path to unicode
|
# For python2 convert path to unicode
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
@ -99,12 +114,8 @@ def create_app():
|
|||||||
|
|
||||||
if os.environ.get('FLASK_DEBUG'):
|
if os.environ.get('FLASK_DEBUG'):
|
||||||
cache_buster.init_cache_busting(app)
|
cache_buster.init_cache_busting(app)
|
||||||
|
|
||||||
log.info('Starting Calibre Web...')
|
log.info('Starting Calibre Web...')
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
log.info('*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
|
||||||
print('*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
|
||||||
sys.exit(5)
|
|
||||||
Principal(app)
|
Principal(app)
|
||||||
lm.init_app(app)
|
lm.init_app(app)
|
||||||
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
||||||
|
@ -26,16 +26,19 @@ from datetime import datetime
|
|||||||
import json
|
import json
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
# Improve this to check if scholarly is available in a global way, like other pythonic libraries
|
|
||||||
have_scholar = True
|
|
||||||
try:
|
try:
|
||||||
from scholarly import scholarly
|
from lxml.html.clean import clean_html
|
||||||
except ImportError:
|
except ImportError:
|
||||||
have_scholar = False
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Improve this to check if scholarly is available in a global way, like other pythonic libraries
|
||||||
|
try:
|
||||||
|
from scholarly import scholarly
|
||||||
|
have_scholar = True
|
||||||
|
except ImportError:
|
||||||
|
have_scholar = False
|
||||||
|
|
||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
||||||
@ -57,6 +60,8 @@ except ImportError:
|
|||||||
pass # We're not using Python 3
|
pass # We're not using Python 3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
editbook = Blueprint('editbook', __name__)
|
editbook = Blueprint('editbook', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@ -459,9 +464,11 @@ def edit_book_series_index(series_index, book):
|
|||||||
# Handle book comments/description
|
# Handle book comments/description
|
||||||
def edit_book_comments(comments, book):
|
def edit_book_comments(comments, book):
|
||||||
modif_date = False
|
modif_date = False
|
||||||
|
if comments:
|
||||||
|
comments = clean_html(comments)
|
||||||
if len(book.comments):
|
if len(book.comments):
|
||||||
if book.comments[0].text != comments:
|
if book.comments[0].text != comments:
|
||||||
book.comments[0].text = comments
|
book.comments[0].text = clean_html(comments)
|
||||||
modif_date = True
|
modif_date = True
|
||||||
else:
|
else:
|
||||||
if comments:
|
if comments:
|
||||||
@ -515,6 +522,8 @@ def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string):
|
|||||||
to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0
|
to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0
|
||||||
elif c.datatype == 'comments':
|
elif c.datatype == 'comments':
|
||||||
to_save[cc_string] = Markup(to_save[cc_string]).unescape()
|
to_save[cc_string] = Markup(to_save[cc_string]).unescape()
|
||||||
|
if to_save[cc_string]:
|
||||||
|
to_save[cc_string] = clean_html(to_save[cc_string])
|
||||||
elif c.datatype == 'datetime':
|
elif c.datatype == 'datetime':
|
||||||
try:
|
try:
|
||||||
to_save[cc_string] = datetime.strptime(to_save[cc_string], "%Y-%m-%d")
|
to_save[cc_string] = datetime.strptime(to_save[cc_string], "%Y-%m-%d")
|
||||||
|
@ -38,6 +38,7 @@ from flask_login import current_user
|
|||||||
from sqlalchemy.sql.expression import true, false, and_, text, func
|
from sqlalchemy.sql.expression import true, false, and_, text, func
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
from markupsafe import escape
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@ -97,10 +98,11 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
|||||||
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
||||||
else:
|
else:
|
||||||
settings = dict()
|
settings = dict()
|
||||||
txt = (u"%s -> %s: %s" % (
|
link = '<a href="{}">{}</a>"'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss
|
||||||
|
txt = u"{} -> {}: {}".format(
|
||||||
old_book_format,
|
old_book_format,
|
||||||
new_book_format,
|
new_book_format,
|
||||||
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + book.title + "</a>"))
|
link)
|
||||||
settings['old_book_format'] = old_book_format
|
settings['old_book_format'] = old_book_format
|
||||||
settings['new_book_format'] = new_book_format
|
settings['new_book_format'] = new_book_format
|
||||||
WorkerThread.add(user_id, TaskConvert(file_path, book.id, txt, settings, kindle_mail, user_id))
|
WorkerThread.add(user_id, TaskConvert(file_path, book.id, txt, settings, kindle_mail, user_id))
|
||||||
@ -778,7 +780,7 @@ def render_task_status(tasklist):
|
|||||||
|
|
||||||
ret['taskMessage'] = "{}: {}".format(_(task.name), task.message)
|
ret['taskMessage'] = "{}: {}".format(_(task.name), task.message)
|
||||||
ret['progress'] = "{} %".format(int(task.progress * 100))
|
ret['progress'] = "{} %".format(int(task.progress * 100))
|
||||||
ret['user'] = user
|
ret['user'] = escape(user) # prevent xss
|
||||||
renderedtasklist.append(ret)
|
renderedtasklist.append(ret)
|
||||||
|
|
||||||
return renderedtasklist
|
return renderedtasklist
|
||||||
|
@ -31,7 +31,7 @@ from babel.dates import format_date
|
|||||||
from flask import Blueprint, request, url_for
|
from flask import Blueprint, request, url_for
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
from markupsafe import escape
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
|
|
||||||
@ -129,6 +129,10 @@ def formatseriesindex_filter(series_index):
|
|||||||
return series_index
|
return series_index
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@jinjia.app_template_filter('escapedlink')
|
||||||
|
def escapedlink_filter(url, text):
|
||||||
|
return "<a href='{}'>{}</a>".format(url, escape(text))
|
||||||
|
|
||||||
@jinjia.app_template_filter('uuidfilter')
|
@jinjia.app_template_filter('uuidfilter')
|
||||||
def uuidfilter(var):
|
def uuidfilter(var):
|
||||||
return uuid4()
|
return uuid4()
|
||||||
|
File diff suppressed because one or more lines are too long
@ -20,6 +20,15 @@ function getPath() {
|
|||||||
var jsFileLocation = $("script[src*=jquery]").attr("src"); // the js file path
|
var jsFileLocation = $("script[src*=jquery]").attr("src"); // the js file path
|
||||||
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path
|
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function elementSorter(a, b) {
|
||||||
|
a = +a.slice(0, -2);
|
||||||
|
b = +b.slice(0, -2);
|
||||||
|
if (a > b) return 1;
|
||||||
|
if (a < b) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Generic control/related handler to show/hide fields based on a checkbox' value
|
// Generic control/related handler to show/hide fields based on a checkbox' value
|
||||||
// e.g.
|
// e.g.
|
||||||
// <input type="checkbox" data-control="stuff-to-show">
|
// <input type="checkbox" data-control="stuff-to-show">
|
||||||
@ -714,3 +723,4 @@ $(function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,6 +22,26 @@ var selections = [];
|
|||||||
var reload = false;
|
var reload = false;
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
|
$('#tasktable').bootstrapTable({
|
||||||
|
formatNoMatches: function () {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
striped: true
|
||||||
|
});
|
||||||
|
if ($('#tasktable').length) {
|
||||||
|
setInterval(function () {
|
||||||
|
$.ajax({
|
||||||
|
method: "get",
|
||||||
|
url: getPath() + "/ajax/emailstat",
|
||||||
|
async: true,
|
||||||
|
timeout: 900,
|
||||||
|
success: function (data) {
|
||||||
|
$('#table').bootstrapTable("load", data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
$("#books-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table",
|
$("#books-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table",
|
||||||
function (e, rowsAfter, rowsBefore) {
|
function (e, rowsAfter, rowsBefore) {
|
||||||
var rows = rowsAfter;
|
var rows = rowsAfter;
|
||||||
|
@ -120,9 +120,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if entry.series|length > 0 %}
|
{% if entry.series|length > 0 %}
|
||||||
<p>{{_("Book %(index)s of %(range)s", index=entry.series_index|formatfloat(2), range=("<a href='" + url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id) + "'>" + entry.series[0].name + "</a>")|safe) }}</p>
|
<p>{{_("Book %(index)s of %(range)s", index=entry.series_index | formatfloat(2), range=(url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id)|escapedlink(entry.series[0].name))|safe)}}</p>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="discover load-more">
|
<div class="discover load-more">
|
||||||
<h2 class="{{title}}">{{_(title)}}</h2>
|
<h2 class="{{title}}">{{title}}</h2>
|
||||||
<div class="filterheader hidden-xs hidden-sm">
|
<div class="filterheader hidden-xs hidden-sm">
|
||||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h2>{{_('Tasks')}}</h2>
|
<h2>{{_('Tasks')}}</h2>
|
||||||
<table class="table table-no-bordered" id="table" data-url="{{ url_for('web.get_email_status_json') }}" data-sort-name="starttime" data-sort-order="asc">
|
<table class="table table-no-bordered" id="tasktable" data-url="{{ url_for('web.get_email_status_json') }}" data-sort-name="starttime" data-sort-order="asc">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% if g.user.role_admin() %}
|
{% if g.user.role_admin() %}
|
||||||
@ -27,32 +27,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
||||||
<script>
|
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
|
||||||
|
<script>
|
||||||
// ToDo: Move to js file
|
// ToDo: Move to js file
|
||||||
$('#table').bootstrapTable({
|
</script>
|
||||||
formatNoMatches: function () {
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
striped: true
|
|
||||||
});
|
|
||||||
setInterval(function() {
|
|
||||||
$.ajax({
|
|
||||||
method:"get",
|
|
||||||
url: "{{ url_for('web.get_email_status_json')}}",
|
|
||||||
async: true,
|
|
||||||
timeout: 900,
|
|
||||||
success:function(data){
|
|
||||||
$('#table').bootstrapTable("load", data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
function elementSorter(a, b) {
|
|
||||||
a = +a.slice(0, -2);
|
|
||||||
b = +b.slice(0, -2);
|
|
||||||
if (a > b) return 1;
|
|
||||||
if (a < b) return -1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -73,9 +73,9 @@ def store_user_session():
|
|||||||
user_session = User_Sessions(flask_session.get('_user_id', ""), flask_session.get('_id', ""))
|
user_session = User_Sessions(flask_session.get('_user_id', ""), flask_session.get('_id', ""))
|
||||||
session.add(user_session)
|
session.add(user_session)
|
||||||
session.commit()
|
session.commit()
|
||||||
log.info("Login and store session : " + flask_session.get('_id', ""))
|
log.debug("Login and store session : " + flask_session.get('_id', ""))
|
||||||
else:
|
else:
|
||||||
log.info("Found stored session : " + flask_session.get('_id', ""))
|
log.debug("Found stored session: " + flask_session.get('_id', ""))
|
||||||
except (exc.OperationalError, exc.InvalidRequestError) as e:
|
except (exc.OperationalError, exc.InvalidRequestError) as e:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
@ -84,7 +84,7 @@ def store_user_session():
|
|||||||
|
|
||||||
def delete_user_session(user_id, session_key):
|
def delete_user_session(user_id, session_key):
|
||||||
try:
|
try:
|
||||||
log.info("Deleted session_key : " + session_key)
|
log.debug("Deleted session_key: " + session_key)
|
||||||
session.query(User_Sessions).filter(User_Sessions.user_id==user_id,
|
session.query(User_Sessions).filter(User_Sessions.user_id==user_id,
|
||||||
User_Sessions.session_key==session_key).delete()
|
User_Sessions.session_key==session_key).delete()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
@ -84,7 +84,7 @@ except ImportError:
|
|||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def add_security_headers(resp):
|
def add_security_headers(resp):
|
||||||
# resp.headers['Content-Security-Policy']= "script-src 'self' https://www.googleapis.com https://api.douban.com https://comicvine.gamespot.com;"
|
# resp.headers['Content-Security-Policy']= "script-src 'self'" https://www.googleapis.com https://api.douban.com https://comicvine.gamespot.com;"
|
||||||
resp.headers['X-Content-Type-Options'] = 'nosniff'
|
resp.headers['X-Content-Type-Options'] = 'nosniff'
|
||||||
resp.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
resp.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||||
resp.headers['X-XSS-Protection'] = '1; mode=block'
|
resp.headers['X-XSS-Protection'] = '1; mode=block'
|
||||||
|
@ -30,7 +30,6 @@ Flask-Dance>=2.0.0,<5.1.0
|
|||||||
SQLAlchemy-Utils>=0.33.5,<0.38.0
|
SQLAlchemy-Utils>=0.33.5,<0.38.0
|
||||||
|
|
||||||
# extracting metadata
|
# extracting metadata
|
||||||
lxml>=3.8.0,<4.7.0
|
|
||||||
rarfile>=2.7
|
rarfile>=2.7
|
||||||
scholarly>=1.2.0, <1.3
|
scholarly>=1.2.0, <1.3
|
||||||
|
|
||||||
|
@ -12,3 +12,4 @@ SQLAlchemy>=1.3.0,<1.5.0
|
|||||||
tornado>=4.1,<6.2
|
tornado>=4.1,<6.2
|
||||||
Wand>=0.4.4,<0.7.0
|
Wand>=0.4.4,<0.7.0
|
||||||
unidecode>=0.04.19,<1.3.0
|
unidecode>=0.04.19,<1.3.0
|
||||||
|
lxml>=3.8.0,<4.7.0
|
||||||
|
Loading…
Reference in New Issue
Block a user