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'",
"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
2 changes: 1 addition & 1 deletion examples/bootstrap4/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% import 'admin/model/inline_list_base.html' as base with context %}

{% macro render_field(field) %}
{{ field }}
{{ field }} 69696

{% if h.is_field_error(field.errors) %}
<ul class="form-text input-errors">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% import 'admin/lib.html' as lib with context %}

<div class="inline-form-field">
{{ lib.render_form_fields(field.form, form_opts=form_opts) }}
</div>
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') );
});
});
</script>
</div>
{% endmacro %}
23 changes: 18 additions & 5 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,8 +111,8 @@
{% 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)">
{{ page_size }} {{ _gettext('items') }}
<a class="{{ btn_class }} a-unlink" data-toggle="dropdown" href="#">
{{ page_size }} {{ _gettext('items') }} ss
</a>
<div class="dropdown-menu">
{% for option in page_size_options %}
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
{% call lib.form_tag(action=url_for('.create_view', url=return_url)) %}
{% call lib.form_tag(action=url_for('.create_view', url=return_url)) %}
<div class="modal-body">
{{ lib.render_form_fields(form, form_opts=form_opts) }}
{{ lib.render_form_fields(form, form_opts=form_opts) }} 77
</div>
<div class="modal-footer">
{{ lib.render_form_buttons(return_url, extra=None, is_modal=True) }}
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 %}
32 changes: 32 additions & 0 deletions flask_admin/tests/csp/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import secrets

import pytest
from flask_admin.base import Admin
from flask_sqlalchemy import SQLAlchemy


@pytest.fixture
def db(app):
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///"
# app.config["SQLALCHEMY_ECHO"] = True
# app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
yield db

with app.app_context():
db.session.close()
db.engine.dispose()


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


@pytest.fixture
def admin(app, babel, nonce):
def csp_nonce_generator():
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 place holder 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