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


UCC Code Repository

Contents of /bunnyblog/modules/cgiutils.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1 - (hide 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 svn-admin 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