Added handling for missing flask-wtf dependency
Added CSRF protection (via flask-wtf) Moved upload function to js file Fixed error page in case of csrf failure
This commit is contained in:
parent
5edde53fed
commit
50919d4721
2
cps.py
2
cps.py
|
@ -49,7 +49,7 @@ try:
|
||||||
from cps.kobo import kobo, get_kobo_activated
|
from cps.kobo import kobo, get_kobo_activated
|
||||||
from cps.kobo_auth import kobo_auth
|
from cps.kobo_auth import kobo_auth
|
||||||
kobo_available = get_kobo_activated()
|
kobo_available = get_kobo_activated()
|
||||||
except ImportError:
|
except (ImportError, AttributeError): # Catch also error for not installed flask-wtf (missing csrf decorator)
|
||||||
kobo_available = False
|
kobo_available = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -43,6 +43,12 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
lxml_present = False
|
lxml_present = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
|
wtf_present = True
|
||||||
|
except ImportError:
|
||||||
|
wtf_present = False
|
||||||
|
|
||||||
mimetypes.init()
|
mimetypes.init()
|
||||||
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
||||||
mimetypes.add_type('application/epub+zip', '.epub')
|
mimetypes.add_type('application/epub+zip', '.epub')
|
||||||
|
@ -75,6 +81,12 @@ lm.login_view = 'web.login'
|
||||||
lm.anonymous_user = ub.Anonymous
|
lm.anonymous_user = ub.Anonymous
|
||||||
lm.session_protection = 'strong'
|
lm.session_protection = 'strong'
|
||||||
|
|
||||||
|
if wtf_present:
|
||||||
|
csrf = CSRFProtect()
|
||||||
|
csrf.init_app(app)
|
||||||
|
else:
|
||||||
|
csrf = None
|
||||||
|
|
||||||
ub.init_db(cli.settingspath)
|
ub.init_db(cli.settingspath)
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
config = config_sql.load_configuration(ub.session)
|
config = config_sql.load_configuration(ub.session)
|
||||||
|
@ -105,6 +117,11 @@ def create_app():
|
||||||
log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
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" ***')
|
print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
||||||
sys.exit(6)
|
sys.exit(6)
|
||||||
|
if not wtf_present:
|
||||||
|
log.info('*** "flask-wtf" is needed for calibre-web to run. Please install it using pip: "pip install flask-wtf" ***')
|
||||||
|
print('*** "flask-wtf" is needed for calibre-web to run. Please install it using pip: "pip install flask-wtf" ***')
|
||||||
|
sys.exit(7)
|
||||||
|
|
||||||
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):
|
||||||
|
|
21
cps/about.py
21
cps/about.py
|
@ -29,6 +29,10 @@ from collections import OrderedDict
|
||||||
import babel, pytz, requests, sqlalchemy
|
import babel, pytz, requests, sqlalchemy
|
||||||
import werkzeug, flask, flask_login, flask_principal, jinja2
|
import werkzeug, flask, flask_login, flask_principal, jinja2
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
try:
|
||||||
|
from flask_wtf import __version__ as flaskwtf_version
|
||||||
|
except ImportError:
|
||||||
|
flaskwtf_version = _(u'not installed')
|
||||||
|
|
||||||
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
@ -75,6 +79,7 @@ _VERSIONS = OrderedDict(
|
||||||
Flask=flask.__version__,
|
Flask=flask.__version__,
|
||||||
Flask_Login=flask_loginVersion,
|
Flask_Login=flask_loginVersion,
|
||||||
Flask_Principal=flask_principal.__version__,
|
Flask_Principal=flask_principal.__version__,
|
||||||
|
Flask_WTF=flaskwtf_version,
|
||||||
Werkzeug=werkzeug.__version__,
|
Werkzeug=werkzeug.__version__,
|
||||||
Babel=babel.__version__,
|
Babel=babel.__version__,
|
||||||
Jinja2=jinja2.__version__,
|
Jinja2=jinja2.__version__,
|
||||||
|
@ -84,14 +89,14 @@ _VERSIONS = OrderedDict(
|
||||||
SQLite=sqlite3.sqlite_version,
|
SQLite=sqlite3.sqlite_version,
|
||||||
iso639=isoLanguages.__version__,
|
iso639=isoLanguages.__version__,
|
||||||
pytz=pytz.__version__,
|
pytz=pytz.__version__,
|
||||||
Unidecode = unidecode_version,
|
Unidecode=unidecode_version,
|
||||||
Scholarly = scholarly_version,
|
Scholarly=scholarly_version,
|
||||||
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None,
|
Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
|
||||||
python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
|
python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
|
||||||
Goodreads = u'installed' if bool(services.goodreads_support) else None,
|
Goodreads=u'installed' if bool(services.goodreads_support) else None,
|
||||||
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
||||||
flask_dance = flask_danceVersion,
|
flask_dance=flask_danceVersion,
|
||||||
greenlet = greenlet_Version
|
greenlet=greenlet_Version
|
||||||
)
|
)
|
||||||
_VERSIONS.update(uploader.get_versions())
|
_VERSIONS.update(uploader.get_versions())
|
||||||
|
|
||||||
|
|
10
cps/kobo.py
10
cps/kobo.py
|
@ -47,7 +47,8 @@ from sqlalchemy.exc import StatementError
|
||||||
from sqlalchemy.sql import select
|
from sqlalchemy.sql import select
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub
|
|
||||||
|
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf
|
||||||
from .constants import sqlalchemy_version2
|
from .constants import sqlalchemy_version2
|
||||||
from .helper import get_download_link
|
from .helper import get_download_link
|
||||||
from .services import SyncToken as SyncToken
|
from .services import SyncToken as SyncToken
|
||||||
|
@ -505,7 +506,7 @@ def get_metadata(book):
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||||
|
@ -595,6 +596,7 @@ def add_items_to_shelf(items, shelf):
|
||||||
return items_unknown_to_calibre
|
return items_unknown_to_calibre
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleTagAddItem(tag_id):
|
def HandleTagAddItem(tag_id):
|
||||||
|
@ -624,6 +626,7 @@ def HandleTagAddItem(tag_id):
|
||||||
return make_response('', 201)
|
return make_response('', 201)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleTagRemoveItem(tag_id):
|
def HandleTagRemoveItem(tag_id):
|
||||||
|
@ -983,6 +986,7 @@ def HandleUnimplementedRequest(dummy=None):
|
||||||
|
|
||||||
|
|
||||||
# TODO: Implement the following routes
|
# TODO: Implement the following routes
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
||||||
|
@ -993,6 +997,7 @@ def HandleUserRequest(dummy=None):
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
||||||
|
@ -1026,6 +1031,7 @@ def make_calibre_web_auth_response():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/auth/device", methods=["POST"])
|
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleAuthRequest():
|
def HandleAuthRequest():
|
||||||
|
|
|
@ -23,7 +23,6 @@ if ($(".tiny_editor").length) {
|
||||||
|
|
||||||
$(".datepicker").datepicker({
|
$(".datepicker").datepicker({
|
||||||
format: "yyyy-mm-dd",
|
format: "yyyy-mm-dd",
|
||||||
language: language
|
|
||||||
}).on("change", function () {
|
}).on("change", function () {
|
||||||
// Show localized date over top of the standard YYYY-MM-DD date
|
// Show localized date over top of the standard YYYY-MM-DD date
|
||||||
var pubDate;
|
var pubDate;
|
||||||
|
|
|
@ -112,6 +112,14 @@ $("#btn-upload").change(function() {
|
||||||
$("#form-upload").submit();
|
$("#form-upload").submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#form-upload").uploadprogress({
|
||||||
|
redirect_url: getPath() + "/", //"{{ url_for('web.index')}}",
|
||||||
|
uploadedMsg: $("#form-upload").data("message"), //"{{_('Upload done, processing, please wait...')}}",
|
||||||
|
modalTitle: $("#form-upload").data("title"), //"{{_('Uploading...')}}",
|
||||||
|
modalFooter: $("#form-upload").data("footer"), //"{{_('Close')}}",
|
||||||
|
modalTitleFailed: $("#form-upload").data("failed") //"{{_('Error')}}"
|
||||||
|
});
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
var inp = $('#query').first()
|
var inp = $('#query').first()
|
||||||
if (inp.length) {
|
if (inp.length) {
|
||||||
|
@ -223,6 +231,16 @@ $(function() {
|
||||||
var preFilters = $.Callbacks();
|
var preFilters = $.Callbacks();
|
||||||
$.ajaxPrefilter(preFilters.fire);
|
$.ajaxPrefilter(preFilters.fire);
|
||||||
|
|
||||||
|
// equip all post requests with csrf_token
|
||||||
|
var csrftoken = $("input[name='csrf_token']").val();
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function(xhr, settings) {
|
||||||
|
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrftoken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function restartTimer() {
|
function restartTimer() {
|
||||||
$("#spinner").addClass("hidden");
|
$("#spinner").addClass("hidden");
|
||||||
$("#RestartDialog").modal("hide");
|
$("#RestartDialog").modal("hide");
|
||||||
|
@ -576,7 +594,7 @@ $(function() {
|
||||||
method:"post",
|
method:"post",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/simulatedbchange",
|
url: window.location.pathname + "/../../ajax/simulatedbchange",
|
||||||
data: {config_calibre_dir: $("#config_calibre_dir").val()},
|
data: {config_calibre_dir: $("#config_calibre_dir").val(), csrf_token: $("input[name='csrf_token']").val()},
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
if ( data.change ) {
|
if ( data.change ) {
|
||||||
if ( data.valid ) {
|
if ( data.valid ) {
|
||||||
|
@ -712,7 +730,7 @@ $(function() {
|
||||||
method:"post",
|
method:"post",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../ajax/view",
|
url: getPath() + "/ajax/view",
|
||||||
data: "{\"series\": {\"series_view\": \""+ view +"\"}}",
|
data: "{\"series\": {\"series_view\": \""+ view +"\"}}",
|
||||||
success: function success() {
|
success: function success() {
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
{% if source_formats|length > 0 and conversion_formats|length > 0 %}
|
{% if source_formats|length > 0 and conversion_formats|length > 0 %}
|
||||||
<div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4>
|
<div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4>
|
||||||
<form class="padded-bottom" action="{{ url_for('editbook.convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm">
|
<form class="padded-bottom" action="{{ url_for('editbook.convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
<label class="control-label" for="book_format_from">{{_('Convert from:')}}</label>
|
<label class="control-label" for="book_format_from">{{_('Convert from:')}}</label>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2 class="{{page}}">{{_(title)}}</h2>
|
<h2 class="{{page}}">{{_(title)}}</h2>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="col-xs-12 col-sm-6">
|
<div class="col-xs-12 col-sm-6">
|
||||||
<div class="row form-group">
|
<div class="row form-group">
|
||||||
<div class="btn btn-default disabled" id="merge_books" aria-disabled="true">{{_('Merge selected books')}}</div>
|
<div class="btn btn-default disabled" id="merge_books" aria-disabled="true">{{_('Merge selected books')}}</div>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h2>{{title}}</h2>
|
<h2>{{title}}</h2>
|
||||||
<form role="form" method="POST" class="col-md-10 col-lg-6" action="{{ url_for('admin.db_configuration') }}" autocomplete="off">
|
<form role="form" method="POST" class="col-md-10 col-lg-6" action="{{ url_for('admin.db_configuration') }}" autocomplete="off">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
|
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
|
||||||
<div class="form-group required input-group">
|
<div class="form-group required input-group">
|
||||||
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h2>{{title}}</h2>
|
<h2>{{title}}</h2>
|
||||||
<form role="form" method="POST" autocomplete="off">
|
<form role="form" method="POST" autocomplete="off">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="panel-group col-md-10 col-lg-8">
|
<div class="panel-group col-md-10 col-lg-8">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
|
|
@ -6,8 +6,9 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h2>{{title}}</h2>
|
<h2>{{title}}</h2>
|
||||||
<form role="form" method="POST" autocomplete="off" >
|
<form role="form" method="POST" autocomplete="off" >
|
||||||
<div class="panel-group class="col-md-10 col-lg-6">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="panel-group" class="col-md-10 col-lg-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
|
|
|
@ -214,6 +214,7 @@
|
||||||
<div class="custom_columns">
|
<div class="custom_columns">
|
||||||
<p>
|
<p>
|
||||||
<form id="have_read_form" action="{{ url_for('web.toggle_read', book_id=entry.id)}}" method="POST">
|
<form id="have_read_form" action="{{ url_for('web.toggle_read', book_id=entry.id)}}" method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<label class="block-label">
|
<label class="block-label">
|
||||||
<input id="have_read_cb" data-checked="{{_('Mark As Unread')}}" data-unchecked="{{_('Mark As Read')}}" type="checkbox" {% if have_read %}checked{% endif %} >
|
<input id="have_read_cb" data-checked="{{_('Mark As Unread')}}" data-unchecked="{{_('Mark As Read')}}" type="checkbox" {% if have_read %}checked{% endif %} >
|
||||||
<span>{{_('Read')}}</span>
|
<span>{{_('Read')}}</span>
|
||||||
|
@ -223,6 +224,7 @@
|
||||||
{% if g.user.check_visibility(32768) %}
|
{% if g.user.check_visibility(32768) %}
|
||||||
<p>
|
<p>
|
||||||
<form id="archived_form" action="{{ url_for('web.toggle_archived', book_id=entry.id)}}" method="POST">
|
<form id="archived_form" action="{{ url_for('web.toggle_archived', book_id=entry.id)}}" method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<label class="block-label">
|
<label class="block-label">
|
||||||
<input id="archived_cb" data-checked="{{_('Restore from archive')}}" data-unchecked="{{_('Add to archive')}}" type="checkbox" {% if is_archived %}checked{% endif %} >
|
<input id="archived_cb" data-checked="{{_('Restore from archive')}}" data-unchecked="{{_('Add to archive')}}" type="checkbox" {% if is_archived %}checked{% endif %} >
|
||||||
<span>{{_('Archived')}}</span>
|
<span>{{_('Archived')}}</span>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h1>{{title}}</h1>
|
<h1>{{title}}</h1>
|
||||||
<form role="form" class="col-md-10 col-lg-6" method="POST">
|
<form role="form" class="col-md-10 col-lg-6" method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
{% if feature_support['gmail'] %}
|
{% if feature_support['gmail'] %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="config_email_type">{{_('Choose Server Type')}}</label>
|
<label for="config_email_type">{{_('Choose Server Type')}}</label>
|
||||||
|
@ -72,6 +73,7 @@
|
||||||
<div class="col-md-10 col-lg-6">
|
<div class="col-md-10 col-lg-6">
|
||||||
<h2>{{_('Allowed Domains (Whitelist)')}}</h2>
|
<h2>{{_('Allowed Domains (Whitelist)')}}</h2>
|
||||||
<form id="domain_add_allow" action="{{ url_for('admin.add_domain',allow=1)}}" method="POST">
|
<form id="domain_add_allow" action="{{ url_for('admin.add_domain',allow=1)}}" method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="form-group required">
|
<div class="form-group required">
|
||||||
<label for="domainname_allow">{{_('Add Domain')}}</label>
|
<label for="domainname_allow">{{_('Add Domain')}}</label>
|
||||||
<input type="text" class="form-control" name="domainname" id="domainname_allow" >
|
<input type="text" class="form-control" name="domainname" id="domainname_allow" >
|
||||||
|
@ -98,11 +100,12 @@
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
<form id="domain_add_deny" action="{{ url_for('admin.add_domain',allow=0)}}" method="POST">
|
<form id="domain_add_deny" action="{{ url_for('admin.add_domain',allow=0)}}" method="POST">
|
||||||
<div class="form-group required">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<label for="domainname_deny">{{_('Add Domain')}}</label>
|
<div class="form-group required">
|
||||||
<input type="text" class="form-control" name="domainname" id="domainname_deny" >
|
<label for="domainname_deny">{{_('Add Domain')}}</label>
|
||||||
</div>
|
<input type="text" class="form-control" name="domainname" id="domainname_deny" >
|
||||||
<button id="domain_deny_submit" class="btn btn-default">{{_('Add')}}</button>
|
</div>
|
||||||
|
<button id="domain_deny_submit" class="btn btn-default">{{_('Add')}}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html class="http-error" lang="{{ g.user.locale }}">
|
<html class="http-error">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ instance }} | HTTP Error ({{ error_code }})</title>
|
<title>{{ instance }} | HTTP Error ({{ error_code }})</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
{% if g.user.role_upload() or g.user.role_admin()%}
|
{% if g.user.role_upload() or g.user.role_admin()%}
|
||||||
{% if g.allow_upload %}
|
{% if g.allow_upload %}
|
||||||
<li>
|
<li>
|
||||||
<form id="form-upload" class="navbar-form" action="{{ url_for('editbook.upload') }}" method="post" enctype="multipart/form-data">
|
<form id="form-upload" class="navbar-form" action="{{ url_for('editbook.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"
|
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"
|
||||||
type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple></span>
|
type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple></span>
|
||||||
|
@ -200,17 +200,6 @@
|
||||||
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/jquery.form.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/jquery.form.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/uploadprogress.js') }}"> </script>
|
<script src="{{ url_for('static', filename='js/uploadprogress.js') }}"> </script>
|
||||||
<script type="text/javascript">
|
|
||||||
$(function() {
|
|
||||||
$("#form-upload").uploadprogress({
|
|
||||||
redirect_url: "{{ url_for('web.index')}}",
|
|
||||||
uploadedMsg: "{{_('Upload done, processing, please wait...')}}",
|
|
||||||
modalTitle: "{{_('Uploading...')}}",
|
|
||||||
modalFooter: "{{_('Close')}}",
|
|
||||||
modalTitleFailed: "{{_('Error')}}"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
{% if g.current_theme == 1 %}
|
{% if g.current_theme == 1 %}
|
||||||
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if data == "series" %}
|
{% if data == "series" %}
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button class="update-view btn btn-primary" data-target="series_view" id="grid-button" data-view="grid">Grid</button>
|
<button class="update-view btn btn-primary" data-target="series_view" id="grid-button" data-view="grid">Grid</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<h2 style="margin-top: 0">{{_('Login')}}</h2>
|
<h2 style="margin-top: 0">{{_('Login')}}</h2>
|
||||||
<form method="POST" role="form">
|
<form method="POST" role="form">
|
||||||
<input type="hidden" name="next" value="{{next_url}}">
|
<input type="hidden" name="next" value="{{next_url}}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">{{_('Username')}}</label>
|
<label for="username">{{_('Username')}}</label>
|
||||||
<input type="text" class="form-control" id="username" name="username" placeholder="{{_('Username')}}">
|
<input type="text" class="form-control" id="username" name="username" placeholder="{{_('Username')}}">
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<div class="well col-sm-6 col-sm-offset-2">
|
<div class="well col-sm-6 col-sm-offset-2">
|
||||||
<h2 style="margin-top: 0">{{_('Register New Account')}}</h2>
|
<h2 style="margin-top: 0">{{_('Register New Account')}}</h2>
|
||||||
<form method="POST" role="form">
|
<form method="POST" role="form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
{% if not config.config_register_email %}
|
{% if not config.config_register_email %}
|
||||||
<div class="form-group required">
|
<div class="form-group required">
|
||||||
<label for="name">{{_('Username')}}</label>
|
<label for="name">{{_('Username')}}</label>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<h1 class="{{page}}">{{title}}</h1>
|
<h1 class="{{page}}">{{title}}</h1>
|
||||||
<div class="col-md-10 col-lg-6">
|
<div class="col-md-10 col-lg-6">
|
||||||
<form role="form" id="search" action="{{ url_for('web.advanced_search_form') }}" method="POST">
|
<form role="form" id="search" action="{{ url_for('web.advanced_search_form') }}" method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="book_title">{{_('Book Title')}}</label>
|
<label for="book_title">{{_('Book Title')}}</label>
|
||||||
<input type="text" class="form-control" name="book_title" id="book_title" value="">
|
<input type="text" class="form-control" name="book_title" id="book_title" value="">
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h1>{{title}}</h1>
|
<h1>{{title}}</h1>
|
||||||
<form role="form" method="POST">
|
<form role="form" method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="title">{{_('Title')}}</label>
|
<label for="title">{{_('Title')}}</label>
|
||||||
<input type="text" class="form-control" name="title" id="title" value="{{ shelf.name if shelf.name != None }}">
|
<input type="text" class="form-control" name="title" id="title" value="{{ shelf.name if shelf.name != None }}">
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h1>{{title}}</h1>
|
<h1>{{title}}</h1>
|
||||||
<form role="form" method="POST" autocomplete="off">
|
<form role="form" method="POST" autocomplete="off">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="col-md-10 col-lg-8">
|
<div class="col-md-10 col-lg-8">
|
||||||
{% if new_user or ( g.user and content.name != "Guest" and g.user.role_admin() ) %}
|
{% if new_user or ( g.user and content.name != "Guest" and g.user.role_admin() ) %}
|
||||||
<div class="form-group required">
|
<div class="form-group required">
|
||||||
|
|
|
@ -118,6 +118,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2 class="{{page}}">{{_(title)}}</h2>
|
<h2 class="{{page}}">{{_(title)}}</h2>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="col-xs-12 col-sm-12">
|
<div class="col-xs-12 col-sm-12">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="btn btn-default disabled" id="user_delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
|
<div class="btn btn-default disabled" id="user_delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
|
||||||
|
|
|
@ -84,14 +84,13 @@ except ImportError:
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def add_security_headers(resp):
|
def add_security_headers(resp):
|
||||||
resp.headers['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval';"
|
resp.headers['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:"
|
||||||
if request.endpoint == "editbook.edit_book":
|
if request.endpoint == "editbook.edit_book":
|
||||||
resp.headers['Content-Security-Policy'] += "img-src * data:"
|
resp.headers['Content-Security-Policy'] += " *"
|
||||||
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'
|
||||||
resp.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
resp.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
||||||
# log.debug(request.full_path)
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
web = Blueprint('web', __name__)
|
web = Blueprint('web', __name__)
|
||||||
|
|
|
@ -13,3 +13,4 @@ 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
|
lxml>=3.8.0,<4.7.0
|
||||||
|
flask-wtf>=0.15.0,<0.16.0
|
||||||
|
|
|
@ -18,9 +18,11 @@ classifiers =
|
||||||
Development Status :: 5 - Production/Stable
|
Development Status :: 5 - Production/Stable
|
||||||
License :: OSI Approved :: GNU Affero General Public License v3
|
License :: OSI Approved :: GNU Affero General Public License v3
|
||||||
Programming Language :: Python :: 3
|
Programming Language :: Python :: 3
|
||||||
Programming Language :: Python :: 3.5
|
|
||||||
Programming Language :: Python :: 3.6
|
Programming Language :: Python :: 3.6
|
||||||
Programming Language :: Python :: 3.7
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
|
Programming Language :: Python :: 3.10
|
||||||
Operating System :: OS Independent
|
Operating System :: OS Independent
|
||||||
keywords =
|
keywords =
|
||||||
calibre
|
calibre
|
||||||
|
@ -49,6 +51,7 @@ install_requires =
|
||||||
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
|
lxml>=3.8.0,<4.7.0
|
||||||
|
flask-wtf>=0.15.0,<0.16.0
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
gdrive =
|
gdrive =
|
||||||
|
|
Loading…
Reference in New Issue
Block a user