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