diff --git a/js/10-base.js b/js/10-base.js
index 2fab068ca..d6654cba7 100644
--- a/js/10-base.js
+++ b/js/10-base.js
@@ -105,10 +105,6 @@ Liberapay.init = function() {
$('[data-email-reveal]').one('click', function () {
$(this).html($(this).data('email-reveal'));
});
-
- $('button[data-action="reload"]').on('click', function() {
- location.reload();
- });
};
$(function(){
diff --git a/liberapay/constants.py b/liberapay/constants.py
index 6fed31bc2..6e3aa8f4f 100644
--- a/liberapay/constants.py
+++ b/liberapay/constants.py
@@ -40,7 +40,6 @@ def check_bits(bits):
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"-_.")
-AVATAR_QUERY = '?s=160&d=404'
AVATAR_SOURCES = (
'libravatar bitbucket github gitlab mastodon pleroma twitch twitter'
).split()
diff --git a/liberapay/models/account_elsewhere.py b/liberapay/models/account_elsewhere.py
index 895a3b7ba..ad8d9e123 100644
--- a/liberapay/models/account_elsewhere.py
+++ b/liberapay/models/account_elsewhere.py
@@ -1,6 +1,5 @@
from datetime import timedelta
import json
-from urllib.parse import urlsplit, urlunsplit
import uuid
from markupsafe import Markup
@@ -9,14 +8,14 @@
from postgres.orm import Model
from psycopg2 import IntegrityError
-from ..constants import AVATAR_QUERY, DOMAIN_RE, SUMMARY_MAX_SIZE
+from ..constants import DOMAIN_RE, SUMMARY_MAX_SIZE
from ..cron import logger
from ..elsewhere._base import (
ElsewhereError, InvalidServerResponse, UserNotFound,
)
from ..exceptions import InvalidId
from ..security.crypto import constant_time_compare
-from ..utils import excerpt_intro
+from ..utils import excerpt_intro, tweak_avatar_url
from ..website import website
@@ -114,13 +113,7 @@ def upsert(cls, i):
# Clean up avatar_url
if i.avatar_url:
- scheme, netloc, path, query, fragment = urlsplit(i.avatar_url)
- fragment = ''
- if netloc.endswith('githubusercontent.com') or \
- netloc.endswith('gravatar.com') or \
- netloc.endswith('libravatar.org'):
- query = AVATAR_QUERY
- i.avatar_url = urlunsplit((scheme, netloc, path, query, fragment))
+ i.avatar_url = tweak_avatar_url(i.avatar_url)
d = dict(i.__dict__)
d.pop('email', None)
@@ -193,7 +186,7 @@ def update():
raise
# Return account after propagating avatar_url to participant
- account.participant.update_avatar(check=False)
+ account.participant.update_avatar()
return account
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
index 5d99bfb76..96e85be64 100644
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -30,7 +30,7 @@
from liberapay.billing.payday import compute_next_payday_date
from liberapay.constants import (
- ASCII_ALLOWED_IN_USERNAME, AVATAR_QUERY, BASE64URL_CHARS, CURRENCIES,
+ ASCII_ALLOWED_IN_USERNAME, BASE64URL_CHARS, CURRENCIES,
DONATION_LIMITS, EMAIL_VERIFICATION_TIMEOUT, EVENTS, HTML_A,
PASSWORD_MAX_SIZE, PASSWORD_MIN_SIZE, PAYPAL_CURRENCIES,
PERIOD_CONVERSION_RATES, PRIVILEGES,
@@ -87,6 +87,7 @@
from liberapay.security.crypto import constant_time_compare
from liberapay.utils import (
deserialize, erase_cookie, get_recordable_headers, serialize, set_cookie,
+ tweak_avatar_url,
markdown,
)
from liberapay.utils.emails import (
@@ -2200,7 +2201,7 @@ def is_char_forbidden(char):
return new_public_name
- def update_avatar(self, src=None, cursor=None, avatar_email=None, check=True):
+ def update_avatar(self, src=None, cursor=None, avatar_email=None, refresh=False):
if self.status == 'stub':
assert src is None
@@ -2274,7 +2275,6 @@ def update_avatar(self, src=None, cursor=None, avatar_email=None, check=True):
website.tell_sentry(e)
avatar_id = md5(normalized_email.encode('utf8')).hexdigest()
avatar_url = avatar_origin + '/avatar/' + avatar_id
- avatar_url += AVATAR_QUERY
elif platform is None:
avatar_url = (cursor or self.db).one("""
@@ -2298,7 +2298,14 @@ def update_avatar(self, src=None, cursor=None, avatar_email=None, check=True):
LIMIT 1
""", (self.id, platform, user_id or None))
- if avatar_url and avatar_url != self.avatar_url and check and website.app_conf.check_avatar_urls:
+ avatar_url = tweak_avatar_url(avatar_url, increment=refresh)
+ check_url = (
+ avatar_url and
+ avatar_url != self.avatar_url and
+ self.status != 'stub' and
+ website.app_conf.check_avatar_urls
+ )
+ if check_url:
# Check that the new avatar URL returns a 200.
try:
r = requests.head(avatar_url, allow_redirects=True, timeout=5)
diff --git a/liberapay/utils/__init__.py b/liberapay/utils/__init__.py
index b27a804ee..2a3b4692c 100644
--- a/liberapay/utils/__init__.py
+++ b/liberapay/utils/__init__.py
@@ -9,6 +9,7 @@
import os
import re
import socket
+from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
from pando import Response, json
from pando.utils import to_rfc822, utcnow
@@ -730,3 +731,23 @@ def get_recordable_headers(request):
for k, v in request.headers.items()
if k != b'Cookie'
}
+
+
+def tweak_avatar_url(avatar_url, increment=True):
+ if not avatar_url:
+ return ''
+ # Parse the URL
+ scheme, netloc, path, query, fragment = urlsplit(avatar_url)
+ query = parse_qs(query)
+ # Add parameters inherited from Gravatar (https://wiki.libravatar.org/api/)
+ query['s'] = '160' # size = 160 pixels
+ query['d'] = '404' # default = a 404 HTTP response
+ # Increment the serial number to avoid stale images in a browser's cache
+ try:
+ query[''] = str(int(query[''][-1]) + int(increment))
+ except (KeyError, ValueError):
+ query[''] = '1'
+ # Drop any fragment that might be there
+ fragment = ''
+ # Return the modified URL
+ return urlunsplit((scheme, netloc, path, urlencode(query), fragment))
diff --git a/tests/py/fixtures/TestAvatars.yml b/tests/py/fixtures/TestAvatars.yml
index 3b1ca99f7..d1e4602df 100644
--- a/tests/py/fixtures/TestAvatars.yml
+++ b/tests/py/fixtures/TestAvatars.yml
@@ -3,7 +3,7 @@ interactions:
body: null
headers: {}
method: HEAD
- uri: https://seccdn.libravatar.org/avatar/20f0944fefc09a31e43c55bc30c25cdf?s=160&d=404
+ uri: https://seccdn.libravatar.org/avatar/20f0944fefc09a31e43c55bc30c25cdf?s=160&d=404&=1
response:
body: {string: ''}
headers:
@@ -54,7 +54,7 @@ interactions:
body: null
headers: {}
method: HEAD
- uri: https://liberapay.com/assets/nonexistent.jpg
+ uri: https://liberapay.com/assets/nonexistent.jpg?s=160&d=404&=1
response:
body: {string: ''}
headers:
diff --git a/tests/py/test_elsewhere.py b/tests/py/test_elsewhere.py
index 5fa50faae..b4ccfadcf 100644
--- a/tests/py/test_elsewhere.py
+++ b/tests/py/test_elsewhere.py
@@ -119,10 +119,10 @@ def test_upsert_correctly_updates_the_participant_avatar_url(self):
alice = alice.refetch()
assert alice.avatar_url == libravatar_url
alice.update_avatar(src='github:')
- assert alice.avatar_url == 'fake-github-avatar-url'
+ assert alice.avatar_url == 'fake-github-avatar-url?s=160&d=404&=1'
alice_github_info.avatar_url = 'new-fake-github-avatar-url'
alice_github = AccountElsewhere.upsert(alice_github_info)
- assert alice_github.participant.avatar_url == 'new-fake-github-avatar-url'
+ assert alice_github.participant.avatar_url == 'new-fake-github-avatar-url?s=160&d=404&=1'
@mock.patch('liberapay.elsewhere._base.Platform.get_user_info')
def test_user_pages(self, get_user_info):
diff --git a/www/%username/edit/avatar.spt b/www/%username/edit/avatar.spt
index 6b1ace4a0..db9084c39 100644
--- a/www/%username/edit/avatar.spt
+++ b/www/%username/edit/avatar.spt
@@ -5,17 +5,23 @@ from liberapay.utils import form_post_success, get_participant
participant = get_participant(state, restrict=True, allow_member=True)
if request.method == 'POST':
- src, email = request.body['src'], request.body.get('email', '')
-
- if src not in constants.AVATAR_SOURCES:
- raise response.invalid_input(src, 'src', 'body')
- if email and '@' not in email:
- raise BadEmailAddress(email)
-
- new_avatar_url = participant.update_avatar(src+':', avatar_email=email)
+ action = request.body.get('action', 'set_source')
+ if action == 'refresh':
+ new_avatar_url = participant.update_avatar(refresh=True)
+ elif action == 'set_source':
+ src, email = request.body['src'], request.body.get('email', '')
+ if src not in constants.AVATAR_SOURCES:
+ raise response.invalid_input(src, 'src', 'body')
+ if email and '@' not in email:
+ raise BadEmailAddress(email)
+ new_avatar_url = participant.update_avatar(src+':', avatar_email=email)
+ else:
+ raise response.invalid_input(action, 'action', 'body')
if new_avatar_url:
msg = _("Your new avatar URL is: {0}", new_avatar_url)
- form_post_success(state, msg=msg)
+ else:
+ msg = _("Your profile no longer has an avatar.")
+ form_post_success(state, msg=msg)
else:
src = participant.avatar_src
@@ -80,10 +86,12 @@ subhead = _("Avatar")
% if participant.avatar_url and request.method == 'GET'
-
-
+