Skip to content
Merged
1 change: 1 addition & 0 deletions changes/204.a.canada.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Adds script/style nonce capabilities to Jinja2 webassets.
1 change: 1 addition & 0 deletions changes/204.b.canada.changes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Moved inline styles and JS attributes to classes and event listeners respectfully.
1 change: 1 addition & 0 deletions changes/204.c.canada.changes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Now sets the Cache-Control header to `no-cache, private` for logged in users.
1 change: 1 addition & 0 deletions changes/204.d.canada.changes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Loads image assets instead of `data:image` URIs for stricter Content-Security-Policy support.
11 changes: 8 additions & 3 deletions ckan/lib/webassets_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from webassets import Environment
from webassets.loaders import YAMLLoader

from ckan.common import config, g
# (canada fork only): use CSP nonce to support strict-dynamic
from ckan.common import config, g, request


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -128,9 +129,13 @@ def include_asset(name: str) -> None:

def _to_tag(url: str, type_: str):
if type_ == u'style':
return u'<link href="{}" rel="stylesheet"/>'.format(url)
# (canada fork only): use CSP nonce to support strict-dynamic
return u'<link href="{}" rel="stylesheet" nonce="{}"/>'.format(
url, str(request.environ.get('CSP_NONCE', '')))
elif type_ == u'script':
return u'<script src="{}" type="text/javascript"></script>'.format(url)
# (canada fork only): use CSP nonce to support strict-dynamic
return u'<script src="{}" type="text/javascript" nonce="{}"></script>'.format(
url, str(request.environ.get('CSP_NONCE', '')))
return u''


Expand Down
20 changes: 20 additions & 0 deletions ckan/logic/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
Missing = df.Missing
missing = df.missing

# (canada fork only): http header value validator
# TODO: upstream contrib??
header_bad_value_match = re.compile(r'[^\x20-\x7E\t]')
new_line_match = re.compile(r'[\r\n]')


def owner_org_validator(key: FlattenKey, data: FlattenDataDict,
errors: FlattenErrorDict, context: Context) -> Any:
Expand Down Expand Up @@ -1171,3 +1176,18 @@ def license_choices(value, context):
if value in licenses:
return value
raise Invalid(_('Invalid license'))


# (canada fork only): http header value validator
# TODO: upstream contrib??
def http_header_value_validator(value: Any):
"""
Sanitizes safe values for HTTP headers.

Removes newline characters
"""
value = re.sub(new_line_match, ' ', value)
bad_values = re.search(header_bad_value_match, value)
if bad_values:
raise Invalid(f'Invalid characters for HTTP Header value: {bad_values}')
return value
1 change: 1 addition & 0 deletions ckan/public/base/javascript/modules/metadata-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ckan.module('metadata-button', function(jQuery) {

_onClick: function(event) {
console.log("PRESSED THE BUTTON");
// FIXME: TODO: move style attributes to classes
var div = document.getElementById("metadata_diff");
if (div.style.display === "none") {
div.style.display = "block";
Expand Down
46 changes: 45 additions & 1 deletion ckan/public/base/javascript/modules/resource-upload-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ this.ckan.module('resource-upload-field', function (jQuery) {
// revert to URL for Link option
$('#resource-link-button').on('click', function() {
urlField.attr('type', 'url');
})
})

$('#field-resource-upload').on('change', function() {
if (_nameIsDirty) {
Expand All @@ -34,6 +34,50 @@ this.ckan.module('resource-upload-field', function (jQuery) {

$('input[name="name"]').val(file_name);
});

// (canada fork only): CSP support
let uploadButton = $('#resource-upload-button');
if( uploadButton.length > 0 ){
$(uploadButton).off('click.ResourceEdit');
$(uploadButton).on('click.ResourceEdit', function(_event){
let uploadField = document.getElementById('resource-url-upload');
if( typeof uploadField !== 'undefined' && uploadField != null ){
uploadField.checked = true;
}
document.getElementById('field-resource-upload').click();
});
}
let linkButton = $('#resource-link-button');
if( linkButton.length > 0 ){
$(linkButton).off('click.ResourceEdit');
$(linkButton).on('click.ResourceEdit', function(_event){
let urlField = document.getElementById('resource-url-link');
if( typeof urlField !== 'undefined' && urlField != null ){
urlField.checked = true;
}
document.getElementById('field-resource-url').focus();
});
}
let removeURIButtons = $('.btn-remove-url');
if( removeURIButtons.length > 0 ){
$(removeURIButtons).each(function(_index, _removeURIButton){
$(_removeURIButton).off('click.ResourceEdit');
$(_removeURIButton).on('click.ResourceEdit', function(_event){
let clearUploadField = document.getElementById('field-clear-upload');
if( typeof clearUploadField !== 'undefined' && clearUploadField != null ){
clearUploadField.checked = true;
}
document.getElementById('resource-url-none').checked = true;
document.getElementById($(_removeURIButton).attr('data-first-button')).focus();
if( $(_removeURIButton).attr('data-is-upload') == 'true' || $(_removeURIButton).attr('data-is-upload') == true || $(_removeURIButton).attr('data-is-upload') == 'True' ){
$('#field-resource-upload').replaceWith($('#field-resource-upload').val('').clone(true));
}else{
$('#field-resource-url').val('');
}
});
});
}

}
}
});
3 changes: 2 additions & 1 deletion ckan/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@
{{ h.render_assets('style') }}
{%- block custom_styles %}
{%- if g.site_custom_css -%}
<style>
{# (canada fork only): CSP support #}
<style nonce="{{- h.get_inline_script_nonce() if 'get_inline_script_nonce' in h else '' -}}">
{{ g.site_custom_css | safe }}
</style>
{%- endif %}
Expand Down
3 changes: 2 additions & 1 deletion ckan/templates/dataviewer/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

{# remove any scripts #}
{% block scripts %}
<script>
{# (canada fork only): CSP support #}
<script nonce="{{- h.get_inline_script_nonce() if 'get_inline_script_nonce' in h else '' -}}">
var preload_resource = {{ h.literal(h.dump_json(resource)) }};
</script>
{% endblock %}
Expand Down
39 changes: 13 additions & 26 deletions ckan/templates/package/snippets/resource_upload_field.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@

{% set first_button = 'resource-upload-button' if is_upload_enabled else 'resource-link-button' %}

{% macro remove_button(js='') %}
<button type="button" class="btn btn-danger btn-remove-url"
onclick="
document.getElementById('resource-url-none').checked = true;
document.getElementById('{{ first_button }}').focus();
{{ js }}
">{{ _('Remove') }}</button>
{% macro remove_button(is_upload) %}
{# (canada fork only): CSP support #}
<button type="button" class="btn btn-danger btn-remove-url" data-is-upload="{{- is_upload -}}" data-first-button="{{- first_button -}}">{{ _('Remove') }}</button>
{% endmacro %}

<div data-module="resource-upload-field" class="resource-upload-field form-group">
Expand All @@ -40,19 +36,13 @@
<div role="group" aria-labelledby="resource-menu-label">
{% block url_type_select %}
{% if is_upload_enabled %}
{# (canada fork only): CSP support #}
<button type="button" class="btn btn-default" id="resource-upload-button"
title="{{ _('Upload a file on your computer') }}"
onclick="
document.getElementById('resource-url-upload').checked = true;
document.getElementById('field-resource-upload').click();
"><i class="fa fa-cloud-upload"></i>{{ _("Upload") }}</button>
title="{{ _('Upload a file on your computer') }}"><i class="fa fa-cloud-upload"></i>{{ _("Upload") }}</button>
{% endif %}
{# (canada fork only): CSP support #}
<button type="button" class="btn btn-default" id="resource-link-button"
title="{{ _('Link to a URL on the internet (you can also link to an API)') }}"
onclick="
document.getElementById('resource-url-link').checked = true;
document.getElementById('field-resource-url').focus();
"><i class="fa fa-globe"></i>{{ _('Link') }}</button>
title="{{ _('Link to a URL on the internet (you can also link to an API)') }}"><i class="fa fa-globe"></i>{{ _('Link') }}</button>
{% endblock %}
</div>
</div>
Expand All @@ -67,11 +57,8 @@
{# for existing uploads we show the file name in a readonly input box #}
<input type="checkbox" id="field-clear-upload" value="true">
<div class="upload-type">
<button type="button" class="btn btn-danger btn-remove-url"
onclick="
document.getElementById('field-clear-upload').checked = true;
document.getElementById('field-resource-upload').focus();
">{{ _('Clear Upload') }}</button>
{# (canada fork only): CSP support #}
<button type="button" class="btn btn-danger btn-remove-url" data-is-upload="{{- is_upload -}}" data-first-button="{{- first_button -}}">{{ _('Clear Upload') }}</button>
<label class="form-label">{{ upload_label or _('File') }}</label>
<div class="controls">
{% set existing_name = data.get('url', '').split('/')[-1].split('?')[0].split('#')[0] %}
Expand All @@ -80,8 +67,8 @@
</div>
{% endif %}
<div class="upload-type">
{{ remove_button(
js="$('#field-resource-upload').replaceWith($('#field-resource-upload').val('').clone(true))") }}
{# (canada fork only): CSP support #}
{{ remove_button(is_upload='true') }}
{{ form.input(
'upload',
label=upload_label or _('File'),
Expand All @@ -97,8 +84,8 @@
'checked' if is_url else '' }}>
<div class="select-type">
{% block link_controls %}
{{ remove_button(
js="$('#field-resource-url').val('')") }}
{# (canada fork only): CSP support #}
{{ remove_button(is_upload='false') }}
{{ form.input(
'url',
label=url_label or _('URL'),
Expand Down
19 changes: 12 additions & 7 deletions ckan/templates/user/snippets/recaptcha.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
<div class="form-group">
<div class="controls">
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
{# (canada fork only): CSP support #}
<script src="https://www.google.com/recaptcha/api.js" nonce="{{- h.get_inline_script_nonce() if 'get_inline_script_nonce' in h else '' -}}" async defer></script>
<div class="g-recaptcha" data-sitekey="{{ public_key }}"></div>

<noscript>
<div style="width: 304px; height: 352px; position: relative;">
<div style="width: 304px; height: 352px; position: absolute;">
<iframe title="Recaptcha" src="https://www.google.com/recaptcha/api/fallback?k={{ public_key }}" frameborder="0" scrolling="no" style="width: 304px; height:352px"></iframe>
{# (canada fork only): no inline style attr #}
<div class="user-recaptcha-wrapper">
{# (canada fork only): no inline style attr #}
<div class="user-recaptcha-frame">
{# (canada fork only): no inline style attr #}
<iframe class="user-recaptcha-frame" title="Recaptcha" src="https://www.google.com/recaptcha/api/fallback?k={{ public_key }}" frameborder="0" scrolling="no"></iframe>
</div>

<div style="width: 250px; height: 80px; position: absolute; bottom: 21px; left: 25px; margin: 0; padding: 0; right: 25px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response" style="width: 250px; height: 80px; border: 1px solid #c1c1c1; margin: 0; padding: 0; resize: none;" class="g-recaptcha-response"></textarea>
{# (canada fork only): no inline style attr #}
<div class="user-recaptcha-text-wrapper">
{# (canada fork only): no inline style attr #}
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response user-recaptcha-text"></textarea>
</div>
</div>
</noscript>
Expand Down
2 changes: 2 additions & 0 deletions ckan/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ def set_cache_control_headers_for_response(response: Response) -> Response:
except ValueError:
pass
else:
# (canada fork only): set NOCACHE for logged in sessions
response.cache_control.no_cache = True
response.cache_control.private = True

# Invalidate cached pages upon login/logout
Expand Down
3 changes: 2 additions & 1 deletion ckanext/activity/templates/group/changes.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ <h1 class="page-heading">{{ _('Changes') }}</h1>
{% if i == 0 %}
{# button to show JSON metadata diff for the most recent change - not shown by default #}
<input type="button" data-module="metadata-button" data-module-target="" class="btn" value="Show metadata diff" id="metadata_button"></input>
<div id="metadata_diff" style="display:none;">
{# (canada fork only): no inline style attr #}
<div id="metadata_diff" class="d-none-soft">
{% block group_changes_diff %}
<pre>
{{ activity_diffs[0]['diff']|safe }}
Expand Down
3 changes: 2 additions & 1 deletion ckanext/activity/templates/organization/changes.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ <h1 class="page-heading">{{ _('Changes') }}</h1>
{% if i == 0 %}
{# button to show JSON metadata diff for the most recent change - not shown by default #}
<input type="button" data-module="metadata-button" data-module-target="" class="btn" value="Show metadata diff" id="metadata_button"></input>
<div id="metadata_diff" style="display:none;">
{# (canada fork only): no inline style attr #}
<div id="metadata_diff" class="d-none-soft">
{% block organization_changes_diff %}
<pre>
{{ activity_diffs[0]['diff']|safe }}
Expand Down
3 changes: 2 additions & 1 deletion ckanext/activity/templates/package/changes.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ <h1 class="page-heading">{{ _('Changes') }}</h1>
{% if i == 0 %}
{# button to show JSON metadata diff for the most recent change - not shown by default #}
<input type="button" data-module="metadata-button" data-module-target="" class="btn" value="Show metadata diff" id="metadata_button"></input>
<div id="metadata_diff" style="display:none;">
{# (canada fork only): no inline style attr #}
<div id="metadata_diff" class="d-none-soft">
{% block package_changes_diff %}
<pre>
{{ activity_diffs[0]['diff']|safe }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
#}

{# (canada fork only): translations and blocks #}
{# (canada fork only): no inline style attr #}

<div id="activity_types_filter" class="{%- block activity_form_classes -%}{%- endblock -%}" style="margin-bottom: 15px;" data-module="activity-stream">
<div id="activity_types_filter" class="{%- block activity_form_classes -%}{%- endblock -%} mb-4" data-module="activity-stream">
<label for="activity_types_filter_select" class="{%- block activity_label_classes -%}{%- endblock -%}">{{ _('Activity type') }}</label>
<select id="activity_types_filter_select" class="{%- block activity_select_classes -%}{%- endblock -%}">
<option {% if not activity_types %}selected{% endif %} data-url="{{ h.url_for(blueprint, id=id) }}">
Expand Down
3 changes: 2 additions & 1 deletion ckanext/activity/templates/snippets/pagination.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{% set class_prev = "btn btn-default" if newer_activities_url else "btn disabled" %}
{% set class_next = "btn btn-default" if older_activities_url else "btn disabled" %}

<div id="activity_page_buttons" class="activity_buttons" style="margin-top: 25px;">
{# (canada fork only): no inline style attr #}
<div id="activity_page_buttons" class="activity_buttons mt-5">
<a href="{{ newer_activities_url }}" class="{{ class_prev }}">{{ _('Newer activities') }}</a>
<a href="{{ older_activities_url }}" class="{{ class_next }}">{{ _('Older activities') }}</a>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
</a>
</div>

<form id="followee-content" action="/dashboard" style=" display:none;">
{# (canada fork only): no inline style attr #}
<form id="followee-content" action="/dashboard" class="d-none-soft">
<div class="popover-header">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-search"></i></span>
Expand Down
17 changes: 15 additions & 2 deletions ckanext/datatablesview/public/datatablesview.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ function initFilterObserver () {
// (e.g. "4 of 1000 entries (filtered from...)")
const filterObserver = new MutationObserver(function (e) {
const infoText = document.getElementById('dtprv_info').innerText
// FIXME: TODO: move style attributes to classes
if (!infoText.includes('(')) {
document.getElementById('filterinfoicon').style.visibility = 'hidden'
} else {
Expand Down Expand Up @@ -421,8 +422,9 @@ this.ckan.module('datatables_view', function (jQuery) {
display: $.fn.dataTable.Responsive.display.modal({
header: function (row) {
// add clipboard and print buttons to modal record display
// (canada fork only): CSP support
var data = row.data();
return '<span style="font-size:150%;font-weight:bold;">Details:</span>&nbsp;&nbsp;<div class=" dt-buttons btn-group">' +
return '<span class="font-weight-bold fw-bold">Details:</span>&nbsp;&nbsp;<div class=" dt-buttons btn-group">' +
'<button id="modalcopy-button" class="btn btn-default" title="' + that._('Copy to clipboard') + '" onclick="copyModal(\'' +
packagename + '&mdash;' + resourcename + '\')"><i class="fa fa-copy"></i></button>' +
'<button id="modalprint-button" class="btn btn-default" title="' + that._('Print') + '" onclick="printModal(\'' +
Expand Down Expand Up @@ -462,9 +464,10 @@ this.ckan.module('datatables_view', function (jQuery) {
const colid = 'dtcol-' + validateId(colname) + '-' + i
const coltype = $(thecol).data('type')
const placeholderText = formatdateflag && coltype.substr(0, 9) === 'timestamp' ? ' placeholder="yyyy-mm-dd"' : ''
// (canada fork only): CSP support
$('<input id="' + colid + '" name="' + colid + '" autosave="' + colid + '"' +
placeholderText +
' class="fhead form-control input-sm" type="search" results="10" autocomplete="on" style="width:100%"/>')
' class="fhead form-control input-sm canada-width-full" type="search" results="10" autocomplete="on"/>')
.appendTo($(thecol).empty())
.on('keyup search', function (event) {
const colSelector = colname + ':name'
Expand Down Expand Up @@ -595,6 +598,16 @@ this.ckan.module('datatables_view', function (jQuery) {
}
}, // end stateSaveParams
initComplete: function (settings, json) {

// (canada fork only): no inline event handler for CSP support
let refitColButton = $('#refit-button');
if( refitColButton.length > 0 ){
$(refitColButton).off('click.dt_refit');
$(refitColButton).on('click.dt_refit', function(_event){
fitColText();
});
}

// this callback is invoked by DataTables when table is fully rendered
const api = this.api()
// restore some data-dependent saved states now that data is loaded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
<br/>
<a class="dt-show-all" href="javascript:void(0);"><span class="mrgn-rght-md"><i class="fa fa-eye"></i>&nbsp;{{ _('Show All') }}</span></a>
<a class="dt-hide-all" href="javascript:void(0);"><span><i class="fa fa-eye-slash"></i>&nbsp;{{ _('Hide All') }}</span></a>
<script>
{# (canada fork only): script nonce for CSP support #}
<script nonce="{{- h.get_inline_script_nonce() if 'get_inline_script_nonce' in h else '' -}}">
window.addEventListener('load', function(){
$(document).ready(function(){
let dtShowAll = $('a.dt-show-all');
Expand Down
Loading