]> git.ipfire.org Git - ipfire.org.git/blob - src/web/users.py
6dee34b9585074a06373bf9799862d1800d5ada4
[ipfire.org.git] / src / web / users.py
1 #!/usr/bin/python
2
3 import PIL
4 import imghdr
5 import io
6 import ldap
7 import logging
8 import os.path
9 import tornado.web
10
11 from .. import countries
12
13 from . import base
14 from . import ui_modules
15
16 COLOUR_LIGHT = (237,232,232)
17 COLOUR_DARK = (49,53,60)
18
19 class IndexHandler(base.BaseHandler):
20 @tornado.web.authenticated
21 def get(self):
22 results = None
23
24 # Query Term
25 q = self.get_argument("q", None)
26
27 # Peform search
28 if q:
29 results = self.backend.accounts.search(q)
30
31 self.render("users/index.html", q=q, results=results)
32
33
34 class ShowHandler(base.BaseHandler):
35 @tornado.web.authenticated
36 def get(self, uid):
37 account = self.backend.accounts.get_by_uid(uid)
38 if not account:
39 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
40
41 self.render("users/show.html", account=account)
42
43
44 class AvatarHandler(base.BaseHandler):
45 def get(self, uid):
46 # Get the desired size of the avatar file
47 size = self.get_argument("size", None)
48
49 try:
50 size = int(size)
51 except (TypeError, ValueError):
52 size = None
53
54 logging.debug("Querying for avatar of %s" % uid)
55
56 # Fetch user account
57 account = self.backend.accounts.get_by_uid(uid)
58 if not account:
59 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
60
61 # Allow downstream to cache this for a year
62 self.set_expires(31536000)
63
64 # Resize avatar
65 avatar = account.get_avatar(size)
66
67 # If there is no avatar, we serve a default image
68 if not avatar:
69 logging.debug("No avatar uploaded for %s" % account)
70
71 # Generate a random avatar with only one letter
72 avatar = self._get_avatar(account, size=size)
73
74 # Guess content type
75 type = imghdr.what(None, avatar)
76
77 # If we could not guess the type, we will try something else
78 if not type:
79 # Could this be an SVG file?
80 if avatar.startswith(b"<"):
81 type = "svg+xml"
82
83 # Set headers about content
84 self.set_header("Content-Disposition", "inline; filename=\"%s.%s\"" % (account.uid, type))
85 if type:
86 self.set_header("Content-Type", "image/%s" % type)
87
88 # Deliver payload
89 self.finish(avatar)
90
91 def _get_avatar(self, account, size=None, **args):
92 letter = ("%s" % account)[0].upper()
93
94 if size is None:
95 size = 256
96
97 # The generated avatar cannot be larger than 1024px
98 if size >= 2048:
99 size = 2048
100
101 # Cache key
102 key = "avatar:letter:%s:%s" % (letter, size)
103
104 # Fetch avatar from the cache
105 avatar = self.memcached.get(key)
106 if not avatar:
107 avatar = self._make_avatar(letter, size=size, **args)
108
109 # Cache for forever
110 self.memcached.set(key, avatar)
111
112 return avatar
113
114 def _make_avatar(self, letter, format="PNG", size=None, **args):
115 # Generate an image of the correct size
116 image = PIL.Image.new("RGBA", (size, size), COLOUR_LIGHT)
117
118 # Have a canvas
119 draw = PIL.ImageDraw.Draw(image)
120
121 # Load font
122 font = PIL.ImageFont.truetype(os.path.join(
123 self.application.settings.get("static_path", ""),
124 "fonts/Prompt-Bold.ttf"
125 ), size, encoding="unic")
126
127 # Determine size of the printed letter
128 w, h = font.getsize(letter)
129
130 # Mukta seems to be very broken and the height needs to be corrected
131 h //= 0.7
132
133 # Draw the letter in the center
134 draw.text(((size - w) / 2, (size - h) / 2), letter,
135 font=font, fill=COLOUR_DARK)
136
137 with io.BytesIO() as f:
138 # If writing out the image does not work with optimization,
139 # we try to write it out without any optimization.
140 try:
141 image.save(f, format, optimize=True, **args)
142 except:
143 image.save(f, format, **args)
144
145 return f.getvalue()
146
147
148 class EditHandler(base.BaseHandler):
149 @tornado.web.authenticated
150 def get(self, uid):
151 account = self.backend.accounts.get_by_uid(uid)
152 if not account:
153 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
154
155 # Check for permissions
156 if not account.can_be_managed_by(self.current_user):
157 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
158
159 self.render("users/edit.html", account=account, countries=countries.get_all())
160
161 @tornado.web.authenticated
162 def post(self, uid):
163 account = self.backend.accounts.get_by_uid(uid)
164 if not account:
165 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
166
167 # Check for permissions
168 if not account.can_be_managed_by(self.current_user):
169 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
170
171 # Unfortunately this cannot be wrapped into a transaction
172 try:
173 account.first_name = self.get_argument("first_name")
174 account.last_name = self.get_argument("last_name")
175 account.nickname = self.get_argument("nickname", None)
176 account.street = self.get_argument("street", None)
177 account.city = self.get_argument("city", None)
178 account.postal_code = self.get_argument("postal_code", None)
179 account.country_code = self.get_argument("country_code", None)
180 account.description = self.get_argument("description", None)
181
182 # Avatar
183 try:
184 filename, data, mimetype = self.get_file("avatar")
185
186 if not mimetype.startswith("image/"):
187 raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype)
188
189 account.upload_avatar(data)
190 except TypeError:
191 pass
192
193 # Email
194 account.mail_routing_address = self.get_argument("mail_routing_address", None)
195
196 # Telephone
197 account.phone_numbers = self.get_argument("phone_numbers", "").splitlines()
198 account.sip_routing_address = self.get_argument("sip_routing_address", None)
199 except ldap.STRONG_AUTH_REQUIRED as e:
200 raise tornado.web.HTTPError(403, "%s" % e) from e
201
202 # Redirect back to user page
203 self.redirect("/users/%s" % account.uid)
204
205
206 class DeleteHandler(base.BaseHandler):
207 @tornado.web.authenticated
208 def get(self, uid):
209 account = self.backend.accounts.get_by_uid(uid)
210 if not account:
211 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
212
213 # Check for permissions
214 if not account.can_be_deleted_by(self.current_user):
215 raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account))
216
217 self.render("users/delete.html", account=account)
218
219 @tornado.web.authenticated
220 async def post(self, uid):
221 account = self.backend.accounts.get_by_uid(uid)
222 if not account:
223 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
224
225 # Check for permissions
226 if not account.can_be_deleted_by(self.current_user):
227 raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account))
228
229 # Delete!
230 with self.db.transaction():
231 await account.delete(self.current_user)
232
233 self.render("users/deleted.html", account=account)
234
235
236 class PasswdHandler(base.BaseHandler):
237 @tornado.web.authenticated
238 def get(self, uid):
239 account = self.backend.accounts.get_by_uid(uid)
240 if not account:
241 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
242
243 # Check for permissions
244 if not account.can_be_managed_by(self.current_user):
245 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
246
247 self.render("users/passwd.html", account=account)
248
249 @tornado.web.authenticated
250 def post(self, uid):
251 account = self.backend.accounts.get_by_uid(uid)
252 if not account:
253 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
254
255 # Check for permissions
256 if not account.can_be_managed_by(self.current_user):
257 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
258
259 # Get current password
260 password = self.get_argument("password")
261
262 # Get new password
263 password1 = self.get_argument("password1")
264 password2 = self.get_argument("password2")
265
266 # Passwords must match
267 if not password1 == password2:
268 raise tornado.web.HTTPError(400, "Passwords do not match")
269
270 # XXX Check password complexity
271
272 # Check if old password matches
273 if not account.check_password(password):
274 raise tornado.web.HTTPError(403, "Incorrect password for %s" % account)
275
276 # Save new password
277 account.passwd(password1)
278
279 # Redirect back to user's page
280 self.redirect("/users/%s" % account.uid)
281
282
283 class GroupIndexHandler(base.BaseHandler):
284 @tornado.web.authenticated
285 def get(self):
286 # Only staff can see other groups
287 if not self.current_user.is_staff():
288 raise tornado.web.HTTPError(403)
289
290 self.render("users/groups/index.html")
291
292
293 class GroupShowHandler(base.BaseHandler):
294 @tornado.web.authenticated
295 def get(self, gid):
296 # Only staff can see other groups
297 if not self.current_user.is_staff():
298 raise tornado.web.HTTPError(403)
299
300 # Fetch group
301 group = self.backend.groups.get_by_gid(gid)
302 if not group:
303 raise tornado.web.HTTPError(404, "Could not find group %s" % gid)
304
305 self.render("users/groups/show.html", group=group)
306
307
308 class ListModule(ui_modules.UIModule):
309 def render(self, accounts, show_created_at=False):
310 return self.render_string("users/modules/list.html", accounts=accounts,
311 show_created_at=show_created_at)