Merge with master

This commit is contained in:
Oliver Falk
2022-02-22 13:55:26 +01:00
39 changed files with 3649 additions and 2605 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, C901
max-line-length = 79
max-complexity = 18
select = B,C,E,F,W,T4,B9
exclude = .git,__pycache__,.virtualenv

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@ db.sqlite3.SAVE
node_modules/
config_local.py
locale/*/LC_MESSAGES/django.mo
.DS_Store

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"]

242
config.py
View File

@@ -1,11 +1,12 @@
''' yes
# -*- coding: utf-8 -*-
"""
Configuration overrides for settings.py
'''
"""
import os
import sys
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.contrib.messages import constants as message_constants
from ivatar.settings import BASE_DIR
@@ -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.6"
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,
@@ -196,6 +209,23 @@ CACHE_IMAGES_MAX_AGE = 5 * 60
CACHE_RESPONSE = True
# Trusted URLs for default redirection
TRUSTED_DEFAULT_URLS = [
"https://ui-avatars.com/api/",
"http://gravatar.com/avatar/",
"https://gravatar.com/avatar/",
"http://www.gravatar.org/avatar/",
"https://www.gravatar.org/avatar/",
"https://secure.gravatar.com/avatar/",
"http://0.gravatar.com/avatar/",
"https://0.gravatar.com/avatar/",
"https://avatars.dicebear.com/api/",
"https://badges.fedoraproject.org/static/img/",
"http://www.planet-libre.org/themes/planetlibre/images/",
"https://www.azuracast.com/img/",
"https://reps.mozilla.org/static/base/img/remo/",
]
# 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,7 +1,8 @@
#!/usr/bin/env python
'''
# -*- coding: utf-8 -*-
"""
Import the whole libravatar export
'''
"""
import os
from os.path import isfile, isdir, join
@@ -9,13 +10,18 @@ import sys
import base64
from io import BytesIO
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ivatar.settings") # pylint: disable=wrong-import-position
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "ivatar.settings"
) # pylint: disable=wrong-import-position
django.setup() # pylint: disable=wrong-import-position
from django.contrib.auth.models import User
from PIL import Image
from django_openid_auth.models import UserOpenID
from ivatar.settings import JPEG_QUALITY
from ivatar.ivataraccount.read_libravatar_export import read_gzdata as libravatar_read_gzdata
from ivatar.ivataraccount.read_libravatar_export import (
read_gzdata as libravatar_read_gzdata,
)
from ivatar.ivataraccount.models import ConfirmedEmail
from ivatar.ivataraccount.models import ConfirmedOpenId
from ivatar.ivataraccount.models import Photo
@@ -26,54 +32,63 @@ if len(sys.argv) < 2:
exit(-255)
if not isdir(sys.argv[1]):
print("First argument to '%s' must be a directory containing the exports" % sys.argv[0])
print(
"First argument to '%s' must be a directory containing the exports"
% sys.argv[0]
)
exit(-255)
PATH = sys.argv[1]
for file in os.listdir(PATH):
if not file.endswith('.xml.gz'):
if not file.endswith(".xml.gz"):
continue
if isfile(join(PATH, file)):
fh = open(join(PATH, file), 'rb')
fh = open(join(PATH, file), "rb")
items = libravatar_read_gzdata(fh.read())
print('Adding user "%s"' % items['username'])
(user, created) = User.objects.get_or_create(username=items['username'])
user.password = items['password']
print('Adding user "%s"' % items["username"])
(user, created) = User.objects.get_or_create(username=items["username"])
user.password = items["password"]
user.save()
saved_photos = {}
for photo in items['photos']:
photo_id = photo['id']
data = base64.decodebytes(bytes(photo['data'], 'utf-8'))
for photo in items["photos"]:
photo_id = photo["id"]
data = base64.decodebytes(bytes(photo["data"], "utf-8"))
pilobj = Image.open(BytesIO(data))
out = BytesIO()
pilobj.save(out, pilobj.format, quality=JPEG_QUALITY)
out.seek(0)
photo = Photo()
photo.user = user
photo.ip_address = '0.0.0.0'
photo.ip_address = "0.0.0.0"
photo.format = file_format(pilobj.format)
photo.data = out.read()
photo.save()
saved_photos[photo_id] = photo
for email in items['emails']:
for email in items["emails"]:
try:
ConfirmedEmail.objects.get_or_create(email=email['email'], user=user,
photo=saved_photos.get(email['photo_id']))
except django.db.utils.IntegrityError:
print('%s not unique?' % email['email'])
for openid in items['openids']:
try:
ConfirmedOpenId.objects.get_or_create(openid=openid['openid'], user=user,
photo=saved_photos.get(openid['photo_id'])) # pylint: disable=no-member
UserOpenID.objects.get_or_create(
user_id=user.id,
claimed_id=openid['openid'],
display_id=openid['openid'],
ConfirmedEmail.objects.get_or_create(
email=email["email"],
user=user,
photo=saved_photos.get(email["photo_id"]),
)
except django.db.utils.IntegrityError:
print('%s not unique?' % openid['openid'])
print("%s not unique?" % email["email"])
for openid in items["openids"]:
try:
ConfirmedOpenId.objects.get_or_create(
openid=openid["openid"],
user=user,
photo=saved_photos.get(openid["photo_id"]),
) # pylint: disable=no-member
UserOpenID.objects.get_or_create(
user_id=user.id,
claimed_id=openid["openid"],
display_id=openid["openid"],
)
except django.db.utils.IntegrityError:
print("%s not unique?" % openid["openid"])
fh.close()

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,4 +1,5 @@
'''
# -*- coding: utf-8 -*-
"""
Module init
'''
"""
app_label = __name__ # pylint: disable=invalid-name

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Register models in admin
'''
"""
from django.contrib import admin
from .models import Photo, ConfirmedEmail, UnconfirmedEmail

View File

@@ -1,10 +1,11 @@
'''
# -*- coding: utf-8 -*-
"""
Classes for our ivatar.ivataraccount.forms
'''
"""
from urllib.parse import urlsplit, urlunsplit
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from ipware import get_client_ip
@@ -20,93 +21,97 @@ MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5
class AddEmailForm(forms.Form):
'''
"""
Form to handle adding email addresses
'''
"""
email = forms.EmailField(
label=_('Email'),
label=_("Email"),
min_length=MIN_LENGTH_EMAIL,
max_length=MAX_LENGTH_EMAIL,
)
def clean_email(self):
'''
"""
Enforce lowercase email
'''
"""
# TODO: Domain restriction as in libravatar?
return self.cleaned_data['email'].lower()
return self.cleaned_data["email"].lower()
def save(self, request):
'''
"""
Save the model, ensuring some safety
'''
"""
user = request.user
# Enforce the maximum number of unconfirmed emails a user can have
num_unconfirmed = user.unconfirmedemail_set.count()
max_num_unconfirmed_emails = getattr(
settings,
'MAX_NUM_UNCONFIRMED_EMAILS',
MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT)
settings, "MAX_NUM_UNCONFIRMED_EMAILS", MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
)
if num_unconfirmed >= max_num_unconfirmed_emails:
self.add_error(None, _('Too many unconfirmed mail addresses!'))
self.add_error(None, _("Too many unconfirmed mail addresses!"))
return False
# Check whether or not a confirmation email has been
# sent by this user already
if UnconfirmedEmail.objects.filter( # pylint: disable=no-member
user=user, email=self.cleaned_data['email']).exists():
self.add_error(
'email',
_('Address already added, currently unconfirmed'))
user=user, email=self.cleaned_data["email"]
).exists():
self.add_error("email", _("Address already added, currently unconfirmed"))
return False
# Check whether or not the email is already confirmed (by someone)
check_mail = ConfirmedEmail.objects.filter(
email=self.cleaned_data['email'])
check_mail = ConfirmedEmail.objects.filter(email=self.cleaned_data["email"])
if check_mail.exists():
msg = _('Address already confirmed (by someone else)')
msg = _("Address already confirmed (by someone else)")
if check_mail.first().user == request.user:
msg = _('Address already confirmed (by you)')
self.add_error('email', msg)
msg = _("Address already confirmed (by you)")
self.add_error("email", msg)
return False
unconfirmed = UnconfirmedEmail()
unconfirmed.email = self.cleaned_data['email']
unconfirmed.email = self.cleaned_data["email"]
unconfirmed.user = user
unconfirmed.save()
unconfirmed.send_confirmation_mail(url=request.build_absolute_uri('/')[:-1])
unconfirmed.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1])
return True
class UploadPhotoForm(forms.Form):
'''
"""
Form handling photo upload
'''
"""
photo = forms.FileField(
label=_('Photo'),
error_messages={'required': _('You must choose an image to upload.')})
label=_("Photo"),
error_messages={"required": _("You must choose an image to upload.")},
)
not_porn = forms.BooleanField(
label=_('suitable for all ages (i.e. no offensive content)'),
label=_("suitable for all ages (i.e. no offensive content)"),
required=True,
error_messages={
'required':
_('We only host "G-rated" images and so this field must be checked.')
})
"required": _(
'We only host "G-rated" images and so this field must be checked.'
)
},
)
can_distribute = forms.BooleanField(
label=_('can be freely copied'),
label=_("can be freely copied"),
required=True,
error_messages={
'required':
_('This field must be checked since we need to be able to distribute photos to third parties.')
})
"required": _(
"This field must be checked since we need to be able to distribute photos to third parties."
)
},
)
@staticmethod
def save(request, data):
'''
"""
Save the model and assign it to the current user
'''
"""
# Link this file to the user's profile
photo = Photo()
photo.user = request.user
@@ -119,47 +124,48 @@ class UploadPhotoForm(forms.Form):
class AddOpenIDForm(forms.Form):
'''
"""
Form to handle adding OpenID
'''
"""
openid = forms.URLField(
label=_('OpenID'),
label=_("OpenID"),
min_length=MIN_LENGTH_URL,
max_length=MAX_LENGTH_URL,
initial='http://'
initial="http://",
)
def clean_openid(self):
'''
"""
Enforce restrictions
'''
"""
# Lowercase hostname port of the URL
url = urlsplit(self.cleaned_data['openid'])
url = urlsplit(self.cleaned_data["openid"])
data = urlunsplit(
(url.scheme.lower(), url.netloc.lower(), url.path,
url.query, url.fragment))
(url.scheme.lower(), url.netloc.lower(), url.path, url.query, url.fragment)
)
# TODO: Domain restriction as in libravatar?
return data
def save(self, user):
'''
"""
Save the model, ensuring some safety
'''
"""
if ConfirmedOpenId.objects.filter( # pylint: disable=no-member
openid=self.cleaned_data['openid']).exists():
self.add_error('openid', _('OpenID already added and confirmed!'))
openid=self.cleaned_data["openid"]
).exists():
self.add_error("openid", _("OpenID already added and confirmed!"))
return False
if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member
openid=self.cleaned_data['openid']).exists():
self.add_error(
'openid',
_('OpenID already added, but not confirmed yet!'))
openid=self.cleaned_data["openid"]
).exists():
self.add_error("openid", _("OpenID already added, but not confirmed yet!"))
return False
unconfirmed = UnconfirmedOpenId()
unconfirmed.openid = self.cleaned_data['openid']
unconfirmed.openid = self.cleaned_data["openid"]
unconfirmed.user = user
unconfirmed.save()
@@ -167,40 +173,50 @@ class AddOpenIDForm(forms.Form):
class UpdatePreferenceForm(forms.ModelForm):
'''
"""
Form for updating user preferences
'''
"""
class Meta: # pylint: disable=too-few-public-methods
'''
"""
Meta class for UpdatePreferenceForm
'''
"""
model = UserPreference
fields = ['theme']
fields = ["theme"]
class UploadLibravatarExportForm(forms.Form):
'''
"""
Form handling libravatar user export upload
'''
"""
export_file = forms.FileField(
label=_('Export file'),
error_messages={'required': _('You must choose an export file to upload.')})
label=_("Export file"),
error_messages={"required": _("You must choose an export file to upload.")},
)
not_porn = forms.BooleanField(
label=_('suitable for all ages (i.e. no offensive content)'),
label=_("suitable for all ages (i.e. no offensive content)"),
required=True,
error_messages={
'required':
_('We only host "G-rated" images and so this field must be checked.')
})
"required": _(
'We only host "G-rated" images and so this field must be checked.'
)
},
)
can_distribute = forms.BooleanField(
label=_('can be freely copied'),
label=_("can be freely copied"),
required=True,
error_messages={
'required':
_('This field must be checked since we need to be able to\
distribute photos to third parties.')
})
"required": _(
"This field must be checked since we need to be able to\
distribute photos to third parties."
)
},
)
class DeleteAccountForm(forms.Form):
password = forms.CharField(label=_('Password'), required=False, widget=forms.PasswordInput())
password = forms.CharField(
label=_("Password"), required=False, widget=forms.PasswordInput()
)

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Helper method to fetch Gravatar image
'''
"""
from ssl import SSLError
from urllib.request import urlopen, HTTPError, URLError
import hashlib
@@ -11,43 +12,47 @@ URL_TIMEOUT = 5 # in seconds
def get_photo(email):
'''
"""
Fetch photo from Gravatar, given an email address
'''
hash_object = hashlib.new('md5')
hash_object.update(email.lower().encode('utf-8'))
thumbnail_url = 'https://secure.gravatar.com/avatar/' + \
hash_object.hexdigest() + '?s=%i&d=404' % AVATAR_MAX_SIZE
image_url = 'https://secure.gravatar.com/avatar/' + hash_object.hexdigest(
) + '?s=512&d=404'
"""
hash_object = hashlib.new("md5")
hash_object.update(email.lower().encode("utf-8"))
thumbnail_url = (
"https://secure.gravatar.com/avatar/"
+ hash_object.hexdigest()
+ "?s=%i&d=404" % AVATAR_MAX_SIZE
)
image_url = (
"https://secure.gravatar.com/avatar/" + hash_object.hexdigest() + "?s=512&d=404"
)
# Will redirect to the public profile URL if it exists
service_url = 'http://www.gravatar.com/' + hash_object.hexdigest()
service_url = "http://www.gravatar.com/" + hash_object.hexdigest()
try:
urlopen(image_url, timeout=URL_TIMEOUT)
except HTTPError as exc:
if exc.code != 404 and exc.code != 503:
print( # pragma: no cover
'Gravatar fetch failed with an unexpected %s HTTP error' %
exc.code)
"Gravatar fetch failed with an unexpected %s HTTP error" % exc.code
)
return False
except URLError as exc: # pragma: no cover
print(
'Gravatar fetch failed with URL error: %s' %
exc.reason) # pragma: no cover
"Gravatar fetch failed with URL error: %s" % exc.reason
) # pragma: no cover
return False # pragma: no cover
except SSLError as exc: # pragma: no cover
print(
'Gravatar fetch failed with SSL error: %s' %
exc.reason) # pragma: no cover
"Gravatar fetch failed with SSL error: %s" % exc.reason
) # pragma: no cover
return False # pragma: no cover
return {
'thumbnail_url': thumbnail_url,
'image_url': image_url,
'width': AVATAR_MAX_SIZE,
'height': AVATAR_MAX_SIZE,
'service_url': service_url,
'service_name': 'Gravatar'
"thumbnail_url": thumbnail_url,
"image_url": image_url,
"width": AVATAR_MAX_SIZE,
"height": AVATAR_MAX_SIZE,
"service_url": service_url,
"service_name": "Gravatar",
}

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Our models for ivatar.ivataraccount
'''
"""
import base64
import hashlib
@@ -18,7 +19,7 @@ from django.db import models
from django.utils import timezone
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy, reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail
from django.template.loader import render_to_string
@@ -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 %}

View File

@@ -6,7 +6,16 @@
{% block title %}{% trans 'Your Profile' %}{% endblock title %}
{% block content %}
<script type="text/javascript">
function add_active(id){
var elems = document.querySelector(".active");
if(elems !== null){
elems.classList.remove("active");
}
element = document.getElementById(id);
element.classList.add("active");
}
</script>
<h1>
{% trans 'Your Profile' %} -
{% if user.first_name and user.last_name %}
@@ -66,43 +75,122 @@ outline: inherit;
display: contents;
}
}
@media only screen and (max-width: 470px) {
p {
padding-top: 2rem;
}
h3{
line-height: 3.4rem;
}
}
</style>
<noscript>
<style type="text/css">
.profile-container > ul{
display:block;
}
</style>
</noscript>
{% if user.confirmedemail_set.count or user.confirmedopenid_set.count %}
<h3>{% trans 'You have the following confirmed identities:' %}</h3>
<div class="row">
<div class="row profileid">
{% for email in user.confirmedemail_set.all %}
{% if user.confirmedemail_set.all|length == 1%}
<form action="{% url 'remove_confirmed_email' email.id %}" method="post">
{% csrf_token %}
<div class="panel" style="width:172px;margin-left:20px;float:left">
<div class="panel-heading" style="padding-right:0">
<h3 class="panel-title" title="{{ email.email }}" style="display:inline-flex"><a href="{% url 'assign_photo_email' email.id %}"><i class="fa fa-edit"></i></a>&nbsp;
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this email address?' %}')"><i class="fa fa-trash"></i></button>&nbsp;
{{ email.email|truncatechars:12 }}</h3>
</div>
<div class="panel-body" style="height:130px">
<center>
<img title="{% trans 'Access count' %}: {{ email.access_count }}" style="max-height:100px;max-width:100px" src="{% if email.photo %}{% url 'raw_image' email.photo.id %}{% else %}{% static '/img/nobody/80.png' %}{% endif %}">
</center>
</div>
<div id="email-conf-{{ forloop.counter }}" class="profile-container active">
<img title="{% trans 'Access count' %}: {{ email.access_count }}" src="{% if email.photo %}{% url 'raw_image' email.photo.id %}{% else %}{% static '/img/nobody/120.png' %}{% endif %}">
<h3 class="panel-title email-profile" title="{{ email.email }}">
{{ email.email }}
</h3>
<ul>
<li>
<a href="{% url 'assign_photo_email' email.id %}">
Change Profile Picture
</a>
</li>
<li class="email-delete">
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this email address?' %}')">
Delete Email Adress
</button>
</li>
</ul>
</div>
</form>
{% else %}
<form action="{% url 'remove_confirmed_email' email.id %}" method="post">
{% csrf_token %}
<div id="email-conf-{{ forloop.counter }}" class="profile-container" onclick="add_active('email-conf-{{ forloop.counter }}')">
<img title="{% trans 'Access count' %}: {{ email.access_count }}" src="{% if email.photo %}{% url 'raw_image' email.photo.id %}{% else %}{% static '/img/nobody/120.png' %}{% endif %}">
<h3 class="panel-title email-profile" title="{{ email.email }}">
{{ email.email }}
</h3>
<ul>
<li>
<a href="{% url 'assign_photo_email' email.id %}">
Change Profile Picture
</a>
</li>
<li class="email-delete">
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this email address?' %}')">
Delete Email Adress
</button>
</li>
</ul>
</div>
</form>
{% endif %}
{% endfor %}
{% for openid in user.confirmedopenid_set.all %}
{% if user.confirmedopenid_set.all|length == 1 %}
<form action="{% url 'remove_confirmed_openid' openid.id %}" method="post">{% csrf_token %}
<div class="panel" style="width:172px;margin-left:20px;float:left">
<div class="panel-heading" style="padding-right:0">
<h3 class="panel-title" title="{{ openid.openid }}" style="display:inline-flex"><a href="{% url 'assign_photo_openid' openid.pk %}"><i class="fa fa-edit"></i></a>&nbsp;
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this OpenID?' %}')"><i class="fa fa-trash"></i></button>&nbsp;
{{ openid.openid|cut:"http://"|cut:"https://"|truncatechars:12 }}</h3>
</div>
<div class="panel-body" style="height:130px">
<center>
<img title="{% trans 'Access count' %}: {{ openid.access_count }}" style="max-height:100px;max-width:100px" src="{% if openid.photo %}{% url 'raw_image' openid.photo.id %}{% else %}{% static '/img/nobody/80.png' %}{% endif %}">
</center>
<div>
<div id="id-conf-{{ forloop.counter }}" class="profile-container active">
<img title="{% trans 'Access count' %}: {{ openid.access_count }}" src="{% if openid.photo %}{% url 'raw_image' openid.photo.id %}{% else %}{% static '/img/nobody/120.png' %}{% endif %}">
<h3 class="panel-title email-profile" title="{{ openid.openid }}">
{{ openid.openid }}
</h3>
<ul>
<li>
<a href="{% url 'assign_photo_openid' openid.pk %}">
Change OpenID Picture
</a>
</li>
<li>
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this OpenID?' %}')">
Delete OpenID
</button>
</li>
</ul>
</div>
</div>
</form>
{% else %}
<form action="{% url 'remove_confirmed_openid' openid.id %}" method="post">{% csrf_token %}
<div>
<div id="id-conf-{{ forloop.counter }}" class="profile-container" onclick="add_active('id-conf-{{ forloop.counter }}')">
<img title="{% trans 'Access count' %}: {{ openid.access_count }}" src="{% if openid.photo %}{% url 'raw_image' openid.photo.id %}{% else %}{% static '/img/nobody/120.png' %}{% endif %}">
<h3 class="panel-title email-profile" title="{{ openid.openid }}">
{{ openid.openid }}
</h3>
<ul>
<li>
<a href="{% url 'assign_photo_openid' openid.pk %}">
Change OpenID Picture
</a>
</li>
<li>
<button type="submit" class="nobutton" onclick="return confirm('{% trans 'Are you sure that you want to delete this OpenID?' %}')">
Delete OpenID
</button>
</li>
</ul>
</div>
</div>
</form>
{% endif %}
{% endfor %}
</div>
{% endif %}
@@ -131,8 +219,7 @@ outline: inherit;
{# TODO: (expires in xx hours) #}
{% endfor %}
{% endif %}
<p>
<p style="padding-top:5px;">
{% if not max_emails %}<a href="{% url 'add_email' %}" class="button" >{% trans 'Add a new email address' %}</a>&nbsp;{% endif %}
<a href="{% url 'add_openid' %}" class="button">{% trans 'Add a new OpenID' %}</a></p>
</p>
@@ -145,9 +232,7 @@ outline: inherit;
<h3 class="panel-title"><a href="{% url 'delete_photo' photo.pk %}" onclick="return confirm('{% trans 'Are you sure that you want to delete this image?' %}')"><i class="fa fa-trash"></i></a> {% trans 'Image' %} {{ forloop.counter }}</h3>
</div>
<div class="panel-body" style="height:130px">
<center>
<img title="{% trans 'Access count' %}: {{ photo.access_count }}" style="max-height:100px;max-width:100px" src="{% url 'raw_image' photo.id %}">
</center>
</div>
</div>
{% endfor %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,17 @@
'''
# -*- 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
@@ -18,108 +20,139 @@ from . views import RemoveConfirmedEmailView, AssignPhotoEmailView
from .views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView
from .views import ImportPhotoView, RawImageView, DeletePhotoView
from .views import UploadPhotoView, AssignPhotoOpenIDView
from . views import AvatarCreatorView, AvatarView
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
from .views import AvatarCreatorView, AvatarView
# 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'),
"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(
'profile/(?P<profile_username>.+)',
"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('avatar_creator/', AvatarCreatorView.as_view(), name='avatar_creator'),
path('avatar_view/', AvatarView.as_view(), name='avataaar'),
path('password_set/', PasswordSetView.as_view(), name='password_set'),
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"),
path("avatar_creator/", AvatarCreatorView.as_view(), name="avatar_creator"),
path("avatar_view/", AvatarView.as_view(), name="avataaar"),
url(
r'remove_unconfirmed_openid/(?P<openid_id>\d+)',
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

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""
Django settings for ivatar project.
"""
@@ -6,7 +7,7 @@ import os
import logging
log_level = logging.DEBUG # pylint: disable=invalid-name
logger = logging.getLogger('ivatar') # pylint: disable=invalid-name
logger = logging.getLogger("ivatar") # pylint: disable=invalid-name
logger.setLevel(log_level)
PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__))
@@ -14,7 +15,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '=v(+-^t#ahv^a&&e)uf36g8algj$d1@6ou^w(r0@%)#8mlc*zk'
SECRET_KEY = "=v(+-^t#ahv^a&&e)uf36g8algj$d1@6ou^w(r0@%)#8mlc*zk"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@@ -25,52 +26,52 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = 'ivatar.urls'
ROOT_URLCONF = "ivatar.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = 'ivatar.wsgi.application'
WSGI_APPLICATION = "ivatar.wsgi.application"
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
@@ -80,16 +81,16 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa
},
]
@@ -97,9 +98,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@@ -109,15 +110,10 @@ USE_TZ = True
# Static files configuration (esp. req. during dev.)
PROJECT_ROOT = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
os.pardir
)
)
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static")
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import

View File

@@ -82,14 +82,17 @@ h2 {
letter-spacing: 0.05rem;
font-weight: 500;
}
h3 {
font-family: 'Lato', sans-serif;
font-size: 24px;
margin-bottom: 2rem;
color: #545454;
}
@media only screen and (max-width: 470px) {
h3{
font-size: 20px;
}
}
h4 {
font-family: 'Lato', sans-serif;
font-size: 25px;
@@ -404,7 +407,6 @@ transition: all 0.3s;
top: 26rem;
}
}
@media only screen and (max-width: 620px) {
#page .container #home-form {
margin-bottom: 2rem;
@@ -728,3 +730,138 @@ color:#335ECF;
margin-top: 11rem !important;
}
}
.profile-container{
border-top: solid 5px #2F95EDB3;
display: grid;
padding-top: 1rem;
padding-bottom: 1rem;
}
.profile-container img{
margin: 0.5em;
}
.panel-body.profile > div, .panel-body.profile > img {
text-align: left;
}
.panel-heading.profile{
background: none;
border-top-left-radius: unset;
border-top-right-radius: unset;
}
.profile-container > h3{
color: #353535;
font-weight: bold;
}
.profile-container > ul > li > a, .profile-container button{
color: #353535;
text-decoration: none;
}
.profile-container.active{
border-top: solid 5px #335ECF;
}
.profile-container ul > li > button:hover, .profile-container ul > li > a:hover{
color: #335ECF;
}
.email-profile { grid-area: email; }
.profile-container{
padding-top: 2rem;
}
.profile-container > img {
grid-area: img;
margin: auto;
}
.profile-container > ul {
grid-area: list;
list-style-type: none;
padding:0;
font-size: 18px;
}
.profile-container > ul > li{
padding-top: 0.5rem;
}
@media only screen and (max-width: 420px) {
.profile-container > ul > li {
padding-top: 0.85rem;
}
}
.profile-container > ul > li > a:hover{
text-decoration: none;
}
.profile-container {
display: grid;
grid-template-areas:
'img email email email email email'
'img list list list list list';
grid-gap: 0;
grid-template-columns: 20% 80%;
}
@media only screen and (max-width: 700px) {
.profile-container {
grid-template-columns: 40% 60%;
}
}
.profile-container > div, profile-container > img {
text-align: center;
}
.profile-container.active > img{
max-height:120px;
max-width:120px;
}
@media only screen and (max-width: 420px) {
.profile-container.active > img {
max-height:80px;
max-width:80px;
}
}
.profile-container > ul{
display:none
}
.profile-container.active > ul{
display:block;
}
.profile-container > img{
max-height:80px;
max-width:80px;
}
h3.panel-title{
margin-top: unset;
}
.profile-container > h3{
padding-top: 26px;
}
@media only screen and (max-width: 470px) {
.profile-container > h3{
padding-top: 20px;
}
}
.profile-container:hover {
cursor: pointer;
}
.profile-container.active > h3{
padding-top: 12px;
}
.profile-container.active{
cursor: pointer;
background: #dcdcdcb5;
}
.profile-container.active:hover{
cursor:auto;
}
@media only screen and (min-width: 768px) {
.profile-container:hover ul {
display:block !important;
}
.profile-container:hover {
background: #dcdcdcb5;
}
.profile-container:hover img{
max-height:120px;
max-width:120px;
}
.profile-container:hover h3{
padding-top: 12px;
}
}
.alert-success {
color: #353535;
background-color: #3582d71f;
}

View File

@@ -1,8 +1,9 @@
'''
# -*- coding: utf-8 -*-
"""
Test various other parts of ivatar/libravatar in order
to increase the overall test coverage. Test in here, didn't
fit anywhere else.
'''
"""
from django.test import TestCase
from django.contrib.auth.models import User
@@ -12,35 +13,35 @@ from ivatar.ivataraccount.models import pil_format, UserPreference
class Tester(TestCase):
'''
"""
Main test class
'''
"""
user = None
username = random_string()
def setUp(self):
'''
"""
Prepare tests.
- Create user
'''
"""
self.user = User.objects.create_user(
username=self.username,
)
def test_pil_format(self):
'''
"""
Test pil format function
'''
self.assertEqual(pil_format('jpg'), 'JPEG')
self.assertEqual(pil_format('jpeg'), 'JPEG')
self.assertEqual(pil_format('png'), 'PNG')
self.assertEqual(pil_format('gif'), 'GIF')
self.assertEqual(pil_format('abc'), None)
"""
self.assertEqual(pil_format("jpg"), "JPEG")
self.assertEqual(pil_format("jpeg"), "JPEG")
self.assertEqual(pil_format("png"), "PNG")
self.assertEqual(pil_format("gif"), "GIF")
self.assertEqual(pil_format("abc"), None)
def test_userprefs_str(self):
'''
"""
Test if str representation of UserPreferences is as expected
'''
up = UserPreference(theme='default', user=self.user)
"""
up = UserPreference(theme="default", user=self.user)
print(up)

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Test our views in ivatar.ivataraccount.views and ivatar.views
'''
"""
# pylint: disable=too-many-lines
import os
import django
@@ -11,33 +12,34 @@ from django.contrib.auth.models import User
from ivatar.utils import random_string
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup()
class Tester(TestCase): # pylint: disable=too-many-public-methods
'''
"""
Main test class
'''
"""
client = Client()
user = None
username = random_string()
password = random_string()
email = '%s@%s.%s' % (username, random_string(), random_string(2))
email = "%s@%s.%s" % (username, random_string(), random_string(2))
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
openid = 'http://%s.%s.%s/' % (username, random_string(), 'org')
openid = "http://%s.%s.%s/" % (username, random_string(), "org")
def login(self):
'''
"""
Login as user
'''
"""
self.client.login(username=self.username, password=self.password)
def setUp(self):
'''
"""
Prepare for tests.
- Create user
'''
"""
self.user = User.objects.create_user(
username=self.username,
password=self.password,
@@ -47,19 +49,19 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
"""
Test contact page
"""
response = self.client.get(reverse('contact'))
self.assertEqual(response.status_code, 200, 'no 200 ok?')
response = self.client.get(reverse("contact"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
def test_description_page(self):
"""
Test description page
"""
response = self.client.get(reverse('description'))
self.assertEqual(response.status_code, 200, 'no 200 ok?')
response = self.client.get(reverse("description"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
def test_security_page(self):
"""
Test security page
"""
response = self.client.get(reverse('security'))
self.assertEqual(response.status_code, 200, 'no 200 ok?')
response = self.client.get(reverse("security"))
self.assertEqual(response.status_code, 200, "no 200 ok?")

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Test our utils from ivatar.utils
'''
"""
from django.test import TestCase
@@ -8,18 +9,18 @@ from ivatar.utils import openid_variations
class Tester(TestCase):
'''
"""
Main test class
'''
"""
def test_openid_variations(self):
'''
"""
Test if the OpenID variation "generator" does the correct thing
'''
openid0 = 'http://user.url/'
openid1 = 'http://user.url'
openid2 = 'https://user.url/'
openid3 = 'https://user.url'
"""
openid0 = "http://user.url/"
openid1 = "http://user.url"
openid2 = "https://user.url/"
openid3 = "https://user.url"
# First variation
self.assertEqual(openid_variations(openid0)[0], openid0)

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
Test our views in ivatar.ivataraccount.views and ivatar.views
'''
"""
# pylint: disable=too-many-lines
import os
import django
@@ -10,33 +11,34 @@ from django.contrib.auth.models import User
from ivatar.utils import random_string
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup()
class Tester(TestCase): # pylint: disable=too-many-public-methods
'''
"""
Main test class
'''
"""
client = Client()
user = None
username = random_string()
password = random_string()
email = '%s@%s.%s' % (username, random_string(), random_string(2))
email = "%s@%s.%s" % (username, random_string(), random_string(2))
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
openid = 'http://%s.%s.%s/' % (username, random_string(), 'org')
openid = "http://%s.%s.%s/" % (username, random_string(), "org")
def login(self):
'''
"""
Login as user
'''
"""
self.client.login(username=self.username, password=self.password)
def setUp(self):
'''
"""
Prepare for tests.
- Create user
'''
"""
self.user = User.objects.create_user(
username=self.username,
password=self.password,
@@ -46,8 +48,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
"""
Test incorrect digest
"""
response = self.client.get('/avatar/%s' % 'x'*65, follow=True)
response = self.client.get("/avatar/%s" % "x" * 65, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/deadbeef.png',
msg_prefix='Why does an invalid hash not redirect to deadbeef?')
expected_url="/static/img/deadbeef.png",
msg_prefix="Why does an invalid hash not redirect to deadbeef?",
)

View File

@@ -1,22 +1,27 @@
'''
# -*- coding: utf-8 -*-
"""
Unit tests for WSGI
'''
"""
import unittest
import os
import django
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup()
class TestCase(unittest.TestCase):
'''
"""
Simple testcase to see if WSGI loads correctly
'''
"""
def test_run_wsgi(self):
'''
"""
Run wsgi import
'''
"""
import ivatar.wsgi # pylint: disable=import-outside-toplevel
self.assertEqual(ivatar.wsgi.application.__class__,
django.core.handlers.wsgi.WSGIHandler)
self.assertEqual(
ivatar.wsgi.application.__class__, django.core.handlers.wsgi.WSGIHandler
)

View File

@@ -1,8 +1,9 @@
'''
# -*- coding: utf-8 -*-
"""
Classes for our ivatar.tools.forms
'''
"""
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList
@@ -12,45 +13,40 @@ from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL
class CheckDomainForm(forms.Form):
'''
"""
Form handling domain check
'''
"""
domain = forms.CharField(
label=_('Domain'),
label=_("Domain"),
required=True,
error_messages={
'required':
_('Cannot check without a domain name.')
}
error_messages={"required": _("Cannot check without a domain name.")},
)
class CheckForm(forms.Form):
'''
"""
Form handling check
'''
"""
mail = forms.EmailField(
label=_('E-Mail'),
label=_("E-Mail"),
required=False,
min_length=MIN_LENGTH_EMAIL,
max_length=MAX_LENGTH_EMAIL,
error_messages={
'required':
_('Cannot check without a domain name.')
})
error_messages={"required": _("Cannot check without a domain name.")},
)
openid = forms.CharField(
label=_('OpenID'),
label=_("OpenID"),
required=False,
min_length=MIN_LENGTH_URL,
max_length=MAX_LENGTH_URL,
error_messages={
'required':
_('Cannot check without an openid name.')
})
error_messages={"required": _("Cannot check without an openid name.")},
)
size = forms.IntegerField(
label=_('Size'),
label=_("Size"),
initial=80,
min_value=5,
max_value=AVATAR_MAX_SIZE,
@@ -58,24 +54,24 @@ class CheckForm(forms.Form):
)
default_opt = forms.ChoiceField(
label=_('Default'),
label=_("Default"),
required=False,
widget=forms.RadioSelect,
choices=[
('retro', _('Retro style (similar to GitHub)')),
('robohash', _('Roboter style')),
('pagan', _('Retro adventure character')),
('wavatar', _('Wavatar style')),
('monsterid', _('Monster style')),
('identicon', _('Identicon style')),
('mm', _('Mystery man')),
('mmng', _('Mystery man NextGen')),
('none', _('None')),
("retro", _("Retro style (similar to GitHub)")),
("robohash", _("Roboter style")),
("pagan", _("Retro adventure character")),
("wavatar", _("Wavatar style")),
("monsterid", _("Monster style")),
("identicon", _("Identicon style")),
("mm", _("Mystery man")),
("mmng", _("Mystery man NextGen")),
("none", _("None")),
],
)
default_url = forms.URLField(
label=_('Default URL'),
label=_("Default URL"),
min_length=1,
max_length=MAX_LENGTH_URL,
required=False,
@@ -83,28 +79,27 @@ class CheckForm(forms.Form):
def clean(self):
self.cleaned_data = super().clean()
mail = self.cleaned_data.get('mail')
openid = self.cleaned_data.get('openid')
default_url = self.cleaned_data.get('default_url')
default_opt = self.cleaned_data.get('default_opt')
if default_url and default_opt and default_opt != 'none':
if not 'default_url' in self._errors:
self._errors['default_url'] = ErrorList()
if not 'default_opt' in self._errors:
self._errors['default_opt'] = ErrorList()
mail = self.cleaned_data.get("mail")
openid = self.cleaned_data.get("openid")
default_url = self.cleaned_data.get("default_url")
default_opt = self.cleaned_data.get("default_opt")
if default_url and default_opt and default_opt != "none":
if "default_url" not in self._errors:
self._errors["default_url"] = ErrorList()
if "default_opt" not in self._errors:
self._errors["default_opt"] = ErrorList()
errstring = _('Only default URL OR default keyword may be specified')
self._errors['default_url'].append(errstring)
self._errors['default_opt'].append(errstring)
errstring = _("Only default URL OR default keyword may be specified")
self._errors["default_url"].append(errstring)
self._errors["default_opt"].append(errstring)
if not mail and not openid:
raise ValidationError(_('Either OpenID or mail must be specified'))
raise ValidationError(_("Either OpenID or mail must be specified"))
return self.cleaned_data
def clean_openid(self):
data = self.cleaned_data['openid']
data = self.cleaned_data["openid"]
return data.lower()
def clean_mail(self):
data = self.cleaned_data['mail']
print(data)
data = self.cleaned_data["mail"]
return data.lower()

View File

@@ -1,57 +1,48 @@
'''
# -*- coding: utf-8 -*-
"""
Test our views in ivatar.ivataraccount.views and ivatar.views
'''
"""
# pylint: disable=too-many-lines
from urllib.parse import urlsplit
from io import BytesIO
import io
import os
import django
from django.test import TestCase
from django.test import Client
from django.urls import reverse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
import hashlib
from libravatar import libravatar_url
from PIL import Image
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup()
# pylint: disable=wrong-import-position
from ivatar import settings
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId
from ivatar.utils import random_string
# pylint: enable=wrong-import-position
class Tester(TestCase): # pylint: disable=too-many-public-methods
'''
"""
Main test class
'''
"""
client = Client()
user = None
username = random_string()
password = random_string()
email = '%s@%s.%s' % (username, random_string(), random_string(2))
email = "%s@%s.%s" % (username, random_string(), random_string(2))
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
openid = 'http://%s.%s.%s/' % (username, random_string(), 'org')
openid = "http://%s.%s.%s/" % (username, random_string(), "org")
def login(self):
'''
"""
Login as user
'''
"""
self.client.login(username=self.username, password=self.password)
def setUp(self):
'''
"""
Prepare for tests.
- Create user
'''
"""
self.user = User.objects.create_user(
username=self.username,
password=self.password,
@@ -61,12 +52,12 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
"""
Test check page
"""
response = self.client.get(reverse('tools_check'))
self.assertEqual(response.status_code, 200, 'no 200 ok?')
response = self.client.get(reverse("tools_check"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
def test_check_domain(self):
"""
Test check domain page
"""
response = self.client.get(reverse('tools_check_domain'))
self.assertEqual(response.status_code, 200, 'no 200 ok?')
response = self.client.get(reverse("tools_check_domain"))
self.assertEqual(response.status_code, 200, "no 200 ok?")

View File

@@ -1,12 +1,13 @@
'''
# -*- coding: utf-8 -*-
"""
ivatar/tools URL configuration
'''
"""
from django.conf.urls import url
from .views import CheckView, CheckDomainView
urlpatterns = [ # pylint: disable=invalid-name
url('check/', CheckView.as_view(), name='tools_check'),
url('check_domain/', CheckDomainView.as_view(), name='tools_check_domain'),
url('check_domain$', CheckDomainView.as_view(), name='tools_check_domain'),
url("check/", CheckView.as_view(), name="tools_check"),
url("check_domain/", CheckDomainView.as_view(), name="tools_check_domain"),
url("check_domain$", CheckDomainView.as_view(), name="tools_check_domain"),
]

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
View classes for ivatar/tools/
'''
"""
from socket import inet_ntop, AF_INET6
import hashlib
import random
@@ -16,49 +17,59 @@ from libravatar import SECURE_BASE_URL as LIBRAVATAR_SECURE_BASE_URL
from libravatar import BASE_URL as LIBRAVATAR_BASE_URL
from ivatar.settings import SECURE_BASE_URL, BASE_URL
from .forms import CheckDomainForm, CheckForm # pylint: disable=relative-beyond-top-level
from .forms import (
CheckDomainForm,
CheckForm,
) # pylint: disable=relative-beyond-top-level
class CheckDomainView(FormView):
'''
"""
View class for checking a domain
'''
template_name = 'check_domain.html'
"""
template_name = "check_domain.html"
form_class = CheckDomainForm
success_url = reverse('tools_check_domain')
success_url = reverse("tools_check_domain")
def form_valid(self, form):
result = {}
super().form_valid(form)
domain = form.cleaned_data['domain']
result['avatar_server_http'] = lookup_avatar_server(domain, False)
if result['avatar_server_http']:
result['avatar_server_http_ipv4'] = lookup_ip_address(
result['avatar_server_http'],
False)
result['avatar_server_http_ipv6'] = lookup_ip_address(
result['avatar_server_http'],
True)
result['avatar_server_https'] = lookup_avatar_server(domain, True)
if result['avatar_server_https']:
result['avatar_server_https_ipv4'] = lookup_ip_address(
result['avatar_server_https'],
False)
result['avatar_server_https_ipv6'] = lookup_ip_address(
result['avatar_server_https'],
True)
return render(self.request, self.template_name, {
'form': form,
'result': result,
})
domain = form.cleaned_data["domain"]
result["avatar_server_http"] = lookup_avatar_server(domain, False)
if result["avatar_server_http"]:
result["avatar_server_http_ipv4"] = lookup_ip_address(
result["avatar_server_http"], False
)
result["avatar_server_http_ipv6"] = lookup_ip_address(
result["avatar_server_http"], True
)
result["avatar_server_https"] = lookup_avatar_server(domain, True)
if result["avatar_server_https"]:
result["avatar_server_https_ipv4"] = lookup_ip_address(
result["avatar_server_https"], False
)
result["avatar_server_https_ipv6"] = lookup_ip_address(
result["avatar_server_https"], True
)
return render(
self.request,
self.template_name,
{
"form": form,
"result": result,
},
)
class CheckView(FormView):
'''
"""
View class for checking an e-mail or openid address
'''
template_name = 'check.html'
"""
template_name = "check.html"
form_class = CheckForm
success_url = reverse('tools_check')
success_url = reverse("tools_check")
def form_valid(self, form):
mailurl = None
@@ -73,82 +84,88 @@ class CheckView(FormView):
super().form_valid(form)
if form.cleaned_data['default_url']:
default_url = form.cleaned_data['default_url']
elif form.cleaned_data['default_opt'] and form.cleaned_data['default_opt'] != 'none':
default_url = form.cleaned_data['default_opt']
if form.cleaned_data["default_url"]:
default_url = form.cleaned_data["default_url"]
elif (
form.cleaned_data["default_opt"]
and form.cleaned_data["default_opt"] != "none"
):
default_url = form.cleaned_data["default_opt"]
else:
default_url = None
if 'size' in form.cleaned_data:
size = form.cleaned_data['size']
if form.cleaned_data['mail']:
if "size" in form.cleaned_data:
size = form.cleaned_data["size"]
if form.cleaned_data["mail"]:
mailurl = libravatar_url(
email=form.cleaned_data['mail'],
size=size,
default=default_url)
email=form.cleaned_data["mail"], size=size, default=default_url
)
mailurl = mailurl.replace(LIBRAVATAR_BASE_URL, BASE_URL)
mailurl_secure = libravatar_url(
email=form.cleaned_data['mail'],
email=form.cleaned_data["mail"],
size=size,
https=True,
default=default_url)
default=default_url,
)
mailurl_secure = mailurl_secure.replace(
LIBRAVATAR_SECURE_BASE_URL,
SECURE_BASE_URL)
LIBRAVATAR_SECURE_BASE_URL, SECURE_BASE_URL
)
mail_hash = parse_user_identity(
email=form.cleaned_data['mail'],
openid=None)[0]
hash_obj = hashlib.new('sha256')
hash_obj.update(form.cleaned_data['mail'].encode('utf-8'))
email=form.cleaned_data["mail"], openid=None
)[0]
hash_obj = hashlib.new("sha256")
hash_obj.update(form.cleaned_data["mail"].encode("utf-8"))
mail_hash256 = hash_obj.hexdigest()
mailurl_secure_256 = mailurl_secure.replace(
mail_hash,
mail_hash256)
if form.cleaned_data['openid']:
if not form.cleaned_data['openid'].startswith('http://') and \
not form.cleaned_data['openid'].startswith('https://'):
form.cleaned_data['openid'] = 'http://%s' % form.cleaned_data['openid']
mailurl_secure_256 = mailurl_secure.replace(mail_hash, mail_hash256)
if form.cleaned_data["openid"]:
if not form.cleaned_data["openid"].startswith(
"http://"
) and not form.cleaned_data["openid"].startswith("https://"):
form.cleaned_data["openid"] = "http://%s" % form.cleaned_data["openid"]
openidurl = libravatar_url(
openid=form.cleaned_data['openid'],
size=size,
default=default_url)
openid=form.cleaned_data["openid"], size=size, default=default_url
)
openidurl = openidurl.replace(LIBRAVATAR_BASE_URL, BASE_URL)
openidurl_secure = libravatar_url(
openid=form.cleaned_data['openid'],
openid=form.cleaned_data["openid"],
size=size,
https=True,
default=default_url)
default=default_url,
)
openidurl_secure = openidurl_secure.replace(
LIBRAVATAR_SECURE_BASE_URL,
SECURE_BASE_URL)
LIBRAVATAR_SECURE_BASE_URL, SECURE_BASE_URL
)
openid_hash = parse_user_identity(
openid=form.cleaned_data['openid'],
email=None)[0]
openid=form.cleaned_data["openid"], email=None
)[0]
return render(self.request, self.template_name, {
'form': form,
'mailurl': mailurl,
'openidurl': openidurl,
'mailurl_secure': mailurl_secure,
'mailurl_secure_256': mailurl_secure_256,
'openidurl_secure': openidurl_secure,
'mail_hash': mail_hash,
'mail_hash256': mail_hash256,
'openid_hash': openid_hash,
'size': size,
})
return render(
self.request,
self.template_name,
{
"form": form,
"mailurl": mailurl,
"openidurl": openidurl,
"mailurl_secure": mailurl_secure,
"mailurl_secure_256": mailurl_secure_256,
"openidurl_secure": openidurl_secure,
"mail_hash": mail_hash,
"mail_hash256": mail_hash256,
"openid_hash": openid_hash,
"size": size,
},
)
def lookup_avatar_server(domain, https):
'''
"""
Extract the avatar server from an SRV record in the DNS zone
The SRV records should look like this:
_avatars._tcp.example.com. IN SRV 0 0 80 avatars.example.com
_avatars-sec._tcp.example.com. IN SRV 0 0 443 avatars.example.com
'''
"""
if domain and len(domain) > 60:
domain = domain[:60]
@@ -161,27 +178,35 @@ def lookup_avatar_server(domain, https):
DNS.DiscoverNameServers()
try:
dns_request = DNS.Request(name=service_name, qtype='SRV').req()
dns_request = DNS.Request(name=service_name, qtype="SRV").req()
except DNS.DNSError as message:
print("DNS Error: %s (%s)" % (message, domain))
return None
if dns_request.header['status'] == 'NXDOMAIN':
if dns_request.header["status"] == "NXDOMAIN":
# Not an error, but no point in going any further
return None
if dns_request.header['status'] != 'NOERROR':
print("DNS Error: status=%s (%s)" % (dns_request.header['status'], domain))
if dns_request.header["status"] != "NOERROR":
print("DNS Error: status=%s (%s)" % (dns_request.header["status"], domain))
return None
records = []
for answer in dns_request.answers:
if ('data' not in answer) or (not answer['data']) or \
(not answer['typename']) or (answer['typename'] != 'SRV'):
if (
("data" not in answer)
or (not answer["data"])
or (not answer["typename"])
or (answer["typename"] != "SRV")
):
continue
record = {'priority': int(answer['data'][0]), 'weight': int(answer['data'][1]),
'port': int(answer['data'][2]), 'target': answer['data'][3]}
record = {
"priority": int(answer["data"][0]),
"weight": int(answer["data"][1]),
"port": int(answer["data"][2]),
"target": answer["data"][3],
}
records.append(record)
@@ -194,38 +219,38 @@ def lookup_avatar_server(domain, https):
def srv_hostname(records):
'''
"""
Return the right (target, port) pair from a list of SRV records.
'''
"""
if len(records) < 1:
return (None, None)
if len(records) == 1:
ret = records[0]
return (ret['target'], ret['port'])
return (ret["target"], ret["port"])
# Keep only the servers in the top priority
priority_records = []
total_weight = 0
top_priority = records[0]['priority'] # highest priority = lowest number
top_priority = records[0]["priority"] # highest priority = lowest number
for ret in records:
if ret['priority'] > top_priority:
if ret["priority"] > top_priority:
# ignore the record (ret has lower priority)
continue
# Take care - this if is only a if, if the above if
# uses continue at the end. else it should be an elsif
if ret['priority'] < top_priority:
if ret["priority"] < top_priority:
# reset the aretay (ret has higher priority)
top_priority = ret['priority']
top_priority = ret["priority"]
total_weight = 0
priority_records = []
total_weight += ret['weight']
total_weight += ret["weight"]
if ret['weight'] > 0:
if ret["weight"] > 0:
priority_records.append((total_weight, ret))
else:
# zero-weigth elements must come first
@@ -233,7 +258,7 @@ def srv_hostname(records):
if len(priority_records) == 1:
unused, ret = priority_records[0] # pylint: disable=unused-variable
return (ret['target'], ret['port'])
return (ret["target"], ret["port"])
# Select first record according to RFC2782 weight ordering algorithm (page 3)
random_number = random.randint(0, total_weight)
@@ -242,9 +267,9 @@ def srv_hostname(records):
weighted_index, ret = record
if weighted_index >= random_number:
return (ret['target'], ret['port'])
return (ret["target"], ret["port"])
print('There is something wrong with our SRV weight ordering algorithm')
print("There is something wrong with our SRV weight ordering algorithm")
return (None, None)
@@ -263,19 +288,21 @@ def lookup_ip_address(hostname, ipv6):
print("DNS Error: %s (%s)" % (message, hostname))
return None
if dns_request.header['status'] != 'NOERROR':
print("DNS Error: status=%s (%s)" % (dns_request.header['status'], hostname))
if dns_request.header["status"] != "NOERROR":
print("DNS Error: status=%s (%s)" % (dns_request.header["status"], hostname))
return None
for answer in dns_request.answers:
if ('data' not in answer) or (not answer['data']):
if ("data" not in answer) or (not answer["data"]):
continue
if (ipv6 and answer['typename'] != 'AAAA') or (not ipv6 and answer['typename'] != 'A'):
if (ipv6 and answer["typename"] != "AAAA") or (
not ipv6 and answer["typename"] != "A"
):
continue # skip CNAME records
if ipv6:
return inet_ntop(AF_INET6, answer['data'])
return inet_ntop(AF_INET6, answer["data"])
return answer['data']
return answer["data"]
return None

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
ivatar URL configuration
'''
"""
from django.contrib import admin
from django.urls import path, include
from django.conf.urls import url
@@ -10,33 +11,48 @@ from ivatar import settings
from .views import AvatarImageView, GravatarProxyView, StatsView
urlpatterns = [ # pylint: disable=invalid-name
path('admin/', admin.site.urls),
path('i18n/', include('django.conf.urls.i18n')),
url('openid/', include('django_openid_auth.urls')),
url('tools/', include('ivatar.tools.urls')),
path("admin/", admin.site.urls),
path("i18n/", include("django.conf.urls.i18n")),
url("openid/", include("django_openid_auth.urls")),
url("tools/", include("ivatar.tools.urls")),
url(r"avatar/(?P<digest>\w{64})", AvatarImageView.as_view(), name="avatar_view"),
url(r"avatar/(?P<digest>\w{32})", AvatarImageView.as_view(), name="avatar_view"),
url(r"avatar/$", AvatarImageView.as_view(), name="avatar_view"),
url(
r'avatar/(?P<digest>\w{64})',
AvatarImageView.as_view(), name='avatar_view'),
r"avatar/(?P<digest>\w*)",
RedirectView.as_view(url="/static/img/deadbeef.png"),
name="invalid_hash",
),
url(
r'avatar/(?P<digest>\w{32})',
AvatarImageView.as_view(), name='avatar_view'),
url(r'avatar/$', AvatarImageView.as_view(), name='avatar_view'),
r"gravatarproxy/(?P<digest>\w*)",
GravatarProxyView.as_view(),
name="gravatarproxy",
),
url(
r'avatar/(?P<digest>\w*)',
RedirectView.as_view(url='/static/img/deadbeef.png'), name='invalid_hash'),
url(
r'gravatarproxy/(?P<digest>\w*)',
GravatarProxyView.as_view(), name='gravatarproxy'),
url('description/', TemplateView.as_view(template_name='description.html'), name='description'),
"description/",
TemplateView.as_view(template_name="description.html"),
name="description",
),
# The following two are TODO TODO TODO TODO TODO
url('run_your_own/',
TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'),
url('features/', TemplateView.as_view(template_name='features.html'), name='features'),
url('security/', TemplateView.as_view(template_name='security.html'), name='security'),
url('privacy/', TemplateView.as_view(template_name='privacy.html'), name='privacy'),
url('contact/', TemplateView.as_view(template_name='contact.html'), name='contact'),
path('talk_to_us/', RedirectView.as_view(url='/contact'), name='talk_to_us'),
url('stats/', StatsView.as_view(), name='stats'),
url(
"run_your_own/",
TemplateView.as_view(template_name="run_your_own.html"),
name="run_your_own",
),
url(
"features/",
TemplateView.as_view(template_name="features.html"),
name="features",
),
url(
"security/",
TemplateView.as_view(template_name="security.html"),
name="security",
),
url("privacy/", TemplateView.as_view(template_name="privacy.html"), name="privacy"),
url("contact/", TemplateView.as_view(template_name="contact.html"), name="contact"),
path("talk_to_us/", RedirectView.as_view(url="/contact"), name="talk_to_us"),
url("stats/", StatsView.as_view(), name="stats"),
]
MAINTENANCE = False
@@ -47,11 +63,15 @@ except: # pylint: disable=bare-except
pass
if MAINTENANCE:
urlpatterns.append(url('', TemplateView.as_view(template_name='maintenance.html'), name='home'))
urlpatterns.insert(3, url('accounts/', RedirectView.as_view(url='/')))
urlpatterns.append(
url("", TemplateView.as_view(template_name="maintenance.html"), name="home")
)
urlpatterns.insert(3, url("accounts/", RedirectView.as_view(url="/")))
else:
urlpatterns.append(url('', TemplateView.as_view(template_name='home.html'), name='home'))
urlpatterns.insert(3, url('accounts/', include('ivatar.ivataraccount.urls')))
urlpatterns.append(
url("", TemplateView.as_view(template_name="home.html"), name="home")
)
urlpatterns.insert(3, url("accounts/", include("ivatar.ivataraccount.urls")))
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@@ -1,50 +1,56 @@
'''
# -*- coding: utf-8 -*-
"""
Simple module providing reusable random_string function
'''
"""
import random
import string
from PIL import Image, ImageDraw
def random_string(length=10):
'''
"""
Return some random string with default length 10
'''
return ''.join(random.SystemRandom().choice(
string.ascii_lowercase + string.digits) for _ in range(length))
"""
return "".join(
random.SystemRandom().choice(string.ascii_lowercase + string.digits)
for _ in range(length)
)
def openid_variations(openid):
'''
"""
Return the various OpenID variations, ALWAYS in the same order:
- http w/ trailing slash
- http w/o trailing slash
- https w/ trailing slash
- https w/o trailing slash
'''
"""
# Make the 'base' version: http w/ trailing slash
if openid.startswith('https://'):
openid = openid.replace('https://', 'http://')
if openid[-1] != '/':
openid = openid + '/'
if openid.startswith("https://"):
openid = openid.replace("https://", "http://")
if openid[-1] != "/":
openid = openid + "/"
# http w/o trailing slash
var1 = openid[0:-1]
var2 = openid.replace('http://', 'https://')
var2 = openid.replace("http://", "https://")
var3 = var2[0:-1]
return (openid, var1, var2, var3)
def mm_ng(idhash, size=80, add_red=0, add_green=0, add_blue=0): #pylint: disable=too-many-locals
'''
def mm_ng(
idhash, size=80, add_red=0, add_green=0, add_blue=0
): # pylint: disable=too-many-locals
"""
Return an MM (mystery man) image, based on a given hash
add some red, green or blue, if specified
'''
"""
# Make sure the lightest bg color we paint is e0, else
# we do not see the MM any more
if idhash[0] == 'f':
idhash = 'e0'
if idhash[0] == "f":
idhash = "e0"
# How large is the circle?
circlesize = size * 0.6
@@ -62,44 +68,47 @@ def mm_ng(idhash, size=80, add_red=0, add_green=0, add_blue=0): #pylint: disabl
blue = idhash[0:2]
# Add some red (i/a) and make sure it's not over 255
red = hex(int(red, 16)+add_red).replace('0x', '')
red = hex(int(red, 16) + add_red).replace("0x", "")
if int(red, 16) > 255:
red = 'ff'
red = "ff"
if len(red) == 1:
red = '0%s' % red
red = "0%s" % red
# Add some green (i/a) and make sure it's not over 255
green = hex(int(green, 16)+add_green).replace('0x', '')
green = hex(int(green, 16) + add_green).replace("0x", "")
if int(green, 16) > 255:
green = 'ff'
green = "ff"
if len(green) == 1:
green = '0%s' % green
green = "0%s" % green
# Add some blue (i/a) and make sure it's not over 255
blue = hex(int(blue, 16)+add_blue).replace('0x', '')
blue = hex(int(blue, 16) + add_blue).replace("0x", "")
if int(blue, 16) > 255:
blue = 'ff'
blue = "ff"
if len(blue) == 1:
blue = '0%s' % blue
blue = "0%s" % blue
# Assemable the bg color "string" in webnotation. Eg. '#d3d3d3'
bg_color = '#' + red + green + blue
bg_color = "#" + red + green + blue
# Image
image = Image.new('RGB', (size, size))
image = Image.new("RGB", (size, size))
draw = ImageDraw.Draw(image)
# Draw background
draw.rectangle(((0, 0), (size, size)), fill=bg_color)
# Draw MMs head
draw.ellipse((start_x, start_y, end_x, end_y), fill='white')
draw.ellipse((start_x, start_y, end_x, end_y), fill="white")
# Draw MMs 'body'
draw.polygon((
draw.polygon(
(
(start_x + circlesize / 2, size / 2.5),
(size * 0.15, size),
(size-size*0.15, size)),
fill='white')
(size - size * 0.15, size),
),
fill="white",
)
return image

View File

@@ -1,6 +1,7 @@
'''
# -*- coding: utf-8 -*-
"""
views under /
'''
"""
from io import BytesIO
from os import path
import hashlib
@@ -12,7 +13,7 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.http import HttpResponseNotFound, JsonResponse
from django.core.exceptions import ObjectDoesNotExist
from django.core.cache import cache, caches
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.urls import reverse_lazy
from django.db.models import Q
from django.contrib.auth.models import User
@@ -28,6 +29,7 @@ from robohash import Robohash
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
from ivatar.settings import CACHE_RESPONSE
from ivatar.settings import CACHE_IMAGES_MAX_AGE
from ivatar.settings import TRUSTED_DEFAULT_URLS
from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
from .ivataraccount.models import pil_format, file_format
from .utils import mm_ng
@@ -36,16 +38,16 @@ URL_TIMEOUT = 5 # in seconds
def get_size(request, size=DEFAULT_AVATAR_SIZE):
'''
"""
Get size from the URL arguments
'''
"""
sizetemp = None
if 's' in request.GET:
sizetemp = request.GET['s']
if 'size' in request.GET:
sizetemp = request.GET['size']
if "s" in request.GET:
sizetemp = request.GET["s"]
if "size" in request.GET:
sizetemp = request.GET["size"]
if sizetemp:
if sizetemp != '' and sizetemp is not None and sizetemp != '0':
if sizetemp != "" and sizetemp is not None and sizetemp != "0":
try:
if int(sizetemp) > 0:
size = int(sizetemp)
@@ -60,39 +62,54 @@ def get_size(request, size=DEFAULT_AVATAR_SIZE):
class CachingHttpResponse(HttpResponse):
'''
"""
Handle caching of response
'''
def __init__(self, uri, content=b'', content_type=None, status=200, # pylint: disable=too-many-arguments
reason=None, charset=None):
"""
def __init__(
self,
uri,
content=b"",
content_type=None,
status=200, # pylint: disable=too-many-arguments
reason=None,
charset=None,
):
if CACHE_RESPONSE:
caches['filesystem'].set(uri, {
'content': content,
'content_type': content_type,
'status': status,
'reason': reason,
'charset': charset
})
caches["filesystem"].set(
uri,
{
"content": content,
"content_type": content_type,
"status": status,
"reason": reason,
"charset": charset,
},
)
super().__init__(content, content_type, status, reason, charset)
class AvatarImageView(TemplateView):
'''
"""
View to return (binary) image, based on OpenID/Email (both by digest)
'''
"""
# TODO: Do cache resize images!! Memcached?
def options(self, request, *args, **kwargs):
response = HttpResponse("", content_type='text/plain')
response['Allow'] = "404 mm mp retro pagan wavatar monsterid robohash identicon"
response = HttpResponse("", content_type="text/plain")
response["Allow"] = "404 mm mp retro pagan wavatar monsterid robohash identicon"
return response
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
'''
def get(
self, request, *args, **kwargs
): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
"""
Override get from parent class
'''
"""
model = ConfirmedEmail
size = get_size(request)
imgformat = 'png'
imgformat = "png"
obj = None
default = None
forcedefault = False
@@ -102,65 +119,80 @@ class AvatarImageView(TemplateView):
# Check the cache first
if CACHE_RESPONSE:
centry = caches['filesystem'].get(uri)
centry = caches["filesystem"].get(uri)
if centry:
# For DEBUG purpose only print('Cached entry for %s' % uri)
return HttpResponse(
centry['content'],
content_type=centry['content_type'],
status=centry['status'],
reason=centry['reason'],
charset=centry['charset'])
centry["content"],
content_type=centry["content_type"],
status=centry["status"],
reason=centry["reason"],
charset=centry["charset"],
)
# In case no digest at all is provided, return to home page
if 'digest' not in kwargs:
return HttpResponseRedirect(reverse_lazy('home'))
if "digest" not in kwargs:
return HttpResponseRedirect(reverse_lazy("home"))
if 'd' in request.GET:
default = request.GET['d']
if 'default' in request.GET:
default = request.GET['default']
if "d" in request.GET:
default = request.GET["d"]
if "default" in request.GET:
default = request.GET["default"]
if 'f' in request.GET:
if request.GET['f'] == 'y':
# Check if default starts with an URL scheme and if it does,
# check if it's trusted
# Check for :// (schema)
if default is not None and default.find("://") > 0:
# Check if it's trusted, if not, reset to None
if not any(x in default for x in TRUSTED_DEFAULT_URLS):
print(
"Default URL is not in trusted URLs: '%s' ; Kicking it!" % default
)
default = None
if "f" in request.GET:
if request.GET["f"] == "y":
forcedefault = True
if 'forcedefault' in request.GET:
if request.GET['forcedefault'] == 'y':
if "forcedefault" in request.GET:
if request.GET["forcedefault"] == "y":
forcedefault = True
if 'gravatarredirect' in request.GET:
if request.GET['gravatarredirect'] == 'y':
if "gravatarredirect" in request.GET:
if request.GET["gravatarredirect"] == "y":
gravatarredirect = True
if 'gravatarproxy' in request.GET:
if request.GET['gravatarproxy'] == 'n':
if "gravatarproxy" in request.GET:
if request.GET["gravatarproxy"] == "n":
gravatarproxy = False
try:
obj = model.objects.get(digest=kwargs['digest'])
obj = model.objects.get(digest=kwargs["digest"])
except ObjectDoesNotExist:
try:
obj = model.objects.get(digest_sha256=kwargs['digest'])
obj = model.objects.get(digest_sha256=kwargs["digest"])
except ObjectDoesNotExist:
model = ConfirmedOpenId
try:
d = kwargs['digest'] # pylint: disable=invalid-name
d = kwargs["digest"] # pylint: disable=invalid-name
# OpenID is tricky. http vs. https, versus trailing slash or not
# However, some users eventually have added their variations already
# and therfore we need to use filter() and first()
obj = model.objects.filter(
Q(digest=d) |
Q(alt_digest1=d) |
Q(alt_digest2=d) |
Q(alt_digest3=d)).first()
except: # pylint: disable=bare-except
Q(digest=d)
| Q(alt_digest1=d)
| Q(alt_digest2=d)
| Q(alt_digest3=d)
).first()
except Exception: # pylint: disable=bare-except
pass
# If that mail/openid doesn't exist, or has no photo linked to it
if not obj or not obj.photo or forcedefault:
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ '?s=%i' % size
gravatar_url = (
"https://secure.gravatar.com/avatar/"
+ kwargs["digest"]
+ "?s=%i" % size
)
# If we have redirection to Gravatar enabled, this overrides all
# default= settings, except forcedefault!
@@ -169,119 +201,115 @@ class AvatarImageView(TemplateView):
# Request to proxy Gravatar image - only if not forcedefault
if gravatarproxy and not forcedefault:
url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \
+ '?s=%i' % size + '&default=%s' % default
url = (
reverse_lazy("gravatarproxy", args=[kwargs["digest"]])
+ "?s=%i" % size
)
# Ensure we do not convert None to string 'None'
if default:
url += "&default=%s" % default
return HttpResponseRedirect(url)
# Return the default URL, as specified, or 404 Not Found, if default=404
if default:
# Proxy to gravatar to generate wavatar - lazy me
if str(default) == 'wavatar':
url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \
+ '?s=%i' % size + '&default=%s&f=y' % default
if str(default) == "wavatar":
url = (
reverse_lazy("gravatarproxy", args=[kwargs["digest"]])
+ "?s=%i" % size
+ "&default=%s&f=y" % default
)
return HttpResponseRedirect(url)
if str(default) == str(404):
return HttpResponseNotFound(_('<h1>Image not found</h1>'))
return HttpResponseNotFound(_("<h1>Image not found</h1>"))
if str(default) == 'monsterid':
monsterdata = BuildMonster(seed=kwargs['digest'], size=(size, size))
if str(default) == "monsterid":
monsterdata = BuildMonster(seed=kwargs["digest"], size=(size, size))
data = BytesIO()
monsterdata.save(data, 'PNG', quality=JPEG_QUALITY)
monsterdata.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'robohash':
roboset = 'any'
if request.GET.get('robohash'):
roboset = request.GET.get('robohash')
robohash = Robohash(kwargs['digest'])
if str(default) == "robohash":
roboset = "any"
if request.GET.get("robohash"):
roboset = request.GET.get("robohash")
robohash = Robohash(kwargs["digest"])
robohash.assemble(roboset=roboset, sizex=size, sizey=size)
data = BytesIO()
robohash.img.save(data, format='png')
robohash.img.save(data, format="png")
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'retro':
identicon = Identicon.render(kwargs['digest'])
if str(default) == "retro":
identicon = Identicon.render(kwargs["digest"])
data = BytesIO()
img = Image.open(BytesIO(identicon))
img = img.resize((size, size), Image.ANTIALIAS)
img.save(data, 'PNG', quality=JPEG_QUALITY)
img.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'pagan':
paganobj = pagan.Avatar(kwargs['digest'])
if str(default) == "pagan":
paganobj = pagan.Avatar(kwargs["digest"])
data = BytesIO()
img = paganobj.img.resize((size, size), Image.ANTIALIAS)
img.save(data, 'PNG', quality=JPEG_QUALITY)
img.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'identicon':
if str(default) == "identicon":
p = Pydenticon5() # pylint: disable=invalid-name
# In order to make use of the whole 32 bytes digest, we need to redigest them.
newdigest = hashlib.md5(bytes(kwargs['digest'], 'utf-8')).hexdigest()
newdigest = hashlib.md5(
bytes(kwargs["digest"], "utf-8")
).hexdigest()
img = p.draw(newdigest, size, 0)
data = BytesIO()
img.save(data, 'PNG', quality=JPEG_QUALITY)
img.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'mmng':
mmngimg = mm_ng(idhash=kwargs['digest'], size=size)
if str(default) == "mmng":
mmngimg = mm_ng(idhash=kwargs["digest"], size=size)
data = BytesIO()
mmngimg.save(data, 'PNG', quality=JPEG_QUALITY)
mmngimg.save(data, "PNG", quality=JPEG_QUALITY)
data.seek(0)
response = CachingHttpResponse(
uri,
data,
content_type='image/png')
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
response = CachingHttpResponse(uri, data, content_type="image/png")
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
if str(default) == 'mm' or str(default) == 'mp':
if str(default) == "mm" or str(default) == "mp":
# If mm is explicitly given, we need to catch that
static_img = path.join('static', 'img', 'mm', '%s%s' % (str(size), '.png'))
static_img = path.join(
"static", "img", "mm", "%s%s" % (str(size), ".png")
)
if not path.isfile(static_img):
# We trust this exists!!!
static_img = path.join('static', 'img', 'mm', '512.png')
static_img = path.join("static", "img", "mm", "512.png")
# We trust static/ is mapped to /static/
return HttpResponseRedirect('/' + static_img)
return HttpResponseRedirect("/" + static_img)
return HttpResponseRedirect(default)
static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png'))
static_img = path.join(
"static", "img", "nobody", "%s%s" % (str(size), ".png")
)
if not path.isfile(static_img):
# We trust this exists!!!
static_img = path.join('static', 'img', 'nobody', '512.png')
static_img = path.join("static", "img", "nobody", "512.png")
# We trust static/ is mapped to /static/
return HttpResponseRedirect('/' + static_img)
return HttpResponseRedirect("/" + static_img)
imgformat = obj.photo.format
photodata = Image.open(BytesIO(obj.photo.data))
@@ -298,31 +326,35 @@ class AvatarImageView(TemplateView):
obj.photo.save()
obj.access_count += 1
obj.save()
if imgformat == 'jpg':
imgformat = 'jpeg'
response = CachingHttpResponse(
uri,
data,
content_type='image/%s' % imgformat)
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
if imgformat == "jpg":
imgformat = "jpeg"
response = CachingHttpResponse(uri, data, content_type="image/%s" % imgformat)
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
class GravatarProxyView(View):
'''
"""
Proxy request to Gravatar and return the image from there
'''
"""
# TODO: Do cache images!! Memcached?
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
'''
def get(
self, request, *args, **kwargs
): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
"""
Override get from parent class
'''
"""
def redir_default(default=None):
url = reverse_lazy(
'avatar_view',
args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
url = (
reverse_lazy("avatar_view", args=[kwargs["digest"]])
+ "?s=%i" % size
+ "&forcedefault=y"
)
if default is not None:
url += '&default=%s' % default
url += "&default=%s" % default
return HttpResponseRedirect(url)
size = get_size(request)
@@ -330,70 +362,73 @@ class GravatarProxyView(View):
default = None
try:
if str(request.GET['default']) != 'None':
default = request.GET['default']
except: # pylint: disable=bare-except
if str(request.GET["default"]) != "None":
default = request.GET["default"]
except Exception: # pylint: disable=bare-except
pass
if str(default) != 'wavatar':
if str(default) != "wavatar":
# This part is special/hackish
# Check if the image returned by Gravatar is their default image, if so,
# redirect to our default instead.
gravatar_test_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ '?s=%i' % 50
if cache.get(gravatar_test_url) == 'default':
gravatar_test_url = (
"https://secure.gravatar.com/avatar/"
+ kwargs["digest"]
+ "?s=%i&d=%i" % (50, 404)
)
if cache.get(gravatar_test_url) == "default":
# DEBUG only
# print("Cached Gravatar response: Default.")
return redir_default(default)
try:
testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
data = BytesIO(testdata.read())
if hashlib.md5(data.read()).hexdigest() == '71bc262d627971d13fe6f3180b93062a':
cache.set(gravatar_test_url, 'default', 60)
urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
except HTTPError as exc:
if exc.code == 404:
cache.set(gravatar_test_url, "default", 60)
else:
print("Gravatar test url fetch failed: %s" % exc)
return redir_default(default)
except Exception as exc: # pylint: disable=broad-except
print('Gravatar test url fetch failed: %s' % exc)
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ '?s=%i' % size + '&d=%s' % default
gravatar_url = (
"https://secure.gravatar.com/avatar/" + kwargs["digest"] + "?s=%i" % size
)
if default:
gravatar_url += "&d=%s" % default
try:
if cache.get(gravatar_url) == 'err':
print('Cached Gravatar fetch failed with URL error')
if cache.get(gravatar_url) == "err":
print("Cached Gravatar fetch failed with URL error: %s" % gravatar_url)
return redir_default(default)
gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT)
except HTTPError as exc:
if exc.code != 404 and exc.code != 503:
print(
'Gravatar fetch failed with an unexpected %s HTTP error' %
exc.code)
cache.set(gravatar_url, 'err', 30)
"Gravatar fetch failed with an unexpected %s HTTP error: %s"
% (exc.code, gravatar_url)
)
cache.set(gravatar_url, "err", 30)
return redir_default(default)
except URLError as exc:
print(
'Gravatar fetch failed with URL error: %s' %
exc.reason)
cache.set(gravatar_url, 'err', 30)
print("Gravatar fetch failed with URL error: %s" % exc.reason)
cache.set(gravatar_url, "err", 30)
return redir_default(default)
except SSLError as exc:
print(
'Gravatar fetch failed with SSL error: %s' %
exc.reason)
cache.set(gravatar_url, 'err', 30)
print("Gravatar fetch failed with SSL error: %s" % exc.reason)
cache.set(gravatar_url, "err", 30)
return redir_default(default)
try:
data = BytesIO(gravatarimagedata.read())
img = Image.open(data)
data.seek(0)
response = HttpResponse(
data.read(),
content_type='image/%s' % file_format(img.format))
response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE
data.read(), content_type="image/%s" % file_format(img.format)
)
response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE
return response
except ValueError as exc:
print('Value error: %s' % exc)
print("Value error: %s" % exc)
return redir_default(default)
# We shouldn't reach this point... But make sure we do something
@@ -401,14 +436,17 @@ class GravatarProxyView(View):
class StatsView(TemplateView, JsonResponse):
'''
"""
Return stats
'''
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
"""
def get(
self, request, *args, **kwargs
): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements
retval = {
'users': User.objects.all().count(),
'mails': ConfirmedEmail.objects.all().count(),
'openids': ConfirmedOpenId.objects.all().count(), # pylint: disable=no-member
"users": User.objects.all().count(),
"mails": ConfirmedEmail.objects.all().count(),
"openids": ConfirmedOpenId.objects.all().count(), # pylint: disable=no-member
}
return JsonResponse(retval)

View File

@@ -1,21 +1,26 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import urllib.request
import sys
import os
sys.stderr.buffer.write(b'%s' % bytes(os.environ.get("QUERY_STRING", "No Query String in url"), 'utf-8'))
sys.stderr.buffer.write(
b"%s" % bytes(os.environ.get("QUERY_STRING", "No Query String in url"), "utf-8")
)
link = 'https://www.libravatar.org/avatar/%s' % os.environ.get("QUERY_STRING", 'x'*32)
sys.stderr.buffer.write(b'%s' % bytes(link, 'utf-8'))
link = "https://www.libravatar.org/avatar/%s" % os.environ.get("QUERY_STRING", "x" * 32)
sys.stderr.buffer.write(b"%s" % bytes(link, "utf-8"))
data = None
with urllib.request.urlopen(link) as f:
data = f.read()
for header in f.headers._headers:
if header[0] == 'Content-Type':
sys.stdout.buffer.write(b"%s: %s\n\n" % (bytes(header[0], 'utf-8'), bytes(header[1], 'utf-8')))
if header[0] == "Content-Type":
sys.stdout.buffer.write(
b"%s: %s\n\n" % (bytes(header[0], "utf-8"), bytes(header[1], "utf-8"))
)
sys.stdout.flush()
break

View File

@@ -1,26 +1,37 @@
autopep8
bcrypt
defusedxml
Django
Django < 4.0
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,14 +39,4 @@ 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
py-avataaars

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>

View File

@@ -60,6 +60,12 @@ ivatar/Libravatar more secure by reporting security issues to us.
title="https://www.linkedin.com/in/naharronak/" target="_new">
Ronak Nahar</a>:
Spotted and reported open server status from Apache HTTPD.</li>
<li>
<a href="https://daniel.priv.no/"
title="https://daniel.priv.no/" target="_new">
Daniel Aleksandersen</a>:
Spotted and reported an open redirect vulnerability, as described in <a href="https://cwe.mitre.org/data/definitions/601.html" taget="_new">CWE-601</a>.</li>
</ul>
<div style="height:40px"></div>