CHANGES: document the Rebase Remarks feature from #1375
[git-cola.git] / cola / gravatar.py
blobb6f39df2fe96632da0354d431ad1aaa3353b8168
1 import time
2 import hashlib
4 from qtpy import QtCore
5 from qtpy import QtGui
6 from qtpy import QtWidgets
7 from qtpy import QtNetwork
9 from . import core
10 from . import icons
11 from . import qtutils
12 from .compat import parse
13 from .models import prefs
14 from .widgets import defs
17 class Gravatar:
18 @staticmethod
19 def url_for_email(email, imgsize):
20 email_hash = md5_hexdigest(email)
21 # Python2.6 requires byte strings for urllib2.quote() so we have
22 # to force
23 default_url = 'https://git-cola.github.io/images/git-64x64.jpg'
24 encoded_url = parse.quote(core.encode(default_url), core.encode(''))
25 query = '?s=%d&d=%s' % (imgsize, core.decode(encoded_url))
26 url = 'https://gravatar.com/avatar/' + email_hash + query
27 return url
30 def md5_hexdigest(value):
31 """Return the md5 hexdigest for a value.
33 Used for implementing the gravatar API. Not used for security purposes.
34 """
35 # https://github.com/git-cola/git-cola/issues/1157
36 # ValueError: error:060800A3:
37 # digital envelope routines: EVP_DigestInit_ex: disabled for fips
39 # Newer versions of Python, including Centos8's patched Python3.6 and
40 # mainline Python 3.9+ have a "usedoforsecurity" parameter which allows us
41 # to continue using hashlib.md5().
42 encoded_value = core.encode(value)
43 result = ''
44 try:
45 # This could raise ValueError in theory but we always use encoded bytes
46 # so that does not happen in practice.
47 # pylint: disable=unexpected-keyword-arg
48 result = hashlib.md5(encoded_value, usedforsecurity=False).hexdigest()
49 except TypeError:
50 # Fallback to trying hashlib.md5 directly.
51 result = hashlib.md5(encoded_value).hexdigest()
52 return core.decode(result)
55 class GravatarLabel(QtWidgets.QLabel):
56 def __init__(self, context, parent=None):
57 QtWidgets.QLabel.__init__(self, parent)
59 self.context = context
60 self.email = None
61 self.response = None
62 self.timeout = 0
63 self.imgsize = defs.medium_icon
64 self.pixmaps = {}
65 self._default_pixmap_bytes = None
67 self.network = QtNetwork.QNetworkAccessManager()
68 # pylint: disable=no-member
69 self.network.finished.connect(self.network_finished)
71 def set_email(self, email):
72 """Update the author icon based on the specified email"""
73 pixmap = self.pixmaps.get(email, None)
74 if pixmap is not None:
75 self.setPixmap(pixmap)
76 return
77 if self.timeout > 0 and (int(time.time()) - self.timeout) < (5 * 60):
78 self.set_pixmap_from_response()
79 return
80 if email == self.email and self.response is not None:
81 self.set_pixmap_from_response()
82 return
83 self.email = email
84 self.request(email)
86 def request(self, email):
87 if prefs.enable_gravatar(self.context):
88 url = Gravatar.url_for_email(email, self.imgsize)
89 self.network.get(QtNetwork.QNetworkRequest(QtCore.QUrl(url)))
90 else:
91 self.pixmaps[email] = self.set_pixmap_from_response()
93 def default_pixmap_as_bytes(self):
94 if self._default_pixmap_bytes is None:
95 xres = self.imgsize
96 pixmap = icons.cola().pixmap(xres)
97 byte_array = QtCore.QByteArray()
98 buf = QtCore.QBuffer(byte_array)
99 buf.open(QtCore.QIODevice.WriteOnly)
100 pixmap.save(buf, 'PNG')
101 buf.close()
102 self._default_pixmap_bytes = byte_array
103 else:
104 byte_array = self._default_pixmap_bytes
105 return byte_array
107 def network_finished(self, reply):
108 email = self.email
110 header = QtCore.QByteArray(b'Location')
111 location = core.decode(bytes(reply.rawHeader(header))).strip()
112 if location:
113 request_location = Gravatar.url_for_email(self.email, self.imgsize)
114 relocated = location != request_location
115 else:
116 relocated = False
117 no_error = qtutils.enum_value(
118 QtNetwork.QNetworkReply.NetworkError.NoError # pylint: disable=no-member
120 reply_error = qtutils.enum_value(reply.error())
121 if reply_error == no_error:
122 if relocated:
123 # We could do get_url(parse.unquote(location)) to
124 # download the default image.
125 # Save bandwidth by using a pixmap.
126 self.response = self.default_pixmap_as_bytes()
127 else:
128 self.response = reply.readAll()
129 self.timeout = 0
130 else:
131 self.response = self.default_pixmap_as_bytes()
132 self.timeout = int(time.time())
134 pixmap = self.set_pixmap_from_response()
136 # If the email has not changed (e.g. no other requests)
137 # then we know that this pixmap corresponds to this specific
138 # email address. We can't blindly trust self.email else
139 # we may add cache entries for thee wrong email address.
140 url = Gravatar.url_for_email(email, self.imgsize)
141 if url == reply.url().toString():
142 self.pixmaps[email] = pixmap
144 def set_pixmap_from_response(self):
145 if self.response is None:
146 self.response = self.default_pixmap_as_bytes()
147 pixmap = QtGui.QPixmap()
148 pixmap.loadFromData(self.response)
149 self.setPixmap(pixmap)
150 return pixmap