/[theodore]/bunnyblog/modules/cgiutils.py


UCC Code Repository

Contents of /bunnyblog/modules/cgiutils.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1 - (show annotations) (download) (as text)
Tue Jan 29 14:32:01 2008 UTC (12 years, 2 months ago) by svn-admin
File MIME type: text/x-python
File size: 17989 byte(s)
Re-import of repository after repository database corruption.

1 # Version 0.3.4
2 # 2005/10/29
3
4 # Copyright Michael Foord 2004 & 2005
5 # cgiutils.py
6 # Functions and constants useful for working with CGIs
7
8 # http://www.voidspace.org.uk/python/modules.shtml
9
10 # Released subject to the BSD License
11 # Please see http://www.voidspace.org.uk/documents/BSD-LICENSE.txt
12
13 # For information about bugfixes, updates and support, please join the Pythonutils mailing list.
14 # http://voidspace.org.uk/mailman/listinfo/pythonutils_voidspace.org.uk
15 # Comments, suggestions and bug reports welcome.
16 # Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
17 # E-mail fuzzyman@voidspace.org.uk
18
19 import os
20 import sys
21
22 __all__ = [
23 'serverline',
24 'SENDMAIL',
25 'validchars',
26 'alphanums',
27 'getrequest',
28 'getform',
29 'getall',
30 'isblank',
31 'formencode',
32 'formdecode',
33 'mailme',
34 'sendmailme',
35 'createhtmlmail',
36 'environdata',
37 'validemail',
38 'cgiprint',
39 'ucgiprint',
40 'replace',
41 'error',
42 'makeindexline',
43 'istrue',
44 'randomstring',
45 ]
46
47 serverline = "Content-Type: text/html"
48
49 # A common location of sendmail on servers
50 SENDMAIL = "/usr/sbin/sendmail"
51 validchars = 'abcdefghijklmnopqrstuvwxyz0123456789!-_*'
52 alphanums = 'abcdefghijklmnopqrstuvwxyz0123456789'
53
54 #######################################################
55 # Some functions for dealing with CGI forms (instances of FieldStorage)
56
57 def getrequest(valuelist=None, nolist=False):
58 """
59 Initialise the ``FieldStorage`` and return the specified list of values as
60 a dictionary.
61
62 If you don't specify a list of values, then *all* values will be returned.
63
64 If you set ``nolist`` to ``True`` then any parameters supplied as lists
65 will only have their first entry returned.
66 """
67 import cgi
68 form = cgi.FieldStorage()
69 if valuelist is not None:
70 return getform(valuelist, form, nolist=nolist)
71 else:
72 return getall(form, nolist=nolist)
73
74 def getform(valuelist, theform, notpresent='', nolist=False):
75 """
76 This function, given a CGI form, extracts the data from it, based on
77 valuelist passed in. Any non-present values are set to '' - although this
78 can be changed.
79
80 It also takes a keyword argument 'nolist'. If this is True list values only
81 return their first value.
82
83 Returns a dictionary.
84 """
85 data = {}
86 for field in valuelist:
87 if not theform.has_key(field):
88 data[field] = notpresent
89 else:
90 if not isinstance(theform[field], list):
91 data[field] = theform[field].value
92 else:
93 if not nolist:
94 # allows for list type values
95 data[field] = [x.value for x in theform[field]]
96 else:
97 # just fetch the first item
98 data[field] = theform[field][0].value
99 return data
100
101 def getall(theform, nolist=False):
102 """
103 Passed a form (FieldStorage instance) return all the values.
104 This doesn't take into account file uploads.
105
106 Also accepts the 'nolist' keyword argument as ``getform``.
107
108 Returns a dictionary.
109 """
110 data = {}
111 for field in theform.keys():
112 if not isinstance(theform[field], list):
113 data[field] = theform[field].value
114 else:
115 if not nolist:
116 # allows for list type values
117 data[field] = [x.value for x in theform[field]]
118 else:
119 # just fetch the first item
120 data[field] = theform[field][0].value
121 return data
122
123 def isblank(indict):
124 """
125 Passed an indict of values it checks if any of the values are set.
126
127 Returns ``True`` if every member of the indict is empty (evaluates as False).
128
129 I use it on a form processed with getform to tell if my CGI has been
130 activated without any values.
131 """
132 return not [val for val in indict.values() if val]
133
134 def formencode(theform):
135 """
136 A version that turns a cgi form into a single string.
137 It only handles single and list values, not multipart.
138 This allows the contents of a form requested to be encoded into a single value as part of another request.
139 """
140 from urllib import urlencode, quote_plus
141 return quote_plus(urlencode(getall(theform)))
142
143 def formdecode(thestring):
144 """Decode a single string back into a form like dictionary."""
145 from cgi import parse_qs
146 from urllib import unquote_plus
147 return parse_qs(unquote_plus(thestring), True)
148
149
150 #############################################################
151 # Functions for handling emails
152 #
153 # Use mailme for sending email - specify a path to sendmail *or* a host, port etc (optionally username)
154
155
156 def mailme(to_email, msg, email_subject=None, from_email=None,
157 host='localhost', port=25, username=None, password=None,
158 html=True, sendmail=None):
159 """
160 This function will send an email using ``sendmail`` or ``smtplib``, depending
161 on what parameters you pass it.
162
163 If you want to use ``sendmail`` to send the email then set
164 ``sendmail='/path/to/sendmail'``. (The ``SENDMAIL`` value from Constants_ often
165 works).
166
167 If you aren't using sendmail then you will need to set ``host`` and ``port`` to
168 the correct values. If your server requires authentication then you'll need to
169 supply the correct ``username`` and ``password``.
170
171 ``to_email`` can be a single email address, *or* a list of addresses.
172
173 ``mailme`` *assumes* you are sending an html email created by
174 ``createhtmlmail``. If this isn't the case then set ``html=False``.
175
176 Some servers won't let you send a message without supplying a ``from_email``.
177 """
178 if sendmail is not None:
179 # use sendmailme if specified
180 return sendmailme(to_email, msg, email_subject, from_email,
181 html, sendmail)
182 if not isinstance(to_email, list):
183 # if we have a single email then change it into a list
184 to_email = [to_email]
185 #
186 import smtplib
187 #
188 head = "To: %s\r\n" % ','.join(to_email)
189 if from_email is not None:
190 head += ('From: %s\r\n' % from_email)
191 # subject is in the body of an html email
192 if not html and email_subject is not None:
193 head += ("Subject: %s\r\n\r\n" % email_subject)
194 msg = head + msg
195 #
196 server = smtplib.SMTP(host, port)
197 if username:
198 server.login(username, password)
199 server.sendmail(from_email, to_email, msg)
200 server.quit()
201
202
203 def sendmailme(to_email, msg, email_subject=None, from_email=None,
204 html=True, sendmail=SENDMAIL):
205 """
206 Quick and dirty, pipe a message to sendmail. Can only work on UNIX type systems
207 with sendmail.
208
209 Will need the path to sendmail - defaults to the 'SENDMAIL' constant.
210
211 ``to_email`` can be a single email address, *or* a list of addresses.
212
213 *Assumes* you are sending an html email created by ``createhtmlmail``. If this
214 isn't the case then set ``html=False``.
215 """
216 if not isinstance(to_email, list):
217 to_email = [to_email]
218 o = os.popen("%s -t" % sendmail,"w")
219 o.write("To: %s\r\n" % ','.join(to_email))
220 if from_email:
221 o.write("From: %s\r\n" % from_email)
222 if not html and email_subject:
223 o.write("Subject: %s\r\n" % email_subject)
224 o.write("\r\n")
225 o.write("%s\r\n" % msg)
226 o.close()
227
228 def createhtmlmail(subject, html, text=None):
229 """
230 Create a mime-message that will render as HTML or text as appropriate.
231 If no text is supplied we use htmllib to guess a text rendering.
232 (so html needs to be well formed)
233
234 Adapted from recipe 13.5 from Python Cookbook 2
235 """
236 import MimeWriter, mimetools, StringIO
237 if text is None:
238 # produce an approximate text from the HTML input
239 import htmllib
240 import formatter
241 textout = StringIO.StringIO()
242 formtext = formatter.AbstractFormatter(formatter.DumbWriter(textout))
243 parser = htmllib.HTMLParser(formtext)
244 parser.feed(html)
245 parser.close()
246 text = textout.getvalue()
247 del textout, formtext, parser
248 out = StringIO.StringIO() # output buffer for our message
249 htmlin = StringIO.StringIO(html) # input buffer for the HTML
250 txtin = StringIO.StringIO(text) # input buffer for the plain text
251 writer = MimeWriter.MimeWriter(out)
252 # Set up some basic headers. Place subject here because smtplib.sendmail
253 # expects it to be in the message, as relevant RFCs prescribe.
254 writer.addheader("Subject", subject)
255 writer.addheader("MIME-Version", "1.0")
256 # Start the multipart section of the message. Multipart/alternative seems
257 # to work better on some MUAs than multipart/mixed.
258 writer.startmultipartbody("alternative")
259 writer.flushheaders()
260 # the plain-text section: just copied through, assuming iso-8859-1 # XXXX always true ?
261 subpart = writer.nextpart()
262 pout = subpart.startbody("text/plain", [("charset", 'iso-8859-l')])
263 pout.write(txtin.read())
264 txtin.close()
265 # the HTML subpart of the message: quoted-printable, just in case
266 subpart = writer.nextpart()
267 subpart.addheader("Content-Transfer-Encoding", "quoted-printable")
268 pout = subpart.startbody("text/html", [("charset", 'us-ascii')])
269 mimetools.encode(htmlin, pout, 'quoted-printable')
270 htmlin.close()
271 # You're done; close your writer and return the message as a string
272 writer.lastpart()
273 msg = out.getvalue()
274 out.close()
275 return msg
276
277 def environdata():
278 """Returns some data about the CGI environment, in a way that can be mailed."""
279 ENVIRONLIST = [ 'REQUEST_URI','HTTP_USER_AGENT','REMOTE_ADDR','HTTP_FROM','REMOTE_HOST','REMOTE_PORT','SERVER_SOFTWARE','HTTP_REFERER','REMOTE_IDENT','REMOTE_USER','QUERY_STRING','DATE_LOCAL' ] # XXX put this in template ??
280 environs = []
281 environs.append("\n\n---------------------------------------\n")
282 for x in ENVIRONLIST:
283 if os.environ.has_key(x):
284 environs.append("%s: %s\n" % (x, os.environ[x]))
285 environs.append("---------------------------------------\n")
286 return ''.join(environs)
287
288 def validemail(email):
289 """
290 A quick function to do a basic email validation.
291 Returns False or the email address.
292 """
293 if ' ' in email:
294 return False
295 dot = email.rfind('.')
296 at = email.find('@')
297 if dot == -1 or at < 1 or at > dot:
298 return False
299 return email
300
301 ##########################################################
302
303 def error(errorval=''):
304 """The generic error function."""
305 print serverline
306 print
307 print '''<html><head><title>An Error Has Occurred</title>
308 <body><center>
309 <h1>Very Sorry</h1>
310 <h2>An Error Has Occurred</h2>'''
311 if errorval:
312 print '<h3>%s</h3>' % errorval
313 print '</center></body></html>'
314 sys.exit()
315
316 #########################################################
317
318 def makeindexline(url, startpage, total, numonpage=10, pagesonscreen=5):
319 """
320 Make a menu line for a given number of inputs, with a certain number per page.
321 Will look something like : ::
322
323 First Previous 22 23 24 25 26 27 28 29 30 31 32 Next Last
324
325 Each number or word will be a link to the relevant page.
326
327 url should be in the format : ``'<a href="script.py?startpage=%s">%s</a>'`` -
328 it will have the two ``%s`` values filled in by the function.
329
330 The url will automatically be put between ``<strong></strong>`` tags. Your
331 script needs to accepts a parameter ``start`` telling it which page to display.
332
333 ``startpage`` is the page actually being viewed - which won't be a link.
334
335 ``total`` is the number of total inputs.
336
337 ``numonpage`` is the number of inputs per page - this tells makeindexline how
338 many pages to divide the total into.
339
340 The links shown will be some before startpage and some after. The amount of
341 pages links are shown for is ``pagesonscreen``. (The actual total number shown
342 will be *2 \* pagesonscreen + 1*).
343
344 The indexes generated are *a bit* like the ones created by google. Unlike
345 google however, next and previous jump you into the *middle* of the next set of
346 links. i.e. If you are on page 27 next will take you to 33 and previous to 21.
347 (assuming pagesonscreen is 5). This makes it possible to jump more quickly
348 through a lot of links. Also - the current page will always be in the center of
349 the index. (So you never *need* Next just to get to the next page).
350 """
351 b = '<strong>%s</strong>'
352 url = b % url
353 outlist = []
354 last = ''
355 next = ''
356 numpages = total//numonpage
357 if total%numonpage:
358 numpages += 1
359 if startpage - pagesonscreen > 1:
360 outlist.append(url % (1, 'First'))
361 outlist.append('&nbsp;')
362 outlist.append(url % (startpage-pagesonscreen-1, 'Previous'))
363 outlist.append('&nbsp;')
364 index = max(startpage - pagesonscreen, 1)
365 end = min(startpage+pagesonscreen, numpages)
366 while index <= end:
367 if index == startpage:
368 outlist.append(b % startpage)
369 else:
370 outlist.append(url % (index, index))
371 index += 1
372 outlist.append('&nbsp;')
373 if (startpage+pagesonscreen) < numpages:
374 outlist.append(url % (startpage+pagesonscreen+1, 'Next'))
375 outlist.append('&nbsp;')
376 outlist.append(url % (numpages, 'Last'))
377
378 return '&nbsp;'.join(outlist)
379
380 ######################################
381
382 def istrue(value):
383 """
384 Accepts a string as input.
385
386 If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns
387 ``True``.
388
389 If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns
390 ``False``.
391
392 ``istrue`` is not case sensitive.
393
394 Any other input will raise a ``KeyError``.
395 """
396 return {
397 'yes': True, 'no': False,
398 'on': True, 'off': False,
399 '1': True, '0': False,
400 'true': True, 'false': False,
401 }[value.lower()]
402
403 def randomstring(length):
404 """
405 Return a random string of length 'length'.
406
407 The string is comprised only of numbers and lowercase letters.
408 """
409 import random
410 outstring = []
411 while length > 0:
412 length -=1
413 outstring.append(alphanums[int(random.random()*36)])
414 return ''.join(outstring)
415
416 ##################################
417
418 def cgiprint(inline='', unbuff=False, line_end='\r\n'):
419 """
420 Print to the ``stdout``.
421
422 Set ``unbuff=True`` to flush the buffer after every write.
423
424 It prints the inline you send it, followed by the ``line_end``. By default this
425 is ``\r\n`` - which is the standard specified by the RFC for http headers.
426 """
427 sys.stdout.write(inline)
428 sys.stdout.write(line_end)
429 if unbuff:
430 sys.stdout.flush()
431
432 def ucgiprint(inline='', unbuff=False, encoding='UTF-8', line_end='\r\n'):
433 """
434 A unicode version of ``cgiprint``. It allows you to store everything in your
435 script as unicode and just do your encoding in one place.
436
437 Print to the ``stdout``.
438
439 Set ``unbuff=True`` to flush the buffer after every write.
440
441 It prints the inline you send it, followed by the ``line_end``. By default this
442 is ``\r\n`` - which is the standard specified by the RFC for http headers.
443
444 ``inline`` should be a unicode string.
445
446 ``encoding`` is the encoding used to encode ``inline`` to a byte-string. It
447 defaults to ``UTF-8``, set it to ``None`` if you pass in ``inline`` as a byte
448 string rather than a unicode string.
449 """
450 if encoding:
451 inline = inline.encode(encoding)
452 # don't need to encode the line endings
453 sys.stdout.write(inline)
454 sys.stdout.write(line_end)
455 if unbuff:
456 sys.stdout.flush()
457
458 def replace(instring, indict):
459 """
460 This function provides a simple but effective template system for your html
461 pages. Effectively it is a convenient way of doing multiple replaces in a
462 single string.
463
464 Takes a string and a dictionary of replacements.
465
466 This function goes through the string and replaces every occurrence of every
467 dicitionary key with it's value.
468
469 ``indict`` can also be a list of tuples instead of a dictionary (or anything
470 accepted by the dict function).
471 """
472 indict = dict(indict)
473 if len(indict) > 40:
474 regex = re.compile("(%s)" % "|".join(map(re.escape, indict.keys())))
475 # For each match, look-up corresponding value in dictionary
476 return regex.sub(lambda mo: indict[mo.string[mo.start():mo.end()]],
477 instring)
478 for key in indict:
479 instring = instring.replace(key, indict[key])
480 return instring
481
482 ############################
483
484 if __name__ == '__main__':
485 print 'No tests yet - sorry'
486
487 """
488 TODO/ISSUES
489 ===========
490
491 The indexes generated by makeindexline use next to jump 10 pages. This is
492 different to what people will expect if they are used to the 'Google' type
493 index lines.
494
495 createhtmlmail assumes iso-8859-1 input encoding for the html
496
497 email functions to support 'cc' and 'bcc'
498
499 Need doctests
500
501 Changelog
502 ==========
503 2005/10/29 Version 0.3.4
504 Shortened ``isblank``.
505
506 2005/09/21 Version 0.3.3
507 Fixed bug in ``getall`.
508 Fixed bug in ``getrequest``.
509
510 2005/08/27 Version 0.3.2
511 Large dictionary replaces use a regex approach.
512
513 2005/08/20 Version 0.3.1
514 Improved istrue function.
515 Added __all__.
516 Various other code/doc improvements.
517
518 2005/04/07 Version 0.3.0
519 Changed the email functions, this may break things (but it's better this way)
520 Added createhtmlemail, removed loginmailme
521 mailme is now a wrapper for sendmailme, mailme, *and* the old loginmailme
522
523 2005/03/20 Version 0.2.0
524 Added ucgiprint and replace.
525
526 2005/02/18 Version 0.1.0
527 The first numbered version.
528 """

Managed by UCC Webmasters ViewVC Help
Powered by ViewVC 1.1.26