v1.5 massive (code) update

This commit is contained in:
Oliver Falk
2021-09-16 08:28:49 +00:00
parent 12d69576af
commit 355af2351d
16 changed files with 2377 additions and 1731 deletions

View File

@@ -7,8 +7,12 @@ omit =
import_libravatar.py
requirements.txt
static/admin/*
static/humans.txt
static/img/robots.txt
**/static/humans.txt
**/static/img/robots.txt
ivatar/ivataraccount/read_libravatar_export.py
templates/maintenance.html
encryption_test.py
libravatarproxy.py
[html]

6
.flake8 Normal file
View File

@@ -0,0 +1,6 @@
[flake8]
ignore = E501, W503, E402
max-line-length = 79
max-complexity = 18
select = B,C,E,F,W,T4,B9
exclude = .git,__pycache__,.virtualenv

74
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,74 @@
fail_fast: true
repos:
- repo: meta
hooks:
- id: check-useless-excludes
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.4.0
hooks:
- id: prettier
files: \.(css|js|md|markdown|json)
- repo: https://github.com/python/black
rev: 21.9b0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-added-large-files
- id: check-ast
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-vcs-permalinks
- id: check-xml
- id: check-yaml
args:
- --unsafe
- id: end-of-file-fixer
- id: fix-encoding-pragma
- id: forbid-new-submodules
- id: no-commit-to-branch
args:
- --branch
- gh-pages
- id: requirements-txt-fixer
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
- repo: local
hooks:
- id: shfmt
name: shfmt
minimum_pre_commit_version: 2.4.0
language: golang
additional_dependencies:
- mvdan.cc/sh/v3/cmd/shfmt@v3.1.1
entry: shfmt
args:
- -w
- -i
- '4'
types:
- shell
- repo: https://github.com/asottile/blacken-docs
rev: v1.11.0
hooks:
- id: blacken-docs
- repo: https://github.com/hcodes/yaspeller.git
rev: v7.0.0
hooks:
- id: yaspeller
types:
- markdown
- repo: https://github.com/kadrach/pre-commit-gitlabci-lint
rev: 22d0495c9894e8b27cc37c2ed5d9a6b46385a44c
hooks:
- id: gitlabci-lint
args: ["https://git.linux-kernel.at"]

223
config.py
View File

@@ -1,6 +1,7 @@
''' yes
# -*- coding: utf-8 -*-
"""
Configuration overrides for settings.py
'''
"""
import os
import sys
@@ -14,50 +15,61 @@ from ivatar.settings import INSTALLED_APPS
from ivatar.settings import TEMPLATES
ADMIN_USERS = []
ALLOWED_HOSTS = ['*']
ALLOWED_HOSTS = ["*"]
INSTALLED_APPS.extend([
'django_extensions',
'django_openid_auth',
'bootstrap4',
'anymail',
'ivatar',
'ivatar.ivataraccount',
'ivatar.tools',
])
INSTALLED_APPS.extend(
[
"django_extensions",
"django_openid_auth",
"bootstrap4",
"anymail",
"ivatar",
"ivatar.ivataraccount",
"ivatar.tools",
]
)
MIDDLEWARE.extend([
'django.middleware.locale.LocaleMiddleware',
])
MIDDLEWARE.extend(
[
"django.middleware.locale.LocaleMiddleware",
]
)
MIDDLEWARE.insert(
0, 'ivatar.middleware.MultipleProxyMiddleware',
0,
"ivatar.middleware.MultipleProxyMiddleware",
)
AUTHENTICATION_BACKENDS = (
# Enable this to allow LDAP authentication.
# See INSTALL for more information.
# 'django_auth_ldap.backend.LDAPBackend',
'django_openid_auth.auth.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
"django_openid_auth.auth.OpenIDBackend",
"django.contrib.auth.backends.ModelBackend",
)
TEMPLATES[0]['DIRS'].extend([
os.path.join(BASE_DIR, 'templates'),
])
TEMPLATES[0]['OPTIONS']['context_processors'].append(
'ivatar.context_processors.basepage',
TEMPLATES[0]["DIRS"].extend(
[
os.path.join(BASE_DIR, "templates"),
]
)
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
"ivatar.context_processors.basepage",
)
OPENID_CREATE_USERS = True
OPENID_UPDATE_DETAILS_FROM_SREG = True
SITE_NAME = os.environ.get('SITE_NAME', 'libravatar')
IVATAR_VERSION = '1.4'
SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
IVATAR_VERSION = "1.5"
SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/')
BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/')
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
LOGIN_REDIRECT_URL = reverse_lazy('profile')
SECURE_BASE_URL = os.environ.get(
"SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/"
)
BASE_URL = os.environ.get("BASE_URL", "http://avatars.linux-kernel.at/avatar/")
LOGIN_REDIRECT_URL = reverse_lazy("profile")
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
MAX_NUM_PHOTOS = 5
@@ -75,119 +87,120 @@ MIN_LENGTH_EMAIL = 6 # eg. x@x.xx
MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
BOOTSTRAP4 = {
'include_jquery': False,
'javascript_in_head': False,
'css_url': {
'href': '/static/css/bootstrap.min.css',
'integrity':
'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB',
'crossorigin': 'anonymous',
"include_jquery": False,
"javascript_in_head": False,
"css_url": {
"href": "/static/css/bootstrap.min.css",
"integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB",
"crossorigin": "anonymous",
},
'javascript_url': {
'url': '/static/js/bootstrap.min.js',
'integrity': '',
'crossorigin': 'anonymous',
"javascript_url": {
"url": "/static/js/bootstrap.min.js",
"integrity": "",
"crossorigin": "anonymous",
},
'popper_url': {
'url': '/static/js/popper.min.js',
'integrity':
'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49',
'crossorigin': 'anonymous',
"popper_url": {
"url": "/static/js/popper.min.js",
"integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49",
"crossorigin": "anonymous",
},
}
if 'EMAIL_BACKEND' in os.environ:
EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] # pragma: no cover
if "EMAIL_BACKEND" in os.environ:
EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] # pragma: no cover
else:
if 'test' in sys.argv or 'collectstatic' in sys.argv:
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
if "test" in sys.argv or "collectstatic" in sys.argv:
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
else:
try:
ANYMAIL = { # pragma: no cover
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
"MAILGUN_API_KEY": os.environ["IVATAR_MAILGUN_API_KEY"],
"MAILGUN_SENDER_DOMAIN": os.environ["IVATAR_MAILGUN_SENDER_DOMAIN"],
}
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
except Exception as exc: # pragma: nocover
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover
except Exception: # pragma: nocover # pylint: disable=broad-except
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'ivatar@mg.linux-kernel.at')
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'ivatar@mg.linux-kernel.at')
SERVER_EMAIL = os.environ.get("SERVER_EMAIL", "ivatar@mg.linux-kernel.at")
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "ivatar@mg.linux-kernel.at")
try:
from ivatar.settings import DATABASES
except ImportError: # pragma: no cover
DATABASES = [] # pragma: no cover
if 'default' not in DATABASES:
DATABASES['default'] = { # pragma: no cover
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
if "default" not in DATABASES:
DATABASES["default"] = { # pragma: no cover
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
if 'MYSQL_DATABASE' in os.environ:
DATABASES['default'] = { # pragma: no cover
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ['MYSQL_DATABASE'],
'USER': os.environ['MYSQL_USER'],
'PASSWORD': os.environ['MYSQL_PASSWORD'],
'HOST': 'mysql',
if "MYSQL_DATABASE" in os.environ:
DATABASES["default"] = { # pragma: no cover
"ENGINE": "django.db.backends.mysql",
"NAME": os.environ["MYSQL_DATABASE"],
"USER": os.environ["MYSQL_USER"],
"PASSWORD": os.environ["MYSQL_PASSWORD"],
"HOST": "mysql",
}
if 'POSTGRESQL_DATABASE' in os.environ:
DATABASES['default'] = { # pragma: no cover
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['POSTGRESQL_DATABASE'],
'USER': os.environ['POSTGRESQL_USER'],
'PASSWORD': os.environ['POSTGRESQL_PASSWORD'],
'HOST': 'postgresql',
if "POSTGRESQL_DATABASE" in os.environ:
DATABASES["default"] = { # pragma: no cover
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["POSTGRESQL_DATABASE"],
"USER": os.environ["POSTGRESQL_USER"],
"PASSWORD": os.environ["POSTGRESQL_PASSWORD"],
"HOST": "postgresql",
}
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer"
USE_X_FORWARDED_HOST = True
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',]
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [
"avatars.linux-kernel.at",
"localhost",
]
DEFAULT_AVATAR_SIZE = 80
LANGUAGES = (
('de', _('Deutsch')),
('en', _('English')),
('ca', _('Català')),
('cs', _('Česky')),
('es', _('Español')),
('eu', _('Basque')),
('fr', _('Français')),
('it', _('Italiano')),
('ja', _('日本語')),
('nl', _('Nederlands')),
('pt', _('Português')),
('ru', _('Русский')),
('sq', _('Shqip')),
('tr', _('Türkçe')),
('uk', _('Українська')),
("de", _("Deutsch")),
("en", _("English")),
("ca", _("Català")),
("cs", _("Česky")),
("es", _("Español")),
("eu", _("Basque")),
("fr", _("Français")),
("it", _("Italiano")),
("ja", _("日本語")),
("nl", _("Nederlands")),
("pt", _("Português")),
("ru", _("Русский")),
("sq", _("Shqip")),
("tr", _("Türkçe")),
("uk", _("Українська")),
)
MESSAGE_TAGS = {
message_constants.DEBUG: 'debug',
message_constants.INFO: 'info',
message_constants.SUCCESS: 'success',
message_constants.WARNING: 'warning',
message_constants.ERROR: 'danger',
message_constants.DEBUG: "debug",
message_constants.INFO: "info",
message_constants.SUCCESS: "success",
message_constants.WARNING: "warning",
message_constants.ERROR: "danger",
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': [
'127.0.0.1:11211',
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": [
"127.0.0.1:11211",
],
},
'filesystem': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/ivatar_cache',
'TIMEOUT': 900, # 15 minutes
}
"filesystem": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/ivatar_cache",
"TIMEOUT": 900, # 15 minutes
},
}
# This is 5 minutes caching for generated/resized images,
@@ -197,5 +210,5 @@ CACHE_IMAGES_MAX_AGE = 5 * 60
CACHE_RESPONSE = True
# This MUST BE THE LAST!
if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')):
if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")):
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover

View File

@@ -1,4 +1,6 @@
'''
# -*- coding: utf-8 -*-
"""
Module init
'''
"""
app_label = __name__ # pylint: disable=invalid-name

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Default: useful variables for the base page templates.
'''
"""
from ipware import get_client_ip
from ivatar.settings import IVATAR_VERSION, SITE_NAME, MAX_PHOTO_SIZE
@@ -9,27 +10,28 @@ from ivatar.settings import MAX_NUM_UNCONFIRMED_EMAILS
def basepage(request):
'''
"""
Our contextprocessor adds additional context variables
in order to be used in the templates
'''
"""
context = {}
if 'openid_identifier' in request.GET:
context['openid_identifier'] = \
request.GET['openid_identifier'] # pragma: no cover
if "openid_identifier" in request.GET:
context["openid_identifier"] = request.GET[
"openid_identifier"
] # pragma: no cover
client_ip = get_client_ip(request)[0]
context['client_ip'] = client_ip
context['ivatar_version'] = IVATAR_VERSION
context['site_name'] = SITE_NAME
context['site_url'] = request.build_absolute_uri('/')[:-1]
context['max_file_size'] = MAX_PHOTO_SIZE
context['BASE_URL'] = BASE_URL
context['SECURE_BASE_URL'] = SECURE_BASE_URL
context['max_emails'] = False
context["client_ip"] = client_ip
context["ivatar_version"] = IVATAR_VERSION
context["site_name"] = SITE_NAME
context["site_url"] = request.build_absolute_uri("/")[:-1]
context["max_file_size"] = MAX_PHOTO_SIZE
context["BASE_URL"] = BASE_URL
context["SECURE_BASE_URL"] = SECURE_BASE_URL
context["max_emails"] = False
if request.user:
if not request.user.is_anonymous:
unconfirmed = request.user.unconfirmedemail_set.count()
if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS:
context['max_emails'] = True
context["max_emails"] = True
return context

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Our models for ivatar.ivataraccount
'''
"""
import base64
import hashlib
@@ -37,48 +38,49 @@ from .gravatar import get_photo as get_gravatar_photo
def file_format(image_type):
'''
"""
Helper method returning a short image type
'''
if image_type == 'JPEG':
return 'jpg'
elif image_type == 'PNG':
return 'png'
elif image_type == 'GIF':
return 'gif'
"""
if image_type == "JPEG":
return "jpg"
elif image_type == "PNG":
return "png"
elif image_type == "GIF":
return "gif"
return None
def pil_format(image_type):
'''
"""
Helper method returning the 'encoder name' for PIL
'''
if image_type == 'jpg' or image_type == 'jpeg':
return 'JPEG'
elif image_type == 'png':
return 'PNG'
elif image_type == 'gif':
return 'GIF'
"""
if image_type == "jpg" or image_type == "jpeg":
return "JPEG"
elif image_type == "png":
return "PNG"
elif image_type == "gif":
return "GIF"
logger.info('Unsupported file format: %s', image_type)
logger.info("Unsupported file format: %s", image_type)
return None
class UserPreference(models.Model):
'''
"""
Holds the user users preferences
'''
"""
THEMES = (
('default', 'Default theme'),
('clime', 'climes theme'),
('green', 'green theme'),
('red', 'red theme'),
("default", "Default theme"),
("clime", "climes theme"),
("green", "green theme"),
("red", "red theme"),
)
theme = models.CharField(
max_length=10,
choices=THEMES,
default='default',
default="default",
)
user = models.OneToOneField(
@@ -88,13 +90,14 @@ class UserPreference(models.Model):
)
def __str__(self):
return 'Preference (%i) for %s' % (self.pk, self.user)
return "Preference (%i) for %s" % (self.pk, self.user)
class BaseAccountModel(models.Model):
'''
"""
Base, abstract model, holding fields we use in all cases
'''
"""
user = models.ForeignKey(
User,
on_delete=models.deletion.CASCADE,
@@ -103,40 +106,43 @@ class BaseAccountModel(models.Model):
add_date = models.DateTimeField(default=timezone.now)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Class attributes
'''
"""
abstract = True
class Photo(BaseAccountModel):
'''
"""
Model holding the photos and information about them
'''
"""
ip_address = models.GenericIPAddressField(unpack_ipv4=True)
data = models.BinaryField()
format = models.CharField(max_length=3)
access_count = models.BigIntegerField(default=0, editable=False)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Class attributes
'''
verbose_name = _('photo')
verbose_name_plural = _('photos')
"""
verbose_name = _("photo")
verbose_name_plural = _("photos")
def import_image(self, service_name, email_address):
'''
"""
Allow to import image from other (eg. Gravatar) service
'''
"""
image_url = False
if service_name == 'Gravatar':
if service_name == "Gravatar":
gravatar = get_gravatar_photo(email_address)
if gravatar:
image_url = gravatar['image_url']
image_url = gravatar["image_url"]
if service_name == 'Libravatar':
if service_name == "Libravatar":
image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE)
if not image_url:
@@ -146,13 +152,12 @@ class Photo(BaseAccountModel):
# No idea how to test this
# pragma: no cover
except HTTPError as exc:
print('%s import failed with an HTTP error: %s' %
(service_name, exc.code))
print("%s import failed with an HTTP error: %s" % (service_name, exc.code))
return False
# No idea how to test this
# pragma: no cover
except URLError as exc:
print('%s import failed: %s' % (service_name, exc.reason))
print("%s import failed: %s" % (service_name, exc.reason))
return False
data = image.read()
@@ -164,35 +169,36 @@ class Photo(BaseAccountModel):
self.format = file_format(img.format)
if not self.format:
print('Unable to determine format: %s' % img) # pragma: no cover
print("Unable to determine format: %s" % img) # pragma: no cover
return False # pragma: no cover
self.data = data
super().save()
return True
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
'''
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
"""
Override save from parent, taking care about the image
'''
"""
# Use PIL to read the file format
try:
img = Image.open(BytesIO(self.data))
# Testing? Ideas anyone?
except Exception as exc: # pylint: disable=broad-except
# For debugging only
print('Exception caught in Photo.save(): %s' % exc)
print("Exception caught in Photo.save(): %s" % exc)
return False
self.format = file_format(img.format)
if not self.format:
print('Format not recognized')
print("Format not recognized")
return False
return super().save(force_insert, force_update, using, update_fields)
def perform_crop(self, request, dimensions, email, openid):
'''
"""
Helper to crop the image
'''
"""
if request.user.photo_set.count() == 1:
# This is the first photo, assign to all confirmed addresses
for addr in request.user.confirmedemail_set.all():
@@ -217,34 +223,40 @@ class Photo(BaseAccountModel):
img = Image.open(BytesIO(self.data))
# This should be anyway checked during save...
dimensions['a'], \
dimensions['b'] = img.size # pylint: disable=invalid-name
if dimensions['a'] > MAX_PIXELS or dimensions['b'] > MAX_PIXELS:
dimensions["a"], dimensions["b"] = img.size # pylint: disable=invalid-name
if dimensions["a"] > MAX_PIXELS or dimensions["b"] > MAX_PIXELS:
messages.error(
request,
_('Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s' % {
max_pixels: MAX_PIXELS,
}))
return HttpResponseRedirect(reverse_lazy('profile'))
_(
"Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s"
% {
"max_pixels": MAX_PIXELS,
}
),
)
return HttpResponseRedirect(reverse_lazy("profile"))
if dimensions['w'] == 0 and dimensions['h'] == 0:
dimensions['w'], dimensions['h'] = dimensions['a'], dimensions['b']
min_from_w_h = min(dimensions['w'], dimensions['h'])
dimensions['w'], dimensions['h'] = min_from_w_h, min_from_w_h
elif ((dimensions['w'] < 0)
or ((dimensions['x'] + dimensions['w']) > dimensions['a'])
or (dimensions['h'] < 0)
or ((dimensions['y'] + dimensions['h']) > dimensions['b'])):
messages.error(
request,
_('Crop outside of original image bounding box'))
return HttpResponseRedirect(reverse_lazy('profile'))
if dimensions["w"] == 0 and dimensions["h"] == 0:
dimensions["w"], dimensions["h"] = dimensions["a"], dimensions["b"]
min_from_w_h = min(dimensions["w"], dimensions["h"])
dimensions["w"], dimensions["h"] = min_from_w_h, min_from_w_h
elif (
(dimensions["w"] < 0)
or ((dimensions["x"] + dimensions["w"]) > dimensions["a"])
or (dimensions["h"] < 0)
or ((dimensions["y"] + dimensions["h"]) > dimensions["b"])
):
messages.error(request, _("Crop outside of original image bounding box"))
return HttpResponseRedirect(reverse_lazy("profile"))
cropped = img.crop((
dimensions['x'],
dimensions['y'],
dimensions['x'] + dimensions['w'],
dimensions['y'] + dimensions['h']))
cropped = img.crop(
(
dimensions["x"],
dimensions["y"],
dimensions["x"] + dimensions["w"],
dimensions["y"] + dimensions["h"],
)
)
# cropped.load()
# Resize the image only if it's larger than the specified max width.
cropped_w, cropped_h = cropped.size
@@ -260,26 +272,26 @@ class Photo(BaseAccountModel):
self.data = data.read()
self.save()
return HttpResponseRedirect(reverse_lazy('profile'))
return HttpResponseRedirect(reverse_lazy("profile"))
def __str__(self):
return '%s (%i) from %s' % (self.format, self.pk or 0, self.user)
return "%s (%i) from %s" % (self.format, self.pk or 0, self.user)
# pylint: disable=too-few-public-methods
class ConfirmedEmailManager(models.Manager):
'''
"""
Manager for our confirmed email addresses model
'''
"""
@staticmethod
def create_confirmed_email(user, email_address, is_logged_in):
'''
"""
Helper method to create confirmed email address
'''
"""
confirmed = ConfirmedEmail()
confirmed.user = user
confirmed.ip_address = '0.0.0.0'
confirmed.ip_address = "0.0.0.0"
confirmed.email = email_address
confirmed.save()
@@ -293,14 +305,15 @@ class ConfirmedEmailManager(models.Manager):
class ConfirmedEmail(BaseAccountModel):
'''
"""
Model holding our confirmed email addresses, as well as the relation
to the assigned photo
'''
"""
email = models.EmailField(unique=True, max_length=MAX_LENGTH_EMAIL)
photo = models.ForeignKey(
Photo,
related_name='emails',
related_name="emails",
blank=True,
null=True,
on_delete=models.deletion.SET_NULL,
@@ -311,123 +324,129 @@ class ConfirmedEmail(BaseAccountModel):
access_count = models.BigIntegerField(default=0, editable=False)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Class attributes
'''
verbose_name = _('confirmed email')
verbose_name_plural = _('confirmed emails')
"""
verbose_name = _("confirmed email")
verbose_name_plural = _("confirmed emails")
def set_photo(self, photo):
'''
"""
Helper method to set photo
'''
"""
self.photo = photo
self.save()
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
'''
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
"""
Override save from parent, add digest
'''
"""
self.digest = hashlib.md5(
self.email.strip().lower().encode('utf-8')
self.email.strip().lower().encode("utf-8")
).hexdigest()
self.digest_sha256 = hashlib.sha256(
self.email.strip().lower().encode('utf-8')
self.email.strip().lower().encode("utf-8")
).hexdigest()
return super().save(force_insert, force_update, using, update_fields)
def __str__(self):
return '%s (%i) from %s' % (self.email, self.pk, self.user)
return "%s (%i) from %s" % (self.email, self.pk, self.user)
class UnconfirmedEmail(BaseAccountModel):
'''
"""
Model holding unconfirmed email addresses as well as the verification key
'''
"""
email = models.EmailField(max_length=MAX_LENGTH_EMAIL)
verification_key = models.CharField(max_length=64)
last_send_date = models.DateTimeField(null=True, blank=True)
last_status = models.TextField(max_length=2047, null=True, blank=True)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Class attributes
'''
verbose_name = _('unconfirmed email')
verbose_name_plural = _('unconfirmed emails')
"""
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
verbose_name = _("unconfirmed email")
verbose_name_plural = _("unconfirmed emails")
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
if not self.verification_key:
hash_object = hashlib.new('sha256')
hash_object = hashlib.new("sha256")
hash_object.update(
urandom(1024) + self.user.username.encode('utf-8') # pylint: disable=no-member
urandom(1024)
+ self.user.username.encode("utf-8") # pylint: disable=no-member
) # pylint: disable=no-member
self.verification_key = hash_object.hexdigest()
super(UnconfirmedEmail, self).save(
force_insert,
force_update,
using,
update_fields)
force_insert, force_update, using, update_fields
)
def send_confirmation_mail(self, url=SECURE_BASE_URL):
'''
"""
Send confirmation mail to that mail address
'''
link = url + \
reverse(
'confirm_email',
kwargs={'verification_key': self.verification_key})
email_subject = _('Confirm your email address on %s') % \
SITE_NAME
email_body = render_to_string('email_confirmation.txt', {
'verification_link': link,
'site_name': SITE_NAME,
})
"""
link = url + reverse(
"confirm_email", kwargs={"verification_key": self.verification_key}
)
email_subject = _("Confirm your email address on %s") % SITE_NAME
email_body = render_to_string(
"email_confirmation.txt",
{
"verification_link": link,
"site_name": SITE_NAME,
},
)
self.last_send_date = timezone.now()
self.last_status = 'OK'
self.last_status = "OK"
# if settings.DEBUG:
# print('DEBUG: %s' % link)
try:
send_mail(
email_subject, email_body, DEFAULT_FROM_EMAIL,
[self.email])
send_mail(email_subject, email_body, DEFAULT_FROM_EMAIL, [self.email])
except Exception as e:
self.last_status = "%s" % e
self.save()
return True
def __str__(self):
return '%s (%i) from %s' % (self.email, self.pk, self.user)
return "%s (%i) from %s" % (self.email, self.pk, self.user)
class UnconfirmedOpenId(BaseAccountModel):
'''
"""
Model holding unconfirmed OpenIDs
'''
"""
openid = models.URLField(unique=False, max_length=MAX_LENGTH_URL)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Meta class
'''
verbose_name = _('unconfirmed OpenID')
verbose_name_plural = ('unconfirmed_OpenIDs')
"""
verbose_name = _("unconfirmed OpenID")
verbose_name_plural = "unconfirmed_OpenIDs"
def __str__(self):
return '%s (%i) from %s' % (self.openid, self.pk, self.user)
return "%s (%i) from %s" % (self.openid, self.pk, self.user)
class ConfirmedOpenId(BaseAccountModel):
'''
"""
Model holding confirmed OpenIDs, as well as the relation to
the assigned photo
'''
"""
openid = models.URLField(unique=True, max_length=MAX_LENGTH_URL)
photo = models.ForeignKey(
Photo,
related_name='openids',
related_name="openids",
blank=True,
null=True,
on_delete=models.deletion.SET_NULL,
@@ -444,25 +463,27 @@ class ConfirmedOpenId(BaseAccountModel):
access_count = models.BigIntegerField(default=0, editable=False)
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Meta class
'''
verbose_name = _('confirmed OpenID')
verbose_name_plural = _('confirmed OpenIDs')
"""
verbose_name = _("confirmed OpenID")
verbose_name_plural = _("confirmed OpenIDs")
def set_photo(self, photo):
'''
"""
Helper method to save photo
'''
"""
self.photo = photo
self.save()
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
url = urlsplit(self.openid)
if url.username: # pragma: no cover
password = url.password or ''
netloc = url.username + ':' + password + '@' + url.hostname
password = url.password or ""
netloc = url.username + ":" + password + "@" + url.hostname
else:
netloc = url.hostname
lowercase_url = urlunsplit(
@@ -470,37 +491,44 @@ class ConfirmedOpenId(BaseAccountModel):
)
self.openid = lowercase_url
self.digest = hashlib.sha256(openid_variations(lowercase_url)[0].encode('utf-8')).hexdigest()
self.alt_digest1 = hashlib.sha256(openid_variations(lowercase_url)[1].encode('utf-8')).hexdigest()
self.alt_digest2 = hashlib.sha256(openid_variations(lowercase_url)[2].encode('utf-8')).hexdigest()
self.alt_digest3 = hashlib.sha256(openid_variations(lowercase_url)[3].encode('utf-8')).hexdigest()
self.digest = hashlib.sha256(
openid_variations(lowercase_url)[0].encode("utf-8")
).hexdigest()
self.alt_digest1 = hashlib.sha256(
openid_variations(lowercase_url)[1].encode("utf-8")
).hexdigest()
self.alt_digest2 = hashlib.sha256(
openid_variations(lowercase_url)[2].encode("utf-8")
).hexdigest()
self.alt_digest3 = hashlib.sha256(
openid_variations(lowercase_url)[3].encode("utf-8")
).hexdigest()
return super().save(force_insert, force_update, using, update_fields)
def __str__(self):
return '%s (%i) (%s)' % (self.openid, self.pk, self.user)
return "%s (%i) (%s)" % (self.openid, self.pk, self.user)
class OpenIDNonce(models.Model):
'''
"""
Model holding OpenID Nonces
See also: https://github.com/edx/django-openid-auth/
'''
"""
server_url = models.CharField(max_length=255)
timestamp = models.IntegerField()
salt = models.CharField(max_length=128)
def __str__(self):
return '%s (%i) (timestamp: %i)' % (
self.server_url,
self.pk,
self.timestamp)
return "%s (%i) (timestamp: %i)" % (self.server_url, self.pk, self.timestamp)
class OpenIDAssociation(models.Model):
'''
"""
Model holding the relation/association about OpenIDs
'''
"""
server_url = models.TextField(max_length=2047)
handle = models.CharField(max_length=255)
secret = models.TextField(max_length=255) # stored base64 encoded
@@ -509,56 +537,62 @@ class OpenIDAssociation(models.Model):
assoc_type = models.TextField(max_length=64)
def __str__(self):
return '%s (%i) (%s, lifetime: %i)' % (
return "%s (%i) (%s, lifetime: %i)" % (
self.server_url,
self.pk,
self.assoc_type,
self.lifetime)
self.lifetime,
)
class DjangoOpenIDStore(OpenIDStore):
'''
"""
The Python openid library needs an OpenIDStore subclass to persist data
related to OpenID authentications. This one uses our Django models.
'''
"""
@staticmethod
def storeAssociation(server_url, association): # pragma: no cover
'''
"""
Helper method to store associations
'''
"""
assoc = OpenIDAssociation(
server_url=server_url,
handle=association.handle,
secret=base64.encodebytes(association.secret),
issued=association.issued,
lifetime=association.issued,
assoc_type=association.assoc_type)
assoc_type=association.assoc_type,
)
assoc.save()
def getAssociation(self, server_url, handle=None): # pragma: no cover
'''
"""
Helper method to get associations
'''
"""
assocs = []
if handle is not None:
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
server_url=server_url,
handle=handle)
server_url=server_url, handle=handle
)
else:
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
server_url=server_url)
server_url=server_url
)
if not assocs:
return None
associations = []
for assoc in assocs:
if isinstance(assoc.secret, str):
assoc.secret = assoc.secret.split("b'")[1].split("'")[0]
assoc.secret = bytes(assoc.secret, 'utf-8')
association = OIDAssociation(assoc.handle,
assoc.secret = bytes(assoc.secret, "utf-8")
association = OIDAssociation(
assoc.handle,
base64.decodebytes(assoc.secret),
assoc.issued, assoc.lifetime,
assoc.assoc_type)
assoc.issued,
assoc.lifetime,
assoc.assoc_type,
)
expires = 0
try:
# pylint: disable=no-member
@@ -575,12 +609,14 @@ class DjangoOpenIDStore(OpenIDStore):
@staticmethod
def removeAssociation(server_url, handle): # pragma: no cover
'''
"""
Helper method to remove associations
'''
"""
assocs = list(
OpenIDAssociation.objects.filter( # pylint: disable=no-member
server_url=server_url, handle=handle))
server_url=server_url, handle=handle
)
)
assocs_exist = len(assocs) > 0
for assoc in assocs:
assoc.delete()
@@ -588,9 +624,9 @@ class DjangoOpenIDStore(OpenIDStore):
@staticmethod
def useNonce(server_url, timestamp, salt): # pragma: no cover
'''
"""
Helper method to 'use' nonces
'''
"""
# Has nonce expired?
if abs(timestamp - time.time()) > oidnonce.SKEW:
return False
@@ -598,27 +634,30 @@ class DjangoOpenIDStore(OpenIDStore):
nonce = OpenIDNonce.objects.get( # pylint: disable=no-member
server_url__exact=server_url,
timestamp__exact=timestamp,
salt__exact=salt)
salt__exact=salt,
)
except ObjectDoesNotExist:
nonce = OpenIDNonce.objects.create( # pylint: disable=no-member
server_url=server_url, timestamp=timestamp, salt=salt)
server_url=server_url, timestamp=timestamp, salt=salt
)
return True
nonce.delete()
return False
@staticmethod
def cleanupNonces(): # pragma: no cover
'''
"""
Helper method to cleanup nonces
'''
"""
timestamp = int(time.time()) - oidnonce.SKEW
# pylint: disable=no-member
OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete()
@staticmethod
def cleanupAssociations(): # pragma: no cover
'''
"""
Helper method to cleanup associations
'''
"""
OpenIDAssociation.objects.extra( # pylint: disable=no-member
where=['issued + lifetimeint < (%s)' % time.time()]).delete()
where=["issued + lifetimeint < (%s)" % time.time()]
).delete()

View File

@@ -1,22 +1,37 @@
'''
# -*- coding: utf-8 -*-
"""
Reading libravatar export
'''
"""
import binascii
import os
from io import BytesIO
import gzip
import xml.etree.ElementTree
import base64
from PIL import Image
import django
import sys
sys.path.append(
os.path.join(
os.path.dirname(__file__),
"..",
"..",
)
)
SCHEMAROOT = 'https://www.libravatar.org/schemas/export/0.2'
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup()
# pylint: disable=wrong-import-position
from ivatar.settings import SCHEMAROOT
def read_gzdata(gzdata=None):
'''
"""
Read gzipped data file
'''
"""
emails = [] # pylint: disable=invalid-name
openids = [] # pylint: disable=invalid-name
photos = [] # pylint: disable=invalid-name
@@ -26,59 +41,82 @@ def read_gzdata(gzdata=None):
if not gzdata:
return False
fh = gzip.open(BytesIO(gzdata), 'rb') # pylint: disable=invalid-name
fh = gzip.open(BytesIO(gzdata), "rb") # pylint: disable=invalid-name
content = fh.read()
fh.close()
root = xml.etree.ElementTree.fromstring(content)
if not root.tag == '{%s}user' % SCHEMAROOT:
print('Unknown export format: %s' % root.tag)
if not root.tag == "{%s}user" % SCHEMAROOT:
print("Unknown export format: %s" % root.tag)
exit(-1)
# Username
for item in root.findall('{%s}account' % SCHEMAROOT)[0].items():
if item[0] == 'username':
for item in root.findall("{%s}account" % SCHEMAROOT)[0].items():
if item[0] == "username":
username = item[1]
if item[0] == 'password':
if item[0] == "password":
password = item[1]
# Emails
for email in root.findall('{%s}emails' % SCHEMAROOT)[0]:
if email.tag == '{%s}email' % SCHEMAROOT:
emails.append({'email': email.text, 'photo_id': email.attrib['photo_id']})
for email in root.findall("{%s}emails" % SCHEMAROOT)[0]:
if email.tag == "{%s}email" % SCHEMAROOT:
emails.append({"email": email.text, "photo_id": email.attrib["photo_id"]})
# OpenIDs
for openid in root.findall('{%s}openids' % SCHEMAROOT)[0]:
if openid.tag == '{%s}openid' % SCHEMAROOT:
openids.append({'openid': openid.text, 'photo_id': openid.attrib['photo_id']})
for openid in root.findall("{%s}openids" % SCHEMAROOT)[0]:
if openid.tag == "{%s}openid" % SCHEMAROOT:
openids.append(
{"openid": openid.text, "photo_id": openid.attrib["photo_id"]}
)
# Photos
for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]:
if photo.tag == '{%s}photo' % SCHEMAROOT:
for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]:
if photo.tag == "{%s}photo" % SCHEMAROOT:
try:
data = base64.decodebytes(bytes(photo.text, 'utf-8'))
# Safty measures to make sure we do not try to parse
# a binary encoded string
photo.text = photo.text.strip("'")
photo.text = photo.text.strip("\\n")
photo.text = photo.text.lstrip("b'")
data = base64.decodebytes(bytes(photo.text, "utf-8"))
except binascii.Error as exc:
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
print(
"Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s"
% (
photo.attrib["encoding"],
photo.attrib["format"],
photo.attrib["id"],
exc,
)
)
continue
try:
Image.open(BytesIO(data))
except Exception as exc: # pylint: disable=broad-except
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
print(
"Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s"
% (
photo.attrib["encoding"],
photo.attrib["format"],
photo.attrib["id"],
exc,
)
)
continue
else:
# If it is a working image, we can use it
photo.text.replace('\n', '')
photos.append({
'data': photo.text,
'format': photo.attrib['format'],
'id': photo.attrib['id'],
})
photo.text.replace("\n", "")
photos.append(
{
"data": photo.text,
"format": photo.attrib["format"],
"id": photo.attrib["id"],
}
)
return {
'emails': emails,
'openids': openids,
'photos': photos,
'username': username,
'password': password,
"emails": emails,
"openids": openids,
"photos": photos,
"username": username,
"password": password,
}

View File

@@ -23,7 +23,7 @@ input[type=checkbox].image:checked + label:before {letter-spacing: 3px}
<h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
{% for email in emails %}
<div class="checkbox">
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email }}</label>
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email.email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email.email }}</label>
</div>
{% endfor %}
{% endif %}

View File

@@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans 'Export your data' %}{% endblock title %}
{% block content %}
<h1>{% trans 'Export your data' %}</h1>
<p>{% trans 'Libravatar will now export all of your personal data to a compressed XML file.' %}</p>
<form action="{% url 'export' %}" method="post" name="export">{% csrf_token %}
<p><button type="submit" class="button">{% trans 'Export' %}</button>&nbsp;
<a href="{% url 'profile' %}" class="button">{% trans 'Cancel' %}</a>
</p>
</form>
{% endblock content %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,119 +1,155 @@
'''
# -*- coding: utf-8 -*-
"""
URLs for ivatar.ivataraccount
'''
"""
from django.urls import path
from django.conf.urls import url
from django.views.generic import TemplateView
from django.contrib.auth.views import LogoutView
from django.contrib.auth.views import PasswordResetDoneView,\
PasswordResetConfirmView, PasswordResetCompleteView
from django.contrib.auth.views import (
PasswordResetDoneView,
PasswordResetConfirmView,
PasswordResetCompleteView,
)
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
from django.contrib.auth.decorators import login_required
from . views import ProfileView, PasswordResetView
from . views import CreateView, PasswordSetView, AddEmailView
from . views import RemoveUnconfirmedEmailView, ConfirmEmailView
from . views import RemoveConfirmedEmailView, AssignPhotoEmailView
from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
from . views import ImportPhotoView, RawImageView, DeletePhotoView
from . views import UploadPhotoView, AssignPhotoOpenIDView
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
from . views import CropPhotoView
from . views import UserPreferenceView, UploadLibravatarExportView
from . views import ResendConfirmationMailView
from . views import IvatarLoginView
from . views import DeleteAccountView
from .views import ProfileView, PasswordResetView
from .views import CreateView, PasswordSetView, AddEmailView
from .views import RemoveUnconfirmedEmailView, ConfirmEmailView
from .views import RemoveConfirmedEmailView, AssignPhotoEmailView
from .views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
from .views import ImportPhotoView, RawImageView, DeletePhotoView
from .views import UploadPhotoView, AssignPhotoOpenIDView
from .views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
from .views import CropPhotoView
from .views import UserPreferenceView, UploadLibravatarExportView
from .views import ResendConfirmationMailView
from .views import IvatarLoginView
from .views import DeleteAccountView
from .views import ExportView
# Define URL patterns, self documenting
# To see the fancy, colorful evaluation of these use:
# ./manager show_urls
urlpatterns = [ # pylint: disable=invalid-name
path('new/', CreateView.as_view(), name='new_account'),
path('login/', IvatarLoginView.as_view(), name='login'),
path("new/", CreateView.as_view(), name="new_account"),
path("login/", IvatarLoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(next_page="/"), name="logout"),
path(
'logout/', LogoutView.as_view(next_page='/'),
name='logout'),
path('password_change/',
PasswordChangeView.as_view(template_name='password_change.html'),
name='password_change'),
path('password_change/done/',
PasswordChangeDoneView.as_view(template_name='password_change_done.html'),
name='password_change_done'),
path('password_reset/',
PasswordResetView.as_view(template_name='password_reset.html'),
name='password_reset'),
path('password_reset/done/',
PasswordResetDoneView.as_view(
template_name='password_reset_submitted.html'),
name='password_reset_done'),
path('reset/<uidb64>/<token>/',
PasswordResetConfirmView.as_view(
template_name='password_change.html'),
name='password_reset_confirm'),
path('reset/done/',
PasswordResetCompleteView.as_view(
template_name='password_change_done.html'),
name='password_reset_complete'),
path('export/', login_required(
TemplateView.as_view(template_name='export.html')
), name='export'),
path('delete/', DeleteAccountView.as_view(), name='delete'),
path('profile/', ProfileView.as_view(), name='profile'),
url('profile/(?P<profile_username>.+)', ProfileView.as_view(), name='profile_with_profile_username'),
path('add_email/', AddEmailView.as_view(), name='add_email'),
path('add_openid/', AddOpenIDView.as_view(), name='add_openid'),
path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'),
path('password_set/', PasswordSetView.as_view(), name='password_set'),
"password_change/",
PasswordChangeView.as_view(template_name="password_change.html"),
name="password_change",
),
path(
"password_change/done/",
PasswordChangeDoneView.as_view(template_name="password_change_done.html"),
name="password_change_done",
),
path(
"password_reset/",
PasswordResetView.as_view(template_name="password_reset.html"),
name="password_reset",
),
path(
"password_reset/done/",
PasswordResetDoneView.as_view(template_name="password_reset_submitted.html"),
name="password_reset_done",
),
path(
"reset/<uidb64>/<token>/",
PasswordResetConfirmView.as_view(template_name="password_change.html"),
name="password_reset_confirm",
),
path(
"reset/done/",
PasswordResetCompleteView.as_view(template_name="password_change_done.html"),
name="password_reset_complete",
),
path(
"export/",
ExportView.as_view(),
name="export",
),
path("delete/", DeleteAccountView.as_view(), name="delete"),
path("profile/", ProfileView.as_view(), name="profile"),
url(
r'remove_unconfirmed_openid/(?P<openid_id>\d+)',
"profile/(?P<profile_username>.+)",
ProfileView.as_view(),
name="profile_with_profile_username",
),
path("add_email/", AddEmailView.as_view(), name="add_email"),
path("add_openid/", AddOpenIDView.as_view(), name="add_openid"),
path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"),
path("password_set/", PasswordSetView.as_view(), name="password_set"),
url(
r"remove_unconfirmed_openid/(?P<openid_id>\d+)",
RemoveUnconfirmedOpenIDView.as_view(),
name='remove_unconfirmed_openid'),
name="remove_unconfirmed_openid",
),
url(
r'remove_confirmed_openid/(?P<openid_id>\d+)',
RemoveConfirmedOpenIDView.as_view(), name='remove_confirmed_openid'),
r"remove_confirmed_openid/(?P<openid_id>\d+)",
RemoveConfirmedOpenIDView.as_view(),
name="remove_confirmed_openid",
),
url(
r'openid_redirection/(?P<openid_id>\d+)',
RedirectOpenIDView.as_view(), name='openid_redirection'),
r"openid_redirection/(?P<openid_id>\d+)",
RedirectOpenIDView.as_view(),
name="openid_redirection",
),
url(
r'confirm_openid/(?P<openid_id>\w+)',
ConfirmOpenIDView.as_view(), name='confirm_openid'),
r"confirm_openid/(?P<openid_id>\w+)",
ConfirmOpenIDView.as_view(),
name="confirm_openid",
),
url(
r'confirm_email/(?P<verification_key>\w+)',
ConfirmEmailView.as_view(), name='confirm_email'),
r"confirm_email/(?P<verification_key>\w+)",
ConfirmEmailView.as_view(),
name="confirm_email",
),
url(
r'remove_unconfirmed_email/(?P<email_id>\d+)',
RemoveUnconfirmedEmailView.as_view(), name='remove_unconfirmed_email'),
r"remove_unconfirmed_email/(?P<email_id>\d+)",
RemoveUnconfirmedEmailView.as_view(),
name="remove_unconfirmed_email",
),
url(
r'remove_confirmed_email/(?P<email_id>\d+)',
RemoveConfirmedEmailView.as_view(), name='remove_confirmed_email'),
r"remove_confirmed_email/(?P<email_id>\d+)",
RemoveConfirmedEmailView.as_view(),
name="remove_confirmed_email",
),
url(
r'assign_photo_email/(?P<email_id>\d+)',
AssignPhotoEmailView.as_view(), name='assign_photo_email'),
r"assign_photo_email/(?P<email_id>\d+)",
AssignPhotoEmailView.as_view(),
name="assign_photo_email",
),
url(
r'assign_photo_openid/(?P<openid_id>\d+)',
AssignPhotoOpenIDView.as_view(), name='assign_photo_openid'),
r"assign_photo_openid/(?P<openid_id>\d+)",
AssignPhotoOpenIDView.as_view(),
name="assign_photo_openid",
),
url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
url(
r'import_photo/$',
ImportPhotoView.as_view(), name='import_photo'),
r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
ImportPhotoView.as_view(),
name="import_photo",
),
url(
r'import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)',
ImportPhotoView.as_view(), name='import_photo'),
r"import_photo/(?P<email_id>\d+)",
ImportPhotoView.as_view(),
name="import_photo",
),
url(r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"),
url(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"),
url(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"),
url(r"pref/$", UserPreferenceView.as_view(), name="user_preference"),
url(r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"),
url(
r'import_photo/(?P<email_id>\d+)',
ImportPhotoView.as_view(), name='import_photo'),
r"upload_export/(?P<save>save)$",
UploadLibravatarExportView.as_view(),
name="upload_export",
),
url(
r'delete_photo/(?P<pk>\d+)',
DeletePhotoView.as_view(), name='delete_photo'),
url(r'raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
url(r'crop_photo/(?P<pk>\d+)', CropPhotoView.as_view(), name='crop_photo'),
url(r'pref/$', UserPreferenceView.as_view(), name='user_preference'),
url(r'upload_export/$', UploadLibravatarExportView.as_view(), name='upload_export'),
url(r'upload_export/(?P<save>save)$',
UploadLibravatarExportView.as_view(), name='upload_export'),
url(r'resend_confirmation_mail/(?P<email_id>\d+)',
ResendConfirmationMailView.as_view(), name='resend_confirmation_mail'),
r"resend_confirmation_mail/(?P<email_id>\d+)",
ResendConfirmationMailView.as_view(),
name="resend_confirmation_mail",
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,14 @@
# -*- coding: utf-8 -*-
"""
Middleware classes
"""
from django.utils.deprecation import MiddlewareMixin
class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods
class MultipleProxyMiddleware(
MiddlewareMixin
): # pylint: disable=too-few-public-methods
"""
Middleware to rewrite proxy headers for deployments
with multiple proxies
@@ -14,5 +19,7 @@ class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-publi
Rewrites the proxy headers so that forwarded server is
used if available.
"""
if 'HTTP_X_FORWARDED_SERVER' in request.META:
request.META['HTTP_X_FORWARDED_HOST'] = request.META['HTTP_X_FORWARDED_SERVER']
if "HTTP_X_FORWARDED_SERVER" in request.META:
request.META["HTTP_X_FORWARDED_HOST"] = request.META[
"HTTP_X_FORWARDED_SERVER"
]

View File

@@ -2,25 +2,36 @@ autopep8
bcrypt
defusedxml
Django
django-anymail[mailgun]
django-auth-ldap
django-bootstrap4
django-coverage-plugin
django-extensions
django-ipware
django-user-accounts
email-validator
fabric
flake8-respect-noqa
git+https://github.com/ercpe/pydenticon5.git
git+https://github.com/flavono123/identicon.git
git+https://github.com/ofalk/django-openid-auth
git+https://github.com/ofalk/monsterid.git
git+https://github.com/ofalk/Robohash.git@devel
mysqlclient
notsetuptools
pagan
Pillow
pip
psycopg2-binary
py3dns
pydocstyle
pyLibravatar
pylint
PyMySQL
python3-openid
python-coveralls
python-language-server
python-memcached
python3-openid
pytz
rope
setuptools
@@ -28,13 +39,3 @@ six
social-auth-app-django
wheel
yapf
django-anymail[mailgun]
mysqlclient
psycopg2-binary
notsetuptools
git+https://github.com/ofalk/monsterid.git
git+https://github.com/ofalk/Robohash.git@devel
python-memcached
git+https://github.com/ercpe/pydenticon5.git
git+https://github.com/flavono123/identicon.git
pagan

View File

@@ -15,6 +15,7 @@
<li><a href="{% url 'user_preference' %}"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> {% trans 'Preferences' %}</a></li>
<li><a href="{% url 'import_photo' %}"><i class="fa fa-fw fa-envelope-square" aria-hidden="true"></i> {% trans 'Import photo via mail address' %}</a></li>
<li><a href="{% url 'upload_export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Import libravatar XML export' %}</a></li>
<li><a href="{% url 'export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Download your libravatar data' %}</a></li>
<li><a href="{% url 'password_change' %}"><i class="fa fa-fw fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li>
<li><a href="{% url 'password_reset' %}"><i class="fa fa-fw fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>