]> git.ipfire.org Git - ipfire.org.git/blame - src/web/people.py
people: Encourage people to complete their profile on the profile page
[ipfire.org.git] / src / web / people.py
CommitLineData
2cd9af74
MT
1#!/usr/bin/python
2
3210adef 3import PIL
bdaf6b46 4import datetime
3210adef
MT
5import imghdr
6import io
e96e445b 7import ldap
2cd9af74 8import logging
3210adef 9import os.path
2cd9af74 10import tornado.web
2dac7110 11import urllib.parse
2cd9af74 12
0099c2a7
MT
13from .. import countries
14
9b8ff27d 15from . import auth
124a8404 16from . import base
786e9ca8
MT
17from . import ui_modules
18
3210adef
MT
19COLOUR_LIGHT = (237,232,232)
20COLOUR_DARK = (49,53,60)
21
9b8ff27d 22class IndexHandler(auth.CacheMixin, base.BaseHandler):
786e9ca8
MT
23 @tornado.web.authenticated
24 def get(self):
15bb44ee 25 self.render("people/index.html")
786e9ca8 26
2cd9af74 27
c4f1a618 28class AvatarHandler(base.BaseHandler):
0cddf220 29 def get(self, uid):
2cd9af74 30 # Get the desired size of the avatar file
a0f767a7 31 size = self.get_argument("size", None)
2cd9af74
MT
32
33 try:
34 size = int(size)
35 except (TypeError, ValueError):
36 size = None
37
0cddf220 38 logging.debug("Querying for avatar of %s" % uid)
2cd9af74 39
f6ed3d4d 40 # Fetch user account
0cddf220 41 account = self.backend.accounts.get_by_uid(uid)
f6ed3d4d 42 if not account:
0cddf220 43 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
2cd9af74 44
e2998441
MT
45 # Allow downstream to cache this for a year
46 self.set_expires(31536000)
2cd9af74 47
f6ed3d4d
MT
48 # Resize avatar
49 avatar = account.get_avatar(size)
2cd9af74 50
f6ed3d4d
MT
51 # If there is no avatar, we serve a default image
52 if not avatar:
0cddf220
MT
53 logging.debug("No avatar uploaded for %s" % account)
54
3210adef
MT
55 # Generate a random avatar with only one letter
56 avatar = self._get_avatar(account, size=size)
2cd9af74 57
020de883
MT
58 # Guess content type
59 type = imghdr.what(None, avatar)
60
f6ed3d4d 61 # Set headers about content
020de883
MT
62 self.set_header("Content-Disposition", "inline; filename=\"%s.%s\"" % (account.uid, type))
63 self.set_header("Content-Type", "image/%s" % type)
2cd9af74 64
f6ed3d4d 65 # Deliver payload
2cd9af74 66 self.finish(avatar)
786e9ca8 67
3210adef
MT
68 def _get_avatar(self, account, size=None, **args):
69 letter = ("%s" % account)[0].upper()
70
71 if size is None:
72 size = 256
73
74 # The generated avatar cannot be larger than 1024px
75 if size >= 2048:
76 size = 2048
77
78 # Cache key
79 key = "avatar:letter:%s:%s" % (letter, size)
80
81 # Fetch avatar from the cache
82 avatar = self.memcached.get(key)
83 if not avatar:
84 avatar = self._make_avatar(letter, size=size, **args)
85
86 # Cache for forever
87 self.memcached.set(key, avatar)
88
89 return avatar
90
91 def _make_avatar(self, letter, format="PNG", size=None, **args):
92 # Generate an image of the correct size
93 image = PIL.Image.new("RGBA", (size, size), COLOUR_LIGHT)
94
95 # Have a canvas
96 draw = PIL.ImageDraw.Draw(image)
97
98 # Load font
99 font = PIL.ImageFont.truetype(os.path.join(
100 self.application.settings.get("static_path", ""),
332e5878 101 "fonts/Prompt-Bold.ttf"
3210adef
MT
102 ), size, encoding="unic")
103
104 # Determine size of the printed letter
105 w, h = font.getsize(letter)
106
107 # Mukta seems to be very broken and the height needs to be corrected
108 h //= 0.7
109
110 # Draw the letter in the center
111 draw.text(((size - w) / 2, (size - h) / 2), letter,
112 font=font, fill=COLOUR_DARK)
113
114 with io.BytesIO() as f:
115 # If writing out the image does not work with optimization,
116 # we try to write it out without any optimization.
117 try:
118 image.save(f, format, optimize=True, **args)
119 except:
120 image.save(f, format, **args)
121
122 return f.getvalue()
123
786e9ca8 124
9b8ff27d 125class CallsHandler(auth.CacheMixin, base.BaseHandler):
bdaf6b46
MT
126 @tornado.web.authenticated
127 def get(self, uid, date=None):
128 account = self.backend.accounts.get_by_uid(uid)
129 if not account:
130 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
131
d561d931
MT
132 # Check for permissions
133 if not account.can_be_managed_by(self.current_user):
134 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
135
bdaf6b46 136 if date:
44b4640b
MT
137 try:
138 date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
139 except ValueError:
140 raise tornado.web.HTTPError(400, "Invalid date: %s" % date)
bdaf6b46
MT
141 else:
142 date = datetime.date.today()
143
144 self.render("people/calls.html", account=account, date=date)
145
146
9b8ff27d 147class CallHandler(auth.CacheMixin, base.BaseHandler):
68ece434
MT
148 @tornado.web.authenticated
149 def get(self, uid, uuid):
d09d554b
MT
150 account = self.backend.accounts.get_by_uid(uid)
151 if not account:
152 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
153
d561d931
MT
154 # Check for permissions
155 if not account.can_be_managed_by(self.current_user):
156 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
157
68ece434
MT
158 call = self.backend.talk.freeswitch.get_call_by_uuid(uuid)
159 if not call:
160 raise tornado.web.HTTPError(404, "Could not find call %s" % uuid)
161
162 # XXX limit
163
d09d554b 164 self.render("people/call.html", account=account, call=call)
68ece434
MT
165
166
9b8ff27d 167class ConferencesHandler(auth.CacheMixin, base.BaseHandler):
30aeccdb
MT
168 @tornado.web.authenticated
169 def get(self):
170 self.render("people/conferences.html", conferences=self.backend.talk.conferences)
171
172
18b13823
MT
173class GroupsHandler(auth.CacheMixin, base.BaseHandler):
174 @tornado.web.authenticated
175 def get(self):
176 # Only staff can see other groups
177 if not self.current_user.is_staff():
178 raise tornado.web.HTTPError(403)
179
180 self.render("people/groups.html")
181
182
bef47ee8
MT
183class GroupHandler(auth.CacheMixin, base.BaseHandler):
184 @tornado.web.authenticated
185 def get(self, gid):
186 # Only staff can see other groups
187 if not self.current_user.is_staff():
188 raise tornado.web.HTTPError(403)
189
190 # Fetch group
191 group = self.backend.groups.get_by_gid(gid)
192 if not group:
193 raise tornado.web.HTTPError(404, "Could not find group %s" % gid)
194
195 self.render("people/group.html", group=group)
196
197
9b8ff27d 198class SearchHandler(auth.CacheMixin, base.BaseHandler):
786e9ca8
MT
199 @tornado.web.authenticated
200 def get(self):
201 q = self.get_argument("q")
202
203 # Perform the search
51907e45 204 accounts = self.backend.accounts.search(q)
786e9ca8
MT
205
206 # Redirect when only one result was found
207 if len(accounts) == 1:
208 self.redirect("/users/%s" % accounts[0].uid)
209 return
210
211 self.render("people/search.html", q=q, accounts=accounts)
212
f4672785 213
226d2676
MT
214class StatsHandler(auth.CacheMixin, base.BaseHandler):
215 @tornado.web.authenticated
216 def get(self):
b8e992c4
MT
217 # Only staff can see stats
218 if not self.current_user.is_staff():
219 raise tornado.web.HTTPError(403)
220
226d2676
MT
221 self.render("people/stats.html")
222
223
92c4b559 224class SubscribeHandler(auth.CacheMixin, base.BaseHandler):
92c4b559
MT
225 @tornado.web.authenticated
226 def post(self):
227 # Give consent
228 self.current_user.consents_to_promotional_emails = True
229
230 self.render("people/subscribed.html")
231
232
233class UnsubscribeHandler(auth.CacheMixin, base.BaseHandler):
234 @tornado.web.authenticated
235 def get(self):
236 if self.current_user.consents_to_promotional_emails:
237 return self.render("people/unsubscribe.html")
238
239 self.render("people/unsubscribed.html")
240
241 @tornado.web.authenticated
242 def post(self):
243 # Withdraw consent
244 self.current_user.consents_to_promotional_emails = False
245
246 self.render("people/unsubscribed.html")
247
248
9b8ff27d 249class SIPHandler(auth.CacheMixin, base.BaseHandler):
e0daee8f
MT
250 @tornado.web.authenticated
251 def get(self, uid):
252 account = self.backend.accounts.get_by_uid(uid)
253 if not account:
254 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
255
256 # Check for permissions
257 if not account.can_be_managed_by(self.current_user):
258 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
259
260 self.render("people/sip.html", account=account)
261
262
9b8ff27d 263class UsersHandler(auth.CacheMixin, base.BaseHandler):
786e9ca8
MT
264 @tornado.web.authenticated
265 def get(self):
71a3109c
MT
266 # Only staff can see other users
267 if not self.current_user.is_staff():
268 raise tornado.web.HTTPError(403)
269
786e9ca8
MT
270 self.render("people/users.html")
271
272
9b8ff27d 273class UserHandler(auth.CacheMixin, base.BaseHandler):
786e9ca8
MT
274 @tornado.web.authenticated
275 def get(self, uid):
276 account = self.backend.accounts.get_by_uid(uid)
277 if not account:
278 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
279
280 self.render("people/user.html", account=account)
281
282
9b8ff27d 283class UserEditHandler(auth.CacheMixin, base.BaseHandler):
e96e445b
MT
284 @tornado.web.authenticated
285 def get(self, uid):
286 account = self.backend.accounts.get_by_uid(uid)
287 if not account:
288 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
289
290 # Check for permissions
291 if not account.can_be_managed_by(self.current_user):
292 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
293
0099c2a7 294 self.render("people/user-edit.html", account=account, countries=countries.get_all())
e96e445b
MT
295
296 @tornado.web.authenticated
297 def post(self, uid):
298 account = self.backend.accounts.get_by_uid(uid)
299 if not account:
300 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
301
302 # Check for permissions
303 if not account.can_be_managed_by(self.current_user):
304 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
305
306 # Unfortunately this cannot be wrapped into a transaction
307 try:
0099c2a7
MT
308 account.first_name = self.get_argument("first_name")
309 account.last_name = self.get_argument("last_name")
d6e57f73 310 account.nickname = self.get_argument("nickname", None)
0099c2a7
MT
311 account.street = self.get_argument("street", None)
312 account.city = self.get_argument("city", None)
313 account.postal_code = self.get_argument("postal_code", None)
314 account.country_code = self.get_argument("country_code", None)
1c4522dc 315 account.description = self.get_argument("description", None)
e96e445b 316
5cc10421
MT
317 # Avatar
318 try:
319 filename, data, mimetype = self.get_file("avatar")
320
321 if not mimetype.startswith("image/"):
322 raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype)
323
324 account.upload_avatar(data)
9bbf48b8 325 except TypeError:
5cc10421
MT
326 pass
327
e96e445b
MT
328 # Email
329 account.mail_routing_address = self.get_argument("mail_routing_address", None)
330
331 # Telephone
332 account.phone_numbers = self.get_argument("phone_numbers", "").splitlines()
333 account.sip_routing_address = self.get_argument("sip_routing_address", None)
334 except ldap.STRONG_AUTH_REQUIRED as e:
335 raise tornado.web.HTTPError(403, "%s" % e) from e
336
337 # Redirect back to user page
338 self.redirect("/users/%s" % account.uid)
339
340
9b8ff27d 341class UserPasswdHandler(auth.CacheMixin, base.BaseHandler):
3ea97943
MT
342 @tornado.web.authenticated
343 def get(self, uid):
344 account = self.backend.accounts.get_by_uid(uid)
345 if not account:
346 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
347
348 # Check for permissions
349 if not account.can_be_managed_by(self.current_user):
350 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
351
352 self.render("people/passwd.html", account=account)
353
354 @tornado.web.authenticated
355 def post(self, uid):
356 account = self.backend.accounts.get_by_uid(uid)
357 if not account:
358 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
359
360 # Check for permissions
361 if not account.can_be_managed_by(self.current_user):
362 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
363
364 # Get current password
365 password = self.get_argument("password")
366
367 # Get new password
368 password1 = self.get_argument("password1")
369 password2 = self.get_argument("password2")
370
371 # Passwords must match
372 if not password1 == password2:
373 raise tornado.web.HTTPError(400, "Passwords do not match")
374
375 # XXX Check password complexity
376
377 # Check if old password matches
378 if not account.check_password(password):
379 raise tornado.web.HTTPError(403, "Incorrect password for %s" % account)
380
381 # Save new password
382 account.passwd(password1)
383
384 # Redirect back to user's page
385 self.redirect("/users/%s" % account.uid)
386
387
2dac7110 388class SSODiscourse(auth.CacheMixin, base.BaseHandler):
880fd132
MT
389 @base.ratelimit(minutes=24*60, requests=100)
390 @tornado.web.authenticated
391 def get(self):
2dac7110
MT
392 # Fetch Discourse's parameters
393 sso = self.get_argument("sso")
394 sig = self.get_argument("sig")
395
396 # Decode payload
397 try:
880fd132 398 params = self.accounts.decode_discourse_payload(sso, sig)
2dac7110
MT
399
400 # Raise bad request if the signature is invalid
401 except ValueError:
402 raise tornado.web.HTTPError(400)
403
880fd132 404 # Redirect back if user is already logged in
2dac7110 405 args = {
880fd132
MT
406 "nonce" : params.get("nonce"),
407 "external_id" : self.current_user.uid,
2dac7110
MT
408
409 # Pass email address
880fd132 410 "email" : self.current_user.email,
2dac7110
MT
411 "require_activation" : "false",
412
413 # More details about the user
880fd132
MT
414 "username" : self.current_user.uid,
415 "name" : "%s" % self.current_user,
416 "bio" : self.current_user.description or "",
2dac7110
MT
417
418 # Avatar
880fd132 419 "avatar_url" : self.current_user.avatar_url(),
2dac7110
MT
420 "avatar_force_update" : "true",
421
422 # Send a welcome message
423 "suppress_welcome_message" : "false",
424
425 # Group memberships
880fd132
MT
426 "admin" : "true" if self.current_user.is_admin() else "false",
427 "moderator" : "true" if self.current_user.is_moderator() else "false",
2dac7110
MT
428 }
429
430 # Format payload and sign it
431 payload = self.accounts.encode_discourse_payload(**args)
432 signature = self.accounts.sign_discourse_payload(payload)
433
434 qs = urllib.parse.urlencode({
435 "sso" : payload,
436 "sig" : signature,
437 })
438
439 # Redirect user
880fd132 440 self.redirect("%s?%s" % (params.get("return_sso_url"), qs))
2dac7110
MT
441
442
9150881e
MT
443class NewAccountsModule(ui_modules.UIModule):
444 def render(self, days=14):
445 t = datetime.datetime.utcnow() - datetime.timedelta(days=days)
446
447 # Fetch all accounts created after t
448 accounts = self.backend.accounts.get_created_after(t)
449
450 accounts.sort(key=lambda a: a.created_at, reverse=True)
451
452 return self.render_string("people/modules/accounts-new.html",
453 accounts=accounts, t=t)
454
455
786e9ca8
MT
456class AccountsListModule(ui_modules.UIModule):
457 def render(self, accounts=None):
458 if accounts is None:
51907e45 459 accounts = self.backend.accounts
786e9ca8
MT
460
461 return self.render_string("people/modules/accounts-list.html", accounts=accounts)
462
463
c66f2152
MT
464class AgentModule(ui_modules.UIModule):
465 def render(self, account):
466 return self.render_string("people/modules/agent.html", account=account)
467
468
786e9ca8 469class CDRModule(ui_modules.UIModule):
bdaf6b46
MT
470 def render(self, account, date=None, limit=None):
471 cdr = account.get_cdr(date=date, limit=limit)
786e9ca8 472
89e47299
MT
473 return self.render_string("people/modules/cdr.html",
474 account=account, cdr=list(cdr))
786e9ca8
MT
475
476
477class ChannelsModule(ui_modules.UIModule):
478 def render(self, account):
1f38be5a
MT
479 return self.render_string("people/modules/channels.html",
480 account=account, channels=account.sip_channels)
786e9ca8
MT
481
482
68ece434
MT
483class MOSModule(ui_modules.UIModule):
484 def render(self, call):
485 return self.render_string("people/modules/mos.html", call=call)
486
487
b5e2077f 488class PasswordModule(ui_modules.UIModule):
56894a8b 489 def render(self, account=None):
b5e2077f
MT
490 return self.render_string("people/modules/password.html", account=account)
491
492 def javascript_files(self):
493 return "js/zxcvbn.js"
494
495 def embedded_javascript(self):
496 return self.render_string("people/modules/password.js")
497
498
786e9ca8
MT
499class RegistrationsModule(ui_modules.UIModule):
500 def render(self, account):
501 return self.render_string("people/modules/registrations.html", account=account)