mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-02 12:38:12 +08:00
Revised terms and privacy consent dialog, analytics hooks (#1426)
* revised terms and privacy consent * reorg code * fix conditional * Overlay reflects un-dissmissable state * add inline scripts, and consent callback * add csp_directive config hook * revert config.yaml * fix logic error Co-authored-by: Colin Megill <colinmegill@gmail.com>
This commit is contained in:
co-authored by
Colin Megill
parent
666e6d9849
commit
05fcdaf93c
@@ -53,5 +53,8 @@
|
|||||||
{% for script in SCRIPTS %}
|
{% for script in SCRIPTS %}
|
||||||
<script type="text/javascript" src="{{script | safe}}"></script>
|
<script type="text/javascript" src="{{script | safe}}"></script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% for ils in INLINE_SCRIPTS %}
|
||||||
|
<script type="text/javascript">{% include ils %}</script>
|
||||||
|
{% endfor %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,58 +4,50 @@ import { Position, Toaster, Intent } from "@blueprintjs/core";
|
|||||||
|
|
||||||
const ToastTopCenter = Toaster.create({
|
const ToastTopCenter = Toaster.create({
|
||||||
className: "recipe-toaster",
|
className: "recipe-toaster",
|
||||||
position: Position.TOP
|
position: Position.TOP,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ToastBottomCenter = Toaster.create({
|
const ToastBottomCenter = Toaster.create({
|
||||||
className: "recipe-toaster",
|
className: "recipe-toaster",
|
||||||
position: Position.BOTTOM
|
position: Position.BOTTOM,
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A "user" error - eg, bad input
|
A "user" error - eg, bad input
|
||||||
*/
|
*/
|
||||||
export const postUserErrorToast = message =>
|
export const postUserErrorToast = (message) =>
|
||||||
ToastTopCenter.show({ message, intent: Intent.WARNING });
|
ToastTopCenter.show({ message, intent: Intent.WARNING });
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A toast the user must dismiss manually, because they need to act on its information,
|
A toast the user must dismiss manually, because they need to act on its information,
|
||||||
ie., 8 bulk add genes out of 40 were bad. Manually see which ones and fix.
|
ie., 8 bulk add genes out of 40 were bad. Manually see which ones and fix.
|
||||||
*/
|
*/
|
||||||
export const keepAroundErrorToast = message =>
|
export const keepAroundErrorToast = (message) =>
|
||||||
ToastTopCenter.show({ message, timeout: 0, intent: Intent.WARNING });
|
ToastTopCenter.show({ message, timeout: 0, intent: Intent.WARNING });
|
||||||
|
|
||||||
/*
|
/*
|
||||||
a hard network error
|
a hard network error
|
||||||
*/
|
*/
|
||||||
export const postNetworkErrorToast = message =>
|
export const postNetworkErrorToast = (message) =>
|
||||||
ToastTopCenter.show({
|
ToastTopCenter.show({
|
||||||
message,
|
message,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
intent: Intent.DANGER
|
intent: Intent.DANGER,
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Async message to user
|
Async message to user
|
||||||
*/
|
*/
|
||||||
export const postAsyncSuccessToast = message =>
|
export const postAsyncSuccessToast = (message) =>
|
||||||
ToastTopCenter.show({
|
ToastTopCenter.show({
|
||||||
message,
|
message,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
intent: Intent.SUCCESS
|
intent: Intent.SUCCESS,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const postAsyncFailureToast = message =>
|
export const postAsyncFailureToast = (message) =>
|
||||||
ToastTopCenter.show({
|
ToastTopCenter.show({
|
||||||
message,
|
message,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
intent: Intent.WARNING
|
intent: Intent.WARNING,
|
||||||
});
|
|
||||||
|
|
||||||
export const termsOfServiceToast = (message, _onDismiss) =>
|
|
||||||
ToastBottomCenter.show({
|
|
||||||
message,
|
|
||||||
timeout: 0,
|
|
||||||
intent: Intent.PRIMARY,
|
|
||||||
onDismiss: _onDismiss
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
Button,
|
||||||
|
Classes,
|
||||||
|
Position,
|
||||||
|
Colors,
|
||||||
|
Icon,
|
||||||
|
} from "@blueprintjs/core";
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import { termsOfServiceToast } from "../framework/toasters";
|
|
||||||
|
|
||||||
const TosDismissedKey = "cxg.tosDismissed";
|
const CookieDecision = "cxg.cookieDecision";
|
||||||
|
|
||||||
function storageGet(key, defaultValue = null) {
|
function storageGet(key, defaultValue = null) {
|
||||||
try {
|
try {
|
||||||
@@ -23,53 +30,129 @@ function storageSet(key, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@connect(state => ({
|
@connect((state) => ({
|
||||||
tosURL: state.config?.parameters?.about_legal_tos
|
tosURL: state.config?.parameters?.about_legal_tos,
|
||||||
|
privacyURL: state.config?.parameters?.about_legal_privacy,
|
||||||
}))
|
}))
|
||||||
class TermsPrompt extends React.PureComponent {
|
class TermsPrompt extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const { tosURL, privacyURL } = this.props;
|
||||||
|
const cookieDecision = storageGet(CookieDecision, null);
|
||||||
|
const hasDecided = cookieDecision !== null;
|
||||||
this.state = {
|
this.state = {
|
||||||
hasDismissed: storageGet(TosDismissedKey, false)
|
hasDecided,
|
||||||
|
isEnabled: !!tosURL || !!privacyURL,
|
||||||
|
isOpen: !hasDecided,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { hasDismissed } = this.state;
|
const { hasDecided, isEnabled } = this.state;
|
||||||
const { tosURL } = this.props;
|
if (isEnabled && !hasDecided) {
|
||||||
if (!hasDismissed && tosURL) {
|
this.setState({ isOpen: true });
|
||||||
this.popTermsToast();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTermsToastDismissed = () => {
|
handleOK = () => {
|
||||||
this.setState({ hasDismissed: "yes" });
|
this.setState({ isOpen: false });
|
||||||
storageSet(TosDismissedKey, "yes");
|
storageSet(CookieDecision, "yes");
|
||||||
|
if (window.cookieDecisionCallback instanceof Function) {
|
||||||
|
try {
|
||||||
|
window.cookieDecisionCallback();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
popTermsToast() {
|
handleNo = () => {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
storageSet(CookieDecision, "no");
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTos() {
|
||||||
const { tosURL } = this.props;
|
const { tosURL } = this.props;
|
||||||
termsOfServiceToast(
|
if (!tosURL) return null;
|
||||||
|
return (
|
||||||
<span>
|
<span>
|
||||||
By using our site, you are agreeing to our{" "}
|
<Icon icon="info-sign" intent="primary" /> By using this site, you are
|
||||||
|
agreeing to our{" "}
|
||||||
<a
|
<a
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: "white",
|
textDecoration: "underline",
|
||||||
textDecoration: "underline"
|
|
||||||
}}
|
}}
|
||||||
href={tosURL}
|
href={tosURL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Terms of Service
|
terms of service
|
||||||
</a>
|
</a>
|
||||||
</span>,
|
.{" "}
|
||||||
this.onTermsToastDismissed
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPrivacy() {
|
||||||
|
const { privacyURL } = this.props;
|
||||||
|
if (!privacyURL) return null;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
To learn more, read our{" "}
|
||||||
|
<a
|
||||||
|
style={{
|
||||||
|
fontWeight: 700,
|
||||||
|
textDecoration: "underline",
|
||||||
|
}}
|
||||||
|
href={privacyURL}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
privacy policy
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return null;
|
const { isOpen, isEnabled } = this.state;
|
||||||
|
if (!isEnabled || !isOpen) return null;
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
onclose={this.drawerClose}
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={"120px"}
|
||||||
|
position={Position.BOTTOM}
|
||||||
|
canOutsideClickClose={false}
|
||||||
|
hasBackdrop={
|
||||||
|
true /* if the user can't use the app or click outside to dismiss, that should be visually represented with a backdrop */
|
||||||
|
}
|
||||||
|
enforceFocus={false}
|
||||||
|
autoFocus={false}
|
||||||
|
portal={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={Classes.DRAWER_BODY}
|
||||||
|
style={{ backgroundColor: Colors.LIGHT_GRAY1 }}
|
||||||
|
>
|
||||||
|
<div className={Classes.DIALOG_BODY}>
|
||||||
|
<div>
|
||||||
|
{this.renderTos()}
|
||||||
|
<span>
|
||||||
|
We use cookies to help us improve the site and to inform our
|
||||||
|
future efforts, and we also use necessary cookies to make our
|
||||||
|
site work.
|
||||||
|
</span>
|
||||||
|
{this.renderPrivacy()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={Classes.DIALOG_FOOTER} style={{ textAlign: "left" }}>
|
||||||
|
<Button intent="primary" onClick={this.handleOK}>
|
||||||
|
I'm OK with cookies!
|
||||||
|
</Button>{" "}
|
||||||
|
<Button onClick={this.handleNo}>No thanks</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -69,12 +69,15 @@ def dataset_index(dataset=None):
|
|||||||
location = path_join(config.multi_dataset__dataroot, dataset)
|
location = path_join(config.multi_dataset__dataroot, dataset)
|
||||||
|
|
||||||
scripts = config.server__scripts
|
scripts = config.server__scripts
|
||||||
|
inline_scripts = config.server__inline_scripts
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cache_manager = current_app.matrix_data_cache_manager
|
cache_manager = current_app.matrix_data_cache_manager
|
||||||
with cache_manager.data_adaptor(location, config) as data_adaptor:
|
with cache_manager.data_adaptor(location, config) as data_adaptor:
|
||||||
dataset_title = config.get_title(data_adaptor)
|
dataset_title = config.get_title(data_adaptor)
|
||||||
return render_template("index.html", datasetTitle=dataset_title, SCRIPTS=scripts)
|
return render_template(
|
||||||
|
"index.html", datasetTitle=dataset_title, SCRIPTS=scripts, INLINE_SCRIPTS=inline_scripts
|
||||||
|
)
|
||||||
except DatasetAccessError:
|
except DatasetAccessError:
|
||||||
return common_rest.abort_and_log(
|
return common_rest.abort_and_log(
|
||||||
HTTPStatus.BAD_REQUEST, f"Invalid dataset {dataset}", loglevel=logging.INFO, include_exc_info=True
|
HTTPStatus.BAD_REQUEST, f"Invalid dataset {dataset}", loglevel=logging.INFO, include_exc_info=True
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class AppConfig(object):
|
|||||||
self.server__host = dc["server"]["host"]
|
self.server__host = dc["server"]["host"]
|
||||||
self.server__port = dc["server"]["port"]
|
self.server__port = dc["server"]["port"]
|
||||||
self.server__scripts = dc["server"]["scripts"]
|
self.server__scripts = dc["server"]["scripts"]
|
||||||
|
self.server__inline_scripts = dc["server"]["inline_scripts"]
|
||||||
self.server__open_browser = dc["server"]["open_browser"]
|
self.server__open_browser = dc["server"]["open_browser"]
|
||||||
self.server__about_legal_tos = dc["server"]["about_legal_tos"]
|
self.server__about_legal_tos = dc["server"]["about_legal_tos"]
|
||||||
self.server__about_legal_privacy = dc["server"]["about_legal_privacy"]
|
self.server__about_legal_privacy = dc["server"]["about_legal_privacy"]
|
||||||
@@ -57,6 +58,7 @@ class AppConfig(object):
|
|||||||
self.server__flask_secret_key = dc["server"]["flask_secret_key"]
|
self.server__flask_secret_key = dc["server"]["flask_secret_key"]
|
||||||
self.server__generate_cache_control_headers = dc["server"]["generate_cache_control_headers"]
|
self.server__generate_cache_control_headers = dc["server"]["generate_cache_control_headers"]
|
||||||
self.server__server_timing_headers = dc["server"]["server_timing_headers"]
|
self.server__server_timing_headers = dc["server"]["server_timing_headers"]
|
||||||
|
self.server__csp_directives = dc["server"]["csp_directives"]
|
||||||
|
|
||||||
self.multi_dataset__dataroot = dc["multi_dataset"]["dataroot"]
|
self.multi_dataset__dataroot = dc["multi_dataset"]["dataroot"]
|
||||||
self.multi_dataset__index = dc["multi_dataset"]["index"]
|
self.multi_dataset__index = dc["multi_dataset"]["index"]
|
||||||
@@ -129,6 +131,12 @@ class AppConfig(object):
|
|||||||
mapping["adaptor__cxg_adaptor__tiledb_ctx"] = (("adaptor", "cxg_adaptor", "tiledb_ctx"), val)
|
mapping["adaptor__cxg_adaptor__tiledb_ctx"] = (("adaptor", "cxg_adaptor", "tiledb_ctx"), val)
|
||||||
del dc["adaptor"]["cxg_adaptor"]["tiledb_ctx"]
|
del dc["adaptor"]["cxg_adaptor"]["tiledb_ctx"]
|
||||||
|
|
||||||
|
# special case for csp_directives whose value is a dict.
|
||||||
|
val = config.get("server", {}).get("csp_directives", {})
|
||||||
|
if val is not None:
|
||||||
|
mapping["server__csp_directives"] = (("server", "csp_directives"), val)
|
||||||
|
del dc["server"]["csp_directives"]
|
||||||
|
|
||||||
flat_config = flatten(dc)
|
flat_config = flatten(dc)
|
||||||
for key, value in flat_config.items():
|
for key, value in flat_config.items():
|
||||||
# name of the attribute
|
# name of the attribute
|
||||||
@@ -231,6 +239,7 @@ class AppConfig(object):
|
|||||||
self.__check_attr("server__host", str)
|
self.__check_attr("server__host", str)
|
||||||
self.__check_attr("server__port", (type(None), int))
|
self.__check_attr("server__port", (type(None), int))
|
||||||
self.__check_attr("server__scripts", (list, tuple))
|
self.__check_attr("server__scripts", (list, tuple))
|
||||||
|
self.__check_attr("server__inline_scripts", (list, tuple))
|
||||||
self.__check_attr("server__open_browser", bool)
|
self.__check_attr("server__open_browser", bool)
|
||||||
self.__check_attr("server__force_https", bool)
|
self.__check_attr("server__force_https", bool)
|
||||||
self.__check_attr("server__flask_secret_key", (type(None), str))
|
self.__check_attr("server__flask_secret_key", (type(None), str))
|
||||||
@@ -238,6 +247,7 @@ class AppConfig(object):
|
|||||||
self.__check_attr("server__about_legal_tos", (type(None), str))
|
self.__check_attr("server__about_legal_tos", (type(None), str))
|
||||||
self.__check_attr("server__about_legal_privacy", (type(None), str))
|
self.__check_attr("server__about_legal_privacy", (type(None), str))
|
||||||
self.__check_attr("server__server_timing_headers", bool)
|
self.__check_attr("server__server_timing_headers", bool)
|
||||||
|
self.__check_attr("server__csp_directives", (type(None), dict))
|
||||||
|
|
||||||
if self.server__port:
|
if self.server__port:
|
||||||
if not is_port_available(self.server__host, self.server__port):
|
if not is_port_available(self.server__host, self.server__port):
|
||||||
@@ -262,6 +272,18 @@ class AppConfig(object):
|
|||||||
# second, from config file
|
# second, from config file
|
||||||
self.server__flask_secret_key = os.environ.get("CXG_SECRET_KEY", self.server__flask_secret_key)
|
self.server__flask_secret_key = os.environ.get("CXG_SECRET_KEY", self.server__flask_secret_key)
|
||||||
|
|
||||||
|
# CSP Directives are a dict of string: list(string) or string: string
|
||||||
|
if self.server__csp_directives is not None:
|
||||||
|
for k, v in self.server__csp_directives.items():
|
||||||
|
if not isinstance(k, str):
|
||||||
|
raise ConfigurationError(f"CSP directive names must be a string.")
|
||||||
|
if isinstance(v, list):
|
||||||
|
for policy in v:
|
||||||
|
if not isinstance(policy, str):
|
||||||
|
raise ConfigurationError(f"CSP directive value must be a string or list of strings.")
|
||||||
|
elif not isinstance(v, str):
|
||||||
|
raise ConfigurationError(f"CSP directive value must be a string or list of strings.")
|
||||||
|
|
||||||
def handle_data_locator(self, context):
|
def handle_data_locator(self, context):
|
||||||
self.__check_attr("data_locator__s3__region_name", (type(None), bool, str))
|
self.__check_attr("data_locator__s3__region_name", (type(None), bool, str))
|
||||||
if self.data_locator__s3__region_name is True:
|
if self.data_locator__s3__region_name is True:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ server:
|
|||||||
host: "127.0.0.1"
|
host: "127.0.0.1"
|
||||||
port : null
|
port : null
|
||||||
scripts : []
|
scripts : []
|
||||||
|
inline_scripts: []
|
||||||
open_browser: false
|
open_browser: false
|
||||||
about_legal_tos: null
|
about_legal_tos: null
|
||||||
about_legal_privacy: null
|
about_legal_privacy: null
|
||||||
@@ -16,6 +17,7 @@ server:
|
|||||||
flask_secret_key: null
|
flask_secret_key: null
|
||||||
generate_cache_control_headers: false
|
generate_cache_control_headers: false
|
||||||
server_timing_headers: false
|
server_timing_headers: false
|
||||||
|
csp_directives: null
|
||||||
|
|
||||||
presentation:
|
presentation:
|
||||||
max_categories: 1000
|
max_categories: 1000
|
||||||
|
|||||||
+39
-8
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
from flask import json
|
from flask import json
|
||||||
import logging
|
import logging
|
||||||
from flask_talisman import Talisman
|
from flask_talisman import Talisman
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
|
||||||
if os.path.isdir("/opt/python/log"):
|
if os.path.isdir("/opt/python/log"):
|
||||||
# This is the standard location where Amazon EC2 instances store the application logs.
|
# This is the standard location where Amazon EC2 instances store the application logs.
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -54,24 +57,32 @@ class WSGIServer(Server):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _before_adding_routes(app, app_config):
|
def _before_adding_routes(app, app_config):
|
||||||
script_hashes, style_hashes = WSGIServer.load_csp_hashes(app)
|
script_hashes, style_hashes = WSGIServer.get_csp_hashes(app, app_config)
|
||||||
csp = {
|
csp = {
|
||||||
"default-src": "'self'",
|
"default-src": ["'self'"],
|
||||||
"script-src": ["'unsafe-eval'", "'unsafe-inline'"] + script_hashes,
|
"script-src": ["'unsafe-eval'", "'unsafe-inline'"] + script_hashes,
|
||||||
"img-src": ["'self'", "data:"],
|
"img-src": ["'self'", "data:"],
|
||||||
"object-src": "'none'",
|
"object-src": ["'none'"],
|
||||||
"base-uri": "'none'",
|
"base-uri": ["'none'"],
|
||||||
"upgrade-insecure-requests": "",
|
"upgrade-insecure-requests": [""],
|
||||||
"frame-ancestors": "'none'",
|
"frame-ancestors": ["'none'"],
|
||||||
"require-trusted-types-for": "'script'",
|
"require-trusted-types-for": ["'script'"],
|
||||||
}
|
}
|
||||||
if len(style_hashes) > 0:
|
if len(style_hashes) > 0:
|
||||||
csp["style-src"] = style_hashes
|
csp["style-src"] = style_hashes
|
||||||
|
if app_config.server__inline_scripts:
|
||||||
|
csp["script-src"].append("'strict-dynamic'")
|
||||||
|
|
||||||
|
if app_config.server__csp_directives:
|
||||||
|
for k, v in app_config.server__csp_directives.items():
|
||||||
|
if not isinstance(v, list):
|
||||||
|
v = [v]
|
||||||
|
csp[k] = csp.get(k, []) + v
|
||||||
|
|
||||||
Talisman(app, force_https=app_config.server__force_https, frame_options="DENY", content_security_policy=csp)
|
Talisman(app, force_https=app_config.server__force_https, frame_options="DENY", content_security_policy=csp)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_csp_hashes(app):
|
def load_static_csp_hashes(app):
|
||||||
csp_hashes = None
|
csp_hashes = None
|
||||||
try:
|
try:
|
||||||
with app.open_resource("../common/web/csp-hashes.json") as f:
|
with app.open_resource("../common/web/csp-hashes.json") as f:
|
||||||
@@ -88,6 +99,26 @@ class WSGIServer(Server):
|
|||||||
|
|
||||||
return (script_hashes, style_hashes)
|
return (script_hashes, style_hashes)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compute_inline_scp_hashes(app, app_config):
|
||||||
|
inline_scripts = app_config.server__inline_scripts
|
||||||
|
hashes = []
|
||||||
|
for script in inline_scripts:
|
||||||
|
with app.open_resource(f"../common/web/templates/{script}") as f:
|
||||||
|
content = f.read()
|
||||||
|
# we use jinja2 template include, which trims final newline if present.
|
||||||
|
if content[-1] == 0x0A:
|
||||||
|
content = content[0:-1]
|
||||||
|
hash = base64.b64encode(hashlib.sha256(content).digest())
|
||||||
|
hashes.append(f"'sha256-{hash.decode('utf-8')}'")
|
||||||
|
return hashes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_csp_hashes(app, app_config):
|
||||||
|
script_hashes, style_hashes = WSGIServer.load_static_csp_hashes(app)
|
||||||
|
script_hashes += WSGIServer.compute_inline_scp_hashes(app, app_config)
|
||||||
|
return (script_hashes, style_hashes)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app_config = AppConfig()
|
app_config = AppConfig()
|
||||||
|
|||||||
Reference in New Issue
Block a user