Merge branch 'devel' into trust

This commit is contained in:
Oliver Falk
2019-02-21 10:01:07 +01:00
66 changed files with 19545 additions and 330 deletions

View File

@@ -1,6 +1,10 @@
[run]
plugins =
django_coverage_plugin
omit =
node_modules/*
.virtualenv/*
import_libravatar.py
[html]
extra_css = coverage_extra_style.css

4
.gitignore vendored
View File

@@ -10,4 +10,6 @@ htmlcov/
**.pyc
.ropeproject/
db.sqlite3.SAVE
node_modules/
config_local.py
locale/*/LC_MESSAGES/django.mo

View File

@@ -16,6 +16,9 @@ before_script:
test_and_coverage:
stage: test
script:
- echo 'from ivatar.settings import TEMPLATES' > config_local.py
- echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py
- echo "DEBUG = True" >> config_local.py
- python manage.py collectstatic --noinput
- coverage run --source . manage.py test -v3
- coverage report --fail-under=70

View File

@@ -4,14 +4,18 @@ Configuration overrides for settings.py
import os
import sys
from socket import gethostname, gethostbyname
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.contrib.messages import constants as message_constants
from ivatar.settings import BASE_DIR
ADMIN_USERS = []
ALLOWED_HOSTS = [ '*' ]
from ivatar.settings import MIDDLEWARE
from ivatar.settings import INSTALLED_APPS
from ivatar.settings import TEMPLATES
ADMIN_USERS = []
ALLOWED_HOSTS = ['*']
from ivatar.settings import INSTALLED_APPS # noqa
INSTALLED_APPS.extend([
'django_extensions',
'django_openid_auth',
@@ -22,10 +26,12 @@ INSTALLED_APPS.extend([
'ivatar.tools',
])
from ivatar.settings import MIDDLEWARE # noqa
MIDDLEWARE.extend([
'django.middleware.locale.LocaleMiddleware',
])
MIDDLEWARE.insert(
0, 'ivatar.middleware.MultipleProxyMiddleware',
)
AUTHENTICATION_BACKENDS = (
# Enable this to allow LDAP authentication.
@@ -35,7 +41,6 @@ AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
from ivatar.settings import TEMPLATES # noqa
TEMPLATES[0]['DIRS'].extend([
os.path.join(BASE_DIR, 'templates'),
])
@@ -46,16 +51,14 @@ TEMPLATES[0]['OPTIONS']['context_processors'].append(
OPENID_CREATE_USERS = True
OPENID_UPDATE_DETAILS_FROM_SREG = True
SITE_NAME = 'ivatar'
IVATAR_VERSION = '0.1'
SITE_NAME = os.environ.get('SITE_NAME', 'libravatar')
IVATAR_VERSION = '1.0'
SECURE_BASE_URL = 'https://avatars.linux-kernel.at/avatar/'
BASE_URL = 'http://avatars.linux-kernel.at/avatar/'
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
SERVER_EMAIL = 'accounts@mg.linux-kernel.at'
DEFAULT_FROM_EMAIL = SERVER_EMAIL
MAX_NUM_PHOTOS = 5
MAX_NUM_UNCONFIRMED_EMAILS = 5
@@ -76,7 +79,8 @@ BOOTSTRAP4 = {
'javascript_in_head': False,
'css_url': {
'href': '/static/css/bootstrap.min.css',
'integrity': 'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB', # noqa
'integrity':
'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB',
'crossorigin': 'anonymous',
},
'javascript_url': {
@@ -86,18 +90,26 @@ BOOTSTRAP4 = {
},
'popper_url': {
'url': '/static/js/popper.min.js',
'integrity': 'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49', # noqa
'integrity':
'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49',
'crossorigin': 'anonymous',
},
}
if 'test' not in sys.argv and 'collectstatic' not in sys.argv:
ANYMAIL = { # pragma: no cover
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
}
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
DEFAULT_FROM_EMAIL = 'ivatar@mg.linux-kernel.at'
if 'EMAIL_BACKEND' in os.environ:
EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] # pragma: no cover
else:
if 'test' in sys.argv or 'collectstatic' in sys.argv:
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
else:
ANYMAIL = { # pragma: no cover
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
}
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'ivatar@mg.linux-kernel.at')
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'ivatar@mg.linux-kernel.at')
try:
from ivatar.settings import DATABASES
@@ -132,3 +144,43 @@ if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')):
from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
USE_X_FORWARDED_HOST = True
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',]
DEFAULT_AVATAR_SIZE = 80
LANGUAGES = (
('de', _('Deutsch')),
('en', _('English')),
('ca', _('Català')),
('cs', _('Česky')),
('es', _('Español')),
('eu', _('Basque')),
('fr', _('Français')),
('it', _('Italiano')),
('ja', _('日本語')),
('nl', _('Nederlands')),
('pt', _('Português')),
('ru', _('Русский')),
('sq', _('Shqip')),
('tr', _('Türkçe')),
('uk', _('Українська')),
)
MESSAGE_TAGS = {
message_constants.DEBUG: 'debug',
message_constants.INFO: 'info',
message_constants.SUCCESS: 'success',
message_constants.WARNING: 'warning',
message_constants.ERROR: 'danger',
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': [
'127.0.0.1:11211',
],
}
}

View File

@@ -1,9 +0,0 @@
'''
Local config
'''
from ivatar.settings import TEMPLATES # noqa
SESSION_COOKIE_SECURE = False
DEBUG = True
TEMPLATES[0]['OPTIONS']['debug'] = True

139
exportaccounts.py Executable file
View File

@@ -0,0 +1,139 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Oliver Falk <oliver@linux-kernel.at>
#
# This file is part of Libravatar
#
# Libravatar is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Libravatar is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Libravatar. If not, see <http://www.gnu.org/licenses/>.
import base64
import gzip
import json
import os
import sys
from xml.sax import saxutils
import hashlib
# pylint: disable=relative-import
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libravatar.settings")
import libravatar.settings as settings
from libravatar.utils import create_logger, is_hex
import django
django.setup()
from django.contrib.auth.models import User
os.umask(022)
LOGGER = create_logger('exportaccount')
SCHEMA_ROOT = 'https://www.libravatar.org/schemas/export/0.2'
SCHEMA_XSD = '%s/export.xsd' % SCHEMA_ROOT
def xml_header():
return '''<?xml version="1.0" encoding="UTF-8"?>
<user xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="%s %s"
xmlns="%s">\n''' % (SCHEMA_ROOT, SCHEMA_XSD, SCHEMA_ROOT)
def xml_footer():
return '</user>\n'
def xml_account(username, password):
escaped_username = saxutils.quoteattr(username)
escaped_site_url = saxutils.quoteattr(settings.SITE_URL)
escaped_password = saxutils.quoteattr(password)
return ' <account username=%s password=%s site=%s/>\n' % (escaped_username, escaped_password, escaped_site_url)
def xml_email(emails):
returnstring = " <emails>\n"
for email in emails:
returnstring += ' <email photo_id="' + str(email.photo_id) + '">' + email.email.encode('utf-8') + '</email>' + "\n"
returnstring += " </emails>\n"
return returnstring
def xml_openid(openids):
returnstring = " <openids>\n"
for openid in openids:
returnstring += ' <openid photo_id="' + str(openid.photo_id) + '">' + openid.openid.encode('utf-8') + '</openid>' + "\n"
returnstring += " </openids>\n"
return returnstring
def xml_photos(photos):
s = ' <photos>\n'
for photo in photos:
(photo_filename, photo_format, id) = photo
encoded_photo = encode_photo(photo_filename, photo_format)
if encoded_photo:
s += ''' <photo id="%s" encoding="base64" format=%s>
%s
</photo>\n''' % (id, saxutils.quoteattr(photo_format), encoded_photo)
s += ' </photos>\n'
return s
def encode_photo(photo_filename, photo_format):
filename = settings.USER_FILES_ROOT + photo_filename + '.' + photo_format
if not os.path.isfile(filename):
LOGGER.warning('Photo not found: %s', filename)
return None
photo_content = None
with open(filename) as photo:
photo_content = photo.read()
if not photo_content:
LOGGER.warning('Could not read photo: %s', filename)
return None
return base64.b64encode(photo_content)
def main(argv=None):
if argv is None:
argv = sys.argv
if(len(sys.argv) > 1):
userobjs = User.objects.filter(username=sys.argv[1])
else:
userobjs = User.objects.all()
for user in userobjs:
hash_object = hashlib.new('sha256')
hash_object.update(user.username + user.password)
file_hash = hash_object.hexdigest()
photos = []
for photo in user.photos.all():
photo_details = (photo.filename, photo.format, photo.id)
photos.append(photo_details)
username = user.username
dest_filename = settings.EXPORT_FILES_ROOT + file_hash + '.xml.gz'
destination = gzip.open(dest_filename, 'w')
destination.write(xml_header())
destination.write(xml_account(username, user.password))
destination.write(xml_email(user.confirmed_emails.all()))
destination.write(xml_openid(user.confirmed_openids.all()))
destination.write(xml_photos(photos))
destination.write(xml_footer())
destination.close()
print(dest_filename)
return 0
if __name__ == "__main__":
sys.exit(main())

79
import_libravatar.py Normal file
View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python
'''
Import the whole libravatar export
'''
import os
from os.path import isfile, isdir, join
import sys
import base64
from io import BytesIO
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ivatar.settings") # pylint: disable=wrong-import-position
django.setup() # pylint: disable=wrong-import-position
from django.contrib.auth.models import User
from PIL import Image
from django_openid_auth.models import UserOpenID
from ivatar.settings import JPEG_QUALITY
from ivatar.ivataraccount.read_libravatar_export import read_gzdata as libravatar_read_gzdata
from ivatar.ivataraccount.models import ConfirmedEmail
from ivatar.ivataraccount.models import ConfirmedOpenId
from ivatar.ivataraccount.models import Photo
from ivatar.ivataraccount.models import file_format
if len(sys.argv) < 2:
print("First argument to '%s' must be the path to the exports" % sys.argv[0])
exit(-255)
if not isdir(sys.argv[1]):
print("First argument to '%s' must be a directory containing the exports" % sys.argv[0])
exit(-255)
PATH = sys.argv[1]
for file in os.listdir(PATH):
if not file.endswith('.xml.gz'):
continue
if isfile(join(PATH, file)):
fh = open(join(PATH, file), 'rb')
items = libravatar_read_gzdata(fh.read())
print('Adding user "%s"' % items['username'])
(user, created) = User.objects.get_or_create(username=items['username'])
user.password = items['password']
user.save()
saved_photos = {}
for photo in items['photos']:
photo_id = photo['id']
data = base64.decodebytes(bytes(photo['data'], 'utf-8'))
pilobj = Image.open(BytesIO(data))
out = BytesIO()
pilobj.save(out, pilobj.format, quality=JPEG_QUALITY)
out.seek(0)
photo = Photo()
photo.user = user
photo.ip_address = '0.0.0.0'
photo.format = file_format(pilobj.format)
photo.data = out.read()
photo.save()
saved_photos[photo_id] = photo
for email in items['emails']:
try:
ConfirmedEmail.objects.get_or_create(email=email['email'], user=user,
photo=saved_photos.get(email['photo_id']))
except django.db.utils.IntegrityError:
print('%s not unique?' % email['email'])
for openid in items['openids']:
try:
ConfirmedOpenId.objects.get_or_create(openid=openid['openid'], user=user,
photo=saved_photos.get(openid['photo_id'])) # pylint: disable=no-member
UserOpenID.objects.get_or_create(
user_id=user.id,
claimed_id=openid['openid'],
display_id=openid['openid'],
)
except django.db.utils.IntegrityError:
print('%s not unique?' % openid['openid'])
fh.close()

View File

@@ -90,16 +90,14 @@ class UploadPhotoForm(forms.Form):
required=True,
error_messages={
'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(
label=_('can be freely copied'),
required=True,
error_messages={
'required':
_('This field must be checked since we need to be able to\
distribute photos to third parties.')
_('This field must be checked since we need to be able to distribute photos to third parties.')
})
@staticmethod
@@ -191,8 +189,7 @@ class UploadLibravatarExportForm(forms.Form):
required=True,
error_messages={
'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(
label=_('can be freely copied'),
@@ -202,3 +199,6 @@ class UploadLibravatarExportForm(forms.Form):
_('This field must be checked since we need to be able to\
distribute photos to third parties.')
})
class DeleteAccountForm(forms.Form):
password = forms.CharField(label=_('Password'), required=False, widget=forms.PasswordInput())

View File

@@ -5,6 +5,8 @@ from ssl import SSLError
from urllib.request import urlopen, HTTPError, URLError
import hashlib
from .. settings import AVATAR_MAX_SIZE
URL_TIMEOUT = 5 # in seconds
@@ -15,7 +17,7 @@ def get_photo(email):
hash_object = hashlib.new('md5')
hash_object.update(email.lower().encode('utf-8'))
thumbnail_url = 'https://secure.gravatar.com/avatar/' + \
hash_object.hexdigest() + '?s=80&d=404'
hash_object.hexdigest() + '?s=%i&d=404' % AVATAR_MAX_SIZE
image_url = 'https://secure.gravatar.com/avatar/' + hash_object.hexdigest(
) + '?s=512&d=404'
@@ -44,8 +46,8 @@ def get_photo(email):
return {
'thumbnail_url': thumbnail_url,
'image_url': image_url,
'width': 80,
'height': 80,
'width': AVATAR_MAX_SIZE,
'height': AVATAR_MAX_SIZE,
'service_url': service_url,
'service_name': 'Gravatar'
}

View File

@@ -1,3 +1,4 @@
# pylint: disable=invalid-name,missing-docstring
# Generated by Django 2.0.6 on 2018-07-04 12:32
from django.conf import settings
@@ -5,18 +6,18 @@ from django.db import migrations, models
import django.db.models.deletion
def add_preference_to_user(apps, schema_editor):
def add_preference_to_user(apps, schema_editor): # pylint: disable=unused-argument
'''
Make sure all users have preferences set up
'''
from django.contrib.auth.models import User
UserPreference = apps.get_model('ivataraccount', 'UserPreference')
for u in User.objects.filter(userpreference=None):
p = UserPreference.objects.create(user_id=u.pk)
p.save()
UserPreference = apps.get_model('ivataraccount', 'UserPreference') # pylint: disable=invalid-name
for user in User.objects.filter(userpreference=None):
pref = UserPreference.objects.create(user_id=user.pk) # pragma: no cover
pref.save() # pragma: no cover
class Migration(migrations.Migration):
class Migration(migrations.Migration): # pylint: disable=missing-docstring
dependencies = [
('auth', '0009_alter_user_last_name_max_length'),
@@ -27,8 +28,16 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='UserPreference',
fields=[
('theme', models.CharField(choices=[('default', 'Default theme'), ('clime', 'Climes theme')], default='default', max_length=10)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
('theme', models.CharField(
choices=[
('default', 'Default theme'),
('clime', 'Climes theme')],
default='default', max_length=10)),
('user', models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL)),
],
),
migrations.RunPython(add_preference_to_user),

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.1.3 on 2018-12-03 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ivataraccount', '0012_auto_20181107_1732'),
]
operations = [
migrations.AlterField(
model_name='userpreference',
name='theme',
field=models.CharField(choices=[('default', 'Default theme'), ('clime', 'climes theme'), ('green', 'green theme'), ('red', 'red theme')], default='default', max_length=10),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 2.1.5 on 2019-02-18 16:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ivataraccount', '0013_auto_20181203_1421'),
]
operations = [
migrations.AlterModelOptions(
name='unconfirmedemail',
options={'verbose_name': 'unconfirmed email', 'verbose_name_plural': 'unconfirmed emails'},
),
]

View File

@@ -31,7 +31,7 @@ from libravatar import libravatar_url
from ivatar.settings import MAX_LENGTH_EMAIL, logger
from ivatar.settings import MAX_PIXELS, AVATAR_MAX_SIZE, JPEG_QUALITY
from ivatar.settings import MAX_LENGTH_URL
from ivatar.settings import SECURE_BASE_URL, SITE_NAME, SERVER_EMAIL
from ivatar.settings import SECURE_BASE_URL, SITE_NAME, DEFAULT_FROM_EMAIL
from .gravatar import get_photo as get_gravatar_photo
@@ -70,7 +70,8 @@ class UserPreference(models.Model):
THEMES = (
('default', 'Default theme'),
('clime', 'climes theme'),
('falko', 'falkos theme'),
('green', 'green theme'),
('red', 'red theme'),
)
theme = models.CharField(
@@ -135,7 +136,7 @@ class Photo(BaseAccountModel):
image_url = gravatar['image_url']
if service_name == 'Libravatar':
image_url = libravatar_url(email_address)
image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE)
if not image_url:
return False # pragma: no cover
@@ -220,8 +221,9 @@ class Photo(BaseAccountModel):
if dimensions['a'] > MAX_PIXELS or dimensions['b'] > MAX_PIXELS:
messages.error(
request,
_('Image dimensions are too big(max: %s x %s' %
(MAX_PIXELS, MAX_PIXELS)))
_('Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s' % {
max_pixels: MAX_PIXELS,
}))
return HttpResponseRedirect(reverse_lazy('profile'))
if dimensions['w'] == 0 and dimensions['h'] == 0:
@@ -349,8 +351,8 @@ class UnconfirmedEmail(BaseAccountModel):
'''
Class attributes
'''
verbose_name = _('unconfirmed_email')
verbose_name_plural = _('unconfirmed_emails')
verbose_name = _('unconfirmed email')
verbose_name_plural = _('unconfirmed emails')
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
@@ -382,7 +384,7 @@ class UnconfirmedEmail(BaseAccountModel):
# if settings.DEBUG:
# print('DEBUG: %s' % link)
send_mail(
email_subject, email_body, SERVER_EMAIL,
email_subject, email_body, DEFAULT_FROM_EMAIL,
[self.email])
return True
@@ -448,8 +450,8 @@ class ConfirmedOpenId(BaseAccountModel):
lowercase_url = urlunsplit(
(url.scheme.lower(), netloc, url.path, url.query, url.fragment)
)
if lowercase_url[-1] != '/':
lowercase_url += '/'
#if lowercase_url[-1] != '/':
# lowercase_url += '/'
self.openid = lowercase_url
self.digest = hashlib.sha256(lowercase_url.encode('utf-8')).hexdigest()
return super().save(force_insert, force_update, using, update_fields)

View File

@@ -20,6 +20,8 @@ def read_gzdata(gzdata=None):
emails = [] # pylint: disable=invalid-name
openids = [] # pylint: disable=invalid-name
photos = [] # pylint: disable=invalid-name
username = None # pylint: disable=invalid-name
password = None # pylint: disable=invalid-name
if not gzdata:
return False
@@ -32,15 +34,22 @@ def read_gzdata(gzdata=None):
print('Unknown export format: %s' % root.tag)
exit(-1)
# Username
for item in root.findall('{%s}account' % SCHEMAROOT)[0].items():
if item[0] == 'username':
username = item[1]
if item[0] == 'password':
password = item[1]
# Emails
for email in root.findall('{%s}emails' % SCHEMAROOT)[0]:
if email.tag == '{%s}email' % SCHEMAROOT:
emails.append(email.text)
emails.append({'email': email.text, 'photo_id': email.attrib['photo_id']})
# OpenIDs
for openid in root.findall('{%s}openids' % SCHEMAROOT)[0]:
if openid.tag == '{%s}openid' % SCHEMAROOT:
openids.append(openid.text)
openids.append({'openid': openid.text, 'photo_id': openid.attrib['photo_id']})
# Photos
for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]:
@@ -48,14 +57,14 @@ def read_gzdata(gzdata=None):
try:
data = base64.decodebytes(bytes(photo.text, 'utf-8'))
except binascii.Error as exc:
print('Cannot decode photo; Encoding: %s, Format: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], exc))
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
continue
try:
Image.open(BytesIO(data))
except Exception as exc: # pylint: disable=broad-except
print('Cannot decode photo; Encoding: %s, Format: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], exc))
print('Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s' % (
photo.attrib['encoding'], photo.attrib['format'], photo.attrib['id'], exc))
continue
else:
# If it is a working image, we can use it
@@ -63,10 +72,13 @@ def read_gzdata(gzdata=None):
photos.append({
'data': photo.text,
'format': photo.attrib['format'],
'id': photo.attrib['id'],
})
return {
'emails': emails,
'openids': openids,
'photos': photos,
'username': username,
'password': password,
}

View File

@@ -25,7 +25,7 @@
</h3></div>
<div class="panel-body">
<center>
<img src="{{ photo.thumbnail_url }}" alt="{{ photo.service_name }} image">
<img src="{{ photo.thumbnail_url }}" style="max-width: 80px; max-height: 80px;" alt="{{ photo.service_name }} image">
</center>
</div>
</div>

View File

@@ -7,20 +7,6 @@
{% block content %}
<style>
input[type=checkbox] {display:none}
input[type=checkbox].text + label {
padding-left:0;
font-weight:normal;
}
input[type=checkbox].text + label:before {
font-family: FontAwesome;
display: inline-block;
letter-spacing:5px;
font-size:20px;
color:#36b7d7;
vertical-align:middle;
}
input[type=checkbox].text + label:before {content: "\f0c8"}
input[type=checkbox].text:checked + label:before {content: "\f14a"}
input[type=checkbox].image + label:before {
font-family: FontAwesome;
display: inline-block;
@@ -36,7 +22,9 @@ input[type=checkbox].image:checked + label:before {letter-spacing: 3px}
{% if emails %}
<h4>{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}</h4>
{% for email in emails %}
<input type="checkbox" checked name="email_{{ forloop.counter }}" id="email_{{ forloop.counter }}" value="{{ email }}" class="text"><label for="email_{{ forloop.counter }}">{{ email }}</label><br/>
<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>
</div>
{% endfor %}
{% endif %}
{% if photos %}

View File

@@ -8,11 +8,7 @@
{% block content %}
<style>
.jcrop-holder > div > div:nth-child(1) {
outline-width:2px;
outline-style:solid;
outline-color:#36b7d7;
}
</style>
<h1>{% trans 'Crop photo' %}</h1>

View File

@@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans 'Delete your Libravatar account' %}{% endblock title %}
{% block content %}
<h1>{% trans 'Delete your account' %}</h1>
<p><strong>{% trans 'There is no way to undo this operation.' %}</strong></p>
<form method="post" name="deleteaccount" id="form-deleteaccount">{% csrf_token %}
{% if user.password %}
<p>{% trans 'Please confirm your identity by entering your current password.' %}</p>
{{ form.password.errors }}
<div class="form-group" style='max-width:300px;'>
<label for="id_password">{% trans 'Password' %}:</label>
<input type="password" name="password" autofocus required class="form-control" id="id_password">
</div>
{% endif %}
<p>{% trans 'Are you sure you want to <strong>permanently delete</strong> your Libravatar account?' %}</p>
<button type="submit" class="btn btn-danger">{% trans 'Yes, delete all of my stuff' %}</button>
&nbsp;
<button type="cancel" class="btn btn-default" href="{% url 'profile' %}">{% trans 'Cancel' %}</button>
</form>
<div style="height:40px"></div>
{% endblock content %}

View File

@@ -7,11 +7,48 @@
{% block content %}
<h1>{% trans 'Account settings' %}</h1>
{% if has_password %}
<p><a href="{% url 'password_change' %}" class="btn btn-default">{% trans 'Change your password' %}</a></p>
{% else %}
<p><a href="{% url 'password_set' %}" class="btn btn-default">{% trans 'Set a password' %}</a></p>
{% endif %}
<h2>{% trans 'Theme' %}</h2>
<p>
<form method="post" action="{% url 'user_preference' %}">{% csrf_token %}
<div class="form-group">
{% for theme in THEMES %}
<div class="radio">
<input type="radio" name="theme" value="{{ theme.0 }}"
id="theme-{{ theme.0 }}"
{% if user.userpreference.theme == theme.0 %}checked{% endif %}
{% if THEMES|first == theme %}{% if not user.userpreference.theme %}checked{% endif %}{% endif %}
>
<label for="theme-{{ theme.0 }}">{{ theme.1 }}</label>
</div>
{% endfor %}
<br/>
<button type="submit" class="btn btn-default">{% trans 'Save' %}</button>
</div>
</form>
</p>
<!-- TODO: Language stuff not yet fully implemented; Esp. translations are only half-way there
<h2>{% trans 'Language' %}</h2>
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
<div class="form-group">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<div class="radio">
<input type="radio" name="language" value="{{ language.code }}" id="language-{{ language.code }}" {% if language.code == LANGUAGE_CODE %}checked{% endif %}>
<label for="language-{{ language.code }}">{{ language.name_local }}</label>
</div>
{% endfor %}
</div>
<br/>
<button type="submit" class="btn btn-default">{% trans 'Save' %}</button>
</form>
-->
<div style="height:40px"></div>
<!-- <p><a href="{% url 'export' %}" class="btn btn-default">{% trans 'Export your data' %}</a></p> -->

View File

@@ -5,30 +5,6 @@
{% block title %}{% trans 'Upload an export from libravatar' %} - ivatar{% endblock title %}
{% block content %}
<style>
input[type=checkbox] {display:none}
input[type=checkbox] + label {
padding-left:0;
}
input[type=checkbox] + label:before {
font-family: FontAwesome;
display: inline-block;
letter-spacing:5px;
font-size:20px;
color:#36b7d7;
vertical-align:middle;
}
input[type=checkbox] + label:before {content: "\f0c8"}
input[type=checkbox]:checked + label:before {content: "\f14a"}
.uploadbtn:before {
position:absolute;
left:0;
right:0;
text-align:center;
content:"Select file";
font-family: 'Montserrat', sans-serif;
}
</style>
<h1>{% trans 'Upload an export from libravatar' %}</h1>
<form enctype="multipart/form-data" method="post">

View File

@@ -8,30 +8,6 @@
<link rel="prefetch" href="{% static '/js/jcrop.js' %}">{% endblock header %}
{% block content %}
<style>
input[type=checkbox] {display:none}
input[type=checkbox] + label {
padding-left:0;
}
input[type=checkbox] + label:before {
font-family: FontAwesome;
display: inline-block;
letter-spacing:5px;
font-size:20px;
color:#36b7d7;
vertical-align:middle;
}
input[type=checkbox] + label:before {content: "\f0c8"}
input[type=checkbox]:checked + label:before {content: "\f14a"}
.uploadbtn:before {
position:absolute;
left:0;
right:0;
text-align:center;
content:"Select file";
font-family: 'Montserrat', sans-serif;
}
</style>
<h1>{% trans 'Upload a new photo' %}</h1>
<form enctype="multipart/form-data" action="{% url 'upload_photo' %}" method="post">{% csrf_token %}

View File

@@ -12,6 +12,7 @@ from django.test import Client
from django.urls import reverse
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
import hashlib
from libravatar import libravatar_url
@@ -39,6 +40,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
email = '%s@%s.%s' % (username, random_string(), random_string(2))
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
openid = 'http://%s.%s.%s/' % (username, random_string(), 'org')
first_name = random_string()
last_name = random_string()
def login(self):
'''
@@ -54,6 +57,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
self.user = User.objects.create_user(
username=self.username,
password=self.password,
first_name=self.first_name,
last_name=self.last_name,
)
def test_new_user(self):
@@ -581,11 +586,12 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
def test_upload_gif_image(self):
'''
Test if gif is correctly detected
Test if gif is correctly detected and can be viewed
'''
self.login()
url = reverse('upload_photo')
# rb => Read binary
# Broken is _not_ broken - it's just an 'x' :-)
with open(os.path.join(settings.STATIC_ROOT, 'img', 'broken.gif'),
'rb') as photo:
response = self.client.post(url, {
@@ -596,10 +602,59 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
self.assertEqual(
str(list(response.context[0]['messages'])[0]),
'Successfully uploaded',
'Invalid image data should return error message!')
'GIF upload failed?!')
self.assertEqual(
self.user.photo_set.first().format, 'gif',
'Format must be gif, since we uploaded a GIF!')
self.test_confirm_email()
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
urlobj = urlsplit(
libravatar_url(
email=self.user.confirmedemail_set.first().email,
)
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertEqual(
response.status_code,
200,
'unable to fetch avatar?')
def test_upload_jpg_image(self):
'''
Test if jpg is correctly detected and can be viewed
'''
self.login()
url = reverse('upload_photo')
# rb => Read binary
# Broken is _not_ broken - it's just an 'x' :-)
with open(os.path.join(settings.STATIC_ROOT, 'img', 'broken.jpg'),
'rb') as photo:
response = self.client.post(url, {
'photo': photo,
'not_porn': True,
'can_distribute': True,
}, follow=True)
self.assertEqual(
str(list(response.context[0]['messages'])[0]),
'Successfully uploaded',
'JPG upload failed?!')
self.assertEqual(
self.user.photo_set.first().format, 'jpg',
'Format must be jpg, since we uploaded a jpg!')
self.test_confirm_email()
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
urlobj = urlsplit(
libravatar_url(
email=self.user.confirmedemail_set.first().email,
)
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertEqual(
response.status_code,
200,
'unable to fetch avatar?')
def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name
'''
@@ -653,6 +708,22 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
self.user.confirmedemail_set.first().photo,
self.user.photo_set.first())
def test_no_photo_to_email(self):
'''
Test assigning photo to mail address
'''
self.test_confirm_email()
url = reverse(
'assign_photo_email',
args=[self.user.confirmedemail_set.first().id])
response = self.client.post(url, {
'photoNone': True,
}, follow=True)
self.assertEqual(response.status_code, 200, 'cannot un-assign photo?')
self.assertEqual(
self.user.confirmedemail_set.first().photo,
None)
def test_assign_photo_to_email_wo_photo_for_testing_template(self): # pylint: disable=invalid-name
'''
Test assign photo template
@@ -1103,17 +1174,119 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
size=80,
)
)
# Simply delete it, then it digest is 'correct', but
# Simply delete it, then it's digest is 'correct', but
# the hash is no longer there
addr = self.user.confirmedemail_set.first().email
check_hash = hashlib.md5(
addr.strip().lower().encode('utf-8')
).hexdigest()
self.user.confirmedemail_set.first().delete()
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/nobody/80.png',
msg_prefix='Why does this not redirect to Gravatar?')
# Eventually one should check if the data is the same
def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled(self): # pylint: disable=invalid-name
'''
Test fetching avatar via inexisting mail digest
'''
self.test_upload_image()
self.test_confirm_email()
urlobj = urlsplit(
libravatar_url(
email=self.user.confirmedemail_set.first().email,
size=80,
)
)
# Simply delete it, then it digest is 'correct', but
# the hash is no longer there
self.user.confirmedemail_set.first().delete()
url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/nobody/80.png',
msg_prefix='Why does this not redirect to the default img?')
# Eventually one should check if the data is the same
def test_avatar_url_inexisting_mail_digest_w_default_mm(self): # pylint: disable=invalid-name
'''
Test fetching avatar via inexisting mail digest and default 'mm'
'''
urlobj = urlsplit(
libravatar_url(
email='asdf@company.local',
size=80,
default='mm',
)
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/mm/80.png',
msg_prefix='Why does this not redirect to the default img?')
# Eventually one should check if the data is the same
def test_avatar_url_inexisting_mail_digest_w_default_mm_gravatarproxy_disabled(self): # pylint: disable=invalid-name
'''
Test fetching avatar via inexisting mail digest and default 'mm'
'''
urlobj = urlsplit(
libravatar_url(
email='asdf@company.local',
size=80,
default='mm',
)
)
url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/mm/80.png',
msg_prefix='Why does this not redirect to the default img?')
# Eventually one should check if the data is the same
def test_avatar_url_inexisting_mail_digest_wo_default(self): # pylint: disable=invalid-name
'''
Test fetching avatar via inexisting mail digest and default 'mm'
'''
urlobj = urlsplit(
libravatar_url(
email='asdf@company.local',
size=80,
)
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/nobody/80.png',
msg_prefix='Why does this not redirect to the default img?')
# Eventually one should check if the data is the same
def test_avatar_url_inexisting_mail_digest_wo_default_gravatarproxy_disabled(self): # pylint: disable=invalid-name
'''
Test fetching avatar via inexisting mail digest and default 'mm'
'''
urlobj = urlsplit(
libravatar_url(
email='asdf@company.local',
size=80,
)
)
url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/nobody/80.png',
msg_prefix='Why does this not redirect to the default img?')
# Eventually one should check if the data is the same
def test_avatar_url_default(self): # pylint: disable=invalid-name
'''
Test fetching avatar for not existing mail with default specified
@@ -1127,6 +1300,24 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/nobody.png',
msg_prefix='Why does this not redirect to nobody img?')
def test_avatar_url_default_gravatarproxy_disabled(self): # pylint: disable=invalid-name
'''
Test fetching avatar for not existing mail with default specified
'''
urlobj = urlsplit(
libravatar_url(
'xxx@xxx.xxx',
size=80,
default='/static/img/nobody.png',
)
)
url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='/static/img/nobody.png',
@@ -1146,6 +1337,26 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
)
url = '%s?%s' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=False)
self.assertRedirects(
response=response,
expected_url='/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=http://host.tld/img.png',
fetch_redirect_response=False,
msg_prefix='Why does this not redirect to the default img?')
def test_avatar_url_default_external_gravatarproxy_disabled(self): # pylint: disable=invalid-name
'''
Test fetching avatar for not existing mail with external default specified
'''
default = 'http://host.tld/img.png'
urlobj = urlsplit(
libravatar_url(
'xxx@xxx.xxx',
size=80,
default=default,
)
)
url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=False)
self.assertRedirects(
response=response,
expected_url=default,
@@ -1172,3 +1383,109 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
self.test_avatar_url_mail(do_upload_and_confirm=False, size=(20, 20))
img = Image.open(BytesIO(self.user.photo_set.first().data))
self.assertEqual(img.size, (20, 20), 'cropped to 20x20, but resulting image isn\'t 20x20!?')
def test_password_change_view(self):
'''
Test password change view
'''
self.login()
url = reverse('password_change')
response = self.client.get(url)
self.assertEqual(
response.status_code,
200,
'unable to view password change view?')
def test_password_change_view_post_wrong_old_pw(self):
'''
Test password change view post
'''
self.login()
response = self.client.post(
reverse('password_change'), {
'old_password': 'xxx',
'new_password1': self.password,
'new_password2': self.password,
},
follow=True,
)
self.assertContains(
response,
'Your old password was entered incorrectly. Please enter it again.',
1,
200,
'Old password as entered incorrectly, site should raise an error'
)
def test_password_change_view_post_wrong_new_password1(self):
'''
Test password change view post
'''
self.login()
response = self.client.post(
reverse('password_change'), {
'old_password': self.password,
'new_password1': self.password + '.',
'new_password2': self.password,
},
follow=True,
)
self.assertContains(
response,
'The two password fields didn&#39;t match.',
1,
200,
'Old password as entered incorrectly, site should raise an error'
)
def test_password_change_view_post_wrong_new_password2(self):
'''
Test password change view post
'''
self.login()
response = self.client.post(
reverse('password_change'), {
'old_password': self.password,
'new_password1': self.password,
'new_password2': self.password + '.',
},
follow=True,
)
self.assertContains(
response,
'The two password fields didn&#39;t match.',
1,
200,
'Old password as entered incorrectly, site should raise an error'
)
def test_profile_must_list_first_and_lastname(self):
'''
Test if profile view correctly lists first -/last name
'''
self.login()
response = self.client.get(reverse('profile'))
self.assertContains(
response,
self.first_name,
1,
200,
'First name not listed in profile page',
)
self.assertContains(
response,
self.last_name,
1,
200,
'Last name not listed in profile page',
)
self.assertContains(
response,
self.first_name + ' ' + self.last_name,
1,
200,
'First and last name not correctly listed in profile page',
)

View File

@@ -6,11 +6,12 @@ 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 PasswordResetView, PasswordResetDoneView,\
from django.contrib.auth.views import PasswordResetDoneView,\
PasswordResetConfirmView, PasswordResetCompleteView
from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView
from django.contrib.auth.decorators import login_required
from . views import ProfileView, PasswordResetView
from . views import CreateView, PasswordSetView, AddEmailView
from . views import RemoveUnconfirmedEmailView, ConfirmEmailView
from . views import RemoveConfirmedEmailView, AssignPhotoEmailView
@@ -22,6 +23,7 @@ from . views import CropPhotoView
from . views import UserPreferenceView, UploadLibravatarExportView
from . views import ResendConfirmationMailView
from . views import IvatarLoginView
from . views import DeleteAccountView
# Define URL patterns, self documenting
# To see the fancy, colorful evaluation of these use:
@@ -59,12 +61,8 @@ urlpatterns = [ # pylint: disable=invalid-name
path('export/', login_required(
TemplateView.as_view(template_name='export.html')
), name='export'),
path('delete/', login_required(
TemplateView.as_view(template_name='delete.html')
), name='delete'),
path('profile/', login_required(
TemplateView.as_view(template_name='profile.html')
), name='profile'),
path('delete/', DeleteAccountView.as_view(), name='delete'),
path('profile/', ProfileView.as_view(), name='profile'),
path('add_email/', AddEmailView.as_view(), name='add_email'),
path('add_openid/', AddOpenIDView.as_view(), name='add_openid'),
path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'),

View File

@@ -9,6 +9,7 @@ import binascii
from PIL import Image
from django.db.models import ProtectedError
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.utils.decorators import method_decorator
@@ -20,6 +21,7 @@ from django.views.generic.detail import DetailView
from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import UserCreationForm, SetPasswordForm
from django.contrib.auth.views import LoginView
from django.contrib.auth.views import PasswordResetView as PasswordResetViewOriginal
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse_lazy, reverse
@@ -32,11 +34,12 @@ from openid.consumer import consumer
from ipware import get_client_ip
from libravatar import libravatar_url
from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE, JPEG_QUALITY
from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE, JPEG_QUALITY, AVATAR_MAX_SIZE
from .gravatar import get_photo as get_gravatar_photo
from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm
from .forms import UpdatePreferenceForm, UploadLibravatarExportForm
from .forms import DeleteAccountForm
from .models import UnconfirmedEmail, ConfirmedEmail, Photo
from .models import UnconfirmedOpenId, ConfirmedOpenId, DjangoOpenIDStore
from .models import UserPreference
@@ -117,8 +120,8 @@ class AddEmailView(SuccessMessageMixin, FormView):
def form_valid(self, form):
if not form.save(self.request):
return render(self.request, self.template_name, {'form': form})
else:
messages.success(self.request, _('Address added successfully'))
messages.success(self.request, _('Address added successfully'))
return super().form_valid(form)
@@ -143,7 +146,6 @@ class RemoveUnconfirmedEmailView(SuccessMessageMixin, View):
return HttpResponseRedirect(reverse_lazy('profile'))
@method_decorator(login_required, name='dispatch')
class ConfirmEmailView(SuccessMessageMixin, TemplateView):
'''
View class for confirming an unconfirmed email address
@@ -310,14 +312,13 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
if 'email_id' in kwargs:
try:
addr = ConfirmedEmail.objects.get(pk=kwargs['email_id']).email
except ConfirmedEmail.ObjectDoesNotExist:
except ConfirmedEmail.ObjectDoesNotExist: # pylint: disable=no-member
messages.error(
self.request,
_('Address does not exist'))
return context
if 'email_addr' in kwargs:
addr = kwargs['email_addr']
addr = kwargs.get('email_addr', None)
if addr:
gravatar = get_gravatar_photo(addr)
@@ -327,6 +328,7 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
libravatar_service_url = libravatar_url(
email=addr,
default=404,
size=AVATAR_MAX_SIZE,
)
if libravatar_service_url:
try:
@@ -336,8 +338,8 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
else:
context['photos'].append({
'service_url': libravatar_service_url,
'thumbnail_url': libravatar_service_url + '?s=80',
'image_url': libravatar_service_url + '?s=512',
'thumbnail_url': libravatar_service_url + '&s=80',
'image_url': libravatar_service_url + '&s=512',
'width': 80,
'height': 80,
'service_name': 'Libravatar',
@@ -350,18 +352,10 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView):
Handle post to photo import
'''
addr = None
email_id = None
imported = None
if 'email_id' in kwargs:
email_id = kwargs['email_id']
if 'email_id' in request.POST:
email_id = request.POST['email_id']
if 'email_addr' in kwargs:
addr = kwargs['email_addr']
if 'email_addr' in request.POST:
addr = request.POST['email_addr']
email_id = kwargs.get('email_id', request.POST.get('email_id', None))
addr = kwargs.get('emali_addr', request.POST.get('email_addr', None))
if email_id:
email = ConfirmedEmail.objects.filter(
@@ -415,7 +409,7 @@ class RawImageView(DetailView):
def get(self, request, *args, **kwargs):
photo = self.model.objects.get(pk=kwargs['pk']) # pylint: disable=no-member
if not photo.user.id is request.user.id:
if not photo.user.id == request.user.id:
return HttpResponseRedirect(reverse_lazy('home'))
return HttpResponse(
BytesIO(photo.data), content_type='image/%s' % photo.format)
@@ -436,7 +430,7 @@ class DeletePhotoView(SuccessMessageMixin, View):
photo = self.model.objects.get( # pylint: disable=no-member
pk=kwargs['pk'], user=request.user)
photo.delete()
except (self.model.DoesNotExist, ProtectedError):
except (self.model.DoesNotExist, ProtectedError): # pylint: disable=no-member
messages.error(
request,
_('No such image or no permission to delete it'))
@@ -519,7 +513,7 @@ class RemoveUnconfirmedOpenIDView(View):
user=request.user, id=kwargs['openid_id'])
openid.delete()
messages.success(request, _('ID removed'))
except self.model.DoesNotExist: # pragma: no cover # pylint: disable=no-member
except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member
messages.error(request, _('ID does not exist'))
return HttpResponseRedirect(reverse_lazy('profile'))
@@ -543,9 +537,9 @@ class RemoveConfirmedOpenIDView(View):
user_id=request.user.id,
claimed_id=openid.openid)
openidobj.delete()
except:
except Exception as exc: # pylint: disable=broad-except
# Why it is not there?
pass
print('How did we get here: %s' % exc)
openid.delete()
messages.success(request, _('ID removed'))
except self.model.DoesNotExist: # pylint: disable=no-member
@@ -567,7 +561,7 @@ class RedirectOpenIDView(View):
try:
unconfirmed = self.model.objects.get( # pylint: disable=no-member
user=request.user, id=kwargs['openid_id'])
except self.model.DoesNotExist: # pragma: no cover # pylint: disable=no-member
except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member
messages.error(request, _('ID does not exist'))
return HttpResponseRedirect(reverse_lazy('profile'))
@@ -583,9 +577,12 @@ class RedirectOpenIDView(View):
messages.error(request, _('OpenID discovery failed: %s' % exc))
return HttpResponseRedirect(reverse_lazy('profile'))
except UnicodeDecodeError as exc: # pragma: no cover
msg = _('OpenID discovery failed (userid=%s) for %s: %s' %
(request.user.id, user_url.encode('utf-8'), exc))
print(msg)
msg = _('OpenID discovery failed (userid=%(userid)s) for %(userurl)s: %(message)s' % {
userid: request.user.id,
userurl: user_url.encode('utf-8'),
message: exc,
})
print("message: %s" % msg)
messages.error(request, msg)
if auth_request is None: # pragma: no cover
@@ -621,10 +618,12 @@ class ConfirmOpenIDView(View): # pragma: no cover
self.request,
_('Confirmation failed: "') + str(info.message) + '"')
return HttpResponseRedirect(reverse_lazy('profile'))
elif info.status == consumer.CANCEL:
if info.status == consumer.CANCEL:
messages.error(self.request, _('Cancelled by user'))
return HttpResponseRedirect(reverse_lazy('profile'))
elif info.status != consumer.SUCCESS:
if info.status != consumer.SUCCESS:
messages.error(self.request, _('Unknown verification error'))
return HttpResponseRedirect(reverse_lazy('profile'))
@@ -705,7 +704,7 @@ class CropPhotoView(TemplateView):
if 'email' in request.POST:
try:
email = ConfirmedEmail.objects.get(email=request.POST['email'])
except ConfirmedEmail.DoesNotExist:
except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member
pass # Ignore automatic assignment
if 'openid' in request.POST:
@@ -728,8 +727,26 @@ class UserPreferenceView(FormView, UpdateView):
form_class = UpdatePreferenceForm
success_url = reverse_lazy('user_preference')
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
userpref = None
try:
userpref = self.request.user.userpreference
except ObjectDoesNotExist:
userpref = UserPreference(user=self.request.user)
userpref.theme = request.POST['theme']
userpref.save()
return HttpResponseRedirect(reverse_lazy('user_preference'))
def get(self, request, *args, **kwargs):
return render(self.request, self.template_name, {
'THEMES': UserPreference.THEMES,
})
def get_object(self, queryset=None):
return self.request.user.userpreference
(obj, created) = UserPreference.objects.get_or_create(user=self.request.user) # pylint: disable=no-member,unused-variable
return obj
@method_decorator(login_required, name='dispatch')
@@ -852,3 +869,83 @@ class IvatarLoginView(LoginView):
if request.user.is_authenticated:
return HttpResponseRedirect(reverse_lazy('profile'))
return super().get(self, request, args, kwargs)
@method_decorator(login_required, name='dispatch')
class ProfileView(TemplateView):
'''
View class for profile
'''
template_name = 'profile.html'
def get(self, request, *args, **kwargs):
self._confirm_claimed_openid()
return super().get(self, request, args, kwargs)
def _confirm_claimed_openid(self):
openids = self.request.user.useropenid_set.all()
# If there is only one OpenID, we eventually need to add it to the user account
if openids.count() == 1:
# Already confirmed, skip
if ConfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member
return
# For whatever reason, this is in unconfirmed state, skip
if UnconfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member
return
print('need to confirm: %s' % openids.first())
confirmed = ConfirmedOpenId()
confirmed.user = self.request.user
confirmed.ip_address = get_client_ip(self.request)[0]
confirmed.openid = openids.first().claimed_id
confirmed.save()
class PasswordResetView(PasswordResetViewOriginal):
'''
View class for password reset
'''
def post(self, request, *args, **kwargs):
'''
Since we have the mail addresses in ConfirmedEmail model,
we need to set the email on the user object in order for the
PasswordResetView class to pick up the correct user
'''
if 'email' in request.POST:
try:
confirmed_email = ConfirmedEmail.objects.get(email=request.POST['email'])
confirmed_email.user.email = confirmed_email.email
confirmed_email.user.save()
except Exception as exc:
pass
return super().post(self, request, args, kwargs)
@method_decorator(login_required, name='dispatch')
class DeleteAccountView(SuccessMessageMixin, FormView):
'''
View class for account deletion
'''
template_name = 'delete.html'
form_class = DeleteAccountForm
success_url = reverse_lazy('home')
def get(self, request, *args, **kwargs):
return super().get(self, request, args, kwargs)
def post(self, request, *args, **kwargs):
'''
Handle account deletion
'''
if request.user.password:
if 'password' in request.POST:
if not request.user.check_password(request.POST['password']):
messages.error(request, _('Incorrect password'))
return HttpResponseRedirect(reverse_lazy('delete'))
else:
messages.error(request, _('No password given'))
return HttpResponseRedirect(reverse_lazy('delete'))
raise(_('No password given'))
request.user.delete() # should delete all confirmed/unconfirmed/photo objects
return super().post(self, request, args, kwargs)

18
ivatar/middleware.py Normal file
View File

@@ -0,0 +1,18 @@
"""
Middleware classes
"""
from django.utils.deprecation import MiddlewareMixin
class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods
"""
Middleware to rewrite proxy headers for deployments
with multiple proxies
"""
def process_request(self, request):
"""
Rewrites the proxy headers so that forwarded server is
used if available.
"""
if 'HTTP_X_FORWARDED_SERVER' in request.META:
request.META['HTTP_X_FORWARDED_HOST'] = request.META['HTTP_X_FORWARDED_SERVER']

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
@import 'tortin.less';
@bg-hero:@lab-green;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
@import 'tortin.less';
@bg-hero:@lab-red;

File diff suppressed because one or more lines are too long

View File

@@ -258,6 +258,46 @@ background-color:@bg-hero;
.navbar-tortin .navbar-collapse, .navbar-tortin .navbar-form {
border:0;
}
.dropdown-menu {
background-color:@bg-hero;
border:1px solid darken(@bg-hero, 10%);
}
.dropdown-menu>li>a {
color:#FFFFFF;
}
.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover {
background-color:darken(@bg-hero, 10%);
color:#FFFFFF;
}
.checkbox input, .radio input {display:none}
.checkbox input + label, .radio input + label {
padding-left:0;
}
.checkbox input + label:before, .radio input + label:before {
font-family: FontAwesome;
display: inline-block;
letter-spacing:5px;
font-size:20px;
color:@bg-hero;
vertical-align:middle;
}
.checkbox input + label:before {content: "\f0c8"}
.checkbox input:checked + label:before {content: "\f14a"}
.radio input + label:before {content: "\f10c"}
.radio input:checked + label:before {content: "\f192"}
.uploadbtn:before {
position:absolute;
left:0;
right:0;
text-align:center;
content:"Select file";
font-family: 'Montserrat', sans-serif;
}
.jcrop-holder > div > div:nth-child(1) {
outline-width:2px;
outline-style:solid;
outline-color:@bg-hero;
}
@media (max-width:767px) {
.navbar-tortin .navbar-nav .open .dropdown-menu > li > a {
color:#FFFFFF
@@ -311,3 +351,9 @@ background: none;
width:auto;
height:36px;
}
.radio {
color: @bg-hero;
}
input[type="radio"]:checked+label {
font-weight: bold;
}

View File

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

65
ivatar/test_views.py Normal file
View File

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

View File

@@ -14,14 +14,12 @@ class CheckDomainForm(forms.Form):
'''
Form handling domain check
'''
can_distribute = forms.TextInput(
attrs={
'label': _('Domain'),
'required': True,
'error_messages': {
'required':
_('Cannot check without a domain name.')
}
domain = forms.CharField(
label=_('Domain'),
required=True,
error_messages={
'required':
_('Cannot check without a domain name.')
}
)

View File

@@ -0,0 +1,81 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans 'Check domain' %}{% endblock title %}
{% block content %}
<div class="container">
<h1>{% trans 'Check domain' %}</h1>
{% if form.errors %}
<p class="error">{% trans "Please correct errors below:" %}<br>
{% if form.openid_identifier.errors %}
{{ form.openid_identifier.errors|join:', ' }}
{% endif %}
{% if form.next.errors %}
{{ form.next.errors|join:', ' }}
{% endif %}
</p>
{% endif %}
<div style="max-width:640px">
<form method="post" name="lookup">
{% csrf_token %}
<div class="form-group"><label for="id_domain">{% trans 'Domain' %}</label>
<input type="text" name="domain" maxlength="254" minlength="6" class="form-control" placeholder="{% trans 'Domain' %}" {% if form.domain.value %} value="{{ form.domain.value }}" {% endif %} id="id_domain">
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">{% trans 'Check' %}</button>
</div>
</form>
</div>
<!-- TODO TODO TODO: I need better styling -->
{% if result %}
<hr/>
<table>
<tr>
<td valign="top" style="padding: 0px 10px 0px 10px;">{% trans 'HTTP avatar server:' %}</td>
<td>
{% if result.avatar_server_http %}
<tt>{{result.avatar_server_http}}</tt>
{% if result.avatar_server_http_ipv4 %}
<br>{{ result.avatar_server_http_ipv4 }}
{% else %}
<br><strong>{% trans 'Warning: no A record for this hostname' %}</strong>
{% endif %}
{% if result.avatar_server_http_ipv6 %}
<br>{{ result.avatar_server_http_ipv6 }}
{% endif %}
{% else %}
<i>{% trans 'use <tt>http://cdn.libravatar.org</tt>' %}</i>
{% endif %}
</td>
</tr>
<tr>
<td valign="top" style="padding: 0px 10px 0px 10px;">{% trans 'HTTPS avatar server:' %}</td>
<td>
{% if result.avatar_server_https %}
<tt>{{result.avatar_server_https}}</tt>
{% if result.avatar_server_https_ipv4 %}
<br>{{ result.avatar_server_https_ipv4 }}
{% else %}
<br><strong>{% trans 'Warning: no A record for this hostname' %}</strong>
{% endif %}
{% if result.avatar_server_https_ipv6 %}
<br>{{ result.avatar_server_https_ipv6 }}
{% endif %}
{% else %}
<i>{% trans 'use <tt>https://seccdn.libravatar.org</tt>' %}</i>
{% endif %}
</td>
</tr>
</table>
{% endif %}
</div>
<div style="height:40px"></div>
{% endblock content %}

View File

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

View File

@@ -5,6 +5,8 @@ from django.views.generic.edit import FormView
from django.urls import reverse_lazy as reverse
from django.shortcuts import render
import DNS
from libravatar import libravatar_url, parse_user_identity
from libravatar import SECURE_BASE_URL as LIBRAVATAR_SECURE_BASE_URL
from libravatar import BASE_URL as LIBRAVATAR_BASE_URL
@@ -20,7 +22,24 @@ class CheckDomainView(FormView):
'''
template_name = 'check_domain.html'
form_class = CheckDomainForm
success_url = reverse('tools_check_domain')
def form_valid(self, form):
result = {}
super().form_valid(form)
domain = form.cleaned_data['domain']
result['avatar_server_http'] = lookup_avatar_server(domain, False)
if result['avatar_server_http']:
result['avatar_server_http_ipv4'] = lookup_ip_address(result['avatar_server_http'], False)
result['avatar_server_http_ipv6'] = lookup_ip_address(result['avatar_server_http'], True)
result['avatar_server_https'] = lookup_avatar_server(domain, True)
if result['avatar_server_https']:
result['avatar_server_https_ipv4'] = lookup_ip_address(result['avatar_server_https'], False)
result['avatar_server_https_ipv6'] = lookup_ip_address(result['avatar_server_https'], True)
return render(self.request, self.template_name, {
'form': form,
'result': result,
})
class CheckView(FormView):
'''
@@ -69,8 +88,8 @@ class CheckView(FormView):
mail_hash256 = hash_obj.hexdigest()
size = form.cleaned_data['size']
if form.cleaned_data['openid']:
if form.cleaned_data['openid'][-1] != '/':
form.cleaned_data['openid'] += '/'
if not form.cleaned_data['openid'].startswith('http://') and not form.cleaned_data['openid'].startswith('https://'):
form.cleaned_data['openid'] = 'http://%s' % form.cleaned_data['openid']
openidurl = libravatar_url(
openid=form.cleaned_data['openid'],
size=form.cleaned_data['size'],
@@ -100,3 +119,140 @@ class CheckView(FormView):
'openid_hash': openid_hash,
'size': size,
})
def lookup_avatar_server(domain, https):
'''
Extract the avatar server from an SRV record in the DNS zone
The SRV records should look like this:
_avatars._tcp.example.com. IN SRV 0 0 80 avatars.example.com
_avatars-sec._tcp.example.com. IN SRV 0 0 443 avatars.example.com
'''
if domain and len(domain) > 60:
domain = domain[:60]
service_name = None
if https:
service_name = "_avatars-sec._tcp.%s" % domain
else:
service_name = "_avatars._tcp.%s" % domain
DNS.DiscoverNameServers()
try:
dns_request = DNS.Request(name=service_name, qtype='SRV').req()
except DNS.DNSError as message:
print("DNS Error: %s (%s)" % (message, domain))
return None
if dns_request.header['status'] == 'NXDOMAIN':
# Not an error, but no point in going any further
return None
if dns_request.header['status'] != 'NOERROR':
print("DNS Error: status=%s (%s)" % (dns_request.header['status'], domain))
return None
records = []
for answer in dns_request.answers:
if ('data' not in answer) or (not answer['data']) or (not answer['typename']) or (answer['typename'] != 'SRV'):
continue
record = {'priority': int(answer['data'][0]), 'weight': int(answer['data'][1]),
'port': int(answer['data'][2]), 'target': answer['data'][3]}
records.append(record)
target, port = srv_hostname(records)
if target and ((https and port != 443) or (not https and port != 80)):
return "%s:%s" % (target, port)
return target
def srv_hostname(records):
'''
Return the right (target, port) pair from a list of SRV records.
'''
if len(records) < 1:
return (None, None)
if len(records) == 1:
ret = records[0]
return (ret['target'], ret['port'])
# Keep only the servers in the top priority
priority_records = []
total_weight = 0
top_priority = records[0]['priority'] # highest priority = lowest number
for ret in records:
if ret['priority'] > top_priority:
# ignore the record (ret has lower priority)
continue
elif ret['priority'] < top_priority:
# reset the aretay (ret has higher priority)
top_priority = ret['priority']
total_weight = 0
priority_records = []
total_weight += ret['weight']
if ret['weight'] > 0:
priority_records.append((total_weight, ret))
else:
# zero-weigth elements must come first
priority_records.insert(0, (0, ret))
if len(priority_records) == 1:
unused, ret = priority_records[0]
return (ret['target'], ret['port'])
# Select first record according to RFC2782 weight ordering algorithm (page 3)
random_number = random.randint(0, total_weight)
for record in priority_records:
weighted_index, ret = record
if weighted_index >= random_number:
return (ret['target'], ret['port'])
print('There is something wrong with our SRV weight ordering algorithm')
return (None, None)
def lookup_ip_address(hostname, ipv6):
"""
Try to get IPv4 or IPv6 addresses for the given hostname
"""
DNS.DiscoverNameServers()
try:
if ipv6:
dns_request = DNS.Request(name=hostname, qtype=DNS.Type.AAAA).req()
else:
dns_request = DNS.Request(name=hostname, qtype=DNS.Type.A).req()
except DNS.DNSError as message:
print("DNS Error: %s (%s)" % (message, hostname))
return None
if dns_request.header['status'] != 'NOERROR':
print("DNS Error: status=%s (%s)" % (dns_request.header['status'], hostname))
return None
for answer in dns_request.answers:
if ('data' not in answer) or (not answer['data']):
continue
if (ipv6 and answer['typename'] != 'AAAA') or (not ipv6 and answer['typename'] != 'A'):
continue # skip CNAME records
if ipv6:
return inet_ntop(AF_INET6, answer['data'])
else:
return answer['data']
return None

View File

@@ -7,10 +7,11 @@ from django.conf.urls import url
from django.conf.urls.static import static
from django.views.generic import TemplateView, RedirectView
from ivatar import settings
from . views import AvatarImageView
from . views import AvatarImageView, GravatarProxyView
urlpatterns = [ # pylint: disable=invalid-name
path('admin/', admin.site.urls),
path('i18n/', include('django.conf.urls.i18n')),
url('openid/', include('django_openid_auth.urls')),
url('accounts/', include('ivatar.ivataraccount.urls')),
url('tools/', include('ivatar.tools.urls')),
@@ -20,6 +21,16 @@ urlpatterns = [ # pylint: disable=invalid-name
url(
r'avatar/(?P<digest>\w{32})',
AvatarImageView.as_view(), name='avatar_view'),
url(
r'avatar/(?P<digest>\w*)',
TemplateView.as_view(
template_name='error.html',
extra_context={
'errormessage': 'Incorrect digest length',
})),
url(
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
url('run_your_own/', TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'),

View File

@@ -4,18 +4,50 @@ views under /
from io import BytesIO
from os import path
import hashlib
from PIL import Image
from django.views.generic.base import TemplateView
from urllib.request import urlopen
from urllib.error import HTTPError, URLError
from ssl import SSLError
from django.views.generic.base import TemplateView, View
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy
from PIL import Image
from monsterid.id import build_monster as BuildMonster
from pydenticon import Generator as IdenticonGenerator
from robohash import Robohash
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
from . ivataraccount.models import pil_format
from . ivataraccount.models import pil_format, file_format
URL_TIMEOUT = 5 # in seconds
def get_size(request, size=DEFAULT_AVATAR_SIZE):
'''
Get size from the URL arguments
'''
sizetemp = None
if 's' in request.GET:
sizetemp = request.GET['s']
if 'size' in request.GET:
sizetemp = request.GET['size']
if sizetemp:
if sizetemp != '' and sizetemp is not None and sizetemp != '0':
try:
if int(sizetemp) > 0:
size = int(sizetemp)
# Should we receive something we cannot convert to int, leave
# the user with the default value of 80
except ValueError:
pass
if size > int(AVATAR_MAX_SIZE):
size = int(AVATAR_MAX_SIZE)
return size
class AvatarImageView(TemplateView):
@@ -24,16 +56,18 @@ class AvatarImageView(TemplateView):
'''
# TODO: Do cache resize images!! Memcached?
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
'''
Override get from parent class
'''
model = ConfirmedEmail
size = 80
size = get_size(request)
imgformat = 'png'
obj = None
default = None
forcedefault = False
gravatarredirect = False
gravatarproxy = True
if 'd' in request.GET:
default = request.GET['d']
@@ -47,28 +81,13 @@ class AvatarImageView(TemplateView):
if request.GET['forcedefault'] == 'y':
forcedefault = True
sizetemp = None
if 's' in request.GET:
sizetemp = request.GET['s']
if 'size' in request.GET:
sizetemp = request.GET['size']
if sizetemp:
if sizetemp != '' and sizetemp is not None and sizetemp != '0':
size = int(sizetemp)
if 'gravatarredirect' in request.GET:
if request.GET['gravatarredirect'] == 'y':
gravatarredirect = True
if size > int(AVATAR_MAX_SIZE):
size = int(AVATAR_MAX_SIZE)
if len(kwargs['digest']) == 32:
# Fetch by digest from mail
pass
elif len(kwargs['digest']) == 64:
if ConfirmedOpenId.objects.filter( # pylint: disable=no-member
digest=kwargs['digest']).count():
# Fetch by digest from OpenID
model = ConfirmedOpenId
else: # pragma: no cover
# We should actually never ever reach this code...
raise Exception('Digest provided is wrong: %s' % kwargs['digest'])
if 'gravatarproxy' in request.GET:
if request.GET['gravatarproxy'] == 'n':
gravatarproxy = False
try:
obj = model.objects.get(digest=kwargs['digest'])
@@ -76,10 +95,29 @@ class AvatarImageView(TemplateView):
try:
obj = model.objects.get(digest_sha256=kwargs['digest'])
except ObjectDoesNotExist:
pass
model = ConfirmedOpenId
try:
obj = model.objects.get(digest=kwargs['digest'])
except:
pass
# If that mail/openid doesn't exist, or has no photo linked to it
if not obj or not obj.photo or forcedefault:
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ '?s=%i' % size
# If we have redirection to Gravatar enabled, this overrides all
# default= settings, except forcedefault!
if gravatarredirect and not forcedefault:
return HttpResponseRedirect(gravatar_url)
# Request to proxy Gravatar image - only if not forcedefault
if gravatarproxy and not forcedefault:
url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \
+ '?s=%i' % size + '&default=%s' % default
return HttpResponseRedirect(url)
# Return the default URL, as specified, or 404 Not Found, if default=404
if default:
if str(default) == str(404):
@@ -94,6 +132,19 @@ class AvatarImageView(TemplateView):
data,
content_type='image/png')
if str(default) == 'robohash':
roboset = 'any'
if request.GET.get('robohash'):
roboset = request.GET.get('robohash')
robohash = Robohash(kwargs['digest'])
robohash.assemble(roboset=roboset, sizex=size, sizey=size)
data = BytesIO()
robohash.img.save(data, format='png')
data.seek(0)
return HttpResponse(
data,
content_type='image/png')
if str(default) == 'identicon' or str(default) == 'retro':
# Taken from example code
foreground = [
@@ -105,7 +156,15 @@ class AvatarImageView(TemplateView):
'rgb(49,203,115)',
'rgb(141,69,170)']
background = 'rgb(224,224,224)'
padding = (10, 10, 10, 10)
padwidth = int(size/10)
if padwidth < 10:
padwidth = 10
if size < 60:
padwidth = 0
padding = (padwidth, padwidth, padwidth, padwidth)
# Since padding is _added_ around the generated image, we
# need to reduce the image size by padding*2 (left/right, top/bottom)
size = size - 2*padwidth
generator = IdenticonGenerator(
10, 10, digest=hashlib.sha1,
foreground=foreground, background=background)
@@ -118,14 +177,18 @@ class AvatarImageView(TemplateView):
if str(default) == 'mm' or str(default) == 'mp':
# If mm is explicitly given, we need to catch that
pass
else:
return HttpResponseRedirect(default)
static_img = path.join('static', 'img', 'mm', '%s%s' % (str(size), '.png'))
if not path.isfile(static_img):
# We trust this exists!!!
static_img = path.join('static', 'img', 'mm', '512.png')
# We trust static/ is mapped to /static/
return HttpResponseRedirect('/' + static_img)
return HttpResponseRedirect(default)
static_img = path.join('static', 'img', 'mm', '%s%s' % (str(size), '.png'))
static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png'))
if not path.isfile(static_img):
# We trust this exists!!!
static_img = path.join('static', 'img', 'mm', '512.png')
static_img = path.join('static', 'img', 'nobody', '512.png')
# We trust static/ is mapped to /static/
return HttpResponseRedirect('/' + static_img)
@@ -143,3 +206,80 @@ class AvatarImageView(TemplateView):
return HttpResponse(
data,
content_type='image/%s' % imgformat)
class GravatarProxyView(View):
'''
Proxy request to Gravatar and return the image from there
'''
# TODO: Do cache images!! Memcached?
def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument
'''
Override get from parent class
'''
def redir_default(default=None):
url = reverse_lazy(
'avatar_view',
args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y'
if default != None:
url += '&default=%s' % default
return HttpResponseRedirect(url)
size = get_size(request)
gravatarimagedata = None
default = None
try:
if str(request.GET['default']) != 'None':
default = request.GET['default']
except:
pass
# This part is special/hackish
# Check if the image returned by Gravatar is their default image, if so,
# redirect to our default instead.
gravatar_test_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ '?s=%i' % 50
try:
testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT)
data = BytesIO(testdata.read())
if hashlib.md5(data.read()).hexdigest() == '71bc262d627971d13fe6f3180b93062a':
return redir_default(default)
except Exception as exc:
print('Gravatar test url fetch failed: %s' % exc)
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ '?s=%i' % size
try:
gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT)
except HTTPError as exc:
if exc.code != 404 and exc.code != 503:
print(
'Gravatar fetch failed with an unexpected %s HTTP error' %
exc.code)
return redir_default(default)
except URLError as exc:
print(
'Gravatar fetch failed with URL error: %s' %
exc.reason)
return redir_default(default)
except SSLError as exc:
print(
'Gravatar fetch failed with SSL error: %s' %
exc.reason)
return redir_default(default)
try:
data = BytesIO(gravatarimagedata.read())
img = Image.open(data)
data.seek(0)
return HttpResponse(
data.read(),
content_type='image/%s' % file_format(img.format))
except ValueError as exc:
print('Value error: %s' % exc)
return redir_default(default)
# We shouldn't reach this point... But make sure we do something
return redir_default(default)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -34,3 +34,5 @@ psycopg2
notsetuptools
git+https://github.com/ofalk/monsterid.git
git+https://github.com/azaghal/pydenticon.git
git+https://github.com/ofalk/Robohash.git@devel
python-memcached

View File

@@ -11,24 +11,22 @@
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
{% if request.user.is_authenticated %}
<li><a href="{% url 'profile' %}"><i class="fa fa-image" aria-hidden="true"></i> {% trans 'Profile' %}</a></li>
<!--
<li><a href="{% url 'user_preference' %}"><i class="fa fa-cog" aria-hidden="true"></i> {% trans 'Preferences' %}</a></li>
-->
<li><a href="{% url 'import_photo' %}"><i class="fa 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-file-archive-o" aria-hidden="true"></i> {% trans 'Import libravatar XML export' %}</a></li>
<li><a href="{% url 'password_change' %}"><i class="fa fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li>
<li><a href="{% url 'password_reset' %}"><i class="fa fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>
<li><a href="{% url 'profile' %}"><i class="fa fa-fw fa-image" aria-hidden="true"></i> {% trans 'Profile' %}</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 '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 'password_change' %}"><i class="fa fa-fw fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li>
<li><a href="{% url 'password_reset' %}"><i class="fa fa-fw fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>
{% else %}
<li><a href="{% url 'login' %}"><i class="fa fa-sign-in" aria-hidden="true"></i> {% trans 'Local' %}</a></li>
<li><a href="{% url 'new_account' %}"><i class="fa fa-user-plus" aria-hidden="true"></i> {% trans 'Create account' %}</a></li>
<li><a href="{% url 'login' %}"><i class="fa fa-fw fa-sign-in" aria-hidden="true"></i> {% trans 'Local' %}</a></li>
<li><a href="{% url 'new_account' %}"><i class="fa fa-fw fa-user-plus" aria-hidden="true"></i> {% trans 'Create account' %}</a></li>
{% endif %}
</ul>
</li>
{% if user.is_staff %}
<li>
<a href="{% url 'admin:index' %}" target="_new"><i class="fa fa-user-secret" aria-hidden="true"></i> {% trans 'Admin' %}</a>
<a href="{% url 'admin:index' %}" target="_new"><i class="fa fa-fw fa-user-secret" aria-hidden="true"></i> {% trans 'Admin' %}</a>
</li>
{% endif %}
</ul>

View File

@@ -1,72 +1,12 @@
{% load static %}
{% load i18n %}<!DOCTYPE HTML>
{% include 'header.html' %}
<title>iVatar :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title>
<title>{{ site_name }} :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title>
{% spaceless %}
<div id="page">
<div id="header">
{% block topbar_base %}
<nav class="navbar navbar-tortin">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% block topbar %}
{% block site_brand %}
{% if user.is_anonymous %}
<a class="navbar-brand" href="/">
{% else %}
<a class="navbar-brand" href="{% url 'profile' %}">
{% endif %}
ivatar
</a>
</div>
{% endblock %}
{% block nav %}
<div class="collapse navbar-collapse" id="navbar">
<ul class="nav navbar-nav">
{% if not user.is_anonymous %}
<li>
<a href="/"><i class="fa fa-home" aria-hidden="true"></i>
{% trans 'Home' %}
</a>
</li>
{% endif %}
<li>
<a class="nav-link" href="{% url 'contact' %}"><i class="fa fa-envelope" aria-hidden="true"></i>
{% trans 'Contact' %}
</a>
</li>
<li>
<a href="{% url 'security' %}"><i class="fa fa-user-secret" aria-hidden="true"></i>
{% trans 'Security' %}
</a>
</li>
<li class="dropdown" id="tab_tools">
<a class="dropdown-toggle" href="#" id="tools_dropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans 'Tools' %}">
<i class="fa fa-wrench" aria-hidden="true"></i>
{% trans 'Tools' %}
</a>
<ul class="dropdown-menu" aria-labelledby="tools_dropdown">
<li><a id="tools-check" href="{% url 'tools_check' %}">
<i class="fa fa-check-square" aria-hidden="true"></i> {% trans 'Check' %}
</a></li>
</ul>
</li>
</ul>
{% block account_bar %}{% include "_account_bar.html" %}{% endblock %}
</div>
</div>
</nav>
{% endblock %}
{% endblock %}
</nav>
{% endblock %}
{% include 'navbar.html' %}
</div>
{% autoescape off %}{% endautoescape %}
@@ -89,4 +29,3 @@
<script src="{% static '/js/bootstrap.min.js' %}"></script>
<script src="{% static '/js/ivatar.js' %}"></script>
{{ settings }}

View File

@@ -1,19 +1,14 @@
{% load static %}
{% load i18n %}<!DOCTYPE HTML>
{% include 'header.html' %}
<title>iVatar :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title>
<title>{{ site_name }} :: {% block title %}{% trans 'Freeing the Web, one face at a time!' %}{% endblock title %}</title>
{% spaceless %}
<div id="page">
{% autoescape off %}{% endautoescape %}
{% block content %}{% endblock content %}
{% block content %}{% endblock content %}
{% block footer %}{% include 'footer.html' %}{% endblock footer %}
</div>
{% endspaceless %}
<script src="{% static '/js/bootstrap.min.js' %}"></script>
<script src="{% static '/js/ivatar.js' %}"></script>
{{ settings }}

View File

@@ -8,31 +8,31 @@
{% block content %}
There are a few ways to get in touch with the ivatar/libravatar developers:
<h2>IRC</h2>
<h4>IRC</h4>
If you have an IRC client already, you can join #libravatar on chat.freenode.net.
<br/>
Otherwise, you can use this <a href="http://webchat.freenode.net/?channels=libravatar">simple web interface</a>.
Otherwise, you can use this <a href="http://webchat.freenode.net/?channels=libravatar" title="http://webchat.freenode.net/?channels=libravatar">simple web interface</a>.
<br/>
Please keep in mind that you may live in a different timezone than most of the developers. So if you do not get a response, it's not because we're ignoring you, it's probably because we're sleeping :)
<h2>Bug tracker</h2>
<h4>Bug tracker</h4>
If you have a question about a particular bug, or some feedback on it, the best thing to do is to comment on that bug itself.
<br/>
TODO TODO TODO
We use the bugtracker included in our <a href="https://git.linux-kernel.at/oliver/ivatar/issues/" title="https://git.linux-kernel.at/oliver/ivatar/issues/">GitLab instance</a>.
<h2>Mailing list</h2>
<h4>Mailing list</h4>
If you've got a proposal to discuss or prefer to write to us, you can join our developers mailing list.
If you've got a proposal to discuss or prefer to write to us, you can join our <a href="https://launchpad.net/~libravatar-fans#mailing-lists" title="https://launchpad.net/~libravatar-fans#mailing-lists">developers mailing list</a>.
<h2>Identica / Twitter</h2>
<h4>Identica / Twitter</h4>
You can also put short notices to our attention on Identica or Twitter.
You can also put short notices to our attention on <a href="http://identi.ca/libravatar" title="http://identi.ca/libravatar">Identica<a/> or <a href="http://twitter.com/libravatar" title="http://twitter.com/libravatar">Twitter</a>.
<h2>Email</h2>
<h4>Email</h4>
Finally, if you need to email us: <a href="mailto:dev@libravatar.org">dev@libravatar.org</a>
Finally, if you need to email us: <a href="mailto:dev@libravatar.org" title="mailto:dev@libravatar.org">dev@libravatar.org</a>
<div style="height:40px"></div>

View File

@@ -8,7 +8,14 @@
{% block content %}
<h1 class="error">{% trans 'Error!' %}</h1>
<p>{% block errormessage %}{% trans 'Libravatar has encountered an error.' %}{% endblock errormessage %}</p>
<p>{% block errormessage %}
{% trans 'Libravatar has encountered an error.' %}
{% if errormessage %}
<br/>
<br/>
{% blocktrans %}{{ errormessage }}{% endblocktrans %}
{% endif %}
{% endblock errormessage %}</p>
<div style="height:40px"></div>

View File

@@ -4,7 +4,7 @@
<footer>
<div class="container">
<div class="pull-left">
<p><b>{% blocktrans %}{{ site_name }}</b> is an service running the <a href="https://launchpad.net/libravatar">ivatar</a> software, version {{ ivatar_version }} under the <a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPLv3.0</a> license.{% endblocktrans %}
<p><b>{% blocktrans %}{{ site_name }}</b> is a service running the <a href="https://git.linux-kernel.at/oliver/ivatar/">ivatar</a> software, version {{ ivatar_version }} under the <a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPLv3.0</a> license.{% endblocktrans %}
{% block footer %}{% endblock footer %}</p>
</div>
<div class="pull-right">

View File

@@ -25,7 +25,9 @@
<script src="{% static '/js/jquery-3.3.1.slim.min.js' %}"></script>
{% if user.is_authenticated %}
{% if user.userpreference and user.userpreference.theme != 'default' %}
<link rel="stylesheet" href="{% static 'css/' %}{{ user.userpreference.theme }}.css" type="text/css">
{% with 'css/'|add:user.userpreference.theme|add:'.css' as theme_css %}
<link rel="stylesheet" href="{% static theme_css %}" type="text/css">
{% endwith %}
{% endif %}
{% endif %}
<link rel="manifest" href="/manifest.json">

View File

@@ -11,10 +11,32 @@
<div class="hero">
<div class="container">
<header>
<h1>{% trans 'ivatar' %}</h1>
<h1>{{ site_name }}</h1>
<h2>{% trans 'freeing the web one face at a time' %}</h2>
{% if user.is_anonymous %}
<a href="/accounts/login/" class="btn btn-lg btn-primary">{% trans 'Login' %}</a>&nbsp;
<a href="/accounts/new/" class="btn btn-lg btn-primary">{% trans 'Sign up' %}</a>&nbsp;
{% else %}
<div class="btn-group">
<a class="btn btn-lg btn-primary dropdown-toggle" href="#" id="account_dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-user" aria-hidden="true"></i> {{ request.user }}
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<li><a href="{% url 'profile' %}"><i class="fa fa-fw fa-image" aria-hidden="true"></i> {% trans 'Profile' %}</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 '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 'password_change' %}"><i class="fa fa-fw fa-key" aria-hidden="true"></i> {% trans 'Change password' %}</a></li>
<li><a href="{% url 'password_reset' %}"><i class="fa fa-fw fa-unlock-alt" aria-hidden="true"></i> {% trans 'Reset password' %}</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> {% trans 'Logout' %}</a></li>
{% if user.is_staff %}
<li>
<a href="{% url 'admin:index' %}" target="_new"><i class="fa fa-fw fa-user-secret" aria-hidden="true"></i> {% trans 'Admin' %}</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
<a href="/tools/check/" class="btn btn-lg btn-primary">{% trans 'Check email' %}</a>&nbsp;
</header>
</div>
@@ -25,14 +47,14 @@
<div class="container">
<div class="text-center">
<h2>{% trans 'The open avatar service' %}</h2>
<p class="lead">{% blocktrans %}ivatar is a service which delivers your avatar (profile picture) to other websites. If you create an account with us, your photo could start popping up next to forum posts or blog comments on any site where you left your email address.{% endblocktrans %}<br>
<p class="lead">{% blocktrans %}{{ site_name }} is a service which delivers your avatar (profile picture) to other websites. If you create an account with us, your photo could start popping up next to forum posts or blog comments on any site where you left your email address.{% endblocktrans %}<br>
<a href="https://wiki.libravatar.org/description/">{% trans 'Read more...' %}</a></p>
</div>
<hr/>
<div class="row">
<section class="col-md-8">
<h1>{% trans 'Federated Open Source Service' %}</h1>
<p class="lead">{% trans 'This service is powered by <a href="https://www.gnu.org/licenses/agpl.html">Free and Open Source software</a> and allows owners of a domain name to <a href="https://wiki.libravatar.org/running_your_own/">run their own instance</a> of ivatar and serve avatars themselves.' %}</p>
<p class="lead">{% trans 'This service is powered by <a href="https://www.gnu.org/licenses/agpl.html">Free and Open Source software</a> called <a href="https://git.linux-kernel.at/oliver/ivatar">ivatar</a>. With this software you can also <a href="https://wiki.libravatar.org/running_your_own/">run your own instance</a> and serve avatars yourself.' %}</p>
<hr>
<h1>{% trans 'Simple API for Developers' %}</h1>
<p class="lead">{% trans 'Application developers can easily add support for this service using our <a href="https://wiki.libravatar.org/api/">simple API</a> or one of the <a href="https://wiki.libravatar.org/libraries/">libraries and plugins</a> available for a number of platforms and languages.' %}</p>
@@ -43,8 +65,8 @@
<h1>Useful links</h1>
<a class="btn btn-default" href="{% url 'contact' %}">{% trans 'Contact us' %}</a><br/>
<a class="btn btn-default" href="{% url 'security' %}">{% trans 'Security' %}</a><br/>
<a class="btn btn-default" href="https://code.launchpad.net/libravatar">{% trans 'Source code' %}</a><br/>
<a class="btn btn-default" href="https://bugs.launchpad.net/libravatar">{% trans 'Report bugs' %}</a><br/>
<a class="btn btn-default" href="https://git.linux-kernel.at/oliver/ivatar/">{% trans 'Source code' %}</a><br/>
<a class="btn btn-default" href="https://git.linux-kernel.at/oliver/ivatar/issues">{% trans 'Report bugs' %}</a><br/>
<a class="btn btn-default" href="https://answers.launchpad.net/libravatar">{% trans 'Questions' %}</a><br/>
<a class="btn btn-default" href="https://wiki.libravatar.org/">{% trans 'Wiki' %}</a><br/>
<a class="btn btn-default" href="http://blog.libravatar.org/">{% trans 'Blog' %}</a><br/>

58
templates/navbar.html Normal file
View File

@@ -0,0 +1,58 @@
{% load i18n %}
{% block topbar_base %}
<nav class="navbar navbar-tortin">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% block topbar %}
{% block site_brand %}
{% if user.is_anonymous %}
<a class="navbar-brand" href="/">
{% else %}
<a class="navbar-brand" href="{% url 'profile' %}">
{% endif %}
{{ site_name }}</a>
{% endblock %}
{% endblock %}
</div>
{% block nav %}
<div class="collapse navbar-collapse" id="navbar">
<ul class="nav navbar-nav">
<li>
<a href="/"><i class="fa fa-home" aria-hidden="true"></i>
{% trans 'Home' %}
</a>
</li>
<li>
<a class="nav-link" href="{% url 'contact' %}"><i class="fa fa-envelope" aria-hidden="true"></i>
{% trans 'Contact' %}
</a>
</li>
<li>
<a href="{% url 'security' %}"><i class="fa fa-user-secret" aria-hidden="true"></i>
{% trans 'Security' %}
</a>
</li>
<li class="dropdown" id="tab_tools">
<a class="dropdown-toggle" href="#" id="tools_dropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans 'Tools' %}">
<i class="fa fa-wrench" aria-hidden="true"></i>
{% trans 'Tools' %}
</a>
<ul class="dropdown-menu" aria-labelledby="tools_dropdown">
<li><a id="tools-check" href="{% url 'tools_check' %}">
<i class="fa fa-fw fa-check-square" aria-hidden="true"></i> {% trans 'Check' %}
</a></li>
</ul>
</li>
</ul>
{% block account_bar %}{% include "_account_bar.html" %}{% endblock %}
</div>
{% endblock %}
</div>
</nav>
{% endblock %}

View File

@@ -29,10 +29,7 @@
<p>
<button type="submit" class="btn btn-default">{% trans 'Login' %}</button>
<input type="hidden" name="next" id="next"/>
<script>
document.getElementById("next").value = window.location.protocol + '//' + window.location.hostname + "{% url 'profile' %}";
</script>
<input type="hidden" name="next" value="{{ request.build_absolute_uri }}{% url 'profile' %}" />
&nbsp;
<button type="reset" class="btn btn-default" onclick="window.history.back();">{% trans 'Cancel' %}</button>

58
templates/security.html Normal file
View File

@@ -0,0 +1,58 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load bootstrap4 %}
{% block title %}{% trans 'federated avatar hosting service' %}{% endblock %}
{% block content %}
<h4>Reporting security bugs</h4>
If you discover a security issue in ivatar, please report it to us privately so
that we can push a fix to the main service before disclosing the problem
publicly. We will credit you publicly (unless you don't want to) with this
discovery.
<p/><p/>
The best way to do that is to file a security bug on our
<a href="https://git.linux-kernel.at/oliver/ivatar/issues/new"
title="https://git.linux-kernel.at/oliver/ivatar/issues/new">
bug tracker
</a>. Make sure you change the bug visibility (see "This issue is confidential
and should only be visible to team members with at least Reporter access") and
set the 'Security' label.
<p/><p/>
Alternatively, you can talk to us at
<a href="mailto:security@libravatar.org"
title="mailto:security@libravatar.org">
security@libravatar.org
</a>.
<br/>
We will do our best to respond to you within 24-48 hours.
<br/>
Also, please let us know if you are under any kind of publication deadline.
<p/><p/>
<h4>Security Hall of fame</h4>
We would like to thank the following people who have helped make
ivatar/Libravatar more secure by reporting security issues to us.
<ul>
<li>Ahmed Adel Abdelfattah (
<a href="https://twitter.com/00SystemError00"
title="https://twitter.com/00SystemError00">@00SystemError00</a>):
improvement to mail configuration on <code>libravatar.org</code> and
<code>libravatar.com</code></li>
<li>
<a href="https://www.facebook.com/BugHunterID"
title="https://www.facebook.com/BugHunterID">
Putra Adhari</a>:
<a href="https://bugs.launchpad.net/libravatar/+bug/1808720"
title="https://bugs.launchpad.net/libravatar/+bug/1808720">
server-side request forgery</a> in OpenID support</li>
</ul>
<div style="height:40px"></div>
{% endblock %}