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