comparison roundup/cgi/actions.py @ 6066:b011e5ac06d5

Flake8 whitespace; add translate; change use 'is None' not = Changed out some strings like: _("foo bar bas....") into _( "foo" "bar bas") to handle long lines. Verified both forms result in the same string extraction using locale tools xgettext/xpot. Added a couple of translation marks.
author John Rouillard <rouilj@ieee.org>
date Sat, 18 Jan 2020 20:27:02 -0500
parents 8128ca0cb764
children f74d078cfd9a
comparison
equal deleted inserted replaced
6065:1ec4aa670b0c 6066:b011e5ac06d5
12 from roundup.anypy.strings import StringIO 12 from roundup.anypy.strings import StringIO
13 import roundup.anypy.random_ as random_ 13 import roundup.anypy.random_ as random_
14 14
15 from roundup.anypy.html import html_escape 15 from roundup.anypy.html import html_escape
16 16
17 import time
18 from datetime import timedelta 17 from datetime import timedelta
19 18
20 # Also add action to client.py::Client.actions property 19 # Also add action to client.py::Client.actions property
21 __all__ = ['Action', 'ShowAction', 'RetireAction', 'RestoreAction', 'SearchAction', 20 __all__ = ['Action', 'ShowAction', 'RetireAction', 'RestoreAction',
21 'SearchAction',
22 'EditCSVAction', 'EditItemAction', 'PassResetAction', 22 'EditCSVAction', 'EditItemAction', 'PassResetAction',
23 'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction', 23 'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
24 'NewItemAction', 'ExportCSVAction', 'ExportCSVWithIdAction'] 24 'NewItemAction', 'ExportCSVAction', 'ExportCSVWithIdAction']
25 25
26 # used by a couple of routines 26 # used by a couple of routines
27 chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 27 chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
28
28 29
29 class Action: 30 class Action:
30 def __init__(self, client): 31 def __init__(self, client):
31 self.client = client 32 self.client = client
32 self.form = client.form 33 self.form = client.form
36 self.classname = client.classname 37 self.classname = client.classname
37 self.userid = client.userid 38 self.userid = client.userid
38 self.base = client.base 39 self.base = client.base
39 self.user = client.user 40 self.user = client.user
40 self.context = templating.context(client) 41 self.context = templating.context(client)
41 self.loginLimit = RateLimit(client.db.config.WEB_LOGIN_ATTEMPTS_MIN, timedelta(seconds=60)) 42 self.loginLimit = RateLimit(client.db.config.WEB_LOGIN_ATTEMPTS_MIN,
43 timedelta(seconds=60))
42 44
43 def handle(self): 45 def handle(self):
44 """Action handler procedure""" 46 """Action handler procedure"""
45 raise NotImplementedError 47 raise NotImplementedError
46 48
94 if self.base: 96 if self.base:
95 parsed_base_url_tuple = urllib_.urlparse(self.base) 97 parsed_base_url_tuple = urllib_.urlparse(self.base)
96 else: 98 else:
97 raise ValueError(self._("Base url not set. Check configuration.")) 99 raise ValueError(self._("Base url not set. Check configuration."))
98 100
99 info={ 'url': url, 101 info = {'url': url,
100 'base_url': self.base, 102 'base_url': self.base,
101 'base_scheme': parsed_base_url_tuple.scheme, 103 'base_scheme': parsed_base_url_tuple.scheme,
102 'base_netloc': parsed_base_url_tuple.netloc, 104 'base_netloc': parsed_base_url_tuple.netloc,
103 'base_path': parsed_base_url_tuple.path, 105 'base_path': parsed_base_url_tuple.path,
104 'url_scheme': parsed_url_tuple.scheme, 106 'url_scheme': parsed_url_tuple.scheme,
105 'url_netloc': parsed_url_tuple.netloc, 107 'url_netloc': parsed_url_tuple.netloc,
106 'url_path': parsed_url_tuple.path, 108 'url_path': parsed_url_tuple.path,
107 'url_params': parsed_url_tuple.params, 109 'url_params': parsed_url_tuple.params,
108 'url_query': parsed_url_tuple.query, 110 'url_query': parsed_url_tuple.query,
109 'url_fragment': parsed_url_tuple.fragment } 111 'url_fragment': parsed_url_tuple.fragment}
110 112
111 if parsed_base_url_tuple.scheme == "https": 113 if parsed_base_url_tuple.scheme == "https":
112 if parsed_url_tuple.scheme != "https": 114 if parsed_url_tuple.scheme != "https":
113 raise ValueError(self._("Base url %(base_url)s requires https. Redirect url %(url)s uses http.")%info) 115 raise ValueError(self._("Base url %(base_url)s requires https."
116 " Redirect url %(url)s uses http.") %
117 info)
114 else: 118 else:
115 if parsed_url_tuple.scheme not in ('http', 'https'): 119 if parsed_url_tuple.scheme not in ('http', 'https'):
116 raise ValueError(self._("Unrecognized scheme in %(url)s")%info) 120 raise ValueError(self._("Unrecognized scheme in %(url)s") %
121 info)
117 122
118 if parsed_url_tuple.netloc != parsed_base_url_tuple.netloc: 123 if parsed_url_tuple.netloc != parsed_base_url_tuple.netloc:
119 raise ValueError(self._("Net location in %(url)s does not match base: %(base_netloc)s")%info) 124 raise ValueError(self._("Net location in %(url)s does not match "
125 "base: %(base_netloc)s") % info)
120 126
121 if parsed_url_tuple.path.find(parsed_base_url_tuple.path) != 0: 127 if parsed_url_tuple.path.find(parsed_base_url_tuple.path) != 0:
122 raise ValueError(self._("Base path %(base_path)s is not a prefix for url %(url)s")%info) 128 raise ValueError(self._("Base path %(base_path)s is not a "
129 "prefix for url %(url)s") % info)
123 130
124 # I am not sure if this has to be language sensitive. 131 # I am not sure if this has to be language sensitive.
125 # Do ranges depend on the LANG of the user?? 132 # Do ranges depend on the LANG of the user??
126 # Is there a newer spec for URI's than what I am referencing? 133 # Is there a newer spec for URI's than what I am referencing?
127 134
130 # quote it so it doesn't do anything bad otherwise we have a 137 # quote it so it doesn't do anything bad otherwise we have a
131 # different vector to handle. 138 # different vector to handle.
132 allowed_pattern = re.compile(r'''^[A-Za-z0-9@:/?._~%!$&'()*+,;=-]*$''') 139 allowed_pattern = re.compile(r'''^[A-Za-z0-9@:/?._~%!$&'()*+,;=-]*$''')
133 140
134 if not allowed_pattern.match(parsed_url_tuple.path): 141 if not allowed_pattern.match(parsed_url_tuple.path):
135 raise ValueError(self._("Path component (%(url_path)s) in %(url)s is not properly escaped")%info) 142 raise ValueError(self._("Path component (%(url_path)s) in %(url)s "
143 "is not properly escaped") % info)
136 144
137 if not allowed_pattern.match(parsed_url_tuple.params): 145 if not allowed_pattern.match(parsed_url_tuple.params):
138 raise ValueError(self._("Params component (%(url_params)s) in %(url)s is not properly escaped")%info) 146 raise ValueError(self._("Params component (%(url_params)s) in %(url)s is not properly escaped") % info)
139 147
140 if not allowed_pattern.match(parsed_url_tuple.query): 148 if not allowed_pattern.match(parsed_url_tuple.query):
141 raise ValueError(self._("Query component (%(url_query)s) in %(url)s is not properly escaped")%info) 149 raise ValueError(self._("Query component (%(url_query)s) in %(url)s is not properly escaped") % info)
142 150
143 if not allowed_pattern.match(parsed_url_tuple.fragment): 151 if not allowed_pattern.match(parsed_url_tuple.fragment):
144 raise ValueError(self._("Fragment component (%(url_fragment)s) in %(url)s is not properly escaped")%info) 152 raise ValueError(self._("Fragment component (%(url_fragment)s) in %(url)s is not properly escaped") % info)
145 153
146 return(urllib_.urlunparse(parsed_url_tuple)) 154 return(urllib_.urlunparse(parsed_url_tuple))
147 155
148 name = '' 156 name = ''
149 permissionType = None 157 permissionType = None
158
150 def permission(self): 159 def permission(self):
151 """Check whether the user has permission to execute this action. 160 """Check whether the user has permission to execute this action.
152 161
153 True by default. If the permissionType attribute is a string containing 162 True by default. If the permissionType attribute is a string containing
154 a simple permission, check whether the user has that permission. 163 a simple permission, check whether the user has that permission.
161 if (self.permissionType and 170 if (self.permissionType and
162 not self.hasPermission(self.permissionType)): 171 not self.hasPermission(self.permissionType)):
163 info = {'action': self.name, 'classname': self.classname} 172 info = {'action': self.name, 'classname': self.classname}
164 raise exceptions.Unauthorised(self._( 173 raise exceptions.Unauthorised(self._(
165 'You do not have permission to ' 174 'You do not have permission to '
166 '%(action)s the %(classname)s class.')%info) 175 '%(action)s the %(classname)s class.') % info)
167 176
168 _marker = [] 177 _marker = []
169 def hasPermission(self, permission, classname=_marker, itemid=None, property=None): 178
179 def hasPermission(self, permission, classname=_marker, itemid=None,
180 property=None):
170 """Check whether the user has 'permission' on the current class.""" 181 """Check whether the user has 'permission' on the current class."""
171 if classname is self._marker: 182 if classname is self._marker:
172 classname = self.client.classname 183 classname = self.client.classname
173 return self.db.security.hasPermission(permission, self.client.userid, 184 return self.db.security.hasPermission(permission, self.client.userid,
174 classname=classname, itemid=itemid, property=property) 185 classname=classname,
186 itemid=itemid, property=property)
175 187
176 def gettext(self, msgid): 188 def gettext(self, msgid):
177 """Return the localized translation of msgid""" 189 """Return the localized translation of msgid"""
178 return self.client.translator.gettext(msgid) 190 return self.client.translator.gettext(msgid)
179 191
180 _ = gettext 192 _ = gettext
181 193
194
182 class ShowAction(Action): 195 class ShowAction(Action):
183 196
184 typere=re.compile('[@:]type') 197 typere = re.compile('[@:]type')
185 numre=re.compile('[@:]number') 198 numre = re.compile('[@:]number')
186 199
187 def handle(self): 200 def handle(self):
188 """Show a node of a particular class/id.""" 201 """Show a node of a particular class/id."""
189 t = n = '' 202 t = n = ''
190 for key in self.form: 203 for key in self.form:
199 try: 212 try:
200 int(n) 213 int(n)
201 except ValueError: 214 except ValueError:
202 d = {'input': n, 'classname': t} 215 d = {'input': n, 'classname': t}
203 raise exceptions.SeriousError(self._( 216 raise exceptions.SeriousError(self._(
204 '"%(input)s" is not an ID (%(classname)s ID required)')%d) 217 '"%(input)s" is not an ID (%(classname)s ID required)') % d)
205 url = '%s%s%s'%(self.base, t, n) 218 url = '%s%s%s' % (self.base, t, n)
206 raise exceptions.Redirect(url) 219 raise exceptions.Redirect(url)
220
207 221
208 class RetireAction(Action): 222 class RetireAction(Action):
209 name = 'retire' 223 name = 'retire'
210 permissionType = 'Edit' 224 permissionType = 'Edit'
211 225
237 # do the retire 251 # do the retire
238 self.db.getclass(self.classname).retire(itemid) 252 self.db.getclass(self.classname).retire(itemid)
239 self.db.commit() 253 self.db.commit()
240 254
241 self.client.add_ok_message( 255 self.client.add_ok_message(
242 self._('%(classname)s %(itemid)s has been retired')%{ 256 self._('%(classname)s %(itemid)s has been retired') % {
243 'classname': self.classname.capitalize(), 'itemid': itemid}) 257 'classname': self.classname.capitalize(), 'itemid': itemid})
244 258
245 259
246 class RestoreAction(Action): 260 class RestoreAction(Action):
247 name = 'restore' 261 name = 'restore'
259 if self.template == 'index': 273 if self.template == 'index':
260 self.client.nodeid = None 274 self.client.nodeid = None
261 275
262 # check permission 276 # check permission
263 if not self.hasPermission('Restore', classname=self.classname, 277 if not self.hasPermission('Restore', classname=self.classname,
264 itemid=itemid): 278 itemid=itemid):
265 raise exceptions.Unauthorised(self._( 279 raise exceptions.Unauthorised(self._(
266 'You do not have permission to restore %(class)s' 280 'You do not have permission to restore %(class)s'
267 ) % {'class': self.classname}) 281 ) % {'class': self.classname})
268 282
269 # do the restore 283 # do the restore
270 self.db.getclass(self.classname).restore(itemid) 284 self.db.getclass(self.classname).restore(itemid)
271 self.db.commit() 285 self.db.commit()
272 286
273 self.client.add_ok_message( 287 self.client.add_ok_message(
274 self._('%(classname)s %(itemid)s has been restored')%{ 288 self._('%(classname)s %(itemid)s has been restored') % {
275 'classname': self.classname.capitalize(), 'itemid': itemid}) 289 'classname': self.classname.capitalize(), 'itemid': itemid})
276 290
277 291
278 class SearchAction(Action): 292 class SearchAction(Action):
279 name = 'search' 293 name = 'search'
320 # create a query 334 # create a query
321 if not self.hasPermission('Create', 'query'): 335 if not self.hasPermission('Create', 'query'):
322 raise exceptions.Unauthorised(self._( 336 raise exceptions.Unauthorised(self._(
323 "You do not have permission to store queries")) 337 "You do not have permission to store queries"))
324 qid = self.db.query.create(name=queryname, 338 qid = self.db.query.create(name=queryname,
325 klass=self.classname, url=url) 339 klass=self.classname, url=url)
326 else: 340 else:
327 uid = self.db.getuid() 341 uid = self.db.getuid()
328 342
329 # if the queryname is being changed from the old 343 # if the queryname is being changed from the old
330 # (original) value, make sure new queryname is not 344 # (original) value, make sure new queryname is not
339 for qid in qids: 353 for qid in qids:
340 # require an exact name match 354 # require an exact name match
341 if queryname != self.db.query.get(qid, 'name'): 355 if queryname != self.db.query.get(qid, 'name'):
342 continue 356 continue
343 # whoops we found a duplicate; report error and return 357 # whoops we found a duplicate; report error and return
344 message=_("You already own a query named '%s'. Please choose another name.")%(queryname) 358 message = _("You already own a query named '%s'. "
359 "Please choose another name.") % \
360 (queryname)
345 self.client.add_error_message(message) 361 self.client.add_error_message(message)
346 return 362 return
347 363
348 # edit the new way, query name not a key any more 364 # edit the new way, query name not a key any more
349 # see if we match an existing private query 365 # see if we match an existing private query
350 qids = self.db.query.filter(None, {'name': old_queryname, 366 qids = self.db.query.filter(None, {'name': old_queryname,
351 'private_for': uid}) 367 'private_for': uid})
352 if not qids: 368 if not qids:
353 # ok, so there's not a private query for the current user 369 # ok, so there's not a private query for the current user
354 # - see if there's one created by them 370 # - see if there's one created by them
355 qids = self.db.query.filter(None, {'name': old_queryname, 371 qids = self.db.query.filter(None, {'name': old_queryname,
356 'creator': uid}) 372 'creator': uid})
357 373
358 if qids and old_queryname: 374 if qids and old_queryname:
359 # edit query - make sure we get an exact match on the name 375 # edit query - make sure we get an exact match on the name
360 for qid in qids: 376 for qid in qids:
361 if old_queryname != self.db.query.get(qid, 'name'): 377 if old_queryname != self.db.query.get(qid, 'name'):
362 continue 378 continue
363 if not self.hasPermission('Edit', 'query', itemid=qid): 379 if not self.hasPermission('Edit', 'query', itemid=qid):
364 raise exceptions.Unauthorised(self._( 380 raise exceptions.Unauthorised(self._(
365 "You do not have permission to edit queries")) 381 "You do not have permission to edit queries"))
366 self.db.query.set(qid, klass=self.classname, 382 self.db.query.set(qid, klass=self.classname,
367 url=url, name=queryname) 383 url=url, name=queryname)
368 else: 384 else:
369 # create a query 385 # create a query
370 if not self.hasPermission('Create', 'query'): 386 if not self.hasPermission('Create', 'query'):
371 raise exceptions.Unauthorised(self._( 387 raise exceptions.Unauthorised(self._(
372 "You do not have permission to store queries")) 388 "You do not have permission to store queries"))
373 qid = self.db.query.create(name=queryname, 389 qid = self.db.query.create(name=queryname,
374 klass=self.classname, url=url, private_for=uid) 390 klass=self.classname, url=url,
391 private_for=uid)
375 392
376 # and add it to the user's query multilink 393 # and add it to the user's query multilink
377 queries = self.db.user.get(self.userid, 'queries') 394 queries = self.db.user.get(self.userid, 'queries')
378 if qid not in queries: 395 if qid not in queries:
379 queries.append(qid) 396 queries.append(qid)
388 req.form.list.append( 405 req.form.list.append(
389 cgi.MiniFieldStorage( 406 cgi.MiniFieldStorage(
390 "@dispname", queryname 407 "@dispname", queryname
391 ) 408 )
392 ) 409 )
393
394 410
395 def fakeFilterVars(self): 411 def fakeFilterVars(self):
396 """Add a faked :filter form variable for each filtering prop.""" 412 """Add a faked :filter form variable for each filtering prop."""
397 cls = self.db.classes[self.classname] 413 cls = self.db.classes[self.classname]
398 for key in self.form: 414 for key in self.form:
420 self.form.value.append(cgi.MiniFieldStorage(key, v)) 436 self.form.value.append(cgi.MiniFieldStorage(key, v))
421 elif isinstance(prop, hyperdb.Number): 437 elif isinstance(prop, hyperdb.Number):
422 try: 438 try:
423 float(self.form[key].value) 439 float(self.form[key].value)
424 except ValueError: 440 except ValueError:
425 raise exceptions.FormError("Invalid number: "+self.form[key].value) 441 raise exceptions.FormError(_("Invalid number: ") +
442 self.form[key].value)
426 elif isinstance(prop, hyperdb.Integer): 443 elif isinstance(prop, hyperdb.Integer):
427 try: 444 try:
428 val=self.form[key].value 445 val = self.form[key].value
429 if ( str(int(val)) == val ): 446 if (str(int(val)) == val):
430 pass 447 pass
431 else: 448 else:
432 raise ValueError 449 raise ValueError
433 except ValueError: 450 except ValueError:
434 raise exceptions.FormError("Invalid integer: "+val) 451 raise exceptions.FormError(_("Invalid integer: ") +
452 val)
435 453
436 self.form.value.append(cgi.MiniFieldStorage('@filter', key)) 454 self.form.value.append(cgi.MiniFieldStorage('@filter', key))
437 455
438 def getCurrentURL(self, req): 456 def getCurrentURL(self, req):
439 """Get current URL for storing as a query. 457 """Get current URL for storing as a query.
445 We now store the template with the query if the template name is 463 We now store the template with the query if the template name is
446 different from 'index' 464 different from 'index'
447 """ 465 """
448 template = self.getFromForm('template') 466 template = self.getFromForm('template')
449 if template and template != 'index': 467 if template and template != 'index':
450 return req.indexargs_url('', {'@template' : template})[1:] 468 return req.indexargs_url('', {'@template': template})[1:]
451 return req.indexargs_url('', {})[1:] 469 return req.indexargs_url('', {})[1:]
452 470
453 def getFromForm(self, name): 471 def getFromForm(self, name):
454 for key in ('@' + name, ':' + name): 472 for key in ('@' + name, ':' + name):
455 if key in self.form: 473 if key in self.form:
456 return self.form[key].value.strip() 474 return self.form[key].value.strip()
457 return '' 475 return ''
458 476
459 def getQueryName(self): 477 def getQueryName(self):
460 return self.getFromForm('queryname') 478 return self.getFromForm('queryname')
479
461 480
462 class EditCSVAction(Action): 481 class EditCSVAction(Action):
463 name = 'edit' 482 name = 'edit'
464 permissionType = 'Edit' 483 permissionType = 'Edit'
465 484
517 exists = 1 536 exists = 1
518 537
519 # confirm correct weight 538 # confirm correct weight
520 if len(props_without_id) != len(values): 539 if len(props_without_id) != len(values):
521 self.client.add_error_message( 540 self.client.add_error_message(
522 self._('Not enough values on line %(line)s')%{'line':line}) 541 self._('Not enough values on line %(line)s') % \
542 {'line':line})
523 return 543 return
524 544
525 # extract the new values 545 # extract the new values
526 d = {} 546 d = {}
527 for name, value in zip(props_without_id, values): 547 for name, value in zip(props_without_id, values):
528 # check permission to edit this property on this item 548 # check permission to edit this property on this item
529 if exists and not self.hasPermission('Edit', itemid=itemid, 549 if exists and not self.hasPermission('Edit', itemid=itemid,
530 classname=self.classname, property=name): 550 classname=self.classname, property=name):
531 raise exceptions.Unauthorised(self._( 551 raise exceptions.Unauthorised(self._(
532 'You do not have permission to edit %(class)s' 552 'You do not have permission to edit %(class)s'
533 ) % {'class': self.classname}) 553 ) % {'class': self.classname})
534 554
535 prop = cl.properties[name] 555 prop = cl.properties[name]
573 # retire the removed entries 593 # retire the removed entries
574 for itemid in cl.list(): 594 for itemid in cl.list():
575 if itemid not in found: 595 if itemid not in found:
576 # check permission to retire this item 596 # check permission to retire this item
577 if not self.hasPermission('Retire', itemid=itemid, 597 if not self.hasPermission('Retire', itemid=itemid,
578 classname=self.classname): 598 classname=self.classname):
579 raise exceptions.Unauthorised(self._( 599 raise exceptions.Unauthorised(self._(
580 'You do not have permission to retire %(class)s' 600 'You do not have permission to retire %(class)s'
581 ) % {'class': self.classname}) 601 ) % {'class': self.classname})
582 cl.retire(itemid) 602 cl.retire(itemid)
583 603
584 # all OK 604 # all OK
585 self.db.commit() 605 self.db.commit()
586 606
587 self.client.add_ok_message(self._('Items edited OK')) 607 self.client.add_ok_message(self._('Items edited OK'))
608
588 609
589 class EditCommon(Action): 610 class EditCommon(Action):
590 '''Utility methods for editing.''' 611 '''Utility methods for editing.'''
591 612
592 def _editnodes(self, all_props, all_links): 613 def _editnodes(self, all_props, all_links):
595 ''' 616 '''
596 # figure dependencies and re-work links 617 # figure dependencies and re-work links
597 deps = {} 618 deps = {}
598 links = {} 619 links = {}
599 for cn, nodeid, propname, vlist in all_links: 620 for cn, nodeid, propname, vlist in all_links:
600 numeric_id = int (nodeid or 0) 621 numeric_id = int(nodeid or 0)
601 if not (numeric_id > 0 or (cn, nodeid) in all_props): 622 if not (numeric_id > 0 or (cn, nodeid) in all_props):
602 # link item to link to doesn't (and won't) exist 623 # link item to link to doesn't (and won't) exist
603 continue 624 continue
604 625
605 for value in vlist: 626 for value in vlist:
642 # and some nice feedback for the user 663 # and some nice feedback for the user
643 if props: 664 if props:
644 info = ', '.join(map(self._, props)) 665 info = ', '.join(map(self._, props))
645 m.append( 666 m.append(
646 self._('%(class)s %(id)s %(properties)s edited ok') 667 self._('%(class)s %(id)s %(properties)s edited ok')
647 % {'class':cn, 'id':nodeid, 'properties':info}) 668 % {'class': cn, 'id': nodeid, 'properties': info})
648 else: 669 else:
649 # this used to produce a message like: 670 # this used to produce a message like:
650 # issue34 - nothing changed 671 # issue34 - nothing changed
651 # which is confusing if only quiet properties 672 # which is confusing if only quiet properties
652 # changed for the class/id. So don't report 673 # changed for the class/id. So don't report
653 # anything is the user didn't explicitly change 674 # anything if the user didn't explicitly change
654 # a visible (non-quiet) property. 675 # a visible (non-quiet) property.
655 pass 676 pass
656 else: 677 else:
657 # make a new node 678 # make a new node
658 newid = self._createnode(cn, props) 679 newid = self._createnode(cn, props)
660 self.nodeid = newid 681 self.nodeid = newid
661 nodeid = newid 682 nodeid = newid
662 683
663 # and some nice feedback for the user 684 # and some nice feedback for the user
664 m.append(self._('%(class)s %(id)s created') 685 m.append(self._('%(class)s %(id)s created')
665 % {'class':cn, 'id':newid}) 686 % {'class': cn, 'id': newid})
666 687
667 # fill in new ids in links 688 # fill in new ids in links
668 if needed in links: 689 if needed in links:
669 for linkcn, linkid, linkprop in links[needed]: 690 for linkcn, linkid, linkprop in links[needed]:
670 props = all_props[(linkcn, linkid)] 691 props = all_props[(linkcn, linkid)]
718 """Check whether a user is editing his/her own details.""" 739 """Check whether a user is editing his/her own details."""
719 return (self.nodeid == self.userid 740 return (self.nodeid == self.userid
720 and self.db.user.get(self.nodeid, 'username') != 'anonymous') 741 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
721 742
722 _cn_marker = [] 743 _cn_marker = []
744
723 def editItemPermission(self, props, classname=_cn_marker, itemid=None): 745 def editItemPermission(self, props, classname=_cn_marker, itemid=None):
724 """Determine whether the user has permission to edit this item.""" 746 """Determine whether the user has permission to edit this item."""
725 if itemid is None: 747 if itemid is None:
726 itemid = self.nodeid 748 itemid = self.nodeid
727 if classname is self._cn_marker: 749 if classname is self._cn_marker:
728 classname = self.classname 750 classname = self.classname
729 # The user must have permission to edit each of the properties 751 # The user must have permission to edit each of the properties
730 # being changed. 752 # being changed.
731 for p in props: 753 for p in props:
732 if not self.hasPermission('Edit', itemid=itemid, 754 if not self.hasPermission('Edit', itemid=itemid,
733 classname=classname, property=p): 755 classname=classname, property=p):
734 return 0 756 return 0
735 # Since the user has permission to edit all of the properties, 757 # Since the user has permission to edit all of the properties,
736 # the edit is OK. 758 # the edit is OK.
737 return 1 759 return 1
738 760
741 763
742 Base behaviour is to check the user can edit this class. No additional 764 Base behaviour is to check the user can edit this class. No additional
743 property checks are made. 765 property checks are made.
744 """ 766 """
745 767
746 if not classname : 768 if not classname:
747 classname = self.client.classname 769 classname = self.client.classname
748 770
749 if not self.hasPermission('Create', classname=classname): 771 if not self.hasPermission('Create', classname=classname):
750 return 0 772 return 0
751 773
752 # Check Create permission for each property, to avoid being able 774 # Check Create permission for each property, to avoid being able
753 # to set restricted ones on new item creation 775 # to set restricted ones on new item creation
754 for key in props: 776 for key in props:
755 if not self.hasPermission('Create', classname=classname, 777 if not self.hasPermission('Create', classname=classname,
756 property=key): 778 property=key):
757 return 0 779 return 0
758 return 1 780 return 1
781
759 782
760 class EditItemAction(EditCommon): 783 class EditItemAction(EditCommon):
761 def lastUserActivity(self): 784 def lastUserActivity(self):
762 if ':lastactivity' in self.form: 785 if ':lastactivity' in self.form:
763 d = date.Date(self.form[':lastactivity'].value) 786 d = date.Date(self.form[':lastactivity'].value)
786 return [] 809 return []
787 810
788 def handleCollision(self, props): 811 def handleCollision(self, props):
789 message = self._('Edit Error: someone else has edited this %s (%s). ' 812 message = self._('Edit Error: someone else has edited this %s (%s). '
790 'View <a target="_blank" href="%s%s">their changes</a> ' 813 'View <a target="_blank" href="%s%s">their changes</a> '
791 'in a new window.')%(self.classname, ', '.join(props), 814 'in a new window.') % (self.classname, ', '.join(props),
792 self.classname, self.nodeid) 815 self.classname, self.nodeid)
793 self.client.add_error_message(message, escape=False) 816 self.client.add_error_message(message, escape=False)
794 return 817 return
795 818
796 def handle(self): 819 def handle(self):
797 """Perform an edit of an item in the database. 820 """Perform an edit of an item in the database.
829 url = self.base + self.classname 852 url = self.base + self.classname
830 # note that this action might have been called by an index page, so 853 # note that this action might have been called by an index page, so
831 # we will want to include index-page args in this URL too 854 # we will want to include index-page args in this URL too
832 if self.nodeid is not None: 855 if self.nodeid is not None:
833 url += self.nodeid 856 url += self.nodeid
834 url += '?@ok_message=%s&@template=%s'%(urllib_.quote(message), 857 url += '?@ok_message=%s&@template=%s' % (urllib_.quote(message),
835 urllib_.quote(self.template)) 858 urllib_.quote(self.template))
836 if self.nodeid is None: 859 if self.nodeid is None:
837 req = templating.HTMLRequest(self.client) 860 req = templating.HTMLRequest(self.client)
838 url += '&' + req.indexargs_url('', {})[1:] 861 url += '&' + req.indexargs_url('', {})[1:]
839 raise exceptions.Redirect(url) 862 raise exceptions.Redirect(url)
863
840 864
841 class NewItemAction(EditCommon): 865 class NewItemAction(EditCommon):
842 def handle(self): 866 def handle(self):
843 ''' Add a new item to the database. 867 ''' Add a new item to the database.
844 868
852 # parse the props from the form 876 # parse the props from the form
853 try: 877 try:
854 props, links = self.client.parsePropsFromForm(create=1) 878 props, links = self.client.parsePropsFromForm(create=1)
855 except (ValueError, KeyError) as message: 879 except (ValueError, KeyError) as message:
856 self.client.add_error_message(self._('Error: %s') 880 self.client.add_error_message(self._('Error: %s')
857 % str(message)) 881 % str(message))
858 return 882 return
859 883
860 # handle the props - edit or create 884 # handle the props - edit or create
861 try: 885 try:
862 # when it hits the None element, it'll set self.nodeid 886 # when it hits the None element, it'll set self.nodeid
871 # commit now that all the tricky stuff is done 895 # commit now that all the tricky stuff is done
872 self.db.commit() 896 self.db.commit()
873 897
874 # Allow an option to stay on the page to create new things 898 # Allow an option to stay on the page to create new things
875 if '__redirect_to' in self.form: 899 if '__redirect_to' in self.form:
876 raise exceptions.Redirect('%s&@ok_message=%s'%( 900 raise exceptions.Redirect('%s&@ok_message=%s' % (
877 self.examine_url(self.form['__redirect_to'].value), 901 self.examine_url(self.form['__redirect_to'].value),
878 urllib_.quote(messages))) 902 urllib_.quote(messages)))
879 903
880 # otherwise redirect to the new item's page 904 # otherwise redirect to the new item's page
881 raise exceptions.Redirect('%s%s%s?@ok_message=%s&@template=%s' % ( 905 raise exceptions.Redirect('%s%s%s?@ok_message=%s&@template=%s' % (
882 self.base, self.classname, self.nodeid, urllib_.quote(messages), 906 self.base, self.classname, self.nodeid, urllib_.quote(messages),
883 urllib_.quote(self.template))) 907 urllib_.quote(self.template)))
908
884 909
885 class PassResetAction(Action): 910 class PassResetAction(Action):
886 def handle(self): 911 def handle(self):
887 """Handle password reset requests. 912 """Handle password reset requests.
888 913
896 otk = self.form['otk'].value 921 otk = self.form['otk'].value
897 uid = otks.get(otk, 'uid', default=None) 922 uid = otks.get(otk, 'uid', default=None)
898 if uid is None: 923 if uid is None:
899 self.client.add_error_message( 924 self.client.add_error_message(
900 self._("Invalid One Time Key!\n" 925 self._("Invalid One Time Key!\n"
901 "(a Mozilla bug may cause this message " 926 "(a Mozilla bug may cause this message "
902 "to show up erroneously, please check your email)")) 927 "to show up erroneously, please check your email)"))
903 return 928 return
904 929
905 # pull the additional email address if exist 930 # pull the additional email address if exist
906 uaddress = otks.get(otk, 'uaddress', default=None) 931 uaddress = otks.get(otk, 'uaddress', default=None)
907 932
916 941
917 cl = self.db.user 942 cl = self.db.user
918 # XXX we need to make the "default" page be able to display errors! 943 # XXX we need to make the "default" page be able to display errors!
919 try: 944 try:
920 # set the password 945 # set the password
921 cl.set(uid, password=password.Password(newpw, config=self.db.config)) 946 cl.set(uid, password=password.Password(newpw,
947 config=self.db.config))
922 # clear the props from the otk database 948 # clear the props from the otk database
923 otks.destroy(otk) 949 otks.destroy(otk)
924 otks.commit() 950 otks.commit()
925 # commit the password change 951 # commit the password change
926 self.db.commit () 952 self.db.commit()
927 except (ValueError, KeyError) as message: 953 except (ValueError, KeyError) as message:
928 self.client.add_error_message(str(message)) 954 self.client.add_error_message(str(message))
929 return 955 return
930 956
931 # user info 957 # user info
935 else: 961 else:
936 address = uaddress 962 address = uaddress
937 963
938 # send the email 964 # send the email
939 tracker_name = self.db.config.TRACKER_NAME 965 tracker_name = self.db.config.TRACKER_NAME
940 subject = 'Password reset for %s'%tracker_name 966 subject = 'Password reset for %s' % tracker_name
941 body = ''' 967 body = '''
942 The password has been reset for username "%(name)s". 968 The password has been reset for username "%(name)s".
943 969
944 Your password is now: %(password)s 970 Your password is now: %(password)s
945 '''%{'name': name, 'password': newpw} 971 ''' % {'name': name, 'password': newpw}
946 if not self.client.standard_message([address], subject, body): 972 if not self.client.standard_message([address], subject, body):
947 return 973 return
948 974
949 self.client.add_ok_message( 975 self.client.add_ok_message(
950 self._('Password reset and email sent to %s') % address) 976 self._('Password reset and email sent to %s') % address)
979 otks.set(otk, uid=uid, uaddress=address) 1005 otks.set(otk, uid=uid, uaddress=address)
980 otks.commit() 1006 otks.commit()
981 1007
982 # send the email 1008 # send the email
983 tracker_name = self.db.config.TRACKER_NAME 1009 tracker_name = self.db.config.TRACKER_NAME
984 subject = 'Confirm reset of password for %s'%tracker_name 1010 subject = 'Confirm reset of password for %s' % tracker_name
985 body = ''' 1011 body = '''
986 Someone, perhaps you, has requested that the password be changed for your 1012 Someone, perhaps you, has requested that the password be changed for your
987 username, "%(name)s". If you wish to proceed with the change, please follow 1013 username, "%(name)s". If you wish to proceed with the change, please follow
988 the link below: 1014 the link below:
989 1015
990 %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s 1016 %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
991 1017
992 You should then receive another email with the new password. 1018 You should then receive another email with the new password.
993 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk} 1019 ''' % {'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
994 if not self.client.standard_message([address], subject, body): 1020 if not self.client.standard_message([address], subject, body):
995 return 1021 return
996 1022
997 if 'username' in self.form: 1023 if 'username' in self.form:
998 self.client.add_ok_message(self._('Email sent to primary notification address for %s.') % name) 1024 self.client.add_ok_message(self._('Email sent to primary notification address for %s.') % name)
999 else: 1025 else:
1000 self.client.add_ok_message(self._('Email sent to %s.') % address) 1026 self.client.add_ok_message(self._('Email sent to %s.') % address)
1027
1001 1028
1002 class RegoCommon(Action): 1029 class RegoCommon(Action):
1003 def finishRego(self): 1030 def finishRego(self):
1004 # log the new user in 1031 # log the new user in
1005 self.client.userid = self.userid 1032 self.client.userid = self.userid
1010 # update session data 1037 # update session data
1011 self.client.session_api.set(user=user) 1038 self.client.session_api.set(user=user)
1012 1039
1013 # nice message 1040 # nice message
1014 message = self._('You are now registered, welcome!') 1041 message = self._('You are now registered, welcome!')
1015 url = '%suser%s?@ok_message=%s'%(self.base, self.userid, 1042 url = '%suser%s?@ok_message=%s' % (self.base, self.userid,
1016 urllib_.quote(message)) 1043 urllib_.quote(message))
1017 1044
1018 # redirect to the user's page (but not 302, as some email clients seem 1045 # redirect to the user's page (but not 302, as some email clients seem
1019 # to want to reload the page, or something) 1046 # to want to reload the page, or something)
1020 return '''<html><head><title>%s</title></head> 1047 return '''<html><head><title>%s</title></head>
1021 <body><p><a href="%s">%s</a></p> 1048 <body><p><a href="%s">%s</a></p>
1022 <script nonce="%s" type="text/javascript"> 1049 <script nonce="%s" type="text/javascript">
1023 window.setTimeout('window.location = "%s"', 1000); 1050 window.setTimeout('window.location = "%s"', 1000);
1024 </script>'''%(message, url, message, 1051 </script>''' % (message, url, message,
1025 self.client.client_nonce, url) 1052 self.client.client_nonce, url)
1053
1026 1054
1027 class ConfRegoAction(RegoCommon): 1055 class ConfRegoAction(RegoCommon):
1028 def handle(self): 1056 def handle(self):
1029 """Grab the OTK, use it to load up the new user details.""" 1057 """Grab the OTK, use it to load up the new user details."""
1030 try: 1058 try:
1033 except (ValueError, KeyError) as message: 1061 except (ValueError, KeyError) as message:
1034 self.client.add_error_message(str(message)) 1062 self.client.add_error_message(str(message))
1035 return 1063 return
1036 return self.finishRego() 1064 return self.finishRego()
1037 1065
1066
1038 class RegisterAction(RegoCommon, EditCommon, Timestamped): 1067 class RegisterAction(RegoCommon, EditCommon, Timestamped):
1039 name = 'register' 1068 name = 'register'
1040 permissionType = 'Register' 1069 permissionType = 'Register'
1041 1070
1042 def handle(self): 1071 def handle(self):
1055 # disable the check. 1084 # disable the check.
1056 delaytime = self.db.config['WEB_REGISTRATION_DELAY'] 1085 delaytime = self.db.config['WEB_REGISTRATION_DELAY']
1057 1086
1058 if delaytime > 0: 1087 if delaytime > 0:
1059 self.timecheck('opaqueregister', delaytime) 1088 self.timecheck('opaqueregister', delaytime)
1060 1089
1061 # parse the props from the form 1090 # parse the props from the form
1062 try: 1091 try:
1063 props, links = self.client.parsePropsFromForm(create=1) 1092 props, links = self.client.parsePropsFromForm(create=1)
1064 except (ValueError, KeyError) as message: 1093 except (ValueError, KeyError) as message:
1065 self.client.add_error_message(self._('Error: %s') 1094 self.client.add_error_message(self._('Error: %s')
1066 % str(message)) 1095 % str(message))
1067 return 1096 return
1068 1097
1069 # skip the confirmation step? 1098 # skip the confirmation step?
1070 if self.db.config['INSTANT_REGISTRATION']: 1099 if self.db.config['INSTANT_REGISTRATION']:
1071 # handle the create now 1100 # handle the create now
1079 escape=escape) 1108 escape=escape)
1080 return 1109 return
1081 1110
1082 # fix up the initial roles 1111 # fix up the initial roles
1083 self.db.user.set(self.nodeid, 1112 self.db.user.set(self.nodeid,
1084 roles=self.db.config['NEW_WEB_USER_ROLES']) 1113 roles=self.db.config['NEW_WEB_USER_ROLES'])
1085 1114
1086 # commit now that all the tricky stuff is done 1115 # commit now that all the tricky stuff is done
1087 self.db.commit() 1116 self.db.commit()
1088 1117
1089 # finish off by logging the user in 1118 # finish off by logging the user in
1097 if check_user: 1126 if check_user:
1098 try: 1127 try:
1099 user_found = self.db.user.lookup(user_props['username']) 1128 user_found = self.db.user.lookup(user_props['username'])
1100 # if user is found reject the request. 1129 # if user is found reject the request.
1101 raise Reject( 1130 raise Reject(
1102 _("Username '%s' is already used.")%user_props['username']) 1131 _("Username '%s' is already used.") % user_props['username'])
1103 except KeyError: 1132 except KeyError:
1104 # user not found this is what we want. 1133 # user not found this is what we want.
1105 pass 1134 pass
1106 1135
1107 for propname, proptype in self.db.user.getprops().items(): 1136 for propname, proptype in self.db.user.getprops().items():
1108 value = user_props.get(propname, None) 1137 value = user_props.get(propname, None)
1109 if value is None: 1138 if value is None:
1110 pass 1139 pass
1111 elif isinstance(proptype, hyperdb.Date): 1140 elif isinstance(proptype, hyperdb.Date):
1122 1151
1123 # send the email 1152 # send the email
1124 tracker_name = self.db.config.TRACKER_NAME 1153 tracker_name = self.db.config.TRACKER_NAME
1125 tracker_email = self.db.config.TRACKER_EMAIL 1154 tracker_email = self.db.config.TRACKER_EMAIL
1126 if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']: 1155 if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
1127 subject = 'Complete your registration to %s -- key %s'%(tracker_name, 1156 subject = 'Complete your registration to %s -- key %s' % (
1128 otk) 1157 tracker_name, otk)
1129 body = """To complete your registration of the user "%(name)s" with 1158 body = """To complete your registration of the user "%(name)s" with
1130 %(tracker)s, please do one of the following: 1159 %(tracker)s, please do one of the following:
1131 1160
1132 - send a reply to %(tracker_email)s and maintain the subject line as is (the 1161 - send a reply to %(tracker_email)s and maintain the subject line as is (the
1133 reply's additional "Re:" is ok), 1162 reply's additional "Re:" is ok),
1135 - or visit the following URL: 1164 - or visit the following URL:
1136 1165
1137 %(url)s?@action=confrego&otk=%(otk)s 1166 %(url)s?@action=confrego&otk=%(otk)s
1138 1167
1139 """ % {'name': user_props['username'], 'tracker': tracker_name, 1168 """ % {'name': user_props['username'], 'tracker': tracker_name,
1140 'url': self.base, 'otk': otk, 'tracker_email': tracker_email} 1169 'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
1141 else: 1170 else:
1142 subject = 'Complete your registration to %s'%(tracker_name) 1171 subject = 'Complete your registration to %s' % (tracker_name)
1143 body = """To complete your registration of the user "%(name)s" with 1172 body = """To complete your registration of the user "%(name)s" with
1144 %(tracker)s, please visit the following URL: 1173 %(tracker)s, please visit the following URL:
1145 1174
1146 %(url)s?@action=confrego&otk=%(otk)s 1175 %(url)s?@action=confrego&otk=%(otk)s
1147 1176
1148 """ % {'name': user_props['username'], 'tracker': tracker_name, 1177 """ % {'name': user_props['username'], 'tracker': tracker_name,
1149 'url': self.base, 'otk': otk} 1178 'url': self.base, 'otk': otk}
1150 if not self.client.standard_message([user_props['address']], subject, 1179 if not self.client.standard_message([user_props['address']], subject,
1151 body, (tracker_name, tracker_email)): 1180 body,
1181 (tracker_name, tracker_email)):
1152 return 1182 return
1153 1183
1154 # commit changes to the database 1184 # commit changes to the database
1155 self.db.commit() 1185 self.db.commit()
1156 1186
1157 # redirect to the "you're almost there" page 1187 # redirect to the "you're almost there" page
1158 raise exceptions.Redirect('%suser?@template=rego_progress'%self.base) 1188 raise exceptions.Redirect('%suser?@template=rego_progress' % self.base)
1159 1189
1160 def newItemPermission(self, props, classname=None): 1190 def newItemPermission(self, props, classname=None):
1161 """Just check the "Register" permission. 1191 """Just check the "Register" permission.
1162 """ 1192 """
1163 # registration isn't allowed to supply roles 1193 # registration isn't allowed to supply roles
1166 "It is not permitted to supply roles at registration.")) 1196 "It is not permitted to supply roles at registration."))
1167 1197
1168 # technically already checked, but here for clarity 1198 # technically already checked, but here for clarity
1169 return self.hasPermission('Register', classname=classname) 1199 return self.hasPermission('Register', classname=classname)
1170 1200
1201
1171 class LogoutAction(Action): 1202 class LogoutAction(Action):
1172 def handle(self): 1203 def handle(self):
1173 """Make us really anonymous - nuke the session too.""" 1204 """Make us really anonymous - nuke the session too."""
1174 # log us out 1205 # log us out
1175 self.client.make_user_anonymous() 1206 self.client.make_user_anonymous()
1189 # anonymous user and not the user who logged out. If 1220 # anonymous user and not the user who logged out. If
1190 # we don't the user gets an invalid CSRF token error 1221 # we don't the user gets an invalid CSRF token error
1191 # As above choose the home page since everybody can 1222 # As above choose the home page since everybody can
1192 # see that. 1223 # see that.
1193 raise exceptions.Redirect(self.base) 1224 raise exceptions.Redirect(self.base)
1225
1194 1226
1195 class LoginAction(Action): 1227 class LoginAction(Action):
1196 def handle(self): 1228 def handle(self):
1197 """Attempt to log a user in. 1229 """Attempt to log a user in.
1198 1230
1230 1262
1231 clean_url = self.examine_url(self.form['__came_from'].value) 1263 clean_url = self.examine_url(self.form['__came_from'].value)
1232 redirect_url_tuple = urllib_.urlparse(clean_url) 1264 redirect_url_tuple = urllib_.urlparse(clean_url)
1233 # now I have a tuple form for the __came_from url 1265 # now I have a tuple form for the __came_from url
1234 try: 1266 try:
1235 query=urllib_.parse_qs(redirect_url_tuple.query) 1267 query = urllib_.parse_qs(redirect_url_tuple.query)
1236 if "@error_message" in query: 1268 if "@error_message" in query:
1237 del query["@error_message"] 1269 del query["@error_message"]
1238 if "@ok_message" in query: 1270 if "@ok_message" in query:
1239 del query["@ok_message"] 1271 del query["@ok_message"]
1240 if "@action" in query: 1272 if "@action" in query:
1245 except AttributeError: 1277 except AttributeError:
1246 # no query param so nothing to remove. Just define. 1278 # no query param so nothing to remove. Just define.
1247 query = {} 1279 query = {}
1248 pass 1280 pass
1249 1281
1250 redirect_url = urllib_.urlunparse( (redirect_url_tuple.scheme, 1282 redirect_url = urllib_.urlunparse((redirect_url_tuple.scheme,
1251 redirect_url_tuple.netloc, 1283 redirect_url_tuple.netloc,
1252 redirect_url_tuple.path, 1284 redirect_url_tuple.path,
1253 redirect_url_tuple.params, 1285 redirect_url_tuple.params,
1254 urllib_.urlencode(list(sorted(query.items())), doseq=True), 1286 urllib_.urlencode(list(sorted(query.items())), doseq=True),
1255 redirect_url_tuple.fragment) 1287 redirect_url_tuple.fragment)
1256 ) 1288 )
1257 1289
1258 try: 1290 try:
1259 # Implement rate limiting of logins by login name. 1291 # Implement rate limiting of logins by login name.
1260 # Use prefix to prevent key collisions maybe?? 1292 # Use prefix to prevent key collisions maybe??
1261 # set client.db.config.WEB_LOGIN_ATTEMPTS_MIN to 0 1293 # set client.db.config.WEB_LOGIN_ATTEMPTS_MIN to 0
1262 # to disable 1294 # to disable
1263 if self.client.db.config.WEB_LOGIN_ATTEMPTS_MIN: # if 0 - off 1295 if self.client.db.config.WEB_LOGIN_ATTEMPTS_MIN: # if 0 - off
1264 rlkey="LOGIN-" + self.client.user 1296 rlkey = "LOGIN-" + self.client.user
1265 limit=self.loginLimit 1297 limit = self.loginLimit
1266 gcra=Gcra() 1298 gcra = Gcra()
1267 otk=self.client.db.Otk 1299 otk = self.client.db.Otk
1268 try: 1300 try:
1269 val=otk.getall(rlkey) 1301 val = otk.getall(rlkey)
1270 gcra.set_tat_as_string(rlkey, val['tat']) 1302 gcra.set_tat_as_string(rlkey, val['tat'])
1271 except KeyError: 1303 except KeyError:
1272 # ignore if tat not set, it's 1970-1-1 by default. 1304 # ignore if tat not set, it's 1970-1-1 by default.
1273 pass 1305 pass
1274 # see if rate limit exceeded and we need to reject the attempt 1306 # see if rate limit exceeded and we need to reject the attempt
1275 reject=gcra.update(rlkey, limit) 1307 reject = gcra.update(rlkey, limit)
1276 1308
1277 # Calculate a timestamp that will make OTK expire the 1309 # Calculate a timestamp that will make OTK expire the
1278 # unused entry 1 hour in the future 1310 # unused entry 1 hour in the future
1279 ts = time.time() - (60 * 60 * 24 * 7) + 3600 1311 ts = time.time() - (60 * 60 * 24 * 7) + 3600
1280 otk.set(rlkey, tat=gcra.get_tat_as_string(rlkey), 1312 otk.set(rlkey, tat=gcra.get_tat_as_string(rlkey),
1281 __timestamp=ts) 1313 __timestamp=ts)
1282 otk.commit() 1314 otk.commit()
1283 1315
1284 if reject: 1316 if reject:
1285 # User exceeded limits: find out how long to wait 1317 # User exceeded limits: find out how long to wait
1286 status=gcra.status(rlkey, limit) 1318 status = gcra.status(rlkey, limit)
1287 raise Reject(_("Logins occurring too fast. Please wait: %s seconds.")%status['Retry-After']) 1319 raise Reject(_("Logins occurring too fast. Please wait: %s seconds.") % status['Retry-After'])
1288 1320
1289 self.verifyLogin(self.client.user, password) 1321 self.verifyLogin(self.client.user, password)
1290 except exceptions.LoginError as err: 1322 except exceptions.LoginError as err:
1291 self.client.make_user_anonymous() 1323 self.client.make_user_anonymous()
1292 for arg in err.args: 1324 for arg in err.args:
1293 self.client.add_error_message(arg) 1325 self.client.add_error_message(arg)
1294 1326
1295 if '__came_from' in self.form: 1327 if '__came_from' in self.form:
1296 # set a new error 1328 # set a new error
1297 query['@error_message'] = err.args 1329 query['@error_message'] = err.args
1298 redirect_url = urllib_.urlunparse( (redirect_url_tuple.scheme, 1330 redirect_url = urllib_.urlunparse((redirect_url_tuple.scheme,
1299 redirect_url_tuple.netloc, 1331 redirect_url_tuple.netloc,
1300 redirect_url_tuple.path, 1332 redirect_url_tuple.path,
1301 redirect_url_tuple.params, 1333 redirect_url_tuple.params,
1302 urllib_.urlencode(list(sorted(query.items())), doseq=True), 1334 urllib_.urlencode(list(sorted(query.items())), doseq=True),
1303 redirect_url_tuple.fragment ) 1335 redirect_url_tuple.fragment )
1304 ) 1336 )
1305 raise exceptions.Redirect(redirect_url) 1337 raise exceptions.Redirect(redirect_url)
1306 # if no __came_from, send back to base url with error 1338 # if no __came_from, send back to base url with error
1307 return 1339 return
1308 1340
1309 # now we're OK, re-open the database for real, using the user 1341 # now we're OK, re-open the database for real, using the user
1349 return 1 1381 return 1
1350 if not givenpw and not stored: 1382 if not givenpw and not stored:
1351 return 1 1383 return 1
1352 return 0 1384 return 0
1353 1385
1386
1354 class ExportCSVAction(Action): 1387 class ExportCSVAction(Action):
1355 name = 'export' 1388 name = 'export'
1356 permissionType = 'View' 1389 permissionType = 'View'
1357 list_sep = ';' # Separator for list types 1390 list_sep = ';' # Separator for list types
1358 1391
1374 # use error code 400: Bad Request. Do not use 1407 # use error code 400: Bad Request. Do not use
1375 # error code 404: Not Found. 1408 # error code 404: Not Found.
1376 self.client.response_code = 400 1409 self.client.response_code = 400
1377 raise exceptions.NotFound( 1410 raise exceptions.NotFound(
1378 self._('Column "%(column)s" not found in %(class)s') 1411 self._('Column "%(column)s" not found in %(class)s')
1379 % {'column': html_escape(cname), 'class': request.classname}) 1412 % {'column': html_escape(cname),
1413 'class': request.classname})
1380 1414
1381 # full-text search 1415 # full-text search
1382 if request.search_text: 1416 if request.search_text:
1383 matches = self.db.indexer.search( 1417 matches = self.db.indexer.search(
1384 re.findall(r'\b\w{2,25}\b', request.search_text), klass) 1418 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
1397 return 'dummy' 1431 return 'dummy'
1398 1432
1399 wfile = self.client.request.wfile 1433 wfile = self.client.request.wfile
1400 if self.client.charset != self.client.STORAGE_CHARSET: 1434 if self.client.charset != self.client.STORAGE_CHARSET:
1401 wfile = codecs.EncodedFile(wfile, 1435 wfile = codecs.EncodedFile(wfile,
1402 self.client.STORAGE_CHARSET, self.client.charset, 'replace') 1436 self.client.STORAGE_CHARSET,
1437 self.client.charset, 'replace')
1403 1438
1404 writer = csv.writer(wfile) 1439 writer = csv.writer(wfile)
1405 1440
1406 # handle different types of columns. 1441 # handle different types of columns.
1407 def repr_no_right(cls, col): 1442 def repr_no_right(cls, col):
1408 """User doesn't have the right to see the value of col.""" 1443 """User doesn't have the right to see the value of col."""
1409 def fct(arg): 1444 def fct(arg):
1410 return "[hidden]" 1445 return "[hidden]"
1411 return fct 1446 return fct
1447
1412 def repr_link(cls, col): 1448 def repr_link(cls, col):
1413 """Generate a function which returns the string representation of 1449 """Generate a function which returns the string representation of
1414 a link depending on `cls` and `col`.""" 1450 a link depending on `cls` and `col`."""
1415 def fct(arg): 1451 def fct(arg):
1416 if arg == None: 1452 if arg is None:
1417 return "" 1453 return ""
1418 else: 1454 else:
1419 return str(cls.get(arg, col)) 1455 return str(cls.get(arg, col))
1420 return fct 1456 return fct
1457
1421 def repr_list(cls, col): 1458 def repr_list(cls, col):
1422 def fct(arg): 1459 def fct(arg):
1423 if arg == None: 1460 if arg is None:
1424 return "" 1461 return ""
1425 elif type(arg) is list: 1462 elif type(arg) is list:
1426 seq = [str(cls.get(val, col)) for val in arg] 1463 seq = [str(cls.get(val, col)) for val in arg]
1427 # python2/python 3 have different order in lists 1464 # python2/python 3 have different order in lists
1428 # sort to not break tests 1465 # sort to not break tests
1429 seq.sort() 1466 seq.sort()
1430 return self.list_sep.join(seq) 1467 return self.list_sep.join(seq)
1431 return fct 1468 return fct
1469
1432 def repr_date(): 1470 def repr_date():
1433 def fct(arg): 1471 def fct(arg):
1434 if arg == None: 1472 if arg is None:
1435 return "" 1473 return ""
1436 else: 1474 else:
1437 if (arg.local(self.db.getUserTimezone()).pretty('%H:%M') == 1475 if (arg.local(self.db.getUserTimezone()).pretty('%H:%M') ==
1438 '00:00'): 1476 '00:00'):
1439 fmt = '%Y-%m-%d' 1477 fmt = '%Y-%m-%d'
1440 else: 1478 else:
1441 fmt = '%Y-%m-%d %H:%M' 1479 fmt = '%Y-%m-%d %H:%M'
1442 return arg.local(self.db.getUserTimezone()).pretty(fmt) 1480 return arg.local(self.db.getUserTimezone()).pretty(fmt)
1443 return fct 1481 return fct
1482
1444 def repr_val(): 1483 def repr_val():
1445 def fct(arg): 1484 def fct(arg):
1446 if arg == None: 1485 if arg is None:
1447 return "" 1486 return ""
1448 else: 1487 else:
1449 return str(arg) 1488 return str(arg)
1450 return fct 1489 return fct
1451 1490
1452 props = klass.getprops() 1491 props = klass.getprops()
1453 1492
1454 # Determine translation map. 1493 # Determine translation map.
1455 ncols = [] 1494 ncols = []
1456 represent = {} 1495 represent = {}
1457 for col in columns: 1496 for col in columns:
1458 ncols.append(col) 1497 ncols.append(col)
1494 continue 1533 continue
1495 for name in columns: 1534 for name in columns:
1496 # check permission for this property on this item 1535 # check permission for this property on this item
1497 # TODO: Permission filter doesn't work for the 'user' class 1536 # TODO: Permission filter doesn't work for the 'user' class
1498 if not self.hasPermission(self.permissionType, itemid=itemid, 1537 if not self.hasPermission(self.permissionType, itemid=itemid,
1499 classname=request.classname, property=name): 1538 classname=request.classname,
1539 property=name):
1500 repr_function = repr_no_right(request.classname, name) 1540 repr_function = repr_no_right(request.classname, name)
1501 else: 1541 else:
1502 repr_function = represent[name] 1542 repr_function = represent[name]
1503 row.append(repr_function(klass.get(itemid, name))) 1543 row.append(repr_function(klass.get(itemid, name)))
1504 self.client._socket_op(writer.writerow, row) 1544 self.client._socket_op(writer.writerow, row)
1505 return '\n' 1545 return '\n'
1546
1506 1547
1507 class ExportCSVWithIdAction(Action): 1548 class ExportCSVWithIdAction(Action):
1508 ''' A variation of ExportCSVAction that returns ID number rather than 1549 ''' A variation of ExportCSVAction that returns ID number rather than
1509 names. This is the original csv export function. 1550 names. This is the original csv export function.
1510 ''' 1551 '''
1529 # use error code 400: Bad Request. Do not use 1570 # use error code 400: Bad Request. Do not use
1530 # error code 404: Not Found. 1571 # error code 404: Not Found.
1531 self.client.response_code = 400 1572 self.client.response_code = 400
1532 raise exceptions.NotFound( 1573 raise exceptions.NotFound(
1533 self._('Column "%(column)s" not found in %(class)s') 1574 self._('Column "%(column)s" not found in %(class)s')
1534 % {'column': html_escape(cname), 'class': request.classname}) 1575 % {'column': html_escape(cname),
1576 'class': request.classname})
1535 1577
1536 # full-text search 1578 # full-text search
1537 if request.search_text: 1579 if request.search_text:
1538 matches = self.db.indexer.search( 1580 matches = self.db.indexer.search(
1539 re.findall(r'\b\w{2,25}\b', request.search_text), klass) 1581 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
1552 return 'dummy' 1594 return 'dummy'
1553 1595
1554 wfile = self.client.request.wfile 1596 wfile = self.client.request.wfile
1555 if self.client.charset != self.client.STORAGE_CHARSET: 1597 if self.client.charset != self.client.STORAGE_CHARSET:
1556 wfile = codecs.EncodedFile(wfile, 1598 wfile = codecs.EncodedFile(wfile,
1557 self.client.STORAGE_CHARSET, self.client.charset, 'replace') 1599 self.client.STORAGE_CHARSET,
1600 self.client.charset, 'replace')
1558 1601
1559 writer = csv.writer(wfile) 1602 writer = csv.writer(wfile)
1560 self.client._socket_op(writer.writerow, columns) 1603 self.client._socket_op(writer.writerow, columns)
1561 1604
1562 # and search 1605 # and search
1564 row = [] 1607 row = []
1565 # FIXME should this code raise an exception if an item 1608 # FIXME should this code raise an exception if an item
1566 # is included that can't be accessed? Enabling this 1609 # is included that can't be accessed? Enabling this
1567 # check will just skip the row for the inaccessible item. 1610 # check will just skip the row for the inaccessible item.
1568 # This makes it act more like the web interface. 1611 # This makes it act more like the web interface.
1569 #if not self.hasPermission(self.permissionType, itemid=itemid, 1612 # if not self.hasPermission(self.permissionType, itemid=itemid,
1570 # classname=request.classname): 1613 # classname=request.classname):
1571 # continue 1614 # continue
1572 for name in columns: 1615 for name in columns:
1573 # check permission to view this property on this item 1616 # check permission to view this property on this item
1574 if not self.hasPermission(self.permissionType, itemid=itemid, 1617 if not self.hasPermission(self.permissionType, itemid=itemid,
1575 classname=request.classname, property=name): 1618 classname=request.classname,
1619 property=name):
1576 # FIXME: is this correct, or should we just 1620 # FIXME: is this correct, or should we just
1577 # emit a '[hidden]' string. Note that this may 1621 # emit a '[hidden]' string. Note that this may
1578 # allow an attacker to figure out hidden schema 1622 # allow an attacker to figure out hidden schema
1579 # properties. 1623 # properties.
1580 # A bad property name will result in an exception. 1624 # A bad property name will result in an exception.
1593 row.append(str(value)) 1637 row.append(str(value))
1594 self.client._socket_op(writer.writerow, row) 1638 self.client._socket_op(writer.writerow, row)
1595 1639
1596 return '\n' 1640 return '\n'
1597 1641
1642
1598 class Bridge(BaseAction): 1643 class Bridge(BaseAction):
1599 """Make roundup.actions.Action executable via CGI request. 1644 """Make roundup.actions.Action executable via CGI request.
1600 1645
1601 Using this allows users to write actions executable from multiple frontends. 1646 Using this allows users to write actions executable from multiple
1602 CGI Form content is translated into a dictionary, which then is passed as 1647 frontends. CGI Form content is translated into a dictionary, which
1603 argument to 'handle()'. XMLRPC requests have to pass this dictionary 1648 then is passed as argument to 'handle()'. XMLRPC requests have to
1604 directly. 1649 pass this dictionary directly.
1605 """ 1650 """
1606 1651
1607 def __init__(self, *args): 1652 def __init__(self, *args):
1608 1653
1609 # As this constructor is callable from multiple frontends, each with 1654 # As this constructor is callable from multiple frontends, each with

Roundup Issue Tracker: http://roundup-tracker.org/