Package pythonutils :: Module cgiutils
[hide private]
[frames] | no frames]

Source Code for Module pythonutils.cgiutils

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