Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Health master #212

Open
wants to merge 48 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6a96bef
Fix horrible typo
Mar 29, 2021
fd3c049
Add standalone validation feature
Mar 29, 2021
ea061e6
Correctly extract querystring parameter for PUT
Mar 29, 2021
796d323
Raise the correct exception when flag is not set
Mar 29, 2021
dccd770
Improve comment
Jun 25, 2021
4280841
Fix stupid logic
Mar 29, 2021
0d33abd
Merge branch 'master' of https://github.com/CodeYellowBV/django-binde…
Jun 25, 2021
b83003c
Stop earlier when flag not set
Jun 25, 2021
0dd9ead
Merge remote-tracking branch 'jeroen/validate-only' into health_master
stefanmajoor Jun 30, 2021
31a61bf
Add tests for validation flow.
Jun 30, 2021
9be35a2
Some cleanup
Jun 30, 2021
ba74f3c
Merge remote-tracking branch 'origin' into validate-only
Jun 30, 2021
90d72af
Merge remote-tracking branch 'origin-jeroen/validate-only' into healt…
Jul 1, 2021
32996a4
Test _validation_model field for clean
Jul 22, 2021
be297ad
Remove incorrect argument from full_clean
Jul 22, 2021
da40b8e
Merge branch 'download_csv_as_excel' into health_master
stefanmajoor Jul 22, 2021
32b6e2d
Add tests
Jul 22, 2021
27e0ef4
Merge pull request #177 from robinzelders/whitelist_validation_errors…
stefanmajoor Jul 22, 2021
d14787f
do not write to a new worksheet. Take the default worksheet instead
stefanmajoor Jul 23, 2021
70622fd
update doc
Jul 23, 2021
d673457
Add first attempt at getting mssql support working
stefanmajoor Jul 26, 2021
68397df
Merge pull request #182 from robinzelders/whitelist_validation_errors…
robinzelders Aug 4, 2021
ac8ab35
Merge branch 'master' of github.com:CodeYellowBV/django-binder
stefanmajoor Sep 17, 2021
cfa6fa6
add support fo nullable foreign keys in CSVexport plugin
stefanmajoor Sep 17, 2021
3fa6f5e
Merge branch 'add_support_for_nullable_fks_in_csv_export' into health…
stefanmajoor Sep 17, 2021
26e5aa5
Merge remote-tracking branch 'origin-2/master' into health_master # C…
Dec 7, 2021
3c8b239
Do 1 query per with instead of a big combined one to prevent exponent…
daanvdk Jan 18, 2022
292d739
Do not use aggregates at all
daanvdk Jan 18, 2022
528d776
Do not use join for reverse relations
daanvdk Jan 19, 2022
3d21cbe
Merge remote-tracking branch 'origin/fix-issues-with-big-withs' into …
stefanmajoor Jan 19, 2022
5cfc8dc
Do 1 query per with instead of a big combined one to prevent exponent…
daanvdk Jan 18, 2022
aae913f
Do not use aggregates at all
daanvdk Jan 18, 2022
034a13c
Do not use join for reverse relations
daanvdk Jan 19, 2022
22ccc40
Merge branch 'health_master' of github.com:CodeYellowBV/django-binder…
stefanmajoor Jan 26, 2022
4db24f8
Add webpage model
stefanmajoor Feb 18, 2022
846a54f
Implement html field
stefanmajoor Feb 18, 2022
d47f0de
Fix flake issues
stefanmajoor Feb 18, 2022
c4d696e
Documentation
stefanmajoor Feb 18, 2022
4552f56
Add gettext for html field
stefanmajoor Mar 2, 2022
06b33d7
finish sentence
stefanmajoor Mar 2, 2022
44cab36
Add test for nested attributes & also test the content of the error m…
stefanmajoor Mar 2, 2022
4373130
Merge multiple errors in HTMLField
stefanmajoor Mar 3, 2022
581926f
Add noreferrer noopener check for links
stefanmajoor Mar 3, 2022
271a279
linting
stefanmajoor Mar 8, 2022
21288ae
Merge branch 'master' into health_master
stefanmajoor Mar 9, 2022
2c72aff
Merge branch 'html_field' into health_master
stefanmajoor Mar 9, 2022
15e357f
fix merge conflict
stefanmajoor Mar 10, 2022
e96b054
Fix crash on non gotten FKs
stefanmajoor Dec 6, 2024
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
14 changes: 11 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ jobs:

strategy:
matrix:
python-version: ["3.7", "3.8"]
django-version: ["2.1.1", "3.1.4"]
database-engine: ["postgres", "mysql"]
python-version: [ "3.7", "3.8" ]
django-version: [ "2.1.1", "3.1.4" ]
database-engine: [ "postgres", "mysql", "mssql"]

services:
postgres:
Expand All @@ -37,6 +37,14 @@ jobs:
ports:
- 3306:3306


mssqldb:
image: mcr.microsoft.com/mssql/server:2017-latest
env:
ACCEPT_EULA: y
SA_PASSWORD: Test


steps:
- name: Checkout code
uses: actions/checkout@v2
Expand Down
12 changes: 12 additions & 0 deletions binder/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,15 @@ def __add__(self, other):
else:
errors[model] = other.errors[model]
return BinderValidationError(errors)


class BinderSkipSave(BinderException):
"""Used to abort the database transaction when validation was successfull.
Validation is possible when saving (post, put, multi-put) or deleting models."""

http_code = 200
code = 'SkipSave'

def __init__(self):
super().__init__()
self.fields['message'] = 'No validation errors were encountered.'
8 changes: 6 additions & 2 deletions binder/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,12 @@ class Meta:
abstract = True
ordering = ['pk']

def save(self, *args, **kwargs):
self.full_clean() # Never allow saving invalid models!
def save(self, *args, only_validate=False, **kwargs):
# A validation model might not require all validation checks as it is not a full model
# _validation_model can be used to skip validation checks that are meant for complete models that are actually being saved
self._validation_model = only_validate # Set the model as a validation model when we only want to validate the model

self.full_clean() # Never allow saving invalid models!
return super().save(*args, **kwargs)


Expand Down
1 change: 1 addition & 0 deletions binder/plugins/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .html_field import HtmlField # noqa: F401
157 changes: 157 additions & 0 deletions binder/plugins/models/html_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from typing import List

from django.db.models import TextField
from html.parser import HTMLParser
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _

ALLOWED_LINK_PREFIXES = [
'http://',
'https://',
'mailto:'
]


def link_rel_validator(tag, attribute_name, attribute_value) -> List[ValidationError]:
validation_errors = []

rels = attribute_value.split(' ')

if 'noopener' not in rels:

validation_errors.append(ValidationError(
_('Link needs rel="noopener"'),
code='invalid_attribute',
params={
'tag': tag,
},
))

if 'noreferrer' not in rels:
validation_errors.append(ValidationError(
_('Link needs rel="noreferer"'),
code='invalid_attribute',
params={
'tag': tag,
},
))


return validation_errors


def link_validator(tag, attribute_name, attribute_value) -> List[ValidationError]:
validation_errors = []
if not any(map(lambda prefix: attribute_value.startswith(prefix), ALLOWED_LINK_PREFIXES)):
validation_errors.append(ValidationError(
_('Link is not valid'),
code='invalid_attribute',
params={
'tag': tag,
},
))
return validation_errors


class HtmlValidator(HTMLParser):
allowed_tags = [
# General setup
'p', 'br',
# Headers
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7',

# text decoration
'b', 'strong', 'i', 'em', 'u',
# Lists
'ol', 'ul', 'li',

# Special
'a',
]

allowed_attributes = {
'a': ['href', 'rel', 'target']
}

required_attributes = {
'a': ['rel'],
}

special_validators = {
('a', 'href'): link_validator,
('a', 'rel'): link_rel_validator,
}

error_messages = {
'invalid_tag': _('Tag %(tag)s is not allowed'),
'missing_attribute': _('Attribute %(attribute)s is required for tag %(tag)s'),
'invalid_attribute': _('Attribute %(attribute)s not allowed for tag %(tag)s'),
}

def validate(self, value: str) -> List[ValidationError]:
"""
Validates html, and gives a list of validation errors
"""

self.errors = []

self.feed(value)

return self.errors

def handle_starttag(self, tag: str, attrs: list) -> None:
tag_errors = []
if tag not in self.allowed_tags:
tag_errors.append(ValidationError(
self.error_messages['invalid_tag'],
code='invalid_tag',
params={
'tag': tag
},
))

set_attributes = set(map(lambda attr: attr[0], attrs))
required_attributes = set(self.required_attributes.get(tag, []))
missing_attributes = required_attributes - set_attributes
for missing_attribute in missing_attributes:
tag_errors.append(
ValidationError(
self.error_messages['missing_attribute'],
code='missing_attribute',
params={
'tag': tag,
'attribute': missing_attribute
},
)
)

allowed_attributes_for_tag = self.allowed_attributes.get(tag, [])

for (attribute_name, attribute_content) in attrs:
if attribute_name not in allowed_attributes_for_tag:
tag_errors.append(ValidationError(
self.error_messages['invalid_attribute'],
code='invalid_attribute',
params={
'tag': tag,
'attribute': attribute_name
},
))
if (tag, attribute_name) in self.special_validators:
tag_errors += self.special_validators[(tag, attribute_name)](tag, attribute_name, attribute_content)

self.errors += tag_errors


class HtmlField(TextField):
"""
Determine a safe way to save "secure" user provided HTML input, and prevent XSS injections
"""

def validate(self, value: str, _):
# Validate all html tags
validator = HtmlValidator()
errors = validator.validate(value)

if errors:
raise ValidationError(errors)
17 changes: 14 additions & 3 deletions binder/plugins/views/csvexport.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def __init__(self, request: HttpRequest):
# self.writer = self.pandas.ExcelWriter(self.response)

self.work_book = self.openpyxl.Workbook()
self.sheet = self.work_book.create_sheet()
self.sheet = self.work_book._sheets[0]

# The row number we are currently writing to
self._row_number = 0
Expand Down Expand Up @@ -285,12 +285,23 @@ def get_datum(data, key, prefix=''):
else:
# Assume that we have a mapping now
fk_ids = data[head_key]
if type(fk_ids) != list:

if fk_ids is None:
# This case happens if we have a nullable foreign key that is null. Treat this as a many
# to one relation with no values.
fk_ids = []
elif type(fk_ids) != list:
fk_ids = [fk_ids]

# if head_key not in key_mapping:
prefix_key = parent_data['with_mapping'][new_prefix[1:]]
datums = [str(get_datum(key_mapping[prefix_key][fk_id], subkey, new_prefix)) for fk_id in fk_ids]
datums = []
for fk_id in fk_ids:
try:
datums.append(str(get_datum(key_mapping[prefix_key][fk_id], subkey, new_prefix)))
except KeyError:
pass
# datums = [str(get_datum(key_mapping[prefix_key][fk_id], subkey, new_prefix)) for fk_id in fk_ids]
return self.csv_settings.multi_value_delimiter.join(
datums
)
Expand Down
Loading