Mercurial > p > roundup > code
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 |
