django-sqrl-2/sqrl/fields.py

213 lines
6.1 KiB
Python

# -*- coding: utf-8 -*-
from collections import OrderedDict
from django import forms
from django.urls import Resolver404, resolve
from django.core.validators import URLValidator
from django.http import QueryDict
from urllib.parse import urlparse, unquote
from .utils import Base64
class NextUrlField(forms.CharField):
"""
Custom ``CharField`` which validates that a value is a valid next URL.
It validates that by checking that the value can be resolved to a view
hence guaranteeing that when redirected URL will not fail.
"""
default_error_messages = {
'invalid_url': 'Invalid next url.'
}
def to_python(self, value):
"""
Validate that value is a valid URL for this project.
"""
value = super(NextUrlField, self).to_python(value)
if value in self.empty_values:
return value
path = urlparse(value).path
try:
resolve(path)
except Resolver404:
raise forms.ValidationError(self.error_messages['invalid_url'])
else:
return path
class ExtractedNextUrlField(NextUrlField):
"""
Similar to :obj:`.NextUrlField` however this extracts next url from full encoded URL.
"""
default_error_messages = {
'missing_next': 'Missing next query parameter.'
}
def to_python(self, value):
"""
Extract next url from full URL string and then use :obj:`.NextUrlField`
to validate that value is valid URL.
"""
value = forms.CharField.to_python(self, value)
if value in self.empty_values:
return value
decoded = urlparse(unquote(value))
data = QueryDict(decoded.query)
if 'next' not in data:
raise forms.ValidationError(self.error_messages['missing_next'])
return super(ExtractedNextUrlField, self).to_python(data['next'])
class SQRLURLValidator(URLValidator):
"""
Custom URL validator which validates that a URL is a valid SQRL url.
These are the differences with regular HTTP URLs:
* scheme is either sqrl (secure) and qrl (non-secure)
* ``:`` is a valid path separator which can be used to indicate
which section of the SQRL should be used to generate
public/provate keypair for the domain.
Okay, but is there support for qrl schema???
"""
schemes = ['sqrl', 'qrl']
# schemes = ['sqrl']
class SQRLURLField(forms.URLField):
"""
SQRL URL field which uses :obj:`.SQRLURLValidator` for validation.
"""
default_validators = [SQRLURLValidator()]
class Base64Field(forms.CharField):
"""
Field which decodes base64 values using :meth:`.utils.Base64.decode`.
"""
default_error_messages = {
'base64': 'Invalid value. Must be base64url encoded string.',
}
def to_python(self, value):
"""
Decodes base64 value and returns binary data.
"""
value = super(Base64Field, self).to_python(value)
if not value:
return b''
try:
return Base64.decode(value)
except (ValueError, TypeError):
raise forms.ValidationError(self.error_messages['base64'])
class Base64CharField(Base64Field):
"""
Similar to :obj:`.Base64Field` however this field normalizes to ``str`` (``unicode``) data.
"""
default_error_messages = {
'base64_ascii': 'Invalid value. Must be ascii base64url encoded string.',
}
def to_python(self, value):
"""
Returns base64 decoded data as string.
Uses :meth:`.Base64Field.to_python` to decode base64 value
which returns binary data and then this method further
decodes ascii data to return ``str`` (``unicode``) data.
"""
value = super(Base64CharField, self).to_python(value)
if not value:
return ''
try:
return value.decode('ascii')
except UnicodeDecodeError:
raise forms.ValidationError(self.error_messages['base64_ascii'])
class Base64PairsField(Base64CharField):
"""
Field which normalizes base64 encoded multistring key-value pairs to ``OrderedDict``.
Attributes
----------
always_pairs : bool
Boolean which enforces that the value must always be keypairs.
When ``False`` and the value is not a keypair, the value itself
is returned.
"""
default_error_messages = {
'crlf': 'Invalid value. Must be multi-line string separated by CRLF.',
'pairs': 'Invalid value. Must be multi-line string of pair of values.',
}
always_pairs = True
def to_python(self, value):
"""
Normalizes multiline base64 keypairs string to ``OrderedDict``.
"""
value = super(Base64PairsField, self).to_python(value)
if not value:
return OrderedDict()
if not value.endswith('\r\n'):
if self.always_pairs:
raise forms.ValidationError(self.error_messages['crlf'])
else:
return value
try:
return OrderedDict(
line.split('=', 1) for line in filter(None, value.splitlines())
)
except ValueError:
raise forms.ValidationError(self.error_messages['pairs'])
class Base64ConditionalPairsField(Base64PairsField):
"""
Similar to :obj:`.Base64PairsField` but this field does not force
the value to be keypairs.
"""
always_pairs = False
class TildeMultipleValuesField(forms.CharField):
"""
Field which returns tilde-separated list.
"""
def to_python(self, value):
"""
Normalizes to a Python list by splitting string by tilde (~) delimiter.
"""
value = super(TildeMultipleValuesField, self).to_python(value)
if not value:
return []
return value.split('~')
class TildeMultipleValuesFieldChoiceField(TildeMultipleValuesField, forms.ChoiceField):
"""
Similar to :obj:`.TildeMultipleValuesField` however this field also validates
each value to be a valid choice.
"""
def validate(self, value):
for i in value:
super(TildeMultipleValuesFieldChoiceField, self).validate(i)