#!/usr/pkg/bin/python """integration with Paypal shopping cart and IPN notification will document better as it progresses i hope... """ Copyright = """ paypal_ipn - process instant payment notification from Paypal Copyright (C) 2004 John Comeau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. """ errormessage = "Not all needed libraries found, upgrade or check path" try: True # not defined in older Python releases except: True, False = 1, 0 try: import sys, os, types, re, pwd sys.path.append(os.sep.join([pwd.getpwuid(os.geteuid())[5], 'lib', 'python'])) from com.jcomeau import gpl, jclicense except: try: sys.stderr.write("%s\n" % errormessage) except: print errormessage raise # get name this program was called as self = sys.argv[0].split(os.sep)[-1] # now get name we gave it when we wrote it originalself = Copyright.split()[0] # globals and routines that should be in every program # (yes, you could import them, but there are problems in that approach too) def DebugPrint(*whatever): return False # defined instead by pytest module, use that for debugging # other globals, specific to this program def dump_env(): for key in os.environ.keys(): print '%s: %s' % (key, os.environ[key]) def ipn_test(): sys.stdout.write("Content-type: text/html\r\n\r\n") # no extra \n this way print "TESTING:" print "
"
 dump_env()
 if os.getenv('CONTENT_LENGTH') is not None:
  print "\nPOST content:\n"
  print sys.stdin.read(int(os.getenv('CONTENT_LENGTH')))
 print "
" def paypal_ipn(): """process instant payment notification made to run as CGI script, link as 'paypal_ipn.cgi' """ print "Content-Type: text/html\r\n\r\n"; print "Output from this script being emailed to site owner" import sys, os, re, pwd import cgi, urllib, urllib2 testing = False #cgitb.enable() # XXX comment this out before deployment! homedir = pwd.getpwuid(os.geteuid())[5] # if can't get email addr of site, get username at least (only with suexec!) sysadmin = os.getenv('SITE_ADMIN') or pwd.getpwuid(os.geteuid())[0] sys.path.append(os.sep.join([homedir, 'lib', 'python'])) # we had to do the above before we could import gadfly import gadfly timestamp = datetime.datetime.utcnow().isoformat(' ')[0:26] # now let's redirect the output try: childinput, parentoutput = os.pipe() pid = os.fork() if pid != 0: # parent os.close(childinput) os.dup2(parentoutput, sys.stdout.fileno()) os.dup2(parentoutput, sys.stderr.fileno()) else: # child os.close(parentoutput) os.dup2(childinput, sys.stdin.fileno()) email = os.popen('mail -s "paypal IPN %s" %s' % (timestamp, sysadmin)) except: print 'failed redirecting output: %s' % repr(sys.exc_info()) if pid == 0: sys.exit(0) # can't exit in 'try' block without raising exception print "pid %d timestamp: %s" % (pid, timestamp) paypalCert = '/C=US/ST=California/L=Palo Alto/O=PayPal, Inc.' + \ '/OU=Information Systems/CN=www.paypal.com' dbdir = os.sep.join([homedir, 'gadfly']) connection = gadfly.gadfly('paypal', dbdir) cursor = connection.cursor() # this query is just to get the column names cursor.execute("SELECT * FROM transactions WHERE txn_id = ''") ipn_variables = map(lambda column: column[0].lower(), cursor.description) # now make a dictionary with everything set to an empty string ipn_values = dict(map(None, ipn_variables, ('',) * len(ipn_variables))) # set something that's NOT dependent on PayPal's input ipn_values['jcomeau_com_timestamp'] = timestamp ipn_values['jcomeau_com_remote_addr'] = os.getenv('REMOTE_ADDR') or '' try: cursor.execute("INSERT INTO transactions %s values %s" % ( repr(tuple(ipn_values.keys())).replace("'", ""), repr(tuple(ipn_values.values())))) connection.commit() except: print "failed inserting transaction" raise response = [''] try: if os.getenv('CONTENT_LENGTH') is not None: query = sys.stdin.read(int(os.getenv('CONTENT_LENGTH'))) else: query = os.getenv('QUERY_STRING') if len(query) == 0: raise Exception, "nothing sent" ipn_values.update(dict(cgi.parse_qsl(query))) ipn_values.update({'cmd': '_notify-validate'}) url = 'https://www.paypal.com/cgi-bin/webscr' except: dump_env() # XXX debugging raise # XXX debugging ipn_values.update({'cmd': 'wtf? empty post'}) url = 'http://%s%s/%s' % (os.getenv('HTTP_HOST'), '/'.join(os.getenv('SCRIPT_NAME').split('/')[0:-1]), 'ipn_test.cgi') try: query = urllib.urlencode(ipn_values) request = urllib2.Request(url, query) urlconnection = urllib2.urlopen(request) #print repr(urlconnection.info().items()) # doesn't have paypal cert, damn response = map(lambda line: line.strip(), urlconnection.readlines()) except: print 'verification failed at URL %s, values %s' % (url, repr(ipn_values)) raise unexpected = filter(lambda column: column not in ipn_variables, ipn_values.keys()) leftover = dict() for key in unexpected: ipn_values['jcomeau_com_ipn_leftover'] += key + ': ' ipn_values['jcomeau_com_ipn_leftover'] += ipn_values.pop(key) + ', ' print repr(('unexpected', unexpected)) # XXX debugging print "ipn_values: '%s' '%s' '%s'" % ( # XXX debugging ipn_values['jcomeau_com_ipn_leftover'], ipn_values['jcomeau_com_timestamp'], ipn_values['jcomeau_com_remote_addr']) try: sql = "UPDATE transactions" + \ " SET jcomeau_com_ipn_leftover = '%s'" + \ " WHERE jcomeau_com_timestamp = '%s'" + \ " AND jcomeau_com_remote_addr = '%s'" print "sql='%s'" % sql # XXX debugging cursor.execute(sql % ( ipn_values['jcomeau_com_ipn_leftover'], ipn_values['jcomeau_com_timestamp'], ipn_values['jcomeau_com_remote_addr'])) connection.commit() except: print "failed updating unexpected data for transaction" raise print 'response: %s' % repr(response) # XXX debug if response[0] == 'VERIFIED' and len(response) == 1: # check the payment_status=Completed if ipn_values['payment_status'] != 'Completed': print 'nothing to do, payment status is %s' % ipn_values['payment_status'] sys.exit(0) # check that $txn_id has not been previously processed items = map(lambda string: string.strip(), ipn_values['item_number'].split(',')) failed = [] for item in items: try: cursor.execute("INSERT INTO SALES (item_number, txn_id) VALUES (?, ?)", (item, ipn_values['txn_id'])) connection.commit() except: failed.append(item) if len(failed) > 0: # check if txn_id for any failed items is same as ours; if so, suspicious # if not, probably just means someone else bought that item pass # check that receiver_email is a valid PayPal email cursor.execute("SELECT email_addr FROM recipient_addresses") emails = map(lambda email: email[0].lower(), cursor.fetchall()) print 'emails: %s' % repr(emails) # XXX debug if ipn_values['receiver_email'].lower() not in emails: print "%s not a valid recipient address" % ipn_values['receiver_email'] return # check that payment_amount/payment_currency (USD) are correct # process payment pass elif response[0] == 'INVALID' and len(response) == 1: # already logged for manual investigation pass elif response[0] == 'TESTING:': print "testing: %s" % repr(ipn_values) pass else: print 'unexpected response %s' % repr(response) return if __name__ == '__main__': # if this program was imported by another, the above test will fail, # and this following code won't be used... routine = self.split('.')[0] eval('%s()' % routine) else: # if you want something to be done on import, do it here; otherwise pass pass