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

6
.flake8 Normal file
View File

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

View File

@@ -1,7 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
''' # -*- coding: utf-8 -*-
"""
Import the whole libravatar export Import the whole libravatar export
''' """
import os import os
from os.path import isfile, isdir, join from os.path import isfile, isdir, join
@@ -9,13 +10,18 @@ import sys
import base64 import base64
from io import BytesIO from io import BytesIO
import django 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 django.setup() # pylint: disable=wrong-import-position
from django.contrib.auth.models import User from django.contrib.auth.models import User
from PIL import Image from PIL import Image
from django_openid_auth.models import UserOpenID from django_openid_auth.models import UserOpenID
from ivatar.settings import JPEG_QUALITY 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 ConfirmedEmail
from ivatar.ivataraccount.models import ConfirmedOpenId from ivatar.ivataraccount.models import ConfirmedOpenId
from ivatar.ivataraccount.models import Photo from ivatar.ivataraccount.models import Photo
@@ -26,54 +32,63 @@ if len(sys.argv) < 2:
exit(-255) exit(-255)
if not isdir(sys.argv[1]): 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) exit(-255)
PATH = sys.argv[1] PATH = sys.argv[1]
for file in os.listdir(PATH): for file in os.listdir(PATH):
if not file.endswith('.xml.gz'): if not file.endswith(".xml.gz"):
continue continue
if isfile(join(PATH, file)): if isfile(join(PATH, file)):
fh = open(join(PATH, file), 'rb') fh = open(join(PATH, file), "rb")
items = libravatar_read_gzdata(fh.read()) items = libravatar_read_gzdata(fh.read())
print('Adding user "%s"' % items['username']) print('Adding user "%s"' % items["username"])
(user, created) = User.objects.get_or_create(username=items['username']) (user, created) = User.objects.get_or_create(username=items["username"])
user.password = items['password'] user.password = items["password"]
user.save() user.save()
saved_photos = {} saved_photos = {}
for photo in items['photos']: for photo in items["photos"]:
photo_id = photo['id'] photo_id = photo["id"]
data = base64.decodebytes(bytes(photo['data'], 'utf-8')) data = base64.decodebytes(bytes(photo["data"], "utf-8"))
pilobj = Image.open(BytesIO(data)) pilobj = Image.open(BytesIO(data))
out = BytesIO() out = BytesIO()
pilobj.save(out, pilobj.format, quality=JPEG_QUALITY) pilobj.save(out, pilobj.format, quality=JPEG_QUALITY)
out.seek(0) out.seek(0)
photo = Photo() photo = Photo()
photo.user = user photo.user = user
photo.ip_address = '0.0.0.0' photo.ip_address = "0.0.0.0"
photo.format = file_format(pilobj.format) photo.format = file_format(pilobj.format)
photo.data = out.read() photo.data = out.read()
photo.save() photo.save()
saved_photos[photo_id] = photo saved_photos[photo_id] = photo
for email in items['emails']: for email in items["emails"]:
try: try:
ConfirmedEmail.objects.get_or_create(email=email['email'], user=user, ConfirmedEmail.objects.get_or_create(
photo=saved_photos.get(email['photo_id'])) email=email["email"],
except django.db.utils.IntegrityError: user=user,
print('%s not unique?' % email['email']) photo=saved_photos.get(email["photo_id"]),
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: 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() fh.close()

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,11 @@
''' # -*- coding: utf-8 -*-
"""
Classes for our ivatar.ivataraccount.forms Classes for our ivatar.ivataraccount.forms
''' """
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
from django import forms 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 from ipware import get_client_ip
@@ -20,93 +21,97 @@ MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5
class AddEmailForm(forms.Form): class AddEmailForm(forms.Form):
''' """
Form to handle adding email addresses Form to handle adding email addresses
''' """
email = forms.EmailField( email = forms.EmailField(
label=_('Email'), label=_("Email"),
min_length=MIN_LENGTH_EMAIL, min_length=MIN_LENGTH_EMAIL,
max_length=MAX_LENGTH_EMAIL, max_length=MAX_LENGTH_EMAIL,
) )
def clean_email(self): def clean_email(self):
''' """
Enforce lowercase email Enforce lowercase email
''' """
# TODO: Domain restriction as in libravatar? # TODO: Domain restriction as in libravatar?
return self.cleaned_data['email'].lower() return self.cleaned_data["email"].lower()
def save(self, request): def save(self, request):
''' """
Save the model, ensuring some safety Save the model, ensuring some safety
''' """
user = request.user user = request.user
# Enforce the maximum number of unconfirmed emails a user can have # Enforce the maximum number of unconfirmed emails a user can have
num_unconfirmed = user.unconfirmedemail_set.count() num_unconfirmed = user.unconfirmedemail_set.count()
max_num_unconfirmed_emails = getattr( max_num_unconfirmed_emails = getattr(
settings, settings, "MAX_NUM_UNCONFIRMED_EMAILS", MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
'MAX_NUM_UNCONFIRMED_EMAILS', )
MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT)
if num_unconfirmed >= max_num_unconfirmed_emails: 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 return False
# Check whether or not a confirmation email has been # Check whether or not a confirmation email has been
# sent by this user already # sent by this user already
if UnconfirmedEmail.objects.filter( # pylint: disable=no-member if UnconfirmedEmail.objects.filter( # pylint: disable=no-member
user=user, email=self.cleaned_data['email']).exists(): user=user, email=self.cleaned_data["email"]
self.add_error( ).exists():
'email', self.add_error("email", _("Address already added, currently unconfirmed"))
_('Address already added, currently unconfirmed'))
return False return False
# Check whether or not the email is already confirmed (by someone) # Check whether or not the email is already confirmed (by someone)
check_mail = ConfirmedEmail.objects.filter( check_mail = ConfirmedEmail.objects.filter(email=self.cleaned_data["email"])
email=self.cleaned_data['email'])
if check_mail.exists(): 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: if check_mail.first().user == request.user:
msg = _('Address already confirmed (by you)') msg = _("Address already confirmed (by you)")
self.add_error('email', msg) self.add_error("email", msg)
return False return False
unconfirmed = UnconfirmedEmail() unconfirmed = UnconfirmedEmail()
unconfirmed.email = self.cleaned_data['email'] unconfirmed.email = self.cleaned_data["email"]
unconfirmed.user = user unconfirmed.user = user
unconfirmed.save() unconfirmed.save()
unconfirmed.send_confirmation_mail(url=request.build_absolute_uri('/')[:-1]) unconfirmed.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1])
return True return True
class UploadPhotoForm(forms.Form): class UploadPhotoForm(forms.Form):
''' """
Form handling photo upload Form handling photo upload
''' """
photo = forms.FileField( photo = forms.FileField(
label=_('Photo'), label=_("Photo"),
error_messages={'required': _('You must choose an image to upload.')}) error_messages={"required": _("You must choose an image to upload.")},
)
not_porn = forms.BooleanField( 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, required=True,
error_messages={ error_messages={
'required': "required": _(
_('We only host "G-rated" images and so this field must be checked.') 'We only host "G-rated" images and so this field must be checked.'
}) )
},
)
can_distribute = forms.BooleanField( can_distribute = forms.BooleanField(
label=_('can be freely copied'), label=_("can be freely copied"),
required=True, required=True,
error_messages={ error_messages={
'required': "required": _(
_('This field must be checked since we need to be able to distribute photos to third parties.') "This field must be checked since we need to be able to distribute photos to third parties."
}) )
},
)
@staticmethod @staticmethod
def save(request, data): def save(request, data):
''' """
Save the model and assign it to the current user Save the model and assign it to the current user
''' """
# Link this file to the user's profile # Link this file to the user's profile
photo = Photo() photo = Photo()
photo.user = request.user photo.user = request.user
@@ -119,47 +124,48 @@ class UploadPhotoForm(forms.Form):
class AddOpenIDForm(forms.Form): class AddOpenIDForm(forms.Form):
''' """
Form to handle adding OpenID Form to handle adding OpenID
''' """
openid = forms.URLField( openid = forms.URLField(
label=_('OpenID'), label=_("OpenID"),
min_length=MIN_LENGTH_URL, min_length=MIN_LENGTH_URL,
max_length=MAX_LENGTH_URL, max_length=MAX_LENGTH_URL,
initial='http://' initial="http://",
) )
def clean_openid(self): def clean_openid(self):
''' """
Enforce restrictions Enforce restrictions
''' """
# Lowercase hostname port of the URL # Lowercase hostname port of the URL
url = urlsplit(self.cleaned_data['openid']) url = urlsplit(self.cleaned_data["openid"])
data = urlunsplit( data = urlunsplit(
(url.scheme.lower(), url.netloc.lower(), url.path, (url.scheme.lower(), url.netloc.lower(), url.path, url.query, url.fragment)
url.query, url.fragment)) )
# TODO: Domain restriction as in libravatar? # TODO: Domain restriction as in libravatar?
return data return data
def save(self, user): def save(self, user):
''' """
Save the model, ensuring some safety Save the model, ensuring some safety
''' """
if ConfirmedOpenId.objects.filter( # pylint: disable=no-member if ConfirmedOpenId.objects.filter( # pylint: disable=no-member
openid=self.cleaned_data['openid']).exists(): openid=self.cleaned_data["openid"]
self.add_error('openid', _('OpenID already added and confirmed!')) ).exists():
self.add_error("openid", _("OpenID already added and confirmed!"))
return False return False
if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member
openid=self.cleaned_data['openid']).exists(): openid=self.cleaned_data["openid"]
self.add_error( ).exists():
'openid', self.add_error("openid", _("OpenID already added, but not confirmed yet!"))
_('OpenID already added, but not confirmed yet!'))
return False return False
unconfirmed = UnconfirmedOpenId() unconfirmed = UnconfirmedOpenId()
unconfirmed.openid = self.cleaned_data['openid'] unconfirmed.openid = self.cleaned_data["openid"]
unconfirmed.user = user unconfirmed.user = user
unconfirmed.save() unconfirmed.save()
@@ -167,40 +173,50 @@ class AddOpenIDForm(forms.Form):
class UpdatePreferenceForm(forms.ModelForm): class UpdatePreferenceForm(forms.ModelForm):
''' """
Form for updating user preferences Form for updating user preferences
''' """
class Meta: # pylint: disable=too-few-public-methods class Meta: # pylint: disable=too-few-public-methods
''' """
Meta class for UpdatePreferenceForm Meta class for UpdatePreferenceForm
''' """
model = UserPreference model = UserPreference
fields = ['theme'] fields = ["theme"]
class UploadLibravatarExportForm(forms.Form): class UploadLibravatarExportForm(forms.Form):
''' """
Form handling libravatar user export upload Form handling libravatar user export upload
''' """
export_file = forms.FileField( export_file = forms.FileField(
label=_('Export file'), label=_("Export file"),
error_messages={'required': _('You must choose an export file to upload.')}) error_messages={"required": _("You must choose an export file to upload.")},
)
not_porn = forms.BooleanField( 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, required=True,
error_messages={ error_messages={
'required': "required": _(
_('We only host "G-rated" images and so this field must be checked.') 'We only host "G-rated" images and so this field must be checked.'
}) )
},
)
can_distribute = forms.BooleanField( can_distribute = forms.BooleanField(
label=_('can be freely copied'), label=_("can be freely copied"),
required=True, required=True,
error_messages={ error_messages={
'required': "required": _(
_('This field must be checked since we need to be able to\ "This field must be checked since we need to be able to\
distribute photos to third parties.') distribute photos to third parties."
}) )
},
)
class DeleteAccountForm(forms.Form): 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 Helper method to fetch Gravatar image
''' """
from ssl import SSLError from ssl import SSLError
from urllib.request import urlopen, HTTPError, URLError from urllib.request import urlopen, HTTPError, URLError
import hashlib import hashlib
@@ -11,43 +12,47 @@ URL_TIMEOUT = 5 # in seconds
def get_photo(email): def get_photo(email):
''' """
Fetch photo from Gravatar, given an email address Fetch photo from Gravatar, given an email address
''' """
hash_object = hashlib.new('md5') hash_object = hashlib.new("md5")
hash_object.update(email.lower().encode('utf-8')) hash_object.update(email.lower().encode("utf-8"))
thumbnail_url = 'https://secure.gravatar.com/avatar/' + \ thumbnail_url = (
hash_object.hexdigest() + '?s=%i&d=404' % AVATAR_MAX_SIZE "https://secure.gravatar.com/avatar/"
image_url = 'https://secure.gravatar.com/avatar/' + hash_object.hexdigest( + hash_object.hexdigest()
) + '?s=512&d=404' + "?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 # 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: try:
urlopen(image_url, timeout=URL_TIMEOUT) urlopen(image_url, timeout=URL_TIMEOUT)
except HTTPError as exc: except HTTPError as exc:
if exc.code != 404 and exc.code != 503: if exc.code != 404 and exc.code != 503:
print( # pragma: no cover print( # pragma: no cover
'Gravatar fetch failed with an unexpected %s HTTP error' % "Gravatar fetch failed with an unexpected %s HTTP error" % exc.code
exc.code) )
return False return False
except URLError as exc: # pragma: no cover except URLError as exc: # pragma: no cover
print( print(
'Gravatar fetch failed with URL error: %s' % "Gravatar fetch failed with URL error: %s" % exc.reason
exc.reason) # pragma: no cover ) # pragma: no cover
return False # pragma: no cover return False # pragma: no cover
except SSLError as exc: # pragma: no cover except SSLError as exc: # pragma: no cover
print( print(
'Gravatar fetch failed with SSL error: %s' % "Gravatar fetch failed with SSL error: %s" % exc.reason
exc.reason) # pragma: no cover ) # pragma: no cover
return False # pragma: no cover return False # pragma: no cover
return { return {
'thumbnail_url': thumbnail_url, "thumbnail_url": thumbnail_url,
'image_url': image_url, "image_url": image_url,
'width': AVATAR_MAX_SIZE, "width": AVATAR_MAX_SIZE,
'height': AVATAR_MAX_SIZE, "height": AVATAR_MAX_SIZE,
'service_url': service_url, "service_url": service_url,
'service_name': 'Gravatar' "service_name": "Gravatar",
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,16 @@
{% block title %}{% trans 'Your Profile' %}{% endblock title %} {% block title %}{% trans 'Your Profile' %}{% endblock title %}
{% block content %} {% 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> <h1>
{% trans 'Your Profile' %} - {% trans 'Your Profile' %} -
{% if user.first_name and user.last_name %} {% if user.first_name and user.last_name %}
@@ -66,43 +75,122 @@ outline: inherit;
display: contents; display: contents;
} }
} }
@media only screen and (max-width: 470px) {
p {
padding-top: 2rem;
}
h3{
line-height: 3.4rem;
}
}
</style> </style>
<noscript>
<style type="text/css">
.profile-container > ul{
display:block;
}
</style>
</noscript>
{% if user.confirmedemail_set.count or user.confirmedopenid_set.count %} {% if user.confirmedemail_set.count or user.confirmedopenid_set.count %}
<h3>{% trans 'You have the following confirmed identities:' %}</h3> <h3>{% trans 'You have the following confirmed identities:' %}</h3>
<div class="row"> <div class="row profileid">
{% for email in user.confirmedemail_set.all %} {% for email in user.confirmedemail_set.all %}
{% if user.confirmedemail_set.all|length == 1%}
<form action="{% url 'remove_confirmed_email' email.id %}" method="post"> <form action="{% url 'remove_confirmed_email' email.id %}" method="post">
{% csrf_token %} {% csrf_token %}
<div class="panel" style="width:172px;margin-left:20px;float:left"> <div id="email-conf-{{ forloop.counter }}" class="profile-container active">
<div class="panel-heading" style="padding-right:0"> <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" title="{{ email.email }}" style="display:inline-flex"><a href="{% url 'assign_photo_email' email.id %}"><i class="fa fa-edit"></i></a>&nbsp; <h3 class="panel-title email-profile" title="{{ email.email }}">
<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 }}
{{ email.email|truncatechars:12 }}</h3> </h3>
</div> <ul>
<div class="panel-body" style="height:130px"> <li>
<center> <a href="{% url 'assign_photo_email' email.id %}">
<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 %}"> Change Profile Picture
</center> </a>
</div> </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> </div>
</form> </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 %} {% endfor %}
{% for openid in user.confirmedopenid_set.all %} {% 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 %} <form action="{% url 'remove_confirmed_openid' openid.id %}" method="post">{% csrf_token %}
<div class="panel" style="width:172px;margin-left:20px;float:left"> <div>
<div class="panel-heading" style="padding-right:0"> <div id="id-conf-{{ forloop.counter }}" class="profile-container active">
<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; <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 %}">
<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; <h3 class="panel-title email-profile" title="{{ openid.openid }}">
{{ openid.openid|cut:"http://"|cut:"https://"|truncatechars:12 }}</h3> {{ openid.openid }}
</div> </h3>
<div class="panel-body" style="height:130px"> <ul>
<center> <li>
<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 %}"> <a href="{% url 'assign_photo_openid' openid.pk %}">
</center> 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>
</div> </div>
</form> </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 %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@@ -131,8 +219,7 @@ outline: inherit;
{# TODO: (expires in xx hours) #} {# TODO: (expires in xx hours) #}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<p style="padding-top:5px;">
<p>
{% if not max_emails %}<a href="{% url 'add_email' %}" class="button" >{% trans 'Add a new email address' %}</a>&nbsp;{% endif %} {% 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> <a href="{% url 'add_openid' %}" class="button">{% trans 'Add a new OpenID' %}</a></p>
</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> <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>
<div class="panel-body" style="height:130px"> <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 %}"> <img title="{% trans 'Access count' %}: {{ photo.access_count }}" style="max-height:100px;max-width:100px" src="{% url 'raw_image' photo.id %}">
</center>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
""" """
Django settings for ivatar project. Django settings for ivatar project.
""" """
@@ -6,7 +7,7 @@ import os
import logging import logging
log_level = logging.DEBUG # pylint: disable=invalid-name 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) logger.setLevel(log_level)
PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__)) 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! # 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
@@ -25,52 +26,52 @@ ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
ROOT_URLCONF = 'ivatar.urls' ROOT_URLCONF = "ivatar.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [], "DIRS": [],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'ivatar.wsgi.application' WSGI_APPLICATION = "ivatar.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.sqlite3', "ENGINE": "django.db.backends.sqlite3",
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
} }
} }
@@ -80,16 +81,16 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [ 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 # Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/ # 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 USE_I18N = True
@@ -109,15 +110,10 @@ USE_TZ = True
# Static files configuration (esp. req. during dev.) # Static files configuration (esp. req. during dev.)
PROJECT_ROOT = os.path.abspath( PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
os.path.join( STATIC_URL = "/static/"
os.path.dirname(__file__), STATIC_ROOT = os.path.join(BASE_DIR, "static")
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 from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import

View File

@@ -82,14 +82,17 @@ h2 {
letter-spacing: 0.05rem; letter-spacing: 0.05rem;
font-weight: 500; font-weight: 500;
} }
h3 { h3 {
font-family: 'Lato', sans-serif; font-family: 'Lato', sans-serif;
font-size: 24px; font-size: 24px;
margin-bottom: 2rem; margin-bottom: 2rem;
color: #545454; color: #545454;
} }
@media only screen and (max-width: 470px) {
h3{
font-size: 20px;
}
}
h4 { h4 {
font-family: 'Lato', sans-serif; font-family: 'Lato', sans-serif;
font-size: 25px; font-size: 25px;
@@ -404,7 +407,6 @@ transition: all 0.3s;
top: 26rem; top: 26rem;
} }
} }
@media only screen and (max-width: 620px) { @media only screen and (max-width: 620px) {
#page .container #home-form { #page .container #home-form {
margin-bottom: 2rem; margin-bottom: 2rem;
@@ -728,3 +730,138 @@ color:#335ECF;
margin-top: 11rem !important; 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 Test various other parts of ivatar/libravatar in order
to increase the overall test coverage. Test in here, didn't to increase the overall test coverage. Test in here, didn't
fit anywhere else. fit anywhere else.
''' """
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
@@ -12,35 +13,35 @@ from ivatar.ivataraccount.models import pil_format, UserPreference
class Tester(TestCase): class Tester(TestCase):
''' """
Main test class Main test class
''' """
user = None user = None
username = random_string() username = random_string()
def setUp(self): def setUp(self):
''' """
Prepare tests. Prepare tests.
- Create user - Create user
''' """
self.user = User.objects.create_user( self.user = User.objects.create_user(
username=self.username, username=self.username,
) )
def test_pil_format(self): def test_pil_format(self):
''' """
Test pil format function Test pil format function
''' """
self.assertEqual(pil_format('jpg'), 'JPEG') self.assertEqual(pil_format("jpg"), "JPEG")
self.assertEqual(pil_format('jpeg'), 'JPEG') self.assertEqual(pil_format("jpeg"), "JPEG")
self.assertEqual(pil_format('png'), 'PNG') self.assertEqual(pil_format("png"), "PNG")
self.assertEqual(pil_format('gif'), 'GIF') self.assertEqual(pil_format("gif"), "GIF")
self.assertEqual(pil_format('abc'), None) self.assertEqual(pil_format("abc"), None)
def test_userprefs_str(self): def test_userprefs_str(self):
''' """
Test if str representation of UserPreferences is as expected 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) print(up)

View File

@@ -1,6 +1,7 @@
''' # -*- coding: utf-8 -*-
"""
Test our views in ivatar.ivataraccount.views and ivatar.views Test our views in ivatar.ivataraccount.views and ivatar.views
''' """
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
import os import os
import django import django
@@ -11,33 +12,34 @@ from django.contrib.auth.models import User
from ivatar.utils import random_string from ivatar.utils import random_string
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings' os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup() django.setup()
class Tester(TestCase): # pylint: disable=too-many-public-methods class Tester(TestCase): # pylint: disable=too-many-public-methods
''' """
Main test class Main test class
''' """
client = Client() client = Client()
user = None user = None
username = random_string() username = random_string()
password = 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 # 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): def login(self):
''' """
Login as user Login as user
''' """
self.client.login(username=self.username, password=self.password) self.client.login(username=self.username, password=self.password)
def setUp(self): def setUp(self):
''' """
Prepare for tests. Prepare for tests.
- Create user - Create user
''' """
self.user = User.objects.create_user( self.user = User.objects.create_user(
username=self.username, username=self.username,
password=self.password, password=self.password,
@@ -47,19 +49,19 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
""" """
Test contact page Test contact page
""" """
response = self.client.get(reverse('contact')) response = self.client.get(reverse("contact"))
self.assertEqual(response.status_code, 200, 'no 200 ok?') self.assertEqual(response.status_code, 200, "no 200 ok?")
def test_description_page(self): def test_description_page(self):
""" """
Test description page Test description page
""" """
response = self.client.get(reverse('description')) response = self.client.get(reverse("description"))
self.assertEqual(response.status_code, 200, 'no 200 ok?') self.assertEqual(response.status_code, 200, "no 200 ok?")
def test_security_page(self): def test_security_page(self):
""" """
Test security page Test security page
""" """
response = self.client.get(reverse('security')) response = self.client.get(reverse("security"))
self.assertEqual(response.status_code, 200, 'no 200 ok?') self.assertEqual(response.status_code, 200, "no 200 ok?")

View File

@@ -1,6 +1,7 @@
''' # -*- coding: utf-8 -*-
"""
Test our utils from ivatar.utils Test our utils from ivatar.utils
''' """
from django.test import TestCase from django.test import TestCase
@@ -8,18 +9,18 @@ from ivatar.utils import openid_variations
class Tester(TestCase): class Tester(TestCase):
''' """
Main test class Main test class
''' """
def test_openid_variations(self): def test_openid_variations(self):
''' """
Test if the OpenID variation "generator" does the correct thing Test if the OpenID variation "generator" does the correct thing
''' """
openid0 = 'http://user.url/' openid0 = "http://user.url/"
openid1 = 'http://user.url' openid1 = "http://user.url"
openid2 = 'https://user.url/' openid2 = "https://user.url/"
openid3 = 'https://user.url' openid3 = "https://user.url"
# First variation # First variation
self.assertEqual(openid_variations(openid0)[0], openid0) 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 Test our views in ivatar.ivataraccount.views and ivatar.views
''' """
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
import os import os
import django import django
@@ -10,33 +11,34 @@ from django.contrib.auth.models import User
from ivatar.utils import random_string from ivatar.utils import random_string
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings' os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup() django.setup()
class Tester(TestCase): # pylint: disable=too-many-public-methods class Tester(TestCase): # pylint: disable=too-many-public-methods
''' """
Main test class Main test class
''' """
client = Client() client = Client()
user = None user = None
username = random_string() username = random_string()
password = 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 # 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): def login(self):
''' """
Login as user Login as user
''' """
self.client.login(username=self.username, password=self.password) self.client.login(username=self.username, password=self.password)
def setUp(self): def setUp(self):
''' """
Prepare for tests. Prepare for tests.
- Create user - Create user
''' """
self.user = User.objects.create_user( self.user = User.objects.create_user(
username=self.username, username=self.username,
password=self.password, password=self.password,
@@ -46,8 +48,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
""" """
Test incorrect digest 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( self.assertRedirects(
response=response, response=response,
expected_url='/static/img/deadbeef.png', expected_url="/static/img/deadbeef.png",
msg_prefix='Why does an invalid hash not redirect to deadbeef?') msg_prefix="Why does an invalid hash not redirect to deadbeef?",
)

View File

@@ -1,22 +1,27 @@
''' # -*- coding: utf-8 -*-
"""
Unit tests for WSGI Unit tests for WSGI
''' """
import unittest import unittest
import os import os
import django import django
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup() django.setup()
class TestCase(unittest.TestCase): class TestCase(unittest.TestCase):
''' """
Simple testcase to see if WSGI loads correctly Simple testcase to see if WSGI loads correctly
''' """
def test_run_wsgi(self): def test_run_wsgi(self):
''' """
Run wsgi import Run wsgi import
''' """
import ivatar.wsgi # pylint: disable=import-outside-toplevel 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 Classes for our ivatar.tools.forms
''' """
from django import 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.core.exceptions import ValidationError
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
@@ -12,45 +13,40 @@ from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL
class CheckDomainForm(forms.Form): class CheckDomainForm(forms.Form):
''' """
Form handling domain check Form handling domain check
''' """
domain = forms.CharField( domain = forms.CharField(
label=_('Domain'), label=_("Domain"),
required=True, required=True,
error_messages={ error_messages={"required": _("Cannot check without a domain name.")},
'required':
_('Cannot check without a domain name.')
}
) )
class CheckForm(forms.Form): class CheckForm(forms.Form):
''' """
Form handling check Form handling check
''' """
mail = forms.EmailField( mail = forms.EmailField(
label=_('E-Mail'), label=_("E-Mail"),
required=False, required=False,
min_length=MIN_LENGTH_EMAIL, min_length=MIN_LENGTH_EMAIL,
max_length=MAX_LENGTH_EMAIL, max_length=MAX_LENGTH_EMAIL,
error_messages={ error_messages={"required": _("Cannot check without a domain name.")},
'required': )
_('Cannot check without a domain name.')
})
openid = forms.CharField( openid = forms.CharField(
label=_('OpenID'), label=_("OpenID"),
required=False, required=False,
min_length=MIN_LENGTH_URL, min_length=MIN_LENGTH_URL,
max_length=MAX_LENGTH_URL, max_length=MAX_LENGTH_URL,
error_messages={ error_messages={"required": _("Cannot check without an openid name.")},
'required': )
_('Cannot check without an openid name.')
})
size = forms.IntegerField( size = forms.IntegerField(
label=_('Size'), label=_("Size"),
initial=80, initial=80,
min_value=5, min_value=5,
max_value=AVATAR_MAX_SIZE, max_value=AVATAR_MAX_SIZE,
@@ -58,24 +54,24 @@ class CheckForm(forms.Form):
) )
default_opt = forms.ChoiceField( default_opt = forms.ChoiceField(
label=_('Default'), label=_("Default"),
required=False, required=False,
widget=forms.RadioSelect, widget=forms.RadioSelect,
choices=[ choices=[
('retro', _('Retro style (similar to GitHub)')), ("retro", _("Retro style (similar to GitHub)")),
('robohash', _('Roboter style')), ("robohash", _("Roboter style")),
('pagan', _('Retro adventure character')), ("pagan", _("Retro adventure character")),
('wavatar', _('Wavatar style')), ("wavatar", _("Wavatar style")),
('monsterid', _('Monster style')), ("monsterid", _("Monster style")),
('identicon', _('Identicon style')), ("identicon", _("Identicon style")),
('mm', _('Mystery man')), ("mm", _("Mystery man")),
('mmng', _('Mystery man NextGen')), ("mmng", _("Mystery man NextGen")),
('none', _('None')), ("none", _("None")),
], ],
) )
default_url = forms.URLField( default_url = forms.URLField(
label=_('Default URL'), label=_("Default URL"),
min_length=1, min_length=1,
max_length=MAX_LENGTH_URL, max_length=MAX_LENGTH_URL,
required=False, required=False,
@@ -83,28 +79,27 @@ class CheckForm(forms.Form):
def clean(self): def clean(self):
self.cleaned_data = super().clean() self.cleaned_data = super().clean()
mail = self.cleaned_data.get('mail') mail = self.cleaned_data.get("mail")
openid = self.cleaned_data.get('openid') openid = self.cleaned_data.get("openid")
default_url = self.cleaned_data.get('default_url') default_url = self.cleaned_data.get("default_url")
default_opt = self.cleaned_data.get('default_opt') default_opt = self.cleaned_data.get("default_opt")
if default_url and default_opt and default_opt != 'none': if default_url and default_opt and default_opt != "none":
if not 'default_url' in self._errors: if "default_url" not in self._errors:
self._errors['default_url'] = ErrorList() self._errors["default_url"] = ErrorList()
if not 'default_opt' in self._errors: if "default_opt" not in self._errors:
self._errors['default_opt'] = ErrorList() self._errors["default_opt"] = ErrorList()
errstring = _('Only default URL OR default keyword may be specified') errstring = _("Only default URL OR default keyword may be specified")
self._errors['default_url'].append(errstring) self._errors["default_url"].append(errstring)
self._errors['default_opt'].append(errstring) self._errors["default_opt"].append(errstring)
if not mail and not openid: 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 return self.cleaned_data
def clean_openid(self): def clean_openid(self):
data = self.cleaned_data['openid'] data = self.cleaned_data["openid"]
return data.lower() return data.lower()
def clean_mail(self): def clean_mail(self):
data = self.cleaned_data['mail'] data = self.cleaned_data["mail"]
print(data)
return data.lower() return data.lower()

View File

@@ -1,57 +1,48 @@
''' # -*- coding: utf-8 -*-
"""
Test our views in ivatar.ivataraccount.views and ivatar.views Test our views in ivatar.ivataraccount.views and ivatar.views
''' """
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
from urllib.parse import urlsplit
from io import BytesIO
import io
import os import os
import django import django
from django.test import TestCase from django.test import TestCase
from django.test import Client from django.test import Client
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth import authenticate
import hashlib
from libravatar import libravatar_url os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
from PIL import Image
os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings'
django.setup() django.setup()
# pylint: disable=wrong-import-position # 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 from ivatar.utils import random_string
# pylint: enable=wrong-import-position # pylint: enable=wrong-import-position
class Tester(TestCase): # pylint: disable=too-many-public-methods class Tester(TestCase): # pylint: disable=too-many-public-methods
''' """
Main test class Main test class
''' """
client = Client() client = Client()
user = None user = None
username = random_string() username = random_string()
password = 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 # 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): def login(self):
''' """
Login as user Login as user
''' """
self.client.login(username=self.username, password=self.password) self.client.login(username=self.username, password=self.password)
def setUp(self): def setUp(self):
''' """
Prepare for tests. Prepare for tests.
- Create user - Create user
''' """
self.user = User.objects.create_user( self.user = User.objects.create_user(
username=self.username, username=self.username,
password=self.password, password=self.password,
@@ -61,12 +52,12 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
""" """
Test check page Test check page
""" """
response = self.client.get(reverse('tools_check')) response = self.client.get(reverse("tools_check"))
self.assertEqual(response.status_code, 200, 'no 200 ok?') self.assertEqual(response.status_code, 200, "no 200 ok?")
def test_check_domain(self): def test_check_domain(self):
""" """
Test check domain page Test check domain page
""" """
response = self.client.get(reverse('tools_check_domain')) response = self.client.get(reverse("tools_check_domain"))
self.assertEqual(response.status_code, 200, 'no 200 ok?') self.assertEqual(response.status_code, 200, "no 200 ok?")

View File

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

View File

@@ -1,6 +1,7 @@
''' # -*- coding: utf-8 -*-
"""
ivatar URL configuration ivatar URL configuration
''' """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.conf.urls import url from django.conf.urls import url
@@ -10,33 +11,48 @@ from ivatar import settings
from .views import AvatarImageView, GravatarProxyView, StatsView from .views import AvatarImageView, GravatarProxyView, StatsView
urlpatterns = [ # pylint: disable=invalid-name urlpatterns = [ # pylint: disable=invalid-name
path('admin/', admin.site.urls), path("admin/", admin.site.urls),
path('i18n/', include('django.conf.urls.i18n')), path("i18n/", include("django.conf.urls.i18n")),
url('openid/', include('django_openid_auth.urls')), url("openid/", include("django_openid_auth.urls")),
url('tools/', include('ivatar.tools.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( url(
r'avatar/(?P<digest>\w{64})', r"avatar/(?P<digest>\w*)",
AvatarImageView.as_view(), name='avatar_view'), RedirectView.as_view(url="/static/img/deadbeef.png"),
name="invalid_hash",
),
url( url(
r'avatar/(?P<digest>\w{32})', r"gravatarproxy/(?P<digest>\w*)",
AvatarImageView.as_view(), name='avatar_view'), GravatarProxyView.as_view(),
url(r'avatar/$', AvatarImageView.as_view(), name='avatar_view'), name="gravatarproxy",
),
url( url(
r'avatar/(?P<digest>\w*)', "description/",
RedirectView.as_view(url='/static/img/deadbeef.png'), name='invalid_hash'), TemplateView.as_view(template_name="description.html"),
url( name="description",
r'gravatarproxy/(?P<digest>\w*)', ),
GravatarProxyView.as_view(), name='gravatarproxy'),
url('description/', TemplateView.as_view(template_name='description.html'), name='description'),
# The following two are TODO TODO TODO TODO TODO # The following two are TODO TODO TODO TODO TODO
url('run_your_own/', url(
TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'), "run_your_own/",
url('features/', TemplateView.as_view(template_name='features.html'), name='features'), TemplateView.as_view(template_name="run_your_own.html"),
url('security/', TemplateView.as_view(template_name='security.html'), name='security'), name="run_your_own",
url('privacy/', TemplateView.as_view(template_name='privacy.html'), name='privacy'), ),
url('contact/', TemplateView.as_view(template_name='contact.html'), name='contact'), url(
path('talk_to_us/', RedirectView.as_view(url='/contact'), name='talk_to_us'), "features/",
url('stats/', StatsView.as_view(), name='stats'), 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 MAINTENANCE = False
@@ -47,11 +63,15 @@ except: # pylint: disable=bare-except
pass pass
if MAINTENANCE: if MAINTENANCE:
urlpatterns.append(url('', TemplateView.as_view(template_name='maintenance.html'), name='home')) urlpatterns.append(
urlpatterns.insert(3, url('accounts/', RedirectView.as_view(url='/'))) url("", TemplateView.as_view(template_name="maintenance.html"), name="home")
)
urlpatterns.insert(3, url("accounts/", RedirectView.as_view(url="/")))
else: else:
urlpatterns.append(url('', TemplateView.as_view(template_name='home.html'), name='home')) urlpatterns.append(
urlpatterns.insert(3, url('accounts/', include('ivatar.ivataraccount.urls'))) 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) 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 Simple module providing reusable random_string function
''' """
import random import random
import string import string
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
def random_string(length=10): def random_string(length=10):
''' """
Return some random string with default length 10 Return some random string with default length 10
''' """
return ''.join(random.SystemRandom().choice( return "".join(
string.ascii_lowercase + string.digits) for _ in range(length)) random.SystemRandom().choice(string.ascii_lowercase + string.digits)
for _ in range(length)
)
def openid_variations(openid): def openid_variations(openid):
''' """
Return the various OpenID variations, ALWAYS in the same order: Return the various OpenID variations, ALWAYS in the same order:
- http w/ trailing slash - http w/ trailing slash
- http w/o trailing slash - http w/o trailing slash
- https w/ trailing slash - https w/ trailing slash
- https w/o trailing slash - https w/o trailing slash
''' """
# Make the 'base' version: http w/ trailing slash # Make the 'base' version: http w/ trailing slash
if openid.startswith('https://'): if openid.startswith("https://"):
openid = openid.replace('https://', 'http://') openid = openid.replace("https://", "http://")
if openid[-1] != '/': if openid[-1] != "/":
openid = openid + '/' openid = openid + "/"
# http w/o trailing slash # http w/o trailing slash
var1 = openid[0:-1] var1 = openid[0:-1]
var2 = openid.replace('http://', 'https://') var2 = openid.replace("http://", "https://")
var3 = var2[0:-1] var3 = var2[0:-1]
return (openid, var1, var2, var3) 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 Return an MM (mystery man) image, based on a given hash
add some red, green or blue, if specified add some red, green or blue, if specified
''' """
# Make sure the lightest bg color we paint is e0, else # Make sure the lightest bg color we paint is e0, else
# we do not see the MM any more # we do not see the MM any more
if idhash[0] == 'f': if idhash[0] == "f":
idhash = 'e0' idhash = "e0"
# How large is the circle? # How large is the circle?
circlesize = size * 0.6 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] blue = idhash[0:2]
# Add some red (i/a) and make sure it's not over 255 # 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: if int(red, 16) > 255:
red = 'ff' red = "ff"
if len(red) == 1: 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 # 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: if int(green, 16) > 255:
green = 'ff' green = "ff"
if len(green) == 1: 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 # 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: if int(blue, 16) > 255:
blue = 'ff' blue = "ff"
if len(blue) == 1: if len(blue) == 1:
blue = '0%s' % blue blue = "0%s" % blue
# Assemable the bg color "string" in webnotation. Eg. '#d3d3d3' # Assemable the bg color "string" in webnotation. Eg. '#d3d3d3'
bg_color = '#' + red + green + blue bg_color = "#" + red + green + blue
# Image # Image
image = Image.new('RGB', (size, size)) image = Image.new("RGB", (size, size))
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
# Draw background # Draw background
draw.rectangle(((0, 0), (size, size)), fill=bg_color) draw.rectangle(((0, 0), (size, size)), fill=bg_color)
# Draw MMs head # 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 MMs 'body'
draw.polygon(( draw.polygon(
(
(start_x + circlesize / 2, size / 2.5), (start_x + circlesize / 2, size / 2.5),
(size * 0.15, size), (size * 0.15, size),
(size-size*0.15, size)), (size - size * 0.15, size),
fill='white') ),
fill="white",
)
return image return image

View File

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

View File

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

View File

@@ -1,26 +1,37 @@
autopep8 autopep8
bcrypt bcrypt
defusedxml defusedxml
Django Django < 4.0
django-anymail[mailgun]
django-auth-ldap django-auth-ldap
django-bootstrap4 django-bootstrap4
django-coverage-plugin django-coverage-plugin
django-extensions django-extensions
django-ipware django-ipware
django-user-accounts django-user-accounts
email-validator
fabric fabric
flake8-respect-noqa flake8-respect-noqa
git+https://github.com/ercpe/pydenticon5.git
git+https://github.com/flavono123/identicon.git
git+https://github.com/ofalk/django-openid-auth git+https://github.com/ofalk/django-openid-auth
git+https://github.com/ofalk/monsterid.git
git+https://github.com/ofalk/Robohash.git@devel
mysqlclient
notsetuptools
pagan
Pillow Pillow
pip pip
psycopg2-binary
py3dns py3dns
pydocstyle pydocstyle
pyLibravatar pyLibravatar
pylint pylint
PyMySQL PyMySQL
python3-openid
python-coveralls python-coveralls
python-language-server python-language-server
python-memcached
python3-openid
pytz pytz
rope rope
setuptools setuptools
@@ -28,14 +39,4 @@ six
social-auth-app-django social-auth-app-django
wheel wheel
yapf yapf
django-anymail[mailgun]
mysqlclient
psycopg2-binary
notsetuptools
git+https://github.com/ofalk/monsterid.git
git+https://github.com/ofalk/Robohash.git@devel
python-memcached
git+https://github.com/ercpe/pydenticon5.git
git+https://github.com/flavono123/identicon.git
pagan
py-avataaars 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 'user_preference' %}"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> {% trans 'Preferences' %}</a></li>
<li><a href="{% url 'import_photo' %}"><i class="fa fa-fw fa-envelope-square" aria-hidden="true"></i> {% trans 'Import photo via mail address' %}</a></li> <li><a href="{% url 'import_photo' %}"><i class="fa fa-fw fa-envelope-square" aria-hidden="true"></i> {% trans 'Import photo via mail address' %}</a></li>
<li><a href="{% url 'upload_export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Import libravatar XML export' %}</a></li> <li><a href="{% url 'upload_export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Import libravatar XML export' %}</a></li>
<li><a href="{% url 'export' %}"><i class="fa fa-fw fa-file-archive-o" aria-hidden="true"></i> {% trans 'Download your libravatar data' %}</a></li>
<li><a href="{% url 'password_change' %}"><i class="fa fa-fw fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li> <li><a href="{% url 'password_change' %}"><i class="fa fa-fw fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li>
<li><a href="{% url 'password_reset' %}"><i class="fa fa-fw fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li> <li><a href="{% url 'password_reset' %}"><i class="fa fa-fw fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li> <li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>

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"> title="https://www.linkedin.com/in/naharronak/" target="_new">
Ronak Nahar</a>: Ronak Nahar</a>:
Spotted and reported open server status from Apache HTTPD.</li> 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> </ul>
<div style="height:40px"></div> <div style="height:40px"></div>