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 import_libravatar.py
requirements.txt requirements.txt
static/admin/* static/admin/*
static/humans.txt **/static/humans.txt
static/img/robots.txt **/static/img/robots.txt
ivatar/ivataraccount/read_libravatar_export.py
templates/maintenance.html
encryption_test.py
libravatarproxy.py
[html] [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"]

229
config.py
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,84 +1,122 @@
''' # -*- coding: utf-8 -*-
"""
Reading libravatar export Reading libravatar export
''' """
import binascii import binascii
import os
from io import BytesIO from io import BytesIO
import gzip import gzip
import xml.etree.ElementTree import xml.etree.ElementTree
import base64 import base64
from PIL import Image 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): def read_gzdata(gzdata=None):
''' """
Read gzipped data file Read gzipped data file
''' """
emails = [] # pylint: disable=invalid-name emails = [] # pylint: disable=invalid-name
openids = [] # pylint: disable=invalid-name openids = [] # pylint: disable=invalid-name
photos = [] # pylint: disable=invalid-name photos = [] # pylint: disable=invalid-name
username = None # pylint: disable=invalid-name username = None # pylint: disable=invalid-name
password = None # pylint: disable=invalid-name password = None # pylint: disable=invalid-name
if not gzdata: if not gzdata:
return False 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() content = fh.read()
fh.close() fh.close()
root = xml.etree.ElementTree.fromstring(content) root = xml.etree.ElementTree.fromstring(content)
if not root.tag == '{%s}user' % SCHEMAROOT: if not root.tag == "{%s}user" % SCHEMAROOT:
print('Unknown export format: %s' % root.tag) print("Unknown export format: %s" % root.tag)
exit(-1) exit(-1)
# Username # Username
for item in root.findall('{%s}account' % SCHEMAROOT)[0].items(): for item in root.findall("{%s}account" % SCHEMAROOT)[0].items():
if item[0] == 'username': if item[0] == "username":
username = item[1] username = item[1]
if item[0] == 'password': if item[0] == "password":
password = item[1] password = item[1]
# Emails # Emails
for email in root.findall('{%s}emails' % SCHEMAROOT)[0]: for email in root.findall("{%s}emails" % SCHEMAROOT)[0]:
if email.tag == '{%s}email' % SCHEMAROOT: if email.tag == "{%s}email" % SCHEMAROOT:
emails.append({'email': email.text, 'photo_id': email.attrib['photo_id']}) emails.append({"email": email.text, "photo_id": email.attrib["photo_id"]})
# OpenIDs # OpenIDs
for openid in root.findall('{%s}openids' % SCHEMAROOT)[0]: for openid in root.findall("{%s}openids" % SCHEMAROOT)[0]:
if openid.tag == '{%s}openid' % SCHEMAROOT: if openid.tag == "{%s}openid" % SCHEMAROOT:
openids.append({'openid': openid.text, 'photo_id': openid.attrib['photo_id']}) openids.append(
{"openid": openid.text, "photo_id": openid.attrib["photo_id"]}
)
# Photos # Photos
for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]: for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]:
if photo.tag == '{%s}photo' % SCHEMAROOT: if photo.tag == "{%s}photo" % SCHEMAROOT:
try: 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: except binascii.Error as exc:
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % ( print(
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc)) "Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s"
% (
photo.attrib["encoding"],
photo.attrib["format"],
photo.attrib["id"],
exc,
)
)
continue continue
try: try:
Image.open(BytesIO(data)) Image.open(BytesIO(data))
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % ( print(
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc)) "Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s"
% (
photo.attrib["encoding"],
photo.attrib["format"],
photo.attrib["id"],
exc,
)
)
continue continue
else: else:
# If it is a working image, we can use it # If it is a working image, we can use it
photo.text.replace('\n', '') photo.text.replace("\n", "")
photos.append({ photos.append(
'data': photo.text, {
'format': photo.attrib['format'], "data": photo.text,
'id': photo.attrib['id'], "format": photo.attrib["format"],
}) "id": photo.attrib["id"],
}
)
return { return {
'emails': emails, "emails": emails,
'openids': openids, "openids": openids,
'photos': photos, "photos": photos,
'username': username, "username": username,
'password': password, "password": password,
} }

View File

@@ -1,4 +1,4 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
@@ -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> <h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
{% for email in emails %} {% for email in emails %}
<div class="checkbox"> <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> </div>
{% endfor %} {% endfor %}
{% endif %} {% 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 URLs for ivatar.ivataraccount
''' """
from django.urls import path from django.urls import path
from django.conf.urls import url 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 LogoutView
from django.contrib.auth.views import PasswordResetDoneView,\ from django.contrib.auth.views import (
PasswordResetConfirmView, PasswordResetCompleteView PasswordResetDoneView,
PasswordResetConfirmView,
PasswordResetCompleteView,
)
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
from django.contrib.auth.decorators import login_required
from . views import ProfileView, PasswordResetView from .views import ProfileView, PasswordResetView
from . views import CreateView, PasswordSetView, AddEmailView from .views import CreateView, PasswordSetView, AddEmailView
from . views import RemoveUnconfirmedEmailView, ConfirmEmailView from .views import RemoveUnconfirmedEmailView, ConfirmEmailView
from . views import RemoveConfirmedEmailView, AssignPhotoEmailView from .views import RemoveConfirmedEmailView, AssignPhotoEmailView
from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView from .views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
from . views import ImportPhotoView, RawImageView, DeletePhotoView from .views import ImportPhotoView, RawImageView, DeletePhotoView
from . views import UploadPhotoView, AssignPhotoOpenIDView from .views import UploadPhotoView, AssignPhotoOpenIDView
from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView from .views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView
from . views import CropPhotoView from .views import CropPhotoView
from . views import UserPreferenceView, UploadLibravatarExportView from .views import UserPreferenceView, UploadLibravatarExportView
from . views import ResendConfirmationMailView from .views import ResendConfirmationMailView
from . views import IvatarLoginView from .views import IvatarLoginView
from . views import DeleteAccountView from .views import DeleteAccountView
from .views import ExportView
# Define URL patterns, self documenting # Define URL patterns, self documenting
# To see the fancy, colorful evaluation of these use: # To see the fancy, colorful evaluation of these use:
# ./manager show_urls # ./manager show_urls
urlpatterns = [ # pylint: disable=invalid-name urlpatterns = [ # pylint: disable=invalid-name
path('new/', CreateView.as_view(), name='new_account'), path("new/", CreateView.as_view(), name="new_account"),
path('login/', IvatarLoginView.as_view(), name='login'), path("login/", IvatarLoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(next_page="/"), name="logout"),
path( path(
'logout/', LogoutView.as_view(next_page='/'), "password_change/",
name='logout'), PasswordChangeView.as_view(template_name="password_change.html"),
name="password_change",
path('password_change/', ),
PasswordChangeView.as_view(template_name='password_change.html'), path(
name='password_change'), "password_change/done/",
path('password_change/done/', PasswordChangeDoneView.as_view(template_name="password_change_done.html"),
PasswordChangeDoneView.as_view(template_name='password_change_done.html'), name="password_change_done",
name='password_change_done'), ),
path(
path('password_reset/', "password_reset/",
PasswordResetView.as_view(template_name='password_reset.html'), PasswordResetView.as_view(template_name="password_reset.html"),
name='password_reset'), name="password_reset",
path('password_reset/done/', ),
PasswordResetDoneView.as_view( path(
template_name='password_reset_submitted.html'), "password_reset/done/",
name='password_reset_done'), PasswordResetDoneView.as_view(template_name="password_reset_submitted.html"),
path('reset/<uidb64>/<token>/', name="password_reset_done",
PasswordResetConfirmView.as_view( ),
template_name='password_change.html'), path(
name='password_reset_confirm'), "reset/<uidb64>/<token>/",
path('reset/done/', PasswordResetConfirmView.as_view(template_name="password_change.html"),
PasswordResetCompleteView.as_view( name="password_reset_confirm",
template_name='password_change_done.html'), ),
name='password_reset_complete'), path(
"reset/done/",
path('export/', login_required( PasswordResetCompleteView.as_view(template_name="password_change_done.html"),
TemplateView.as_view(template_name='export.html') name="password_reset_complete",
), name='export'), ),
path('delete/', DeleteAccountView.as_view(), name='delete'), path(
path('profile/', ProfileView.as_view(), name='profile'), "export/",
url('profile/(?P<profile_username>.+)', ProfileView.as_view(), name='profile_with_profile_username'), ExportView.as_view(),
path('add_email/', AddEmailView.as_view(), name='add_email'), name="export",
path('add_openid/', AddOpenIDView.as_view(), name='add_openid'), ),
path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'), path("delete/", DeleteAccountView.as_view(), name="delete"),
path('password_set/', PasswordSetView.as_view(), name='password_set'), path("profile/", ProfileView.as_view(), name="profile"),
url( 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(), RemoveUnconfirmedOpenIDView.as_view(),
name='remove_unconfirmed_openid'), name="remove_unconfirmed_openid",
),
url( url(
r'remove_confirmed_openid/(?P<openid_id>\d+)', r"remove_confirmed_openid/(?P<openid_id>\d+)",
RemoveConfirmedOpenIDView.as_view(), name='remove_confirmed_openid'), RemoveConfirmedOpenIDView.as_view(),
name="remove_confirmed_openid",
),
url( url(
r'openid_redirection/(?P<openid_id>\d+)', r"openid_redirection/(?P<openid_id>\d+)",
RedirectOpenIDView.as_view(), name='openid_redirection'), RedirectOpenIDView.as_view(),
name="openid_redirection",
),
url( url(
r'confirm_openid/(?P<openid_id>\w+)', r"confirm_openid/(?P<openid_id>\w+)",
ConfirmOpenIDView.as_view(), name='confirm_openid'), ConfirmOpenIDView.as_view(),
name="confirm_openid",
),
url( url(
r'confirm_email/(?P<verification_key>\w+)', r"confirm_email/(?P<verification_key>\w+)",
ConfirmEmailView.as_view(), name='confirm_email'), ConfirmEmailView.as_view(),
name="confirm_email",
),
url( url(
r'remove_unconfirmed_email/(?P<email_id>\d+)', r"remove_unconfirmed_email/(?P<email_id>\d+)",
RemoveUnconfirmedEmailView.as_view(), name='remove_unconfirmed_email'), RemoveUnconfirmedEmailView.as_view(),
name="remove_unconfirmed_email",
),
url( url(
r'remove_confirmed_email/(?P<email_id>\d+)', r"remove_confirmed_email/(?P<email_id>\d+)",
RemoveConfirmedEmailView.as_view(), name='remove_confirmed_email'), RemoveConfirmedEmailView.as_view(),
name="remove_confirmed_email",
),
url( url(
r'assign_photo_email/(?P<email_id>\d+)', r"assign_photo_email/(?P<email_id>\d+)",
AssignPhotoEmailView.as_view(), name='assign_photo_email'), AssignPhotoEmailView.as_view(),
name="assign_photo_email",
),
url( url(
r'assign_photo_openid/(?P<openid_id>\d+)', r"assign_photo_openid/(?P<openid_id>\d+)",
AssignPhotoOpenIDView.as_view(), name='assign_photo_openid'), AssignPhotoOpenIDView.as_view(),
name="assign_photo_openid",
),
url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
url( url(
r'import_photo/$', r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
ImportPhotoView.as_view(), name='import_photo'), ImportPhotoView.as_view(),
name="import_photo",
),
url( url(
r'import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)', r"import_photo/(?P<email_id>\d+)",
ImportPhotoView.as_view(), name='import_photo'), 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( url(
r'import_photo/(?P<email_id>\d+)', r"upload_export/(?P<save>save)$",
ImportPhotoView.as_view(), name='import_photo'), UploadLibravatarExportView.as_view(),
name="upload_export",
),
url( url(
r'delete_photo/(?P<pk>\d+)', r"resend_confirmation_mail/(?P<email_id>\d+)",
DeletePhotoView.as_view(), name='delete_photo'), ResendConfirmationMailView.as_view(),
url(r'raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'), name="resend_confirmation_mail",
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'),
] ]

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,25 +2,36 @@ autopep8
bcrypt bcrypt
defusedxml defusedxml
Django Django
django-anymail[mailgun]
django-auth-ldap django-auth-ldap
django-bootstrap4 django-bootstrap4
django-coverage-plugin django-coverage-plugin
django-extensions django-extensions
django-ipware django-ipware
django-user-accounts django-user-accounts
email-validator
fabric fabric
flake8-respect-noqa 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/django-openid-auth
git+https://github.com/ofalk/monsterid.git
git+https://github.com/ofalk/Robohash.git@devel
mysqlclient
notsetuptools
pagan
Pillow Pillow
pip pip
psycopg2-binary
py3dns py3dns
pydocstyle pydocstyle
pyLibravatar pyLibravatar
pylint pylint
PyMySQL PyMySQL
python3-openid
python-coveralls python-coveralls
python-language-server python-language-server
python-memcached
python3-openid
pytz pytz
rope rope
setuptools setuptools
@@ -28,13 +39,3 @@ six
social-auth-app-django social-auth-app-django
wheel wheel
yapf 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 '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 '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 '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_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 '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> <li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>