Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ of how to configure Flask-Admin to inject CSP nonce values::
app,
content_security_policy={
"default-src": "'self'",
"script-src": "'self'",
Comment thread
samialfattani marked this conversation as resolved.
"style-src": "'self' 'unsafe-inline' fonts.googleapis.com ",
"font-src": "'self' fonts.gstatic.com data: ",
"img-src": "'self' data: ",
},
content_security_policy_nonce_in=["script-src", "style-src"]
)
Expand Down
8 changes: 8 additions & 0 deletions flask_admin/form/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,20 @@ def __init__(self, template: str) -> None:
self.template = template

def __call__(self, field: Field, **kwargs: t.Any) -> str:
admin = h.g._admin_view.admin
admin_csp_nonce_attribute = (
Markup(f'nonce="{admin.csp_nonce_generator()}"')
if admin.csp_nonce_generator
else ""
)

kwargs.update(
{
"field": field,
"_gettext": gettext,
"_ngettext": ngettext,
"h": h,
"admin_csp_nonce_attribute": admin_csp_nonce_attribute,
}
)

Expand Down
6 changes: 6 additions & 0 deletions flask_admin/static/admin/js/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@
}
}
};

$('.a-unlink').on('click', function(e) {
console.log('click a-unlink');
e.preventDefault();
});

})();
19 changes: 16 additions & 3 deletions flask_admin/templates/bootstrap4/admin/actions.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
{% import 'admin/static.html' as admin_static with context %}

{% macro dropdown(actions, btn_class='nav-link dropdown-toggle') -%}
<a class="{{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)" role="button" aria-haspopup="true"
<a class="{{ btn_class }} a-unlink" data-toggle="dropdown" href="#" role="button" aria-haspopup="true"
aria-expanded="false">{{ _gettext('With selected') }}</a>
<div class="dropdown-menu">
{% for p in actions %}
<a class="dropdown-item" href="javascript:void(0)"
onclick="return modelActions.execute('{{ p[0] }}');">{{ _gettext(p[1]) }}</a>
<a class="dropdown-item p-action" data-action="{{ p[0] }}">{{ _gettext(p[1]) }} </a>
{% endfor %}
</div>

<script {{ admin_csp_nonce_attribute }} >

document.addEventListener('DOMContentLoaded', function ()
{
$('.p-action').on('click', function (e)
{
e.preventDefault();
var action = $(this).data('action');
modelActions.execute(action);
});
});
</script>
{% endmacro %}


{% macro form(actions, url) %}
{% if actions %}
<form id="action_form" action="{{ url }}" method="POST" class="d-none">
Expand Down
17 changes: 15 additions & 2 deletions flask_admin/templates/bootstrap4/admin/file/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
{% if delete_form.csrf_token is defined and delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<button class="confirm-first"
data-msg="{{ _gettext('Are you sure you want to delete \'%(name)s\' recursively?', name=name) }}">
<i class="fa fa-times glyphicon glyphicon-remove"></i>
</button>
</form>
Expand All @@ -100,7 +101,8 @@
{% if delete_form.csrf_token is defined and delete_form.csrf_token %}
{{ delete_form.csrf_token }}
{% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<button class="confirm-first"
data-msg="{{ _gettext('Are you sure you want to delete \'%(name)s\' ?', name=name) }}">
<i class="fa fa-trash glyphicon glyphicon-trash"></i>
</button>
</form>
Expand Down Expand Up @@ -192,4 +194,15 @@
actions,
actions_confirmation) }}
<script {{ admin_csp_nonce_attribute }} src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>


<script {{ admin_csp_nonce_attribute }}>

$('.confirm-first').on('click', function (e)
{
var message = $(this).data('msg');
return confirm(message);
});

</script>
{% endblock %}
2 changes: 1 addition & 1 deletion flask_admin/templates/bootstrap4/admin/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<!-- just enhance the readability -->
<li class="{% if item.is_active(admin_view) %}active {% endif %}dropdown">

<a class="dropdown-toggle {% if is_main_nav %}nav-link{% else %}dropdown-item{% endif %}" data-toggle="dropdown" href="javascript:void(0)">
<a class="dropdown-toggle {% if is_main_nav %}nav-link{% else %}dropdown-item{% endif %} a-unlink" data-toggle="dropdown" href="#">
<!-- show icon -->
{% if item.class_name %}<span class="{{ item.class_name }}"></span> {% endif %}
{{ menu_icon(item) }}{{ item.name }}
Expand Down
10 changes: 5 additions & 5 deletions flask_admin/templates/bootstrap4/admin/lib.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&laquo;</a>
<a class="page-link a-unlink" href="#">&laquo;</a>
</li>
{% endif %}
{% if page > 0 %}
Expand All @@ -36,14 +36,14 @@
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&lt;</a>
<a class="page-link a-unlink" href="#">&lt;</a>
</li>
{% endif %}

{% for p in range(min, max) %}
{% if page == p %}
<li class="page-item active">
<a class="page-link" href="javascript:void(0)">{{ p + 1 }}</a>
<a class="page-link a-unlink" href="#">{{ p + 1 }}</a>
</li>
{% else %}
<li class="page-item">
Expand All @@ -58,7 +58,7 @@
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&gt;</a>
<a class="page-link a-unlink" href="#">&gt;</a>
</li>
{% endif %}
{% if max < pages %}
Expand All @@ -67,7 +67,7 @@
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&raquo;</a>
<a class="page-link a-unlink" href="#">&raquo;</a>
</li>
{% endif %}
</ul>
Expand Down
2 changes: 1 addition & 1 deletion flask_admin/templates/bootstrap4/admin/model/create.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<a href="{{ return_url }}" class="nav-link">{{ _gettext('List') }}</a>
</li>
<li class="nav-item">
<a href="javascript:void(0)" class="nav-link active">{{ _gettext('Create') }}</a>
<a href="#" class="nav-link active a-unlink">{{ _gettext('Create') }}</a>
</li>
</ul>
{% endblock %}
Expand Down
2 changes: 1 addition & 1 deletion flask_admin/templates/bootstrap4/admin/model/details.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</li>
{%- endif -%}
<li class="nav-item">
<a class="nav-link active disabled" href="javascript:void(0)">{{ _gettext('Details') }}</a>
<a class="nav-link active disabled a-unlink" href="#">{{ _gettext('Details') }}</a>
</li>
</ul>
{% endblock %}
Expand Down
2 changes: 1 addition & 1 deletion flask_admin/templates/bootstrap4/admin/model/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</li>
{%- endif -%}
<li class="nav-item">
<a href="javascript:void(0)" class="nav-link active">{{ _gettext('Edit') }}</a>
<a href="#" class="nav-link active a-unlink">{{ _gettext('Edit') }}</a>
</li>
{%- if admin_view.can_view_details -%}
<li class="nav-item">
Expand Down
25 changes: 22 additions & 3 deletions flask_admin/templates/bootstrap4/admin/model/inline_list_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
<input type="checkbox" name="del-{{ subfield.id }}" id="del-{{ subfield.id }}" />
<label for="del-{{ subfield.id }}" class="d-inline">{{ _gettext('Delete?') }}</label>
{% else %}
<a href="javascript:void(0)" value="{{ _gettext('Are you sure you want to delete this record?') }}" class="inline-remove-field"><i class="fa fa-times glyphicon glyphicon-remove"></i></a>
<a href="#" value="{{ _gettext('Are you sure you want to delete this record?') }}" class="inline-remove-field a-unlink">
<i class="fa fa-times glyphicon glyphicon-remove"></i>
</a>
{% endif %}
</div>
</small>
Expand All @@ -32,14 +34,31 @@
<legend>
<small>{{ _gettext('New') }} {{ field.label.text }}</small>
<div class="pull-right">
<a href="javascript:void(0)" value="{{ _gettext('Are you sure you want to delete this record?') }}" class="inline-remove-field"><span class="fa fa-times glyphicon glyphicon-remove"></span></a>
<a href="#" value="{{ _gettext('Are you sure you want to delete this record?') }}" class="inline-remove-field a-unlink">
<span class="fa fa-times glyphicon glyphicon-remove"></span>
</a>
</div>
</legend>
<div class='clearfix'></div>
{{ render(template) }}
</div>
{% endfilter %}
</div>
<a id="{{ field.id }}-button" href="javascript:void(0)" class="btn btn-primary" role="button" onclick="faForm.addInlineField(this, '{{ field.id }}');">{{ _gettext('Add') }} {{ field.label.text }}</a>
<a id="{{ field.id }}-button" href="#" class="btn btn-primary a-unlink" role="button"
data-fieldid="{{ field.id }}"
>{{ _gettext('Add') }} {{ field.label.text }}</a>

<script {{ admin_csp_nonce_attribute }} >

document.addEventListener('DOMContentLoaded', function ()
{
$(`#{{ field.id }}-button`).on('click', function (e)
{
console.log('add inline field');
e.preventDefault();
faForm.addInlineField(this, $(this).data('fieldid') );
Comment thread
samialfattani marked this conversation as resolved.
});
});
</script>
</div>
{% endmacro %}
21 changes: 17 additions & 4 deletions flask_admin/templates/bootstrap4/admin/model/layout.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
{% macro filter_options(btn_class='dropdown-toggle') %}
<a class="nav-link {{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)">{{ _gettext('Add Filter') }}</a>
<a class="nav-link {{ btn_class }}" data-toggle="dropdown" href="#">{{ _gettext('Add Filter') }}</a>
<div class="dropdown-menu field-filters">
{% for k in filter_groups %}
<a href="javascript:void(0)" class="dropdown-item filter" onclick="return false;">{{ k }}</a>
<a class="dropdown-item filter" >{{ k }}</a>
{% endfor %}
</div>

<script {{ admin_csp_nonce_attribute }}>

document.addEventListener('DOMContentLoaded', function ()
{
document.querySelectorAll('.filter').forEach(function (el) {
el.addEventListener('click', function (e) {
e.preventDefault();
return false;
});
});
});
</script>
{% endmacro %}

{% macro export_options(btn_class='dropdown-toggle') %}
{% if admin_view.export_types|length > 1 %}
<li class="dropdown">
<a class="nav-link {{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)" role="button"
<a class="nav-link {{ btn_class }} a-unlink" data-toggle="dropdown" href="#" role="button"
aria-haspopup="true" aria-expanded="false">{{ _gettext('Export') }}</a>
<div class="dropdown-menu">
{% for export_type in admin_view.export_types %}
Expand Down Expand Up @@ -98,7 +111,7 @@
{% endmacro %}

{% macro page_size_form(generator, page_size_options, btn_class='nav-link dropdown-toggle') %}
<a class="{{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)">
<a class="{{ btn_class }} a-unlink" data-toggle="dropdown" href="#">
{{ page_size }} {{ _gettext('items') }}
</a>
<div class="dropdown-menu">
Expand Down
6 changes: 3 additions & 3 deletions flask_admin/templates/bootstrap4/admin/model/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{% block model_menu_bar %}
<ul class="nav nav-tabs">
<li class="nav-item">
<a href="javascript:void(0)" class="nav-link active">{{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}</a>
<a href="#" class="nav-link active a-unlink">{{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}</a>
</li>

{% if admin_view.can_create %}
Expand Down Expand Up @@ -101,9 +101,9 @@
{{ name }}
{% endif %}
{% if admin_view.column_descriptions.get(c) %}
<a class="fa fa-question-circle glyphicon glyphicon-question-sign"
<a class="fa fa-question-circle glyphicon glyphicon-question-sign a-unlink"
title="{{ admin_view.column_descriptions[c] }}"
href="javascript:void(0)" data-role="tooltip"
href="#" data-role="tooltip"
></a>
{% endif %}
</th>
Expand Down
18 changes: 17 additions & 1 deletion flask_admin/templates/bootstrap4/admin/model/row_actions.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,24 @@
{% elif csrf_token is defined and csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return faHelpers.safeConfirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="{{ _gettext('Delete Record') }}">
<button
id="btn-delete-{{ row_id }}" class="btn-delete-row"
data-msg="{{ _gettext('Are you sure you want to delete this record?') }}, (id= {{ row_id }})"
title="{{ _gettext('Delete Record') }}">
<span class="fa fa-trash glyphicon glyphicon-trash"></span>
</button>
</form>

<script {{ admin_csp_nonce_attribute }} >

document.addEventListener('DOMContentLoaded', function ()
{
$('#btn-delete-{{ row_id }}').on('click', function (e)
{
const msg = $(this).data('msg');
return faHelpers.safeConfirm(msg);
});
});
</script>

{% endmacro %}
33 changes: 33 additions & 0 deletions flask_admin/tests/csp/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import secrets
import typing as t

import pytest
from flask import Flask
from flask_admin.base import Admin


@pytest.fixture
def babel(app: Flask) -> t.Generator[object | None, None, None]:
try:
from flask_babel import Babel
except ImportError:
yield None
return

yield Babel(app)


@pytest.fixture
def nonce() -> str:
return secrets.token_urlsafe(32)


@pytest.fixture
def admin(
app: Flask, babel: t.Any | None, nonce: str
) -> t.Generator[Admin, t.Any, None]:
def csp_nonce_generator() -> str:
return nonce

admin = Admin(app, csp_nonce_generator=csp_nonce_generator)
yield admin
1 change: 1 addition & 0 deletions flask_admin/tests/csp/files/dir1/dum.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this file is just a placeholder to avoid having empty directory!
1 change: 1 addition & 0 deletions flask_admin/tests/csp/files/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is test file
Loading