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 [ ! -d .virtualenv ]; then
if [ ! "$(which virtualenv)" == "" ]; then if [ ! "$(which virtualenv)" == "" ]; then
virtualenv -p python3 .virtualenv if [ -f .env ]; then
virtualenv -p python3 .virtualenv
fi
fi fi
fi fi
if [ -f .virtualenv/bin/activate ]; then if [ -f .virtualenv/bin/activate ]; then

1
.gitignore vendored
View File

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

View File

@@ -1,11 +1,16 @@
default: image:
image: name: quay.io/rhn_support_ofalk/fedora35-python3
name: quay.io/rhn_support_ofalk/fedora35-python3 entrypoint:
entrypoint: [ '/bin/sh', '-c' ] - "/bin/sh"
- "-c"
before_script: test_and_coverage:
stage: build
coverage: "/^TOTAL.*\\s+(\\d+\\%)$/"
before_script:
- virtualenv -p python3 /tmp/.virtualenv - virtualenv -p python3 /tmp/.virtualenv
- source /tmp/.virtualenv/bin/activate - source /tmp/.virtualenv/bin/activate
- pip install -U pip
- pip install Pillow - pip install Pillow
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install python-coveralls - pip install python-coveralls
@@ -13,66 +18,93 @@ before_script:
- pip install pycco - pip install pycco
- pip install django_coverage_plugin - pip install django_coverage_plugin
test_and_coverage:
stage: test
coverage: '/^TOTAL.*\s+(\d+\%)$/'
script: script:
- echo 'from ivatar.settings import TEMPLATES' > config_local.py - echo 'from ivatar.settings import TEMPLATES' > config_local.py
- echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py - echo 'TEMPLATES[0]["OPTIONS"]["debug"] = True' >> config_local.py
- echo "DEBUG = True" >> config_local.py - echo "DEBUG = True" >> config_local.py
- python manage.py collectstatic --noinput - echo "from config import CACHES" >> config_local.py
- coverage run --source . manage.py test -v3 - echo "CACHES['default'] = CACHES['filesystem']" >> config_local.py
- coverage report --fail-under=70 - python manage.py collectstatic --noinput
- coverage html - coverage run --source . manage.py test -v3
- coverage report --fail-under=70
- coverage html
artifacts: artifacts:
paths: paths:
- htmlcov/ - htmlcov/
pycco: pycco:
stage: test 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: script:
- /bin/true - "/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 - find ivatar/ -type f -name "*.py"|grep -v __pycache__|grep -v __init__.py|grep
-v /migrations/ | xargs pycco -p -d pycco -i -s
artifacts: artifacts:
paths: paths:
- pycco/ - pycco/
expire_in: 14 days expire_in: 14 days
pages: pages:
before_script:
- /bin/true
- /bin/true
stage: deploy stage: deploy
dependencies: dependencies:
- test_and_coverage - test_and_coverage
- pycco - pycco
script: script:
- mv htmlcov/ public/ - mv htmlcov/ public/
- mv pycco/ public/ - mv pycco/ public/
artifacts: artifacts:
paths: paths:
- public - public
expire_in: 14 days expire_in: 14 days
only: only:
- master - master
build-image: build-image:
image: docker image: docker
only:
- master
- devel
services: services:
- docker:dind - docker:dind
before_script: before_script:
- docker info - docker info
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script: script:
- ls -lah - ls -lah
- | - |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag="" tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'" echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else else
tag=":$CI_COMMIT_REF_SLUG" tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag" echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi fi
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" . - docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
- docker push "$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: hooks:
- id: check-useless-excludes - id: check-useless-excludes
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.6.2 rev: v3.0.0-alpha.4
hooks: hooks:
- id: prettier - id: prettier
files: \.(css|js|md|markdown|json) files: \.(css|js|md|markdown|json)
- repo: https://github.com/python/black - repo: https://github.com/python/black
rev: 22.3.0 rev: 22.12.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0 rev: v4.4.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
- id: check-ast - id: check-ast
@@ -37,8 +37,8 @@ repos:
- id: requirements-txt-fixer - id: requirements-txt-fixer
- id: sort-simple-yaml - id: sort-simple-yaml
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://gitlab.com/pycqa/flake8 - repo: https://github.com/PyCQA/flake8
rev: 3.9.2 rev: 6.0.0
hooks: hooks:
- id: flake8 - id: flake8
- repo: local - 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> LABEL maintainer Oliver Falk <oliver@linux-kernel.at>
EXPOSE 8081 EXPOSE 8081
RUN pip3 install pip --upgrade
ADD . /opt/ivatar-devel ADD . /opt/ivatar-devel
WORKDIR /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 "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 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 source .virtualenv/bin/activate \
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 && python3 manage.py migrate \
ENTRYPOINT python3 ./manage.py runserver 0:8081 && 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 OPENID_UPDATE_DETAILS_FROM_SREG = True
SITE_NAME = os.environ.get("SITE_NAME", "libravatar") SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
IVATAR_VERSION = "1.6.2" IVATAR_VERSION = "1.7.0"
SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2" SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
@@ -191,7 +191,7 @@ MESSAGE_TAGS = {
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache", "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": [ "LOCATION": [
"127.0.0.1:11211", "127.0.0.1:11211",
], ],
@@ -232,15 +232,18 @@ TRUSTED_DEFAULT_URLS = [
"host_equals": "avatars.dicebear.com", "host_equals": "avatars.dicebear.com",
"path_prefix": "/api/", "path_prefix": "/api/",
}, },
{
"schemes": ["https"],
"host_equals": "api.dicebear.com",
"path_prefix": "/",
},
{ {
"schemes": ["https"], "schemes": ["https"],
"host_equals": "badges.fedoraproject.org", "host_equals": "badges.fedoraproject.org",
"path_prefix": "/static/img/", "path_prefix": "/static/img/",
}, },
{ {
"schemes": [ "schemes": ["http"],
"http",
],
"host_equals": "www.planet-libre.org", "host_equals": "www.planet-libre.org",
"path_prefix": "/themes/planetlibre/images/", "path_prefix": "/themes/planetlibre/images/",
}, },

View File

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

View File

@@ -34,7 +34,7 @@ outline: inherit;
<button type="submit" name="photo{{ photo.id }}" class="nobutton"> <button type="submit" name="photo{{ photo.id }}" class="nobutton">
<div class="panel panel-tortin" style="width:132px;margin:0"> <div class="panel panel-tortin" style="width:132px;margin:0">
<div class="panel-heading"> <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>
<div class="panel-body" style="height:130px"> <div class="panel-body" style="height:130px">
<center> <center>
@@ -49,7 +49,7 @@ outline: inherit;
<button type="submit" name="photoNone" class="nobutton"> <button type="submit" name="photoNone" class="nobutton">
<div class="panel panel-tortin" style="width:132px;margin:0"> <div class="panel panel-tortin" style="width:132px;margin:0">
<div class="panel-heading"> <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>
<div class="panel-body" style="height:130px"> <div class="panel-body" style="height:130px">
<center> <center>

View File

@@ -34,7 +34,7 @@ outline: inherit;
<button type="submit" name="photo{{ photo.id }}" class="nobutton"> <button type="submit" name="photo{{ photo.id }}" class="nobutton">
<div class="panel panel-tortin" style="width:132px;margin:0"> <div class="panel panel-tortin" style="width:132px;margin:0">
<div class="panel-heading"> <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>
<div class="panel-body" style="height:130px"> <div class="panel-body" style="height:130px">
<center> <center>
@@ -49,7 +49,7 @@ outline: inherit;
<button type="submit" name="photoNone" class="nobutton"> <button type="submit" name="photoNone" class="nobutton">
<div class="panel panel-tortin" style="width:132px;margin:0"> <div class="panel panel-tortin" style="width:132px;margin:0">
<div class="panel-heading"> <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>
<div class="panel-body" style="height:130px"> <div class="panel-body" style="height:130px">
<center> <center>

View File

@@ -748,6 +748,47 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200, "unable to fetch avatar?") 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 def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name
""" """
Test if unsupported format is correctly detected 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 # Simply delete it, then it's digest is 'correct', but
# the hash is no longer there # the hash is no longer there
addr = self.user.confirmedemail_set.first().email 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() self.user.confirmedemail_set.first().delete()
url = "%s?%s" % (urlobj.path, urlobj.query) url = "%s?%s" % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
self.assertRedirects( self.assertEqual(
response=response, response.redirect_chain[0][0],
expected_url="/static/img/nobody/80.png", "/gravatarproxy/%s?s=80" % digest,
msg_prefix="Why does this not redirect to Gravatar?", "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 # Eventually one should check if the data is the same
def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled( 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() self.user.confirmedemail_set.first().delete()
url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
self.assertRedirects( self.assertEqual(
response=response, response.redirect_chain[0][0],
expected_url="/static/img/nobody/80.png", "/static/img/nobody/80.png",
msg_prefix="Why does this not redirect to the default img?", "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 # Eventually one should check if the data is the same
def test_avatar_url_inexisting_mail_digest_w_default_mm( 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) url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
self.assertRedirects( self.assertEqual(
response=response, response.redirect_chain[0][0],
expected_url="/static/img/mm/80.png", "/static/img/mm/80.png",
msg_prefix="Why does this not redirect to the default img?", "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 # Eventually one should check if the data is the same
def test_avatar_url_inexisting_mail_digest_wo_default( def test_avatar_url_inexisting_mail_digest_wo_default(
@@ -1380,13 +1454,36 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
size=80, size=80,
) )
) )
digest = hashlib.md5("asdf@company.local".lower().encode("utf-8")).hexdigest()
url = "%s?%s" % (urlobj.path, urlobj.query) url = "%s?%s" % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
self.assertRedirects( self.assertEqual(
response=response, response.redirect_chain[0][0],
expected_url="/static/img/nobody/80.png", "/gravatarproxy/%s?s=80" % digest,
msg_prefix="Why does this not redirect to the default img?", "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 # Eventually one should check if the data is the same
def test_avatar_url_inexisting_mail_digest_wo_default_gravatarproxy_disabled( 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) url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
self.assertRedirects( self.assertEqual(
response=response, response.redirect_chain[0][0],
expected_url="/static/img/nobody/80.png", "/static/img/nobody/80.png",
msg_prefix="Why does this not redirect to the default img?", "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 # Eventually one should check if the data is the same
def test_avatar_url_default(self): # pylint: disable=invalid-name def test_avatar_url_default(self): # pylint: disable=invalid-name
""" """
Test fetching avatar for not existing mail with default specified 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( urlobj = urlsplit(
libravatar_url( libravatar_url(
"xxx@xxx.xxx", "xxx@xxx.xxx",
@@ -1422,7 +1528,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
) )
) )
url = "%s?%s" % (urlobj.path, urlobj.query) url = "%s?%s" % (urlobj.path, urlobj.query)
response = self.client.get(url, follow=True) response = self.client.get(url, follow=False)
self.assertRedirects( self.assertRedirects(
response=response, response=response,
expected_url="/static/img/nobody.png", 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 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( urlobj = urlsplit(
libravatar_url( libravatar_url(
"xxx@xxx.xxx", "xxx@xxx.xxx",
@@ -1889,4 +1998,61 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
Test if preferences page works Test if preferences page works
""" """
self.login()
self.client.get(reverse("user_preference")) 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 URLs for ivatar.ivataraccount
""" """
from django.urls import path from django.urls import path, re_path
from django.conf.urls import url
from django.contrib.auth.views import LogoutView from django.contrib.auth.views import LogoutView
from django.contrib.auth.views import ( from django.contrib.auth.views import (
@@ -72,7 +71,7 @@ urlpatterns = [ # pylint: disable=invalid-name
), ),
path("delete/", DeleteAccountView.as_view(), name="delete"), path("delete/", DeleteAccountView.as_view(), name="delete"),
path("profile/", ProfileView.as_view(), name="profile"), path("profile/", ProfileView.as_view(), name="profile"),
url( re_path(
"profile/(?P<profile_username>.+)", "profile/(?P<profile_username>.+)",
ProfileView.as_view(), ProfileView.as_view(),
name="profile_with_profile_username", name="profile_with_profile_username",
@@ -81,73 +80,77 @@ urlpatterns = [ # pylint: disable=invalid-name
path("add_openid/", AddOpenIDView.as_view(), name="add_openid"), path("add_openid/", AddOpenIDView.as_view(), name="add_openid"),
path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"), path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"),
path("password_set/", PasswordSetView.as_view(), name="password_set"), path("password_set/", PasswordSetView.as_view(), name="password_set"),
url( re_path(
r"remove_unconfirmed_openid/(?P<openid_id>\d+)", r"remove_unconfirmed_openid/(?P<openid_id>\d+)",
RemoveUnconfirmedOpenIDView.as_view(), RemoveUnconfirmedOpenIDView.as_view(),
name="remove_unconfirmed_openid", name="remove_unconfirmed_openid",
), ),
url( re_path(
r"remove_confirmed_openid/(?P<openid_id>\d+)", r"remove_confirmed_openid/(?P<openid_id>\d+)",
RemoveConfirmedOpenIDView.as_view(), RemoveConfirmedOpenIDView.as_view(),
name="remove_confirmed_openid", name="remove_confirmed_openid",
), ),
url( re_path(
r"openid_redirection/(?P<openid_id>\d+)", r"openid_redirection/(?P<openid_id>\d+)",
RedirectOpenIDView.as_view(), RedirectOpenIDView.as_view(),
name="openid_redirection", name="openid_redirection",
), ),
url( re_path(
r"confirm_openid/(?P<openid_id>\w+)", r"confirm_openid/(?P<openid_id>\w+)",
ConfirmOpenIDView.as_view(), ConfirmOpenIDView.as_view(),
name="confirm_openid", name="confirm_openid",
), ),
url( re_path(
r"confirm_email/(?P<verification_key>\w+)", r"confirm_email/(?P<verification_key>\w+)",
ConfirmEmailView.as_view(), ConfirmEmailView.as_view(),
name="confirm_email", name="confirm_email",
), ),
url( re_path(
r"remove_unconfirmed_email/(?P<email_id>\d+)", r"remove_unconfirmed_email/(?P<email_id>\d+)",
RemoveUnconfirmedEmailView.as_view(), RemoveUnconfirmedEmailView.as_view(),
name="remove_unconfirmed_email", name="remove_unconfirmed_email",
), ),
url( re_path(
r"remove_confirmed_email/(?P<email_id>\d+)", r"remove_confirmed_email/(?P<email_id>\d+)",
RemoveConfirmedEmailView.as_view(), RemoveConfirmedEmailView.as_view(),
name="remove_confirmed_email", name="remove_confirmed_email",
), ),
url( re_path(
r"assign_photo_email/(?P<email_id>\d+)", r"assign_photo_email/(?P<email_id>\d+)",
AssignPhotoEmailView.as_view(), AssignPhotoEmailView.as_view(),
name="assign_photo_email", name="assign_photo_email",
), ),
url( re_path(
r"assign_photo_openid/(?P<openid_id>\d+)", r"assign_photo_openid/(?P<openid_id>\d+)",
AssignPhotoOpenIDView.as_view(), AssignPhotoOpenIDView.as_view(),
name="assign_photo_openid", name="assign_photo_openid",
), ),
url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"), re_path(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"),
url( re_path(
r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)", r"import_photo/(?P<email_addr>[\w.+-]+@[\w.]+.[\w.]+)",
ImportPhotoView.as_view(), ImportPhotoView.as_view(),
name="import_photo", name="import_photo",
), ),
url( re_path(
r"import_photo/(?P<email_id>\d+)", r"import_photo/(?P<email_id>\d+)",
ImportPhotoView.as_view(), ImportPhotoView.as_view(),
name="import_photo", name="import_photo",
), ),
url(r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"), re_path(
url(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"), r"delete_photo/(?P<pk>\d+)", DeletePhotoView.as_view(), name="delete_photo"
url(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"), ),
url(r"pref/$", UserPreferenceView.as_view(), name="user_preference"), re_path(r"raw_image/(?P<pk>\d+)", RawImageView.as_view(), name="raw_image"),
url(r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"), re_path(r"crop_photo/(?P<pk>\d+)", CropPhotoView.as_view(), name="crop_photo"),
url( 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)$", r"upload_export/(?P<save>save)$",
UploadLibravatarExportView.as_view(), UploadLibravatarExportView.as_view(),
name="upload_export", name="upload_export",
), ),
url( re_path(
r"resend_confirmation_mail/(?P<email_id>\d+)", r"resend_confirmation_mail/(?P<email_id>\d+)",
ResendConfirmationMailView.as_view(), ResendConfirmationMailView.as_view(),
name="resend_confirmation_mail", name="resend_confirmation_mail",

View File

@@ -207,6 +207,13 @@ class ConfirmEmailView(SuccessMessageMixin, TemplateView):
messages.error(request, _("Verification key does not exist")) messages.error(request, _("Verification key does not exist"))
return HttpResponseRedirect(reverse_lazy("profile")) 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 # TODO: Check for a reasonable expiration time in unconfirmed email
(confirmed_id, external_photos) = ConfirmedEmail.objects.create_confirmed_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("jpeg"), "JPEG")
self.assertEqual(pil_format("png"), "PNG") self.assertEqual(pil_format("png"), "PNG")
self.assertEqual(pil_format("gif"), "GIF") self.assertEqual(pil_format("gif"), "GIF")
self.assertEqual(pil_format("webp"), "WEBP")
self.assertEqual(pil_format("abc"), None) self.assertEqual(pil_format("abc"), None)
def test_userprefs_str(self): def test_userprefs_str(self):

View File

@@ -50,11 +50,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
Test incorrect digest Test incorrect digest
""" """
response = self.client.get("/avatar/%s" % "x" * 65, follow=True) response = self.client.get("/avatar/%s" % "x" * 65, follow=True)
self.assertRedirects( self.assertEqual(
response=response, response.redirect_chain[0][0],
expected_url="/static/img/deadbeef.png", "/static/img/deadbeef.png",
msg_prefix="Why does an invalid hash not redirect to deadbeef?", "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): 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, password=self.password,
) )
def test_check(self): def test_check_mail(self):
""" """
Test check page Test check page
""" """
self.login()
response = self.client.get(reverse("tools_check")) response = self.client.get(reverse("tools_check"))
self.assertEqual(response.status_code, 200, "no 200 ok?") self.assertEqual(response.status_code, 200, "no 200 ok?")
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): def test_check_domain(self):
""" """
Test check domain page Test check domain page
""" """
self.login()
response = self.client.get(reverse("tools_check_domain")) response = self.client.get(reverse("tools_check_domain"))
self.assertEqual(response.status_code, 200, "no 200 ok?") self.assertEqual(response.status_code, 200, "no 200 ok?")
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 ivatar/tools URL configuration
""" """
from django.conf.urls import url from django.urls import path, re_path
from .views import CheckView, CheckDomainView from .views import CheckView, CheckDomainView
urlpatterns = [ # pylint: disable=invalid-name urlpatterns = [ # pylint: disable=invalid-name
url("check/", CheckView.as_view(), name="tools_check"), path("check/", CheckView.as_view(), name="tools_check"),
url("check_domain/", CheckDomainView.as_view(), name="tools_check_domain"), path("check_domain/", CheckDomainView.as_view(), name="tools_check_domain"),
url("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 ivatar URL configuration
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include, re_path
from django.conf.urls import url
from django.conf.urls.static import static from django.conf.urls.static import static
from django.views.generic import TemplateView, RedirectView from django.views.generic import TemplateView, RedirectView
from ivatar import settings from ivatar import settings
@@ -13,65 +12,72 @@ from .views import AvatarImageView, GravatarProxyView, StatsView
urlpatterns = [ # pylint: disable=invalid-name urlpatterns = [ # pylint: disable=invalid-name
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("i18n/", include("django.conf.urls.i18n")), path("i18n/", include("django.conf.urls.i18n")),
url("openid/", include("django_openid_auth.urls")), path("openid/", include("django_openid_auth.urls")),
url("tools/", include("ivatar.tools.urls")), path("tools/", include("ivatar.tools.urls")),
url(r"avatar/(?P<digest>\w{64})", AvatarImageView.as_view(), name="avatar_view"), re_path(
url(r"avatar/(?P<digest>\w{32})", AvatarImageView.as_view(), name="avatar_view"), r"avatar/(?P<digest>\w{64})", AvatarImageView.as_view(), name="avatar_view"
url(r"avatar/$", AvatarImageView.as_view(), name="avatar_view"), ),
url( 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*)", r"avatar/(?P<digest>\w*)",
RedirectView.as_view(url="/static/img/deadbeef.png"), RedirectView.as_view(url="/static/img/deadbeef.png"),
name="invalid_hash", name="invalid_hash",
), ),
url( re_path(
r"gravatarproxy/(?P<digest>\w*)", r"gravatarproxy/(?P<digest>\w*)",
GravatarProxyView.as_view(), GravatarProxyView.as_view(),
name="gravatarproxy", name="gravatarproxy",
), ),
url( path(
"description/", "description/",
TemplateView.as_view(template_name="description.html"), TemplateView.as_view(template_name="description.html"),
name="description", name="description",
), ),
# The following two are TODO TODO TODO TODO TODO # The following two are TODO TODO TODO TODO TODO
url( path(
"run_your_own/", "run_your_own/",
TemplateView.as_view(template_name="run_your_own.html"), TemplateView.as_view(template_name="run_your_own.html"),
name="run_your_own", name="run_your_own",
), ),
url( path(
"features/", "features/",
TemplateView.as_view(template_name="features.html"), TemplateView.as_view(template_name="features.html"),
name="features", name="features",
), ),
url( path(
"security/", "security/",
TemplateView.as_view(template_name="security.html"), TemplateView.as_view(template_name="security.html"),
name="security", name="security",
), ),
url("privacy/", TemplateView.as_view(template_name="privacy.html"), name="privacy"), path(
url("contact/", TemplateView.as_view(template_name="contact.html"), name="contact"), "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"), 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 MAINTENANCE = False
try: try:
if settings.MAINTENANCE: if settings.MAINTENANCE:
MAINTENANCE = True MAINTENANCE = True
except: # pylint: disable=bare-except except Exception: # pylint: disable=bare-except
pass pass
if MAINTENANCE: if MAINTENANCE:
urlpatterns.append( 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: else:
urlpatterns.append( 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) 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 random
import string import string
from PIL import Image, ImageDraw from io import BytesIO
from PIL import Image, ImageDraw, ImageSequence
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -158,3 +159,25 @@ def is_trusted_url(url, url_filters):
return True return True
return False 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 UnconfirmedEmail, UnconfirmedOpenId
from .ivataraccount.models import Photo from .ivataraccount.models import Photo
from .ivataraccount.models import pil_format, file_format 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 URL_TIMEOUT = 5 # in seconds
@@ -151,7 +151,8 @@ class AvatarImageView(TemplateView):
if not trusted_url: if not trusted_url:
print( 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 default = None
@@ -318,14 +319,23 @@ class AvatarImageView(TemplateView):
imgformat = obj.photo.format imgformat = obj.photo.format
photodata = Image.open(BytesIO(obj.photo.data)) 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() 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) data.seek(0)
obj.photo.access_count += 1 obj.photo.access_count += 1
obj.photo.save() 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 autopep8
bcrypt bcrypt
defusedxml defusedxml
Django < 4.0 Django
django-anymail[mailgun] django-anymail[mailgun]
django-auth-ldap django-auth-ldap
django-bootstrap4 django-bootstrap4
@@ -27,10 +27,10 @@ py3dns
pydocstyle pydocstyle
pyLibravatar pyLibravatar
pylint pylint
pymemcache
PyMySQL PyMySQL
python-coveralls python-coveralls
python-language-server python-language-server
python-memcached
python3-openid python3-openid
pytz pytz
rope 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> <p>
<button type="submit" class="button">{% trans 'Login' %}</button> <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; &nbsp;
<button type="reset" class="button" onclick="window.history.back();">{% trans 'Cancel' %}</button> <button type="reset" class="button" onclick="window.history.back();">{% trans 'Cancel' %}</button>