46 Commits

Author SHA1 Message Date
Oliver Falk
d9f73bc012 Try fixing sast building 2023-05-16 21:30:34 +02:00
Oliver Falk
df0400375d Merge branch 'set-sast-config-1' into 'devel'
Set sast config 1

See merge request oliver/ivatar!228
2023-05-15 18:58:23 +00:00
Oliver Falk
50569afc25 Set sast config 1 2023-05-15 18:58:22 +00:00
Oliver Falk
927083eb58 Due to 'image is defined in top-level and default entry', move image into each section 2023-05-09 13:12:02 +02:00
Oliver Falk
a2eea54235 Reverse mr mkdir modules, since we have a b0kren ci/cd setup it seems 2023-04-19 13:27:08 +02:00
Oliver Falk
01bcc1ee11 Merge branch 'set-sast-config-1' into 'master'
Configure SAST in `.gitlab-ci.yml`, creating this file if it does not already exist

See merge request oliver/ivatar!224
2023-04-19 11:14:16 +00:00
Oliver Falk
16f809d8a6 Update .gitlab-ci.yml file 2023-04-19 10:46:31 +00:00
Oliver Falk
f01e49d495 Configure SAST in .gitlab-ci.yml, creating this file if it does not already exist 2023-04-19 10:40:05 +00:00
Oliver Falk
fd696ed74c Merge branch 'devel' into 'master'
Merge latest devel branch

See merge request oliver/ivatar!223
2023-04-17 14:17:06 +00:00
Oliver Falk
021a8de4d8 Fix typo and break up lines a bit more 2023-04-17 15:07:20 +02:00
Oliver Falk
cbdaed28da Fix docker build + update fedora base image 2023-04-17 13:44:51 +02:00
Oliver Falk
95410f6e43 Only create virtualenv on toplevel 2023-02-14 21:43:16 +01:00
Oliver Falk
2be7309625 Merge branch 'devel' into 'master'
Update produciton with latest fixes and project setup

See merge request oliver/ivatar!222
2023-02-01 16:17:37 +00:00
Oliver Falk
6deea2758f Add new dicebear endpoint (Fixes #92) 2023-02-01 16:02:10 +00:00
Oliver Falk
2bb1f5f26d Merge branch 'master' into devel 2023-01-24 21:59:46 +01:00
Oliver Falk
3878554dd9 Merge branch 'issue91' into 'master'
Closes issue #91

Closes #91

See merge request oliver/ivatar!221
2023-01-24 20:15:21 +00:00
Oliver Falk
9478177c83 Closes issue #91 2023-01-24 20:15:20 +00:00
Oliver Falk
47837f4516 Add the usual project files in order to build a tarball more easily 2023-01-03 15:37:51 +01:00
Oliver Falk
2276ea962f Merge branch 'devel' into 'master'
Update testing

See merge request oliver/ivatar!220
2023-01-03 07:41:52 +00:00
Oliver Falk
ae3c6beed4 Update testing 2023-01-03 07:41:51 +00:00
Oliver Falk
ff9af3de9b Add attic
These files are not relevant at all, but helped during research,
development, debugging. Hence, don't throw them away, but move them a
bit more out of sight.
2023-01-02 22:42:26 +01:00
Oliver Falk
7e46df0c15 Ignore local env 2023-01-02 22:40:04 +01:00
Oliver Falk
e1547d14c5 Before checking prefs, we need to login of course 2023-01-02 22:32:14 +01:00
Oliver Falk
8f5bc9653b Add __init__.py to tools/
Else the automatic test discovery ignores the directory and silently
skips the test cases.
2023-01-02 22:27:22 +01:00
Oliver Falk
5dbbff49d0 Enhance testing of checking mail + openid a bit further 2023-01-02 22:15:23 +01:00
Oliver Falk
5c8da703cb Login + really use the domain check tool 2023-01-02 21:12:30 +01:00
Oliver Falk
3aeb1ba454 Add test to delete user 2023-01-02 20:52:25 +01:00
Oliver Falk
9e189b3fd2 Update black 2022-12-29 15:15:56 +01:00
Oliver Falk
b8292b5404 Merge branch 'devel' into 'master'
Release 1.7.0

See merge request oliver/ivatar!219
2022-12-06 18:06:33 +00:00
Oliver Falk
5730c2dabf Merge branch 'webp-support' into 'devel'
Webp support

See merge request oliver/ivatar!218
2022-12-06 17:50:48 +00:00
Oliver Falk
dddd24e57f Webp support 2022-12-06 17:50:48 +00:00
Oliver Falk
a6c5899f44 Merge branch 'webp-support' into 'devel'
Webp support

See merge request oliver/ivatar!217
2022-12-05 15:56:13 +00:00
Oliver Falk
ba6f46c6eb Webp support 2022-12-05 15:56:12 +00:00
Oliver Falk
ddfc1e7824 Experimental support for Animated GIFs 2022-12-05 16:16:40 +01:00
Oliver Falk
2761e801df Add util function to resize an animated GIF 2022-12-05 16:15:30 +01:00
Oliver Falk
555a8b0523 Update pre-commit 2022-12-05 16:15:18 +01:00
Oliver Falk
6d984a486a Missing webp test file 2022-11-30 23:15:41 +01:00
Oliver Falk
9dceb7a696 Some jpgs are recognized as MPO (basically jpg with additional data 2022-11-30 11:50:29 +01:00
Oliver Falk
64575a9b99 No absolute URI required any more, actually leads to broken redir 2022-11-30 11:49:37 +01:00
Oliver Falk
a94954d58c Merge branch 'django-4.1' into 'devel'
Changes required for Django > 4

See merge request oliver/ivatar!216
2022-11-22 20:20:51 +00:00
Oliver Falk
d2e4162b6b Yes, this deserves a version increase 2022-11-22 21:03:46 +01:00
Oliver Falk
4afee63137 CACHES may not be empty 2022-11-22 20:35:13 +01:00
Oliver Falk
d486fdef2c Disable caching during tests 2022-11-22 20:26:46 +01:00
Oliver Falk
e945ae2b4d Add missing pymemcache dep and remove old one 2022-11-22 19:48:42 +01:00
Oliver Falk
9565ccc54e Changes required for Django > 4 2022-11-22 19:38:08 +01:00
Oliver Falk
e68c75d74d Update pre-commit config 2022-11-18 13:32:27 +01:00
31 changed files with 630 additions and 158 deletions

1
.buildpacks Normal file
View File

@@ -0,0 +1 @@
https://github.com/heroku/heroku-buildpack-python

4
.env
View File

@@ -1,6 +1,8 @@
if [ ! -d .virtualenv ]; then
if [ ! "$(which virtualenv)" == "" ]; then
virtualenv -p python3 .virtualenv
if [ -f .env ]; then
virtualenv -p python3 .virtualenv
fi
fi
fi
if [ -f .virtualenv/bin/activate ]; then

1
.gitignore vendored
View File

@@ -20,3 +20,4 @@ falko_gravatar.jpg
*.egg-info
dump_all*.sql
dist/
.env.local

View File

@@ -1,11 +1,16 @@
default:
image:
name: quay.io/rhn_support_ofalk/fedora35-python3
entrypoint: [ '/bin/sh', '-c' ]
image:
name: quay.io/rhn_support_ofalk/fedora35-python3
entrypoint:
- "/bin/sh"
- "-c"
before_script:
test_and_coverage:
stage: build
coverage: "/^TOTAL.*\\s+(\\d+\\%)$/"
before_script:
- virtualenv -p python3 /tmp/.virtualenv
- source /tmp/.virtualenv/bin/activate
- pip install -U pip
- pip install Pillow
- pip install -r requirements.txt
- pip install python-coveralls
@@ -13,66 +18,93 @@ before_script:
- pip install pycco
- pip install django_coverage_plugin
test_and_coverage:
stage: test
coverage: '/^TOTAL.*\s+(\d+\%)$/'
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
- coverage html
- echo 'from ivatar.settings import TEMPLATES' > config_local.py
- echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py
- echo "DEBUG = True" >> config_local.py
- echo "from config import CACHES" >> config_local.py
- echo "CACHES['default'] = CACHES['filesystem']" >> config_local.py
- python manage.py collectstatic --noinput
- coverage run --source . manage.py test -v3
- coverage report --fail-under=70
- coverage html
artifacts:
paths:
- htmlcov/
- htmlcov/
pycco:
stage: test
before_script:
- virtualenv -p python3 /tmp/.virtualenv
- source /tmp/.virtualenv/bin/activate
- pip install -U pip
- pip install Pillow
- pip install -r requirements.txt
- pip install python-coveralls
- pip install coverage
- pip install pycco
- pip install django_coverage_plugin
script:
- /bin/true
- find ivatar/ -type f -name "*.py"|grep -v __pycache__|grep -v __init__.py|grep -v /migrations/ | xargs pycco -p -d pycco -i -s
- "/bin/true"
- find ivatar/ -type f -name "*.py"|grep -v __pycache__|grep -v __init__.py|grep
-v /migrations/ | xargs pycco -p -d pycco -i -s
artifacts:
paths:
- pycco/
- pycco/
expire_in: 14 days
pages:
before_script:
- /bin/true
- /bin/true
stage: deploy
dependencies:
- test_and_coverage
- pycco
- test_and_coverage
- pycco
script:
- mv htmlcov/ public/
- mv pycco/ public/
- mv htmlcov/ public/
- mv pycco/ public/
artifacts:
paths:
- public
- public
expire_in: 14 days
only:
- master
- master
build-image:
image: docker
only:
- master
- devel
services:
- docker:dind
- docker:dind
before_script:
- docker info
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
- docker info
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- ls -lah
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else
tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
- docker push "$CI_REGISTRY_IMAGE${tag}"
- ls -lah
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else
tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
- docker push "$CI_REGISTRY_IMAGE${tag}"
semgrep:
extends: semgrep-sast
stage: test
allow_failure: true
image: registry.gitlab.com/gitlab-org/security-products/analyzers/semgrep:latest
variables:
CI_PROJECT_DIR: "/tmp/app"
SECURE_LOG_LEVEL: "debug"
script:
- rm -rf .virtualenv
- /analyzer run
artifacts:
paths:
- gl-sast-report.json
- semgrep.sarif
include:
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
- template: Jobs/Secret-Detection.gitlab-ci.yml

View File

@@ -4,16 +4,16 @@ repos:
hooks:
- id: check-useless-excludes
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.6.2
rev: v3.0.0-alpha.4
hooks:
- id: prettier
files: \.(css|js|md|markdown|json)
- repo: https://github.com/python/black
rev: 22.3.0
rev: 22.12.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
rev: v4.4.0
hooks:
- id: check-added-large-files
- id: check-ast
@@ -37,8 +37,8 @@ repos:
- id: requirements-txt-fixer
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
- repo: local

View File

@@ -1,17 +1,22 @@
FROM quay.io/rhn_support_ofalk/fedora35-python3
FROM quay.io/rhn_support_ofalk/fedora37-python3
LABEL maintainer Oliver Falk <oliver@linux-kernel.at>
EXPOSE 8081
RUN pip3 install pip --upgrade
ADD . /opt/ivatar-devel
WORKDIR /opt/ivatar-devel
RUN pip3 install Pillow && pip3 install -r requirements.txt && pip3 install python-coveralls coverage pycco django_coverage_plugin
RUN pip3 install pip --upgrade \
&& virtualenv .virtualenv \
&& source .virtualenv/bin/activate \
&& pip3 install Pillow \
&& pip3 install -r requirements.txt \
&& pip3 install python-coveralls coverage pycco django_coverage_plugin
RUN echo "DEBUG = True" >> /opt/ivatar-devel/config_local.py
RUN echo "EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'" >> /opt/ivatar-devel/config_local.py
RUN python3 manage.py migrate && python3 manage.py collectstatic --noinput
RUN echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@local.tld', 'admin')" | python manage.py shell
ENTRYPOINT python3 ./manage.py runserver 0:8081
RUN source .virtualenv/bin/activate \
&& python3 manage.py migrate \
&& python3 manage.py collectstatic --noinput \
&& echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@local.tld', 'admin')" | python manage.py shell
ENTRYPOINT source .virtualenv/bin/activate && python3 ./manage.py runserver 0:8081

10
MANIFEST.in Normal file
View File

@@ -0,0 +1,10 @@
include *.py
include *.md
include COPYING
include LICENSE
recursive-include templates *
recursive-include ivatar *
exclude .virtualenv
exclude libravatar.egg-info
global-exclude *.py[co]
global-exclude __pycache__

View File

@@ -0,0 +1,2 @@
https://django-debug-toolbar.readthedocs.io/en/latest/installation.html
https://stackoverflow.com/questions/6548947/how-can-django-debug-toolbar-be-set-to-work-for-just-some-users/6549317#6549317

49
attic/encryption_test.py Executable file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import django
import timeit
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "ivatar.settings"
) # pylint: disable=wrong-import-position
django.setup() # pylint: disable=wrong-import-position
from ivatar.ivataraccount.models import ConfirmedEmail, APIKey
from simplecrypt import decrypt
from binascii import unhexlify
digest = None
digest_sha256 = None
def get_digest_sha256():
digest_sha256 = ConfirmedEmail.objects.first().encrypted_digest_sha256(
secret_key=APIKey.objects.first()
)
return digest_sha256
def get_digest():
digest = ConfirmedEmail.objects.first().encrypted_digest(
secret_key=APIKey.objects.first()
)
return digest
def decrypt_digest():
return decrypt(APIKey.objects.first().secret_key, unhexlify(digest))
def decrypt_digest_256():
return decrypt(APIKey.objects.first().secret_key, unhexlify(digest_sha256))
digest = get_digest()
digest_sha256 = get_digest_sha256()
print("Encrypt digest: %s" % timeit.timeit(get_digest, number=1))
print("Encrypt digest_sha256: %s" % timeit.timeit(get_digest_sha256, number=1))
print("Decrypt digest: %s" % timeit.timeit(decrypt_digest, number=1))
print("Decrypt digest_sha256: %s" % timeit.timeit(decrypt_digest_256, number=1))

View File

@@ -0,0 +1,7 @@
DATABASES['default'] = {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'libravatar',
'USER': 'libravatar',
'PASSWORD': 'libravatar',
'HOST': 'localhost',
}

View File

@@ -60,7 +60,7 @@ OPENID_CREATE_USERS = True
OPENID_UPDATE_DETAILS_FROM_SREG = True
SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
IVATAR_VERSION = "1.6.2"
IVATAR_VERSION = "1.7.0"
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
@@ -191,7 +191,7 @@ MESSAGE_TAGS = {
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": [
"127.0.0.1:11211",
],
@@ -232,15 +232,18 @@ TRUSTED_DEFAULT_URLS = [
"host_equals": "avatars.dicebear.com",
"path_prefix": "/api/",
},
{
"schemes": ["https"],
"host_equals": "api.dicebear.com",
"path_prefix": "/",
},
{
"schemes": ["https"],
"host_equals": "badges.fedoraproject.org",
"path_prefix": "/static/img/",
},
{
"schemes": [
"http",
],
"schemes": ["http"],
"host_equals": "www.planet-libre.org",
"path_prefix": "/themes/planetlibre/images/",
},

View File

@@ -41,12 +41,14 @@ def file_format(image_type):
"""
Helper method returning a short image type
"""
if image_type == "JPEG":
if image_type in ("JPEG", "MPO"):
return "jpg"
elif image_type == "PNG":
return "png"
elif image_type == "GIF":
return "gif"
elif image_type == "WEBP":
return "webp"
return None
@@ -54,12 +56,14 @@ def pil_format(image_type):
"""
Helper method returning the 'encoder name' for PIL
"""
if image_type == "jpg" or image_type == "jpeg":
if image_type in ("jpg", "jpeg", "mpo"):
return "JPEG"
elif image_type == "png":
return "PNG"
elif image_type == "gif":
return "GIF"
elif image_type == "webp":
return "WEBP"
logger.info("Unsupported file format: %s", image_type)
return None

View File

@@ -34,7 +34,7 @@ outline: inherit;
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
<div class="panel panel-tortin" style="width:132px;margin:0">
<div class="panel-heading">
<h3 class="panel-title">{% ifequal email.photo.id photo.id %}<i class="fa fa-check"></i>{% endifequal %} {% trans 'Image' %} {{ forloop.counter }}</h3>
<h3 class="panel-title">{% if email.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'Image' %} {{ forloop.counter }}</h3>
</div>
<div class="panel-body" style="height:130px">
<center>
@@ -49,7 +49,7 @@ outline: inherit;
<button type="submit" name="photoNone" class="nobutton">
<div class="panel panel-tortin" style="width:132px;margin:0">
<div class="panel-heading">
<h3 class="panel-title">{% ifequal email.photo.id photo.id %}<i class="fa fa-check"></i>{% endifequal %} {% trans 'No image' %}</h3>
<h3 class="panel-title">{% if email.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'No image' %}</h3>
</div>
<div class="panel-body" style="height:130px">
<center>

View File

@@ -34,7 +34,7 @@ outline: inherit;
<button type="submit" name="photo{{ photo.id }}" class="nobutton">
<div class="panel panel-tortin" style="width:132px;margin:0">
<div class="panel-heading">
<h3 class="panel-title">{% ifequal openid.photo.id photo.id %}<i class="fa fa-check"></i>{% endifequal %} {% trans 'Image' %} {{ forloop.counter }}</h3>
<h3 class="panel-title">{% if openid.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'Image' %} {{ forloop.counter }}</h3>
</div>
<div class="panel-body" style="height:130px">
<center>
@@ -49,7 +49,7 @@ outline: inherit;
<button type="submit" name="photoNone" class="nobutton">
<div class="panel panel-tortin" style="width:132px;margin:0">
<div class="panel-heading">
<h3 class="panel-title">{% ifequal openid.photo.id photo.id %}<i class="fa fa-check"></i>{% endifequal %} {% trans 'No image' %}</h3>
<h3 class="panel-title">{% if openid.photo.id == photo.id %}<i class="fa fa-check"></i>{% endif %} {% trans 'No image' %}</h3>
</div>
<div class="panel-body" style="height:130px">
<center>

View File

@@ -748,6 +748,47 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
def test_upload_webp_image(self):
"""
Test if webp 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.webp"), "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",
"WEBP upload failed?!",
)
self.assertEqual(
self.user.photo_set.first().format,
"webp",
"Format must be webp, since we uploaded a webp!",
)
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
"""
Test if unsupported format is correctly detected
@@ -1292,16 +1333,37 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
# Simply delete it, then it's digest is 'correct', but
# the hash is no longer there
addr = self.user.confirmedemail_set.first().email
hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest()
digest = 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?",
self.assertEqual(
response.redirect_chain[0][0],
"/gravatarproxy/%s?s=80" % digest,
"Doesn't redirect to Gravatar?",
)
self.assertEqual(
response.redirect_chain[0][1], 302, "Doesn't redirect with 302?"
)
self.assertEqual(
response.redirect_chain[1][0],
"/avatar/%s?s=80&forcedefault=y" % digest,
"Doesn't redirect with default forced on?",
)
self.assertEqual(
response.redirect_chain[1][1], 302, "Doesn't redirect with 302?"
)
self.assertEqual(
response.redirect_chain[2][0],
"/static/img/nobody/80.png",
"Doesn't redirect to static?",
)
# 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(
@@ -1323,11 +1385,17 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
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?",
self.assertEqual(
response.redirect_chain[0][0],
"/static/img/nobody/80.png",
"Doesn't redirect to static?",
)
# 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(
@@ -1361,11 +1429,17 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
)
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?",
self.assertEqual(
response.redirect_chain[0][0],
"/static/img/mm/80.png",
"Doesn't redirect to static?",
)
# 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(
@@ -1380,13 +1454,36 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
size=80,
)
)
digest = hashlib.md5("asdf@company.local".lower().encode("utf-8")).hexdigest()
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?",
self.assertEqual(
response.redirect_chain[0][0],
"/gravatarproxy/%s?s=80" % digest,
"Doesn't redirect to Gravatar?",
)
self.assertEqual(
response.redirect_chain[0][1], 302, "Doesn't redirect with 302?"
)
self.assertEqual(
response.redirect_chain[1][0],
"/avatar/%s?s=80&forcedefault=y" % digest,
"Doesn't redirect with default forced on?",
)
self.assertEqual(
response.redirect_chain[1][1], 302, "Doesn't redirect with 302?"
)
self.assertEqual(
response.redirect_chain[2][0],
"/static/img/nobody/80.png",
"Doesn't redirect to static?",
)
# 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(
@@ -1403,17 +1500,26 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
)
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?",
self.assertEqual(
response.redirect_chain[0][0],
"/static/img/nobody/80.png",
"Doesn't redirect to static?",
)
# 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
"""
# TODO - Find a new way
# Do not run this test, since static serving isn't allowed in testing mode
return
urlobj = urlsplit(
libravatar_url(
"xxx@xxx.xxx",
@@ -1422,7 +1528,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
)
)
url = "%s?%s" % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True)
response = self.client.get(url, follow=False)
self.assertRedirects(
response=response,
expected_url="/static/img/nobody.png",
@@ -1435,6 +1541,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
"""
Test fetching avatar for not existing mail with default specified
"""
# TODO - Find a new way
# Do not run this test, since static serving isn't allowed in testing mode
return
urlobj = urlsplit(
libravatar_url(
"xxx@xxx.xxx",
@@ -1889,4 +1998,61 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
Test if preferences page works
"""
self.login()
self.client.get(reverse("user_preference"))
def test_delete_user(self):
"""
Test if deleting user profile works
"""
self.login()
self.client.get(reverse("delete"))
response = self.client.post(
reverse("delete"),
data={"password": self.password},
follow=True,
)
self.assertEqual(response.status_code, 200, "Deletion worked")
self.assertEqual(User.objects.count(), 0, "No user there any more")
def test_confirm_already_confirmed(self):
"""
Try to confirm a mail address that has been confirmed (by another user)
"""
# Add mail address (stays unconfirmed)
self.test_add_email()
# Create a second user that will conflict
user2 = User.objects.create_user(
username=self.username + "1",
password=self.password,
first_name=self.first_name,
last_name=self.last_name,
)
ConfirmedEmail.objects.create(
email=self.email,
user=user2,
)
# Just to be sure
self.assertEqual(
self.user.unconfirmedemail_set.first().email,
user2.confirmedemail_set.first().email,
"Mail not the same?",
)
# This needs to be cought
try:
self.test_confirm_email()
except AssertionError:
pass
# Request a random page, so we can access the messages
response = self.client.get(reverse("profile"))
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"This mail address has been taken already and cannot be confirmed",
"This should return an error message!",
)

View File

@@ -2,8 +2,7 @@
"""
URLs for ivatar.ivataraccount
"""
from django.urls import path
from django.conf.urls import url
from django.urls import path, re_path
from django.contrib.auth.views import LogoutView
from django.contrib.auth.views import (
@@ -72,7 +71,7 @@ urlpatterns = [ # pylint: disable=invalid-name
),
path("delete/", DeleteAccountView.as_view(), name="delete"),
path("profile/", ProfileView.as_view(), name="profile"),
url(
re_path(
"profile/(?P<profile_username>.+)",
ProfileView.as_view(),
name="profile_with_profile_username",
@@ -81,73 +80,77 @@ urlpatterns = [ # pylint: disable=invalid-name
path("add_openid/", AddOpenIDView.as_view(), name="add_openid"),
path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"),
path("password_set/", PasswordSetView.as_view(), name="password_set"),
url(
re_path(
r"remove_unconfirmed_openid/(?P<openid_id>\d+)",
RemoveUnconfirmedOpenIDView.as_view(),
name="remove_unconfirmed_openid",
),
url(
re_path(
r"remove_confirmed_openid/(?P<openid_id>\d+)",
RemoveConfirmedOpenIDView.as_view(),
name="remove_confirmed_openid",
),
url(
re_path(
r"openid_redirection/(?P<openid_id>\d+)",
RedirectOpenIDView.as_view(),
name="openid_redirection",
),
url(
re_path(
r"confirm_openid/(?P<openid_id>\w+)",
ConfirmOpenIDView.as_view(),
name="confirm_openid",
),
url(
re_path(
r"confirm_email/(?P<verification_key>\w+)",
ConfirmEmailView.as_view(),
name="confirm_email",
),
url(
re_path(
r"remove_unconfirmed_email/(?P<email_id>\d+)",
RemoveUnconfirmedEmailView.as_view(),
name="remove_unconfirmed_email",
),
url(
re_path(
r"remove_confirmed_email/(?P<email_id>\d+)",
RemoveConfirmedEmailView.as_view(),
name="remove_confirmed_email",
),
url(
re_path(
r"assign_photo_email/(?P<email_id>\d+)",
AssignPhotoEmailView.as_view(),
name="assign_photo_email",
),
url(
re_path(
r"assign_photo_openid/(?P<openid_id>\d+)",
AssignPhotoOpenIDView.as_view(),
name="assign_photo_openid",
),
url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
url(
re_path(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
re_path(
r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
ImportPhotoView.as_view(),
name="import_photo",
),
url(
re_path(
r"import_photo/(?P<email_id>\d+)",
ImportPhotoView.as_view(),
name="import_photo",
),
url(r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"),
url(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"),
url(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"),
url(r"pref/$", UserPreferenceView.as_view(), name="user_preference"),
url(r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"),
url(
re_path(
r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"
),
re_path(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"),
re_path(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"),
re_path(r"pref/$", UserPreferenceView.as_view(), name="user_preference"),
re_path(
r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"
),
re_path(
r"upload_export/(?P<save>save)$",
UploadLibravatarExportView.as_view(),
name="upload_export",
),
url(
re_path(
r"resend_confirmation_mail/(?P<email_id>\d+)",
ResendConfirmationMailView.as_view(),
name="resend_confirmation_mail",

View File

@@ -207,6 +207,13 @@ class ConfirmEmailView(SuccessMessageMixin, TemplateView):
messages.error(request, _("Verification key does not exist"))
return HttpResponseRedirect(reverse_lazy("profile"))
if ConfirmedEmail.objects.filter(email=unconfirmed.email).count() > 0:
messages.error(
request,
_("This mail address has been taken already and cannot be confirmed"),
)
return HttpResponseRedirect(reverse_lazy("profile"))
# TODO: Check for a reasonable expiration time in unconfirmed email
(confirmed_id, external_photos) = ConfirmedEmail.objects.create_confirmed_email(

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -37,6 +37,7 @@ class Tester(TestCase):
self.assertEqual(pil_format("jpeg"), "JPEG")
self.assertEqual(pil_format("png"), "PNG")
self.assertEqual(pil_format("gif"), "GIF")
self.assertEqual(pil_format("webp"), "WEBP")
self.assertEqual(pil_format("abc"), None)
def test_userprefs_str(self):

View File

@@ -50,11 +50,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
Test incorrect digest
"""
response = self.client.get("/avatar/%s" % "x" * 65, follow=True)
self.assertRedirects(
response=response,
expected_url="/static/img/deadbeef.png",
msg_prefix="Why does an invalid hash not redirect to deadbeef?",
self.assertEqual(
response.redirect_chain[0][0],
"/static/img/deadbeef.png",
"Doesn't redirect to static?",
)
# self.assertRedirects(
# response=response,
# expected_url="/static/img/deadbeef.png",
# msg_prefix="Why does an invalid hash not redirect to deadbeef?",
# )
def test_stats(self):
"""

0
ivatar/tools/__init__.py Normal file
View File

View File

@@ -48,16 +48,109 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
password=self.password,
)
def test_check(self):
def test_check_mail(self):
"""
Test check page
"""
self.login()
response = self.client.get(reverse("tools_check"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
response = self.client.post(
reverse("tools_check"),
data={"mail": "test@test.com", "size": "85"},
follow=True,
)
self.assertContains(
response,
'value="test@test.com"',
1,
200,
"Value not set again!?",
)
self.assertContains(
response,
"b642b4217b34b1e8d3bd915fc65c4452",
3,
200,
"Wrong md5 hash!?",
)
self.assertContains(
response,
"f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a",
3,
200,
"Wrong sha256 hash!?",
)
self.assertContains(
response,
'value="85"',
1,
200,
"Size should be set based on post params!?",
)
def test_check_openid(self):
"""
Test check page
"""
self.login()
response = self.client.get(reverse("tools_check"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
response = self.client.post(
reverse("tools_check"),
data={"openid": "https://test.com", "size": "85"},
follow=True,
)
self.assertContains(
response,
'value="https://test.com"',
1,
200,
"Value not set again!?",
)
self.assertContains(
response,
"396936bd0bf0603d6784b65d03e96dae90566c36b62661f28d4116c516524bcc",
3,
200,
"Wrong sha256 hash!?",
)
self.assertContains(
response,
'value="85"',
1,
200,
"Size should be set based on post params!?",
)
def test_check_domain(self):
"""
Test check domain page
"""
self.login()
response = self.client.get(reverse("tools_check_domain"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
response = self.client.post(
reverse("tools_check_domain"),
data={"domain": "linux-kernel.at"},
follow=True,
)
self.assertEqual(response.status_code, 200, "no 200 ok?")
self.assertContains(
response,
"http://avatars.linux-kernel.at",
2,
200,
"Not responing with right URL!?",
)
self.assertContains(
response,
"https://avatars.linux-kernel.at",
2,
200,
"Not responing with right URL!?",
)

View File

@@ -3,11 +3,11 @@
ivatar/tools URL configuration
"""
from django.conf.urls import url
from django.urls import path, re_path
from .views import CheckView, CheckDomainView
urlpatterns = [ # pylint: disable=invalid-name
url("check/", CheckView.as_view(), name="tools_check"),
url("check_domain/", CheckDomainView.as_view(), name="tools_check_domain"),
url("check_domain$", CheckDomainView.as_view(), name="tools_check_domain"),
path("check/", CheckView.as_view(), name="tools_check"),
path("check_domain/", CheckDomainView.as_view(), name="tools_check_domain"),
re_path("check_domain$", CheckDomainView.as_view(), name="tools_check_domain"),
]

View File

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

View File

@@ -4,7 +4,8 @@ Simple module providing reusable random_string function
"""
import random
import string
from PIL import Image, ImageDraw
from io import BytesIO
from PIL import Image, ImageDraw, ImageSequence
from urllib.parse import urlparse
@@ -158,3 +159,25 @@ def is_trusted_url(url, url_filters):
return True
return False
def resize_animated_gif(input_pil: Image, size: list) -> BytesIO:
def _thumbnail_frames(image):
for frame in ImageSequence.Iterator(image):
new_frame = frame.copy()
new_frame.thumbnail(size)
yield new_frame
frames = list(_thumbnail_frames(input_pil))
output = BytesIO()
output_image = frames[0]
output_image.save(
output,
format="gif",
save_all=True,
optimize=False,
append_images=frames[1:],
disposal=input_pil.disposal_method,
**input_pil.info,
)
return output

View File

@@ -34,7 +34,7 @@ from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
from .ivataraccount.models import UnconfirmedEmail, UnconfirmedOpenId
from .ivataraccount.models import Photo
from .ivataraccount.models import pil_format, file_format
from .utils import is_trusted_url, mm_ng
from .utils import is_trusted_url, mm_ng, resize_animated_gif
URL_TIMEOUT = 5 # in seconds
@@ -151,7 +151,8 @@ class AvatarImageView(TemplateView):
if not trusted_url:
print(
"Default URL is not in trusted URLs: '%s' ; Kicking it!" % default
"Default URL is not in trusted URLs: '%s' ; Kicking it!"
% default
)
default = None
@@ -318,14 +319,23 @@ class AvatarImageView(TemplateView):
imgformat = obj.photo.format
photodata = Image.open(BytesIO(obj.photo.data))
# If the image is smaller than what was requested, we need
# to use the function resize
if photodata.size[0] < size or photodata.size[1] < size:
photodata = photodata.resize((size, size), Image.ANTIALIAS)
else:
photodata.thumbnail((size, size), Image.ANTIALIAS)
data = BytesIO()
photodata.save(data, pil_format(imgformat), quality=JPEG_QUALITY)
# Animated GIFs need additional handling
if imgformat == "gif" and photodata.is_animated:
# Debug only
# print("Object is animated and has %i frames" % photodata.n_frames)
data = resize_animated_gif(photodata, (size, size))
else:
# If the image is smaller than what was requested, we need
# to use the function resize
if photodata.size[0] < size or photodata.size[1] < size:
photodata = photodata.resize((size, size), Image.ANTIALIAS)
else:
photodata.thumbnail((size, size), Image.ANTIALIAS)
photodata.save(data, pil_format(imgformat), quality=JPEG_QUALITY)
data.seek(0)
obj.photo.access_count += 1
obj.photo.save()

3
pyproject.toml Normal file
View File

@@ -0,0 +1,3 @@
[build-system]
requires = ['setuptools>=40.8.0', 'wheel']
build-backend = 'setuptools.build_meta:__legacy__'

View File

@@ -1,7 +1,7 @@
autopep8
bcrypt
defusedxml
Django < 4.0
Django
django-anymail[mailgun]
django-auth-ldap
django-bootstrap4
@@ -27,10 +27,10 @@ py3dns
pydocstyle
pyLibravatar
pylint
pymemcache
PyMySQL
python-coveralls
python-language-server
python-memcached
python3-openid
pytz
rope

33
setup.cfg Normal file
View File

@@ -0,0 +1,33 @@
[metadata]
name = libravatar
version = 1.7.0
description = A Django application implementing libravatar.org
long_description = file: README.md
url = https://libravatar.org
author = Oliver Falk
author_email = oliver@linux-kernel.at
license = GPLv3
classifiers =
Environment :: Web Environment
Framework :: Django
Framework :: Django :: 3.2
Framework :: Django :: 4.0
Framework :: Django :: 4.1
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content
[options]
include_package_data = true
packages = find:
python_requires = >=3.8
install_requires =
Django >= 3.2

6
setup.py Normal file
View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages
setup(
packages=find_packages(),
)

View File

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