319 lines
11 KiB
Python
319 lines
11 KiB
Python
import base64
|
|
import importlib
|
|
import json
|
|
import random
|
|
import re
|
|
import string
|
|
import unicodedata
|
|
from collections import OrderedDict
|
|
from urllib.parse import urlsplit
|
|
|
|
import django
|
|
from django.contrib.auth import get_user_model
|
|
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
from django.core.validators import ValidationError, validate_email
|
|
from django.db.models import FileField
|
|
from django.db.models.fields import (
|
|
BinaryField,
|
|
DateField,
|
|
DateTimeField,
|
|
EmailField,
|
|
TimeField,
|
|
)
|
|
from django.utils import dateparse
|
|
from django.utils.encoding import force_bytes, force_str
|
|
|
|
from allauth import app_settings
|
|
|
|
|
|
# Magic number 7: if you run into collisions with this number, then you are
|
|
# of big enough scale to start investing in a decent user model...
|
|
MAX_USERNAME_SUFFIX_LENGTH = 7
|
|
USERNAME_SUFFIX_CHARS = [string.digits] * 4 + [string.ascii_letters] * (
|
|
MAX_USERNAME_SUFFIX_LENGTH - 4
|
|
)
|
|
|
|
|
|
def _generate_unique_username_base(txts, regex=None):
|
|
from .account.adapter import get_adapter
|
|
|
|
adapter = get_adapter()
|
|
username = None
|
|
regex = regex or r"[^\w\s@+.-]"
|
|
for txt in txts:
|
|
if not txt:
|
|
continue
|
|
username = unicodedata.normalize("NFKD", force_str(txt))
|
|
username = username.encode("ascii", "ignore").decode("ascii")
|
|
username = force_str(re.sub(regex, "", username).lower())
|
|
# Django allows for '@' in usernames in order to accomodate for
|
|
# project wanting to use e-mail for username. In allauth we don't
|
|
# use this, we already have a proper place for putting e-mail
|
|
# addresses (EmailAddress), so let's not use the full e-mail
|
|
# address and only take the part leading up to the '@'.
|
|
username = username.split("@")[0]
|
|
username = username.strip()
|
|
username = re.sub(r"\s+", "_", username)
|
|
# Finally, validating base username without database lookups etc.
|
|
try:
|
|
username = adapter.clean_username(username, shallow=True)
|
|
break
|
|
except ValidationError:
|
|
pass
|
|
return username or "user"
|
|
|
|
|
|
def get_username_max_length():
|
|
from .account.app_settings import USER_MODEL_USERNAME_FIELD
|
|
|
|
if USER_MODEL_USERNAME_FIELD is not None:
|
|
User = get_user_model()
|
|
max_length = User._meta.get_field(USER_MODEL_USERNAME_FIELD).max_length
|
|
else:
|
|
max_length = 0
|
|
return max_length
|
|
|
|
|
|
def generate_username_candidate(basename, suffix_length):
|
|
max_length = get_username_max_length()
|
|
suffix = "".join(
|
|
random.choice(USERNAME_SUFFIX_CHARS[i]) for i in range(suffix_length)
|
|
)
|
|
return basename[0 : max_length - len(suffix)] + suffix
|
|
|
|
|
|
def generate_username_candidates(basename):
|
|
from .account.app_settings import USERNAME_MIN_LENGTH
|
|
|
|
if len(basename) >= USERNAME_MIN_LENGTH:
|
|
ret = [basename]
|
|
else:
|
|
ret = []
|
|
min_suffix_length = max(1, USERNAME_MIN_LENGTH - len(basename))
|
|
max_suffix_length = min(get_username_max_length(), MAX_USERNAME_SUFFIX_LENGTH)
|
|
for suffix_length in range(min_suffix_length, max_suffix_length):
|
|
ret.append(generate_username_candidate(basename, suffix_length))
|
|
return ret
|
|
|
|
|
|
def generate_unique_username(txts, regex=None):
|
|
from allauth.account.utils import filter_users_by_username
|
|
|
|
from .account.adapter import get_adapter
|
|
from .account.app_settings import USER_MODEL_USERNAME_FIELD
|
|
|
|
adapter = get_adapter()
|
|
basename = _generate_unique_username_base(txts, regex)
|
|
candidates = generate_username_candidates(basename)
|
|
existing_usernames = filter_users_by_username(*candidates).values_list(
|
|
USER_MODEL_USERNAME_FIELD, flat=True
|
|
)
|
|
existing_usernames = set([n.lower() for n in existing_usernames])
|
|
for candidate in candidates:
|
|
if candidate.lower() not in existing_usernames:
|
|
try:
|
|
return adapter.clean_username(candidate, shallow=True)
|
|
except ValidationError:
|
|
pass
|
|
# This really should not happen
|
|
raise NotImplementedError("Unable to find a unique username")
|
|
|
|
|
|
def valid_email_or_none(email):
|
|
ret = None
|
|
try:
|
|
if email:
|
|
validate_email(email)
|
|
if len(email) <= EmailField().max_length:
|
|
ret = email
|
|
except ValidationError:
|
|
pass
|
|
return ret
|
|
|
|
|
|
def email_address_exists(email, exclude_user=None):
|
|
from .account import app_settings as account_settings
|
|
from .account.models import EmailAddress
|
|
|
|
emailaddresses = EmailAddress.objects
|
|
if exclude_user:
|
|
emailaddresses = emailaddresses.exclude(user=exclude_user)
|
|
ret = emailaddresses.filter(email__iexact=email).exists()
|
|
if not ret:
|
|
email_field = account_settings.USER_MODEL_EMAIL_FIELD
|
|
if email_field:
|
|
users = get_user_model().objects
|
|
if exclude_user:
|
|
users = users.exclude(pk=exclude_user.pk)
|
|
ret = users.filter(**{email_field + "__iexact": email}).exists()
|
|
return ret
|
|
|
|
|
|
def import_attribute(path):
|
|
assert isinstance(path, str)
|
|
pkg, attr = path.rsplit(".", 1)
|
|
ret = getattr(importlib.import_module(pkg), attr)
|
|
return ret
|
|
|
|
|
|
def import_callable(path_or_callable):
|
|
if not hasattr(path_or_callable, "__call__"):
|
|
ret = import_attribute(path_or_callable)
|
|
else:
|
|
ret = path_or_callable
|
|
return ret
|
|
|
|
|
|
SERIALIZED_DB_FIELD_PREFIX = "_db_"
|
|
|
|
|
|
def serialize_instance(instance):
|
|
"""
|
|
Since Django 1.6 items added to the session are no longer pickled,
|
|
but JSON encoded by default. We are storing partially complete models
|
|
in the session (user, account, token, ...). We cannot use standard
|
|
Django serialization, as these are models are not "complete" yet.
|
|
Serialization will start complaining about missing relations et al.
|
|
"""
|
|
data = {}
|
|
for k, v in instance.__dict__.items():
|
|
if k.startswith("_") or callable(v):
|
|
continue
|
|
try:
|
|
field = instance._meta.get_field(k)
|
|
if isinstance(field, BinaryField):
|
|
v = force_str(base64.b64encode(v))
|
|
elif isinstance(field, FileField):
|
|
if v and not isinstance(v, str):
|
|
v = v.name
|
|
# Check if the field is serializable. If not, we'll fall back
|
|
# to serializing the DB values which should cover most use cases.
|
|
try:
|
|
json.dumps(v, cls=DjangoJSONEncoder)
|
|
except TypeError:
|
|
v = field.get_prep_value(v)
|
|
k = SERIALIZED_DB_FIELD_PREFIX + k
|
|
except FieldDoesNotExist:
|
|
pass
|
|
data[k] = v
|
|
return json.loads(json.dumps(data, cls=DjangoJSONEncoder))
|
|
|
|
|
|
def deserialize_instance(model, data):
|
|
ret = model()
|
|
for k, v in data.items():
|
|
is_db_value = False
|
|
if k.startswith(SERIALIZED_DB_FIELD_PREFIX):
|
|
k = k[len(SERIALIZED_DB_FIELD_PREFIX) :]
|
|
is_db_value = True
|
|
if v is not None:
|
|
try:
|
|
f = model._meta.get_field(k)
|
|
if isinstance(f, DateTimeField):
|
|
v = dateparse.parse_datetime(v)
|
|
elif isinstance(f, TimeField):
|
|
v = dateparse.parse_time(v)
|
|
elif isinstance(f, DateField):
|
|
v = dateparse.parse_date(v)
|
|
elif isinstance(f, BinaryField):
|
|
v = force_bytes(base64.b64decode(force_bytes(v)))
|
|
elif is_db_value:
|
|
try:
|
|
# This is quite an ugly hack, but will cover most
|
|
# use cases...
|
|
# The signature of `from_db_value` changed in Django 3
|
|
# https://docs.djangoproject.com/en/3.0/releases/3.0/#features-removed-in-3-0
|
|
if django.VERSION < (3, 0):
|
|
v = f.from_db_value(v, None, None, None)
|
|
else:
|
|
v = f.from_db_value(v, None, None)
|
|
except Exception:
|
|
raise ImproperlyConfigured(
|
|
"Unable to auto serialize field '{}', custom"
|
|
" serialization override required".format(k)
|
|
)
|
|
except FieldDoesNotExist:
|
|
pass
|
|
setattr(ret, k, v)
|
|
return ret
|
|
|
|
|
|
def set_form_field_order(form, field_order):
|
|
"""
|
|
This function is a verbatim copy of django.forms.Form.order_fields() to
|
|
support field ordering below Django 1.9.
|
|
|
|
field_order is a list of field names specifying the order. Append fields
|
|
not included in the list in the default order for backward compatibility
|
|
with subclasses not overriding field_order. If field_order is None, keep
|
|
all fields in the order defined in the class. Ignore unknown fields in
|
|
field_order to allow disabling fields in form subclasses without
|
|
redefining ordering.
|
|
"""
|
|
if field_order is None:
|
|
return
|
|
fields = OrderedDict()
|
|
for key in field_order:
|
|
try:
|
|
fields[key] = form.fields.pop(key)
|
|
except KeyError: # ignore unknown fields
|
|
pass
|
|
fields.update(form.fields) # add remaining fields in original order
|
|
form.fields = fields
|
|
|
|
|
|
def build_absolute_uri(request, location, protocol=None):
|
|
"""request.build_absolute_uri() helper
|
|
|
|
Like request.build_absolute_uri, but gracefully handling
|
|
the case where request is None.
|
|
"""
|
|
from .account import app_settings as account_settings
|
|
|
|
if request is None:
|
|
if not app_settings.SITES_ENABLED:
|
|
raise ImproperlyConfigured(
|
|
"Passing `request=None` requires `sites` to be enabled."
|
|
)
|
|
from django.contrib.sites.models import Site
|
|
|
|
site = Site.objects.get_current()
|
|
bits = urlsplit(location)
|
|
if not (bits.scheme and bits.netloc):
|
|
uri = "{proto}://{domain}{url}".format(
|
|
proto=account_settings.DEFAULT_HTTP_PROTOCOL,
|
|
domain=site.domain,
|
|
url=location,
|
|
)
|
|
else:
|
|
uri = location
|
|
else:
|
|
uri = request.build_absolute_uri(location)
|
|
# NOTE: We only force a protocol if we are instructed to do so
|
|
# (via the `protocol` parameter, or, if the default is set to
|
|
# HTTPS. The latter keeps compatibility with the debatable use
|
|
# case of running your site under both HTTP and HTTPS, where one
|
|
# would want to make sure HTTPS links end up in password reset
|
|
# mails even while they were initiated on an HTTP password reset
|
|
# form.
|
|
if not protocol and account_settings.DEFAULT_HTTP_PROTOCOL == "https":
|
|
protocol = account_settings.DEFAULT_HTTP_PROTOCOL
|
|
# (end NOTE)
|
|
if protocol:
|
|
uri = protocol + ":" + uri.partition(":")[2]
|
|
return uri
|
|
|
|
|
|
def get_form_class(forms, form_id, default_form):
|
|
form_class = forms.get(form_id, default_form)
|
|
if isinstance(form_class, str):
|
|
form_class = import_attribute(form_class)
|
|
return form_class
|
|
|
|
|
|
def get_request_param(request, param, default=None):
|
|
if request is None:
|
|
return default
|
|
return request.POST.get(param) or request.GET.get(param, default)
|