diff --git a/cps.py b/cps.py index e4b1ede8..161b1655 100755 --- a/cps.py +++ b/cps.py @@ -49,7 +49,7 @@ try: from cps.kobo import kobo, get_kobo_activated from cps.kobo_auth import kobo_auth kobo_available = get_kobo_activated() -except ImportError: +except (ImportError, AttributeError): # Catch also error for not installed flask-wtf (missing csrf decorator) kobo_available = False try: diff --git a/cps/__init__.py b/cps/__init__.py index 517358c5..f1143d9d 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -43,6 +43,12 @@ try: except ImportError: lxml_present = False +try: + from flask_wtf.csrf import CSRFProtect + wtf_present = True +except ImportError: + wtf_present = False + mimetypes.init() mimetypes.add_type('application/xhtml+xml', '.xhtml') mimetypes.add_type('application/epub+zip', '.epub') @@ -75,6 +81,12 @@ lm.login_view = 'web.login' lm.anonymous_user = ub.Anonymous lm.session_protection = 'strong' +if wtf_present: + csrf = CSRFProtect() + csrf.init_app(app) +else: + csrf = None + ub.init_db(cli.settingspath) # pylint: disable=no-member 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" ***') print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***') 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) # For python2 convert path to unicode if sys.version_info < (3, 0): diff --git a/cps/about.py b/cps/about.py index 66c0ef40..31a16552 100644 --- a/cps/about.py +++ b/cps/about.py @@ -29,6 +29,10 @@ from collections import OrderedDict import babel, pytz, requests, sqlalchemy import werkzeug, flask, flask_login, flask_principal, jinja2 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 .render_template import render_title_template @@ -75,6 +79,7 @@ _VERSIONS = OrderedDict( Flask=flask.__version__, Flask_Login=flask_loginVersion, Flask_Principal=flask_principal.__version__, + Flask_WTF=flaskwtf_version, Werkzeug=werkzeug.__version__, Babel=babel.__version__, Jinja2=jinja2.__version__, @@ -84,14 +89,14 @@ _VERSIONS = OrderedDict( SQLite=sqlite3.sqlite_version, iso639=isoLanguages.__version__, pytz=pytz.__version__, - Unidecode = unidecode_version, - Scholarly = scholarly_version, - Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None, - python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None, - Goodreads = u'installed' if bool(services.goodreads_support) else None, - jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None, - flask_dance = flask_danceVersion, - greenlet = greenlet_Version + Unidecode=unidecode_version, + Scholarly=scholarly_version, + Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None, + python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None, + Goodreads=u'installed' if bool(services.goodreads_support) else None, + jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None, + flask_dance=flask_danceVersion, + greenlet=greenlet_Version ) _VERSIONS.update(uploader.get_versions()) diff --git a/cps/kobo.py b/cps/kobo.py index 7eee3e17..ba9a8d1f 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -47,7 +47,8 @@ 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 + +from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf from .constants import sqlalchemy_version2 from .helper import get_download_link from .services import SyncToken as SyncToken @@ -505,7 +506,7 @@ def get_metadata(book): return metadata - +@csrf.exempt @kobo.route("/v1/library/tags", methods=["POST", "DELETE"]) @requires_kobo_auth # 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 +@csrf.exempt @kobo.route("/v1/library/tags//items", methods=["POST"]) @requires_kobo_auth def HandleTagAddItem(tag_id): @@ -624,6 +626,7 @@ def HandleTagAddItem(tag_id): return make_response('', 201) +@csrf.exempt @kobo.route("/v1/library/tags//items/delete", methods=["POST"]) @requires_kobo_auth def HandleTagRemoveItem(tag_id): @@ -983,6 +986,7 @@ def HandleUnimplementedRequest(dummy=None): # TODO: Implement the following routes +@csrf.exempt @kobo.route("/v1/user/loyalty/", methods=["GET", "POST"]) @kobo.route("/v1/user/profile", methods=["GET", "POST"]) @kobo.route("/v1/user/wishlist", methods=["GET", "POST"]) @@ -993,6 +997,7 @@ def HandleUserRequest(dummy=None): return redirect_or_proxy_request() +@csrf.exempt @kobo.route("/v1/products//prices", methods=["GET", "POST"]) @kobo.route("/v1/products//recommendations", methods=["GET", "POST"]) @kobo.route("/v1/products//nextread", methods=["GET", "POST"]) @@ -1026,6 +1031,7 @@ def make_calibre_web_auth_response(): ) +@csrf.exempt @kobo.route("/v1/auth/device", methods=["POST"]) @requires_kobo_auth def HandleAuthRequest(): diff --git a/cps/static/js/edit_books.js b/cps/static/js/edit_books.js index 00c971d3..66dd4eea 100644 --- a/cps/static/js/edit_books.js +++ b/cps/static/js/edit_books.js @@ -23,7 +23,6 @@ if ($(".tiny_editor").length) { $(".datepicker").datepicker({ format: "yyyy-mm-dd", - language: language }).on("change", function () { // Show localized date over top of the standard YYYY-MM-DD date var pubDate; diff --git a/cps/static/js/main.js b/cps/static/js/main.js index ea068423..2ffaaa87 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -112,6 +112,14 @@ $("#btn-upload").change(function() { $("#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() { var inp = $('#query').first() if (inp.length) { @@ -223,6 +231,16 @@ $(function() { var preFilters = $.Callbacks(); $.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() { $("#spinner").addClass("hidden"); $("#RestartDialog").modal("hide"); @@ -576,7 +594,7 @@ $(function() { method:"post", dataType: "json", 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) { if ( data.change ) { if ( data.valid ) { @@ -712,7 +730,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/view", + url: getPath() + "/ajax/view", data: "{\"series\": {\"series_view\": \""+ view +"\"}}", success: function success() { location.reload(); diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 04eb2c02..a47f400a 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -23,6 +23,7 @@ {% if source_formats|length > 0 and conversion_formats|length > 0 %}

{{_('Convert book format:')}}

+
diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index bbcd0ed6..d7cee112 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -20,6 +20,7 @@ {% endblock %} {% block body %}

{{_(title)}}

+
{{_('Merge selected books')}}
diff --git a/cps/templates/config_db.html b/cps/templates/config_db.html index c5027abf..0090bd95 100644 --- a/cps/templates/config_db.html +++ b/cps/templates/config_db.html @@ -8,6 +8,7 @@

{{title}}

+
diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index e062fae5..8cd0034e 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -8,6 +8,7 @@

{{title}}

+
diff --git a/cps/templates/config_view_edit.html b/cps/templates/config_view_edit.html index 2ea1c53c..b2578312 100644 --- a/cps/templates/config_view_edit.html +++ b/cps/templates/config_view_edit.html @@ -6,8 +6,9 @@ {% block body %}

{{title}}

- -
+ + +

diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 4a3d8f23..6dd1c72a 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -214,6 +214,7 @@

+

+

{{title}}

+ {% if feature_support['gmail'] %}
@@ -72,6 +73,7 @@

{{_('Allowed Domains (Whitelist)')}}

+
@@ -98,11 +100,12 @@ -
- - -
- + +
+ + +
+
diff --git a/cps/templates/http_error.html b/cps/templates/http_error.html index eb628a5f..8abd0c16 100644 --- a/cps/templates/http_error.html +++ b/cps/templates/http_error.html @@ -1,5 +1,5 @@ - + {{ instance }} | HTTP Error ({{ error_code }}) diff --git a/cps/templates/layout.html b/cps/templates/layout.html index a7315c34..7ec79f45 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -61,7 +61,7 @@ {% if g.user.role_upload() or g.user.role_admin()%} {% if g.allow_upload %}
  • -
  • diff --git a/cps/templates/login.html b/cps/templates/login.html index 7ae56d1b..0d5232e3 100644 --- a/cps/templates/login.html +++ b/cps/templates/login.html @@ -4,6 +4,7 @@

    {{_('Login')}}

    +
    diff --git a/cps/templates/register.html b/cps/templates/register.html index db8644fb..307a209b 100644 --- a/cps/templates/register.html +++ b/cps/templates/register.html @@ -3,6 +3,7 @@

    {{_('Register New Account')}}

    + {% if not config.config_register_email %}
    diff --git a/cps/templates/search_form.html b/cps/templates/search_form.html index 5d186ba7..fa57c85b 100644 --- a/cps/templates/search_form.html +++ b/cps/templates/search_form.html @@ -3,6 +3,7 @@

    {{title}}

    +
    diff --git a/cps/templates/shelf_edit.html b/cps/templates/shelf_edit.html index b3c81731..2882d08a 100644 --- a/cps/templates/shelf_edit.html +++ b/cps/templates/shelf_edit.html @@ -3,6 +3,7 @@

    {{title}}

    +
    diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index b4429faf..d48ba9a2 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -3,6 +3,7 @@

    {{title}}

    +
    {% if new_user or ( g.user and content.name != "Guest" and g.user.role_admin() ) %}
    diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index 111d2a09..416f6b7b 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -118,6 +118,7 @@ {% endblock %} {% block body %}

    {{_(title)}}

    +
    {{_('Remove Selections')}}
    diff --git a/cps/web.py b/cps/web.py index 3681c38c..db0be841 100644 --- a/cps/web.py +++ b/cps/web.py @@ -84,14 +84,13 @@ except ImportError: @app.after_request 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": - resp.headers['Content-Security-Policy'] += "img-src * data:" + resp.headers['Content-Security-Policy'] += " *" resp.headers['X-Content-Type-Options'] = 'nosniff' resp.headers['X-Frame-Options'] = 'SAMEORIGIN' resp.headers['X-XSS-Protection'] = '1; mode=block' resp.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' - # log.debug(request.full_path) return resp web = Blueprint('web', __name__) diff --git a/requirements.txt b/requirements.txt index e7f67593..0332185d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ tornado>=4.1,<6.2 Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.3.0 lxml>=3.8.0,<4.7.0 +flask-wtf>=0.15.0,<0.16.0 diff --git a/setup.cfg b/setup.cfg index 9787caa4..cea3f5a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,9 +18,11 @@ classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: GNU Affero General Public License v3 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Operating System :: OS Independent keywords = calibre @@ -49,6 +51,7 @@ install_requires = Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.3.0 lxml>=3.8.0,<4.7.0 + flask-wtf>=0.15.0,<0.16.0 [options.extras_require] gdrive =