1 """WWW request handler (also used in the stand-alone server).
2 """
3 __docformat__ = 'restructuredtext'
5 import base64, binascii, cgi, codecs, mimetypes, os
6 import quopri, random, re, rfc822, stat, sys, time
7 import socket, errno
8 from traceback import format_exc
10 from roundup import roundupdb, date, hyperdb, password
11 from roundup.cgi import templating, cgitb, TranslationService
12 from roundup.cgi.actions import *
13 from roundup.exceptions import *
14 from roundup.cgi.exceptions import *
15 from roundup.cgi.form_parser import FormParser
16 from roundup.mailer import Mailer, MessageSendError, encode_quopri
17 from roundup.cgi import accept_language
18 from roundup import xmlrpc
20 from roundup.anypy.cookie_ import CookieError, BaseCookie, SimpleCookie, \
21 get_cookie_date
22 from roundup.anypy.io_ import StringIO
23 from roundup.anypy import http_
24 from roundup.anypy import urllib_
26 from email.MIMEBase import MIMEBase
27 from email.MIMEText import MIMEText
28 from email.MIMEMultipart import MIMEMultipart
30 def initialiseSecurity(security):
31 '''Create some Permissions and Roles on the security object
33 This function is directly invoked by security.Security.__init__()
34 as a part of the Security object instantiation.
35 '''
36 p = security.addPermission(name="Web Access",
37 description="User may access the web interface")
38 security.addPermissionToRole('Admin', p)
40 # doing Role stuff through the web - make sure Admin can
41 # TODO: deprecate this and use a property-based control
42 p = security.addPermission(name="Web Roles",
43 description="User may manipulate user Roles through the web")
44 security.addPermissionToRole('Admin', p)
46 # used to clean messages passed through CGI variables - HTML-escape any tag
47 # that isn't <a href="">, <i>, <b> and <br> (including XHTML variants) so
48 # that people can't pass through nasties like <script>, <iframe>, ...
49 CLEAN_MESSAGE_RE = r'(<(/?(.*?)(\s*href="[^"]")?\s*/?)>)'
50 def clean_message(message, mc=re.compile(CLEAN_MESSAGE_RE, re.I)):
51 return mc.sub(clean_message_callback, message)
52 def clean_message_callback(match, ok={'a':1,'i':1,'b':1,'br':1}):
53 """ Strip all non <a>,<i>,<b> and <br> tags from a string
54 """
55 if match.group(3).lower() in ok:
56 return match.group(1)
57 return '<%s>'%match.group(2)
60 error_message = ''"""<html><head><title>An error has occurred</title></head>
61 <body><h1>An error has occurred</h1>
62 <p>A problem was encountered processing your request.
63 The tracker maintainers have been notified of the problem.</p>
64 </body></html>"""
67 class LiberalCookie(SimpleCookie):
68 """ Python's SimpleCookie throws an exception if the cookie uses invalid
69 syntax. Other applications on the same server may have done precisely
70 this, preventing roundup from working through no fault of roundup.
71 Numerous other python apps have run into the same problem:
73 trac: http://trac.edgewall.org/ticket/2256
74 mailman: http://bugs.python.org/issue472646
76 This particular implementation comes from trac's solution to the
77 problem. Unfortunately it requires some hackery in SimpleCookie's
78 internals to provide a more liberal __set method.
79 """
80 def load(self, rawdata, ignore_parse_errors=True):
81 if ignore_parse_errors:
82 self.bad_cookies = []
83 self._BaseCookie__set = self._loose_set
84 SimpleCookie.load(self, rawdata)
85 if ignore_parse_errors:
86 self._BaseCookie__set = self._strict_set
87 for key in self.bad_cookies:
88 del self[key]
90 _strict_set = BaseCookie._BaseCookie__set
92 def _loose_set(self, key, real_value, coded_value):
93 try:
94 self._strict_set(key, real_value, coded_value)
95 except CookieError:
96 self.bad_cookies.append(key)
97 dict.__setitem__(self, key, None)
100 class Session:
101 """
102 Needs DB to be already opened by client
104 Session attributes at instantiation:
106 - "client" - reference to client for add_cookie function
107 - "session_db" - session DB manager
108 - "cookie_name" - name of the cookie with session id
109 - "_sid" - session id for current user
110 - "_data" - session data cache
112 session = Session(client)
113 session.set(name=value)
114 value = session.get(name)
116 session.destroy() # delete current session
117 session.clean_up() # clean up session table
119 session.update(set_cookie=True, expire=3600*24*365)
120 # refresh session expiration time, setting persistent
121 # cookie if needed to last for 'expire' seconds
123 """
125 def __init__(self, client):
126 self._data = {}
127 self._sid = None
129 self.client = client
130 self.session_db = client.db.getSessionManager()
132 # parse cookies for session id
133 self.cookie_name = 'roundup_session_%s' % \
134 re.sub('[^a-zA-Z]', '', client.instance.config.TRACKER_NAME)
135 cookies = LiberalCookie(client.env.get('HTTP_COOKIE', ''))
136 if self.cookie_name in cookies:
137 if not self.session_db.exists(cookies[self.cookie_name].value):
138 self._sid = None
139 # remove old cookie
140 self.client.add_cookie(self.cookie_name, None)
141 else:
142 self._sid = cookies[self.cookie_name].value
143 self._data = self.session_db.getall(self._sid)
145 def _gen_sid(self):
146 """ generate a unique session key """
147 while 1:
148 s = '%s%s'%(time.time(), random.random())
149 s = binascii.b2a_base64(s).strip()
150 if not self.session_db.exists(s):
151 break
153 # clean up the base64
154 if s[-1] == '=':
155 if s[-2] == '=':
156 s = s[:-2]
157 else:
158 s = s[:-1]
159 return s
161 def clean_up(self):
162 """Remove expired sessions"""
163 self.session_db.clean()
165 def destroy(self):
166 self.client.add_cookie(self.cookie_name, None)
167 self._data = {}
168 self.session_db.destroy(self._sid)
169 self.client.db.commit()
171 def get(self, name, default=None):
172 return self._data.get(name, default)
174 def set(self, **kwargs):
175 self._data.update(kwargs)
176 if not self._sid:
177 self._sid = self._gen_sid()
178 self.session_db.set(self._sid, **self._data)
179 # add session cookie
180 self.update(set_cookie=True)
182 # XXX added when patching 1.4.4 for backward compatibility
183 # XXX remove
184 self.client.session = self._sid
185 else:
186 self.session_db.set(self._sid, **self._data)
187 self.client.db.commit()
189 def update(self, set_cookie=False, expire=None):
190 """ update timestamp in db to avoid expiration
192 if 'set_cookie' is True, set cookie with 'expire' seconds lifetime
193 if 'expire' is None - session will be closed with the browser
195 XXX the session can be purged within a week even if a cookie
196 lifetime is longer
197 """
198 self.session_db.updateTimestamp(self._sid)
199 self.client.db.commit()
201 if set_cookie:
202 self.client.add_cookie(self.cookie_name, self._sid, expire=expire)
206 class Client:
207 """Instantiate to handle one CGI request.
209 See inner_main for request processing.
211 Client attributes at instantiation:
213 - "path" is the PATH_INFO inside the instance (with no leading '/')
214 - "base" is the base URL for the instance
215 - "form" is the cgi form, an instance of FieldStorage from the standard
216 cgi module
217 - "additional_headers" is a dictionary of additional HTTP headers that
218 should be sent to the client
219 - "response_code" is the HTTP response code to send to the client
220 - "translator" is TranslationService instance
222 During the processing of a request, the following attributes are used:
224 - "db"
225 - "error_message" holds a list of error messages
226 - "ok_message" holds a list of OK messages
227 - "session" is deprecated in favor of session_api (XXX remove)
228 - "session_api" is the interface to store data in session
229 - "user" is the current user's name
230 - "userid" is the current user's id
231 - "template" is the current :template context
232 - "classname" is the current class context name
233 - "nodeid" is the current context item id
235 User Identification:
236 Users that are absent in session data are anonymous and are logged
237 in as that user. This typically gives them all Permissions assigned to the
238 Anonymous Role.
240 Every user is assigned a session. "session_api" is the interface to work
241 with session data.
243 Special form variables:
244 Note that in various places throughout this code, special form
245 variables of the form :<name> are used. The colon (":") part may
246 actually be one of either ":" or "@".
247 """
249 # charset used for data storage and form templates
250 # Note: must be in lower case for comparisons!
251 # XXX take this from instance.config?
252 STORAGE_CHARSET = 'utf-8'
254 #
255 # special form variables
256 #
257 FV_TEMPLATE = re.compile(r'[@:]template')
258 FV_OK_MESSAGE = re.compile(r'[@:]ok_message')
259 FV_ERROR_MESSAGE = re.compile(r'[@:]error_message')
261 # Note: index page stuff doesn't appear here:
262 # columns, sort, sortdir, filter, group, groupdir, search_text,
263 # pagesize, startwith
265 # list of network error codes that shouldn't be reported to tracker admin
266 # (error descriptions from FreeBSD intro(2))
267 IGNORE_NET_ERRORS = (
268 # A write on a pipe, socket or FIFO for which there is
269 # no process to read the data.
270 errno.EPIPE,
271 # A connection was forcibly closed by a peer.
272 # This normally results from a loss of the connection
273 # on the remote socket due to a timeout or a reboot.
274 errno.ECONNRESET,
275 # Software caused connection abort. A connection abort
276 # was caused internal to your host machine.
277 errno.ECONNABORTED,
278 # A connect or send request failed because the connected party
279 # did not properly respond after a period of time.
280 errno.ETIMEDOUT,
281 )
283 def __init__(self, instance, request, env, form=None, translator=None):
284 # re-seed the random number generator
285 random.seed()
286 self.start = time.time()
287 self.instance = instance
288 self.request = request
289 self.env = env
290 self.setTranslator(translator)
291 self.mailer = Mailer(instance.config)
293 # save off the path
294 self.path = env['PATH_INFO']
296 # this is the base URL for this tracker
297 self.base = self.instance.config.TRACKER_WEB
299 # check the tracker_we setting
300 if not self.base.endswith('/'):
301 self.base = self.base + '/'
303 # this is the "cookie path" for this tracker (ie. the path part of
304 # the "base" url)
305 self.cookie_path = urllib_.urlparse(self.base)[2]
306 # cookies to set in http responce
307 # {(path, name): (value, expire)}
308 self._cookies = {}
310 # see if we need to re-parse the environment for the form (eg Zope)
311 if form is None:
312 self.form = cgi.FieldStorage(fp=request.rfile, environ=env)
313 else:
314 self.form = form
316 # turn debugging on/off
317 try:
318 self.debug = int(env.get("ROUNDUP_DEBUG", 0))
319 except ValueError:
320 # someone gave us a non-int debug level, turn it off
321 self.debug = 0
323 # flag to indicate that the HTTP headers have been sent
324 self.headers_done = 0
326 # additional headers to send with the request - must be registered
327 # before the first write
328 self.additional_headers = {}
329 self.response_code = 200
331 # default character set
332 self.charset = self.STORAGE_CHARSET
334 # parse cookies (used for charset lookups)
335 # use our own LiberalCookie to handle bad apps on the same
336 # server that have set cookies that are out of spec
337 self.cookie = LiberalCookie(self.env.get('HTTP_COOKIE', ''))
339 self.user = None
340 self.userid = None
341 self.nodeid = None
342 self.classname = None
343 self.template = None
345 def setTranslator(self, translator=None):
346 """Replace the translation engine
348 'translator'
349 is TranslationService instance.
350 It must define methods 'translate' (TAL-compatible i18n),
351 'gettext' and 'ngettext' (gettext-compatible i18n).
353 If omitted, create default TranslationService.
354 """
355 if translator is None:
356 translator = TranslationService.get_translation(
357 language=self.instance.config["TRACKER_LANGUAGE"],
358 tracker_home=self.instance.config["TRACKER_HOME"])
359 self.translator = translator
360 self._ = self.gettext = translator.gettext
361 self.ngettext = translator.ngettext
363 def main(self):
364 """ Wrap the real main in a try/finally so we always close off the db.
365 """
366 try:
367 if self.env.get('CONTENT_TYPE') == 'text/xml':
368 self.handle_xmlrpc()
369 else:
370 self.inner_main()
371 finally:
372 if hasattr(self, 'db'):
373 self.db.close()
376 def handle_xmlrpc(self):
378 # Pull the raw XML out of the form. The "value" attribute
379 # will be the raw content of the POST request.
380 assert self.form.file
381 input = self.form.value
382 # So that the rest of Roundup can query the form in the
383 # usual way, we create an empty list of fields.
384 self.form.list = []
386 # Set the charset and language, since other parts of
387 # Roundup may depend upon that.
388 self.determine_charset()
389 self.determine_language()
390 # Open the database as the correct user.
391 self.determine_user()
392 self.check_anonymous_access()
394 # Call the appropriate XML-RPC method.
395 handler = xmlrpc.RoundupDispatcher(self.db,
396 self.instance.actions,
397 self.translator,
398 allow_none=True)
399 output = handler.dispatch(input)
401 self.setHeader("Content-Type", "text/xml")
402 self.setHeader("Content-Length", str(len(output)))
403 self.write(output)
405 def inner_main(self):
406 """Process a request.
408 The most common requests are handled like so:
410 1. look for charset and language preferences, set up user locale
411 see determine_charset, determine_language
412 2. figure out who we are, defaulting to the "anonymous" user
413 see determine_user
414 3. figure out what the request is for - the context
415 see determine_context
416 4. handle any requested action (item edit, search, ...)
417 see handle_action
418 5. render a template, resulting in HTML output
420 In some situations, exceptions occur:
422 - HTTP Redirect (generally raised by an action)
423 - SendFile (generally raised by determine_context)
424 serve up a FileClass "content" property
425 - SendStaticFile (generally raised by determine_context)
426 serve up a file from the tracker "html" directory
427 - Unauthorised (generally raised by an action)
428 the action is cancelled, the request is rendered and an error
429 message is displayed indicating that permission was not
430 granted for the action to take place
431 - templating.Unauthorised (templating action not permitted)
432 raised by an attempted rendering of a template when the user
433 doesn't have permission
434 - NotFound (raised wherever it needs to be)
435 percolates up to the CGI interface that called the client
436 """
437 self.ok_message = []
438 self.error_message = []
439 try:
440 self.determine_charset()
441 self.determine_language()
443 try:
444 # make sure we're identified (even anonymously)
445 self.determine_user()
447 # figure out the context and desired content template
448 self.determine_context()
450 # if we've made it this far the context is to a bit of
451 # Roundup's real web interface (not a file being served up)
452 # so do the Anonymous Web Acess check now
453 self.check_anonymous_access()
455 # possibly handle a form submit action (may change self.classname
456 # and self.template, and may also append error/ok_messages)
457 html = self.handle_action()
459 if html:
460 self.write_html(html)
461 return
463 # now render the page
464 # we don't want clients caching our dynamic pages
465 self.additional_headers['Cache-Control'] = 'no-cache'
466 # Pragma: no-cache makes Mozilla and its ilk
467 # double-load all pages!!
468 # self.additional_headers['Pragma'] = 'no-cache'
470 # pages with messages added expire right now
471 # simple views may be cached for a small amount of time
472 # TODO? make page expire time configurable
473 # <rj> always expire pages, as IE just doesn't seem to do the
474 # right thing here :(
475 date = time.time() - 1
476 #if self.error_message or self.ok_message:
477 # date = time.time() - 1
478 #else:
479 # date = time.time() + 5
480 self.additional_headers['Expires'] = rfc822.formatdate(date)
482 # render the content
483 self.write_html(self.renderContext())
484 except SendFile, designator:
485 # The call to serve_file may result in an Unauthorised
486 # exception or a NotModified exception. Those
487 # exceptions will be handled by the outermost set of
488 # exception handlers.
489 self.serve_file(designator)
490 except SendStaticFile, file:
491 self.serve_static_file(str(file))
492 except IOError:
493 # IOErrors here are due to the client disconnecting before
494 # recieving the reply.
495 pass
497 except SeriousError, message:
498 self.write_html(str(message))
499 except Redirect, url:
500 # let's redirect - if the url isn't None, then we need to do
501 # the headers, otherwise the headers have been set before the
502 # exception was raised
503 if url:
504 self.additional_headers['Location'] = str(url)
505 self.response_code = 302
506 self.write_html('Redirecting to <a href="%s">%s</a>'%(url, url))
507 except LoginError, message:
508 # The user tried to log in, but did not provide a valid
509 # username and password. If we support HTTP
510 # authorization, send back a response that will cause the
511 # browser to prompt the user again.
512 if self.instance.config.WEB_HTTP_AUTH:
513 self.response_code = http_.client.UNAUTHORIZED
514 realm = self.instance.config.TRACKER_NAME
515 self.setHeader("WWW-Authenticate",
516 "Basic realm=\"%s\"" % realm)
517 else:
518 self.response_code = http_.client.FORBIDDEN
519 self.renderFrontPage(message)
520 except Unauthorised, message:
521 # users may always see the front page
522 self.response_code = 403
523 self.renderFrontPage(message)
524 except NotModified:
525 # send the 304 response
526 self.response_code = 304
527 self.header()
528 except NotFound, e:
529 self.response_code = 404
530 self.template = '404'
531 try:
532 cl = self.db.getclass(self.classname)
533 self.write_html(self.renderContext())
534 except KeyError:
535 # we can't map the URL to a class we know about
536 # reraise the NotFound and let roundup_server
537 # handle it
538 raise NotFound(e)
539 except FormError, e:
540 self.error_message.append(self._('Form Error: ') + str(e))
541 self.write_html(self.renderContext())
542 except:
543 # Something has gone badly wrong. Therefore, we should
544 # make sure that the response code indicates failure.
545 if self.response_code == http_.client.OK:
546 self.response_code = http_.client.INTERNAL_SERVER_ERROR
547 # Help the administrator work out what went wrong.
548 html = ("<h1>Traceback</h1>"
549 + cgitb.html(i18n=self.translator)
550 + ("<h1>Environment Variables</h1><table>%s</table>"
551 % cgitb.niceDict("", self.env)))
552 if not self.instance.config.WEB_DEBUG:
553 exc_info = sys.exc_info()
554 subject = "Error: %s" % exc_info[1]
555 self.send_error_to_admin(subject, html, format_exc())
556 self.write_html(self._(error_message))
557 else:
558 self.write_html(html)
560 def clean_sessions(self):
561 """Deprecated
562 XXX remove
563 """
564 self.clean_up()
566 def clean_up(self):
567 """Remove expired sessions and One Time Keys.
569 Do it only once an hour.
570 """
571 hour = 60*60
572 now = time.time()
574 # XXX: hack - use OTK table to store last_clean time information
575 # 'last_clean' string is used instead of otk key
576 last_clean = self.db.getOTKManager().get('last_clean', 'last_use', 0)
577 if now - last_clean < hour:
578 return
580 self.session_api.clean_up()
581 self.db.getOTKManager().clean()
582 self.db.getOTKManager().set('last_clean', last_use=now)
583 self.db.commit(fail_ok=True)
585 def determine_charset(self):
586 """Look for client charset in the form parameters or browser cookie.
588 If no charset requested by client, use storage charset (utf-8).
590 If the charset is found, and differs from the storage charset,
591 recode all form fields of type 'text/plain'
592 """
593 # look for client charset
594 charset_parameter = 0
595 if '@charset' in self.form:
596 charset = self.form['@charset'].value
597 if charset.lower() == "none":
598 charset = ""
599 charset_parameter = 1
600 elif 'roundup_charset' in self.cookie:
601 charset = self.cookie['roundup_charset'].value
602 else:
603 charset = None
604 if charset:
605 # make sure the charset is recognized
606 try:
607 codecs.lookup(charset)
608 except LookupError:
609 self.error_message.append(self._('Unrecognized charset: %r')
610 % charset)
611 charset_parameter = 0
612 else:
613 self.charset = charset.lower()
614 # If we've got a character set in request parameters,
615 # set the browser cookie to keep the preference.
616 # This is done after codecs.lookup to make sure
617 # that we aren't keeping a wrong value.
618 if charset_parameter:
619 self.add_cookie('roundup_charset', charset)
621 # if client charset is different from the storage charset,
622 # recode form fields
623 # XXX this requires FieldStorage from Python library.
624 # mod_python FieldStorage is not supported!
625 if self.charset != self.STORAGE_CHARSET:
626 decoder = codecs.getdecoder(self.charset)
627 encoder = codecs.getencoder(self.STORAGE_CHARSET)
628 re_charref = re.compile('&#([0-9]+|x[0-9a-f]+);', re.IGNORECASE)
629 def _decode_charref(matchobj):
630 num = matchobj.group(1)
631 if num[0].lower() == 'x':
632 uc = int(num[1:], 16)
633 else:
634 uc = int(num)
635 return unichr(uc)
637 for field_name in self.form:
638 field = self.form[field_name]
639 if (field.type == 'text/plain') and not field.filename:
640 try:
641 value = decoder(field.value)[0]
642 except UnicodeError:
643 continue
644 value = re_charref.sub(_decode_charref, value)
645 field.value = encoder(value)[0]
647 def determine_language(self):
648 """Determine the language"""
649 # look for language parameter
650 # then for language cookie
651 # last for the Accept-Language header
652 if "@language" in self.form:
653 language = self.form["@language"].value
654 if language.lower() == "none":
655 language = ""
656 self.add_cookie("roundup_language", language)
657 elif "roundup_language" in self.cookie:
658 language = self.cookie["roundup_language"].value
659 elif self.instance.config["WEB_USE_BROWSER_LANGUAGE"]:
660 hal = self.env.get('HTTP_ACCEPT_LANGUAGE')
661 language = accept_language.parse(hal)
662 else:
663 language = ""
665 self.language = language
666 if language:
667 self.setTranslator(TranslationService.get_translation(
668 language,
669 tracker_home=self.instance.config["TRACKER_HOME"]))
671 def determine_user(self):
672 """Determine who the user is"""
673 self.opendb('admin')
675 # get session data from db
676 # XXX: rename
677 self.session_api = Session(self)
679 # take the opportunity to cleanup expired sessions and otks
680 self.clean_up()
682 user = None
683 # first up, try http authorization if enabled
684 if self.instance.config['WEB_HTTP_AUTH']:
685 if 'REMOTE_USER' in self.env:
686 # we have external auth (e.g. by Apache)
687 user = self.env['REMOTE_USER']
688 elif self.env.get('HTTP_AUTHORIZATION', ''):
689 # try handling Basic Auth ourselves
690 auth = self.env['HTTP_AUTHORIZATION']
691 scheme, challenge = auth.split(' ', 1)
692 if scheme.lower() == 'basic':
693 try:
694 decoded = base64.decodestring(challenge)
695 except TypeError:
696 # invalid challenge
697 pass
698 username, password = decoded.split(':')
699 try:
700 login = self.get_action_class('login')(self)
701 login.verifyLogin(username, password)
702 except LoginError, err:
703 self.make_user_anonymous()
704 raise
705 user = username
707 # if user was not set by http authorization, try session lookup
708 if not user:
709 user = self.session_api.get('user')
710 if user:
711 # update session lifetime datestamp
712 self.session_api.update()
714 # if no user name set by http authorization or session lookup
715 # the user is anonymous
716 if not user:
717 user = 'anonymous'
719 # sanity check on the user still being valid,
720 # getting the userid at the same time
721 try:
722 self.userid = self.db.user.lookup(user)
723 except (KeyError, TypeError):
724 user = 'anonymous'
726 # make sure the anonymous user is valid if we're using it
727 if user == 'anonymous':
728 self.make_user_anonymous()
729 else:
730 self.user = user
732 # reopen the database as the correct user
733 self.opendb(self.user)
735 def check_anonymous_access(self):
736 """Check that the Anonymous user is actually allowed to use the web
737 interface and short-circuit all further processing if they're not.
738 """
739 # allow Anonymous to use the "login" and "register" actions (noting
740 # that "register" has its own "Register" permission check)
742 if ':action' in self.form:
743 action = self.form[':action']
744 elif '@action' in self.form:
745 action = self.form['@action']
746 else:
747 action = ''
748 if isinstance(action, list):
749 raise SeriousError('broken form: multiple @action values submitted')
750 elif action != '':
751 action = action.value.lower()
752 if action in ('login', 'register'):
753 return
755 # allow Anonymous to view the "user" "register" template if they're
756 # allowed to register
757 if (self.db.security.hasPermission('Register', self.userid, 'user')
758 and self.classname == 'user' and self.template == 'register'):
759 return
761 # otherwise for everything else
762 if self.user == 'anonymous':
763 if not self.db.security.hasPermission('Web Access', self.userid):
764 raise Unauthorised(self._("Anonymous users are not "
765 "allowed to use the web interface"))
767 def opendb(self, username):
768 """Open the database and set the current user.
770 Opens a database once. On subsequent calls only the user is set on
771 the database object the instance.optimize is set. If we are in
772 "Development Mode" (cf. roundup_server) then the database is always
773 re-opened.
774 """
775 # don't do anything if the db is open and the user has not changed
776 if hasattr(self, 'db') and self.db.isCurrentUser(username):
777 return
779 # open the database or only set the user
780 if not hasattr(self, 'db'):
781 self.db = self.instance.open(username)
782 else:
783 if self.instance.optimize:
784 self.db.setCurrentUser(username)
785 else:
786 self.db.close()
787 self.db = self.instance.open(username)
788 # The old session API refers to the closed database;
789 # we can no longer use it.
790 self.session_api = Session(self)
793 def determine_context(self, dre=re.compile(r'([^\d]+)0*(\d+)')):
794 """Determine the context of this page from the URL:
796 The URL path after the instance identifier is examined. The path
797 is generally only one entry long.
799 - if there is no path, then we are in the "home" context.
800 - if the path is "_file", then the additional path entry
801 specifies the filename of a static file we're to serve up
802 from the instance "html" directory. Raises a SendStaticFile
803 exception.(*)
804 - if there is something in the path (eg "issue"), it identifies
805 the tracker class we're to display.
806 - if the path is an item designator (eg "issue123"), then we're
807 to display a specific item.
808 - if the path starts with an item designator and is longer than
809 one entry, then we're assumed to be handling an item of a
810 FileClass, and the extra path information gives the filename
811 that the client is going to label the download with (ie
812 "file123/image.png" is nicer to download than "file123"). This
813 raises a SendFile exception.(*)
815 Both of the "*" types of contexts stop before we bother to
816 determine the template we're going to use. That's because they
817 don't actually use templates.
819 The template used is specified by the :template CGI variable,
820 which defaults to:
822 - only classname suplied: "index"
823 - full item designator supplied: "item"
825 We set:
827 self.classname - the class to display, can be None
829 self.template - the template to render the current context with
831 self.nodeid - the nodeid of the class we're displaying
832 """
833 # default the optional variables
834 self.classname = None
835 self.nodeid = None
837 # see if a template or messages are specified
838 template_override = ok_message = error_message = None
839 for key in self.form:
840 if self.FV_TEMPLATE.match(key):
841 template_override = self.form[key].value
842 elif self.FV_OK_MESSAGE.match(key):
843 ok_message = self.form[key].value
844 ok_message = clean_message(ok_message)
845 elif self.FV_ERROR_MESSAGE.match(key):
846 error_message = self.form[key].value
847 error_message = clean_message(error_message)
849 # see if we were passed in a message
850 if ok_message:
851 self.ok_message.append(ok_message)
852 if error_message:
853 self.error_message.append(error_message)
855 # determine the classname and possibly nodeid
856 path = self.path.split('/')
857 if not path or path[0] in ('', 'home', 'index'):
858 if template_override is not None:
859 self.template = template_override
860 else:
861 self.template = ''
862 return
863 elif path[0] in ('_file', '@@file'):
864 raise SendStaticFile(os.path.join(*path[1:]))
865 else:
866 self.classname = path[0]
867 if len(path) > 1:
868 # send the file identified by the designator in path[0]
869 raise SendFile(path[0])
871 # see if we got a designator
872 m = dre.match(self.classname)
873 if m:
874 self.classname = m.group(1)
875 self.nodeid = m.group(2)
876 try:
877 klass = self.db.getclass(self.classname)
878 except KeyError:
879 raise NotFound('%s/%s'%(self.classname, self.nodeid))
880 if not klass.hasnode(self.nodeid):
881 raise NotFound('%s/%s'%(self.classname, self.nodeid))
882 # with a designator, we default to item view
883 self.template = 'item'
884 else:
885 # with only a class, we default to index view
886 self.template = 'index'
888 # make sure the classname is valid
889 try:
890 self.db.getclass(self.classname)
891 except KeyError:
892 raise NotFound(self.classname)
894 # see if we have a template override
895 if template_override is not None:
896 self.template = template_override
898 def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
899 """ Serve the file from the content property of the designated item.
900 """
901 m = dre.match(str(designator))
902 if not m:
903 raise NotFound(str(designator))
904 classname, nodeid = m.group(1), m.group(2)
906 try:
907 klass = self.db.getclass(classname)
908 except KeyError:
909 # The classname was not valid.
910 raise NotFound(str(designator))
912 # perform the Anonymous user access check
913 self.check_anonymous_access()
915 # make sure we have the appropriate properties
916 props = klass.getprops()
917 if 'type' not in props:
918 raise NotFound(designator)
919 if 'content' not in props:
920 raise NotFound(designator)
922 # make sure we have permission
923 if not self.db.security.hasPermission('View', self.userid,
924 classname, 'content', nodeid):
925 raise Unauthorised(self._("You are not allowed to view "
926 "this file."))
928 try:
929 mime_type = klass.get(nodeid, 'type')
930 except IndexError, e:
931 raise NotFound(e)
932 # Can happen for msg class:
933 if not mime_type:
934 mime_type = 'text/plain'
936 # if the mime_type is HTML-ish then make sure we're allowed to serve up
937 # HTML-ish content
938 if mime_type in ('text/html', 'text/x-html'):
939 if not self.instance.config['WEB_ALLOW_HTML_FILE']:
940 # do NOT serve the content up as HTML
941 mime_type = 'application/octet-stream'
943 # If this object is a file (i.e., an instance of FileClass),
944 # see if we can find it in the filesystem. If so, we may be
945 # able to use the more-efficient request.sendfile method of
946 # sending the file. If not, just get the "content" property
947 # in the usual way, and use that.
948 content = None
949 filename = None
950 if isinstance(klass, hyperdb.FileClass):
951 try:
952 filename = self.db.filename(classname, nodeid)
953 except AttributeError:
954 # The database doesn't store files in the filesystem
955 # and therefore doesn't provide the "filename" method.
956 pass
957 except IOError:
958 # The file does not exist.
959 pass
960 if not filename:
961 content = klass.get(nodeid, 'content')
963 lmt = klass.get(nodeid, 'activity').timestamp()
965 self._serve_file(lmt, mime_type, content, filename)
967 def serve_static_file(self, file):
968 """ Serve up the file named from the templates dir
969 """
970 # figure the filename - try STATIC_FILES, then TEMPLATES dir
971 for dir_option in ('STATIC_FILES', 'TEMPLATES'):
972 prefix = self.instance.config[dir_option]
973 if not prefix:
974 continue
975 # ensure the load doesn't try to poke outside
976 # of the static files directory
977 prefix = os.path.normpath(prefix)
978 filename = os.path.normpath(os.path.join(prefix, file))
979 if os.path.isfile(filename) and filename.startswith(prefix):
980 break
981 else:
982 raise NotFound(file)
984 # last-modified time
985 lmt = os.stat(filename)[stat.ST_MTIME]
987 # detemine meta-type
988 file = str(file)
989 mime_type = mimetypes.guess_type(file)[0]
990 if not mime_type:
991 if file.endswith('.css'):
992 mime_type = 'text/css'
993 else:
994 mime_type = 'text/plain'
996 self._serve_file(lmt, mime_type, '', filename)
998 def _serve_file(self, lmt, mime_type, content=None, filename=None):
999 """ guts of serve_file() and serve_static_file()
1000 """
1002 # spit out headers
1003 self.additional_headers['Content-Type'] = mime_type
1004 self.additional_headers['Last-Modified'] = rfc822.formatdate(lmt)
1006 ims = None
1007 # see if there's an if-modified-since...
1008 # XXX see which interfaces set this
1009 #if hasattr(self.request, 'headers'):
1010 #ims = self.request.headers.getheader('if-modified-since')
1011 if 'HTTP_IF_MODIFIED_SINCE' in self.env:
1012 # cgi will put the header in the env var
1013 ims = self.env['HTTP_IF_MODIFIED_SINCE']
1014 if ims:
1015 ims = rfc822.parsedate(ims)[:6]
1016 lmtt = time.gmtime(lmt)[:6]
1017 if lmtt <= ims:
1018 raise NotModified
1020 if filename:
1021 self.write_file(filename)
1022 else:
1023 self.additional_headers['Content-Length'] = str(len(content))
1024 self.write(content)
1026 def send_error_to_admin(self, subject, html, txt):
1027 """Send traceback information to admin via email.
1028 We send both, the formatted html (with more information) and
1029 the text version of the traceback. We use
1030 multipart/alternative so the receiver can chose which version
1031 to display.
1032 """
1033 to = [self.mailer.config.ADMIN_EMAIL]
1034 message = MIMEMultipart('alternative')
1035 self.mailer.set_message_attributes(message, to, subject)
1036 part = MIMEBase('text', 'html')
1037 part.set_charset('utf-8')
1038 part.set_payload(html)
1039 encode_quopri(part)
1040 message.attach(part)
1041 part = MIMEText(txt)
1042 message.attach(part)
1043 self.mailer.smtp_send(to, message.as_string())
1045 def renderFrontPage(self, message):
1046 """Return the front page of the tracker."""
1048 self.classname = self.nodeid = None
1049 self.template = ''
1050 self.error_message.append(message)
1051 self.write_html(self.renderContext())
1053 def renderContext(self):
1054 """ Return a PageTemplate for the named page
1055 """
1056 name = self.classname
1057 extension = self.template
1059 # catch errors so we can handle PT rendering errors more nicely
1060 args = {
1061 'ok_message': self.ok_message,
1062 'error_message': self.error_message
1063 }
1064 try:
1065 pt = self.instance.templates.get(name, extension)
1066 # let the template render figure stuff out
1067 result = pt.render(self, None, None, **args)
1068 self.additional_headers['Content-Type'] = pt.content_type
1069 if self.env.get('CGI_SHOW_TIMING', ''):
1070 if self.env['CGI_SHOW_TIMING'].upper() == 'COMMENT':
1071 timings = {'starttag': '<!-- ', 'endtag': ' -->'}
1072 else:
1073 timings = {'starttag': '<p>', 'endtag': '</p>'}
1074 timings['seconds'] = time.time()-self.start
1075 s = self._('%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n'
1076 ) % timings
1077 if hasattr(self.db, 'stats'):
1078 timings.update(self.db.stats)
1079 s += self._("%(starttag)sCache hits: %(cache_hits)d,"
1080 " misses %(cache_misses)d."
1081 " Loading items: %(get_items)f secs."
1082 " Filtering: %(filtering)f secs."
1083 "%(endtag)s\n") % timings
1084 s += '</body>'
1085 result = result.replace('</body>', s)
1086 return result
1087 except templating.NoTemplate, message:
1088 return '<strong>%s</strong>'%cgi.escape(str(message))
1089 except templating.Unauthorised, message:
1090 raise Unauthorised(cgi.escape(str(message)))
1091 except:
1092 # everything else
1093 if self.instance.config.WEB_DEBUG:
1094 return cgitb.pt_html(i18n=self.translator)
1095 exc_info = sys.exc_info()
1096 try:
1097 # If possible, send the HTML page template traceback
1098 # to the administrator.
1099 subject = "Templating Error: %s" % exc_info[1]
1100 self.send_error_to_admin(subject, cgitb.pt_html(), format_exc())
1101 # Now report the error to the user.
1102 return self._(error_message)
1103 except:
1104 # Reraise the original exception. The user will
1105 # receive an error message, and the adminstrator will
1106 # receive a traceback, albeit with less information
1107 # than the one we tried to generate above.
1108 raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
1110 # these are the actions that are available
1111 actions = (
1112 ('edit', EditItemAction),
1113 ('editcsv', EditCSVAction),
1114 ('new', NewItemAction),
1115 ('register', RegisterAction),
1116 ('confrego', ConfRegoAction),
1117 ('passrst', PassResetAction),
1118 ('login', LoginAction),
1119 ('logout', LogoutAction),
1120 ('search', SearchAction),
1121 ('retire', RetireAction),
1122 ('show', ShowAction),
1123 ('export_csv', ExportCSVAction),
1124 )
1125 def handle_action(self):
1126 """ Determine whether there should be an Action called.
1128 The action is defined by the form variable :action which
1129 identifies the method on this object to call. The actions
1130 are defined in the "actions" sequence on this class.
1132 Actions may return a page (by default HTML) to return to the
1133 user, bypassing the usual template rendering.
1135 We explicitly catch Reject and ValueError exceptions and
1136 present their messages to the user.
1137 """
1138 if ':action' in self.form:
1139 action = self.form[':action']
1140 elif '@action' in self.form:
1141 action = self.form['@action']
1142 else:
1143 return None
1145 if isinstance(action, list):
1146 raise SeriousError('broken form: multiple @action values submitted')
1147 else:
1148 action = action.value.lower()
1150 try:
1151 action_klass = self.get_action_class(action)
1153 # call the mapped action
1154 if isinstance(action_klass, type('')):
1155 # old way of specifying actions
1156 return getattr(self, action_klass)()
1157 else:
1158 return action_klass(self).execute()
1160 except (ValueError, Reject), err:
1161 self.error_message.append(str(err))
1163 def get_action_class(self, action_name):
1164 if (hasattr(self.instance, 'cgi_actions') and
1165 action_name in self.instance.cgi_actions):
1166 # tracker-defined action
1167 action_klass = self.instance.cgi_actions[action_name]
1168 else:
1169 # go with a default
1170 for name, action_klass in self.actions:
1171 if name == action_name:
1172 break
1173 else:
1174 raise ValueError('No such action "%s"'%action_name)
1175 return action_klass
1177 def _socket_op(self, call, *args, **kwargs):
1178 """Execute socket-related operation, catch common network errors
1180 Parameters:
1181 call: a callable to execute
1182 args, kwargs: call arguments
1184 """
1185 try:
1186 call(*args, **kwargs)
1187 except socket.error, err:
1188 err_errno = getattr (err, 'errno', None)
1189 if err_errno is None:
1190 try:
1191 err_errno = err[0]
1192 except TypeError:
1193 pass
1194 if err_errno not in self.IGNORE_NET_ERRORS:
1195 raise
1196 except IOError:
1197 # Apache's mod_python will raise IOError -- without an
1198 # accompanying errno -- when a write to the client fails.
1199 # A common case is that the client has closed the
1200 # connection. There's no way to be certain that this is
1201 # the situation that has occurred here, but that is the
1202 # most likely case.
1203 pass
1205 def write(self, content):
1206 if not self.headers_done:
1207 self.header()
1208 if self.env['REQUEST_METHOD'] != 'HEAD':
1209 self._socket_op(self.request.wfile.write, content)
1211 def write_html(self, content):
1212 if not self.headers_done:
1213 # at this point, we are sure about Content-Type
1214 if 'Content-Type' not in self.additional_headers:
1215 self.additional_headers['Content-Type'] = \
1216 'text/html; charset=%s' % self.charset
1217 self.header()
1219 if self.env['REQUEST_METHOD'] == 'HEAD':
1220 # client doesn't care about content
1221 return
1223 if self.charset != self.STORAGE_CHARSET:
1224 # recode output
1225 content = content.decode(self.STORAGE_CHARSET, 'replace')
1226 content = content.encode(self.charset, 'xmlcharrefreplace')
1228 # and write
1229 self._socket_op(self.request.wfile.write, content)
1231 def http_strip(self, content):
1232 """Remove HTTP Linear White Space from 'content'.
1234 'content' -- A string.
1236 returns -- 'content', with all leading and trailing LWS
1237 removed."""
1239 # RFC 2616 2.2: Basic Rules
1240 #
1241 # LWS = [CRLF] 1*( SP | HT )
1242 return content.strip(" \r\n\t")
1244 def http_split(self, content):
1245 """Split an HTTP list.
1247 'content' -- A string, giving a list of items.
1249 returns -- A sequence of strings, containing the elements of
1250 the list."""
1252 # RFC 2616 2.1: Augmented BNF
1253 #
1254 # Grammar productions of the form "#rule" indicate a
1255 # comma-separated list of elements matching "rule". LWS
1256 # is then removed from each element, and empty elements
1257 # removed.
1259 # Split at commas.
1260 elements = content.split(",")
1261 # Remove linear whitespace at either end of the string.
1262 elements = [self.http_strip(e) for e in elements]
1263 # Remove any now-empty elements.
1264 return [e for e in elements if e]
1266 def handle_range_header(self, length, etag):
1267 """Handle the 'Range' and 'If-Range' headers.
1269 'length' -- the length of the content available for the
1270 resource.
1272 'etag' -- the entity tag for this resources.
1274 returns -- If the request headers (including 'Range' and
1275 'If-Range') indicate that only a portion of the entity should
1276 be returned, then the return value is a pair '(offfset,
1277 length)' indicating the first byte and number of bytes of the
1278 content that should be returned to the client. In addition,
1279 this method will set 'self.response_code' to indicate Partial
1280 Content. In all other cases, the return value is 'None'. If
1281 appropriate, 'self.response_code' will be
1282 set to indicate 'REQUESTED_RANGE_NOT_SATISFIABLE'. In that
1283 case, the caller should not send any data to the client."""
1285 # RFC 2616 14.35: Range
1286 #
1287 # See if the Range header is present.
1288 ranges_specifier = self.env.get("HTTP_RANGE")
1289 if ranges_specifier is None:
1290 return None
1291 # RFC 2616 14.27: If-Range
1292 #
1293 # Check to see if there is an If-Range header.
1294 # Because the specification says:
1295 #
1296 # The If-Range header ... MUST be ignored if the request
1297 # does not include a Range header, we check for If-Range
1298 # after checking for Range.
1299 if_range = self.env.get("HTTP_IF_RANGE")
1300 if if_range:
1301 # The grammar for the If-Range header is:
1302 #
1303 # If-Range = "If-Range" ":" ( entity-tag | HTTP-date )
1304 # entity-tag = [ weak ] opaque-tag
1305 # weak = "W/"
1306 # opaque-tag = quoted-string
1307 #
1308 # We only support strong entity tags.
1309 if_range = self.http_strip(if_range)
1310 if (not if_range.startswith('"')
1311 or not if_range.endswith('"')):
1312 return None
1313 # If the condition doesn't match the entity tag, then we
1314 # must send the client the entire file.
1315 if if_range != etag:
1316 return
1317 # The grammar for the Range header value is:
1318 #
1319 # ranges-specifier = byte-ranges-specifier
1320 # byte-ranges-specifier = bytes-unit "=" byte-range-set
1321 # byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec )
1322 # byte-range-spec = first-byte-pos "-" [last-byte-pos]
1323 # first-byte-pos = 1*DIGIT
1324 # last-byte-pos = 1*DIGIT
1325 # suffix-byte-range-spec = "-" suffix-length
1326 # suffix-length = 1*DIGIT
1327 #
1328 # Look for the "=" separating the units from the range set.
1329 specs = ranges_specifier.split("=", 1)
1330 if len(specs) != 2:
1331 return None
1332 # Check that the bytes-unit is in fact "bytes". If it is not,
1333 # we do not know how to process this range.
1334 bytes_unit = self.http_strip(specs[0])
1335 if bytes_unit != "bytes":
1336 return None
1337 # Seperate the range-set into range-specs.
1338 byte_range_set = self.http_strip(specs[1])
1339 byte_range_specs = self.http_split(byte_range_set)
1340 # We only handle exactly one range at this time.
1341 if len(byte_range_specs) != 1:
1342 return None
1343 # Parse the spec.
1344 byte_range_spec = byte_range_specs[0]
1345 pos = byte_range_spec.split("-", 1)
1346 if len(pos) != 2:
1347 return None
1348 # Get the first and last bytes.
1349 first = self.http_strip(pos[0])
1350 last = self.http_strip(pos[1])
1351 # We do not handle suffix ranges.
1352 if not first:
1353 return None
1354 # Convert the first and last positions to integers.
1355 try:
1356 first = int(first)
1357 if last:
1358 last = int(last)
1359 else:
1360 last = length - 1
1361 except:
1362 # The positions could not be parsed as integers.
1363 return None
1364 # Check that the range makes sense.
1365 if (first < 0 or last < 0 or last < first):
1366 return None
1367 if last >= length:
1368 # RFC 2616 10.4.17: 416 Requested Range Not Satisfiable
1369 #
1370 # If there is an If-Range header, RFC 2616 says that we
1371 # should just ignore the invalid Range header.
1372 if if_range:
1373 return None
1374 # Return code 416 with a Content-Range header giving the
1375 # allowable range.
1376 self.response_code = http_.client.REQUESTED_RANGE_NOT_SATISFIABLE
1377 self.setHeader("Content-Range", "bytes */%d" % length)
1378 return None
1379 # RFC 2616 10.2.7: 206 Partial Content
1380 #
1381 # Tell the client that we are honoring the Range request by
1382 # indicating that we are providing partial content.
1383 self.response_code = http_.client.PARTIAL_CONTENT
1384 # RFC 2616 14.16: Content-Range
1385 #
1386 # Tell the client what data we are providing.
1387 #
1388 # content-range-spec = byte-content-range-spec
1389 # byte-content-range-spec = bytes-unit SP
1390 # byte-range-resp-spec "/"
1391 # ( instance-length | "*" )
1392 # byte-range-resp-spec = (first-byte-pos "-" last-byte-pos)
1393 # | "*"
1394 # instance-length = 1 * DIGIT
1395 self.setHeader("Content-Range",
1396 "bytes %d-%d/%d" % (first, last, length))
1397 return (first, last - first + 1)
1399 def write_file(self, filename):
1400 """Send the contents of 'filename' to the user."""
1402 # Determine the length of the file.
1403 stat_info = os.stat(filename)
1404 length = stat_info[stat.ST_SIZE]
1405 # Assume we will return the entire file.
1406 offset = 0
1407 # If the headers have not already been finalized,
1408 if not self.headers_done:
1409 # RFC 2616 14.19: ETag
1410 #
1411 # Compute the entity tag, in a format similar to that
1412 # used by Apache.
1413 etag = '"%x-%x-%x"' % (stat_info[stat.ST_INO],
1414 length,
1415 stat_info[stat.ST_MTIME])
1416 self.setHeader("ETag", etag)
1417 # RFC 2616 14.5: Accept-Ranges
1418 #
1419 # Let the client know that we will accept range requests.
1420 self.setHeader("Accept-Ranges", "bytes")
1421 # RFC 2616 14.35: Range
1422 #
1423 # If there is a Range header, we may be able to avoid
1424 # sending the entire file.
1425 content_range = self.handle_range_header(length, etag)
1426 if content_range:
1427 offset, length = content_range
1428 # RFC 2616 14.13: Content-Length
1429 #
1430 # Tell the client how much data we are providing.
1431 self.setHeader("Content-Length", str(length))
1432 # Send the HTTP header.
1433 self.header()
1434 # If the client doesn't actually want the body, or if we are
1435 # indicating an invalid range.
1436 if (self.env['REQUEST_METHOD'] == 'HEAD'
1437 or self.response_code == http_.client.REQUESTED_RANGE_NOT_SATISFIABLE):
1438 return
1439 # Use the optimized "sendfile" operation, if possible.
1440 if hasattr(self.request, "sendfile"):
1441 self._socket_op(self.request.sendfile, filename, offset, length)
1442 return
1443 # Fallback to the "write" operation.
1444 f = open(filename, 'rb')
1445 try:
1446 if offset:
1447 f.seek(offset)
1448 content = f.read(length)
1449 finally:
1450 f.close()
1451 self.write(content)
1453 def setHeader(self, header, value):
1454 """Override a header to be returned to the user's browser.
1455 """
1456 self.additional_headers[header] = value
1458 def header(self, headers=None, response=None):
1459 """Put up the appropriate header.
1460 """
1461 if headers is None:
1462 headers = {'Content-Type':'text/html; charset=utf-8'}
1463 if response is None:
1464 response = self.response_code
1466 # update with additional info
1467 headers.update(self.additional_headers)
1469 if headers.get('Content-Type', 'text/html') == 'text/html':
1470 headers['Content-Type'] = 'text/html; charset=utf-8'
1472 headers = list(headers.items())
1474 for ((path, name), (value, expire)) in self._cookies.iteritems():
1475 cookie = "%s=%s; Path=%s;"%(name, value, path)
1476 if expire is not None:
1477 cookie += " expires=%s;"%get_cookie_date(expire)
1478 headers.append(('Set-Cookie', cookie))
1480 self._socket_op(self.request.start_response, headers, response)
1482 self.headers_done = 1
1483 if self.debug:
1484 self.headers_sent = headers
1486 def add_cookie(self, name, value, expire=86400*365, path=None):
1487 """Set a cookie value to be sent in HTTP headers
1489 Parameters:
1490 name:
1491 cookie name
1492 value:
1493 cookie value
1494 expire:
1495 cookie expiration time (seconds).
1496 If value is empty (meaning "delete cookie"),
1497 expiration time is forced in the past
1498 and this argument is ignored.
1499 If None, the cookie will expire at end-of-session.
1500 If omitted, the cookie will be kept for a year.
1501 path:
1502 cookie path (optional)
1504 """
1505 if path is None:
1506 path = self.cookie_path
1507 if not value:
1508 expire = -1
1509 self._cookies[(path, name)] = (value, expire)
1511 def set_cookie(self, user, expire=None):
1512 """Deprecated. Use session_api calls directly
1514 XXX remove
1515 """
1517 # insert the session in the session db
1518 self.session_api.set(user=user)
1519 # refresh session cookie
1520 self.session_api.update(set_cookie=True, expire=expire)
1522 def make_user_anonymous(self):
1523 """ Make us anonymous
1525 This method used to handle non-existence of the 'anonymous'
1526 user, but that user is mandatory now.
1527 """
1528 self.userid = self.db.user.lookup('anonymous')
1529 self.user = 'anonymous'
1531 def standard_message(self, to, subject, body, author=None):
1532 """Send a standard email message from Roundup.
1534 "to" - recipients list
1535 "subject" - Subject
1536 "body" - Message
1537 "author" - (name, address) tuple or None for admin email
1539 Arguments are passed to the Mailer.standard_message code.
1540 """
1541 try:
1542 self.mailer.standard_message(to, subject, body, author)
1543 except MessageSendError, e:
1544 self.error_message.append(str(e))
1545 return 0
1546 return 1
1548 def parsePropsFromForm(self, create=0):
1549 return FormParser(self).parse(create=create)
1551 # vim: set et sts=4 sw=4 :