mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-31 15:23:02 +00:00 
			
		
		
		
	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:
		
							
								
								
									
										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): | ||||||
|   | |||||||
| @@ -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__, | ||||||
|   | |||||||
							
								
								
									
										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"> | ||||||
|   | |||||||
| @@ -7,7 +7,8 @@ | |||||||
| <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,6 +100,7 @@ | |||||||
|       </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"> | ||||||
|  |       <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||||
|       <div class="form-group required"> |       <div class="form-group required"> | ||||||
|         <label for="domainname_deny">{{_('Add Domain')}}</label> |         <label for="domainname_deny">{{_('Add Domain')}}</label> | ||||||
|         <input type="text" class="form-control" name="domainname" id="domainname_deny" > |         <input type="text" class="form-control" name="domainname" id="domainname_deny" > | ||||||
|   | |||||||
| @@ -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 = | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Ozzie Isaacs
					Ozzie Isaacs