---- dataentry ---- type : integration # do not change this line systems : Python # the system(s) that the integration is for, separated with commas if more than one name : Python Datafeed Classes # the name of the integration description : Python classes to decrypt and parse an XML datafeed. Includes examples for Python 2 and Python 3. tags : python # tags, separated by commas. don't include the "system" here. date_dt : 2008-02-13 # the date in YYYY-MM-DD format version : 0.5 # if you have a version for your code, enter it here developer_url : http://www.themancan.com # if you'd like a link back to your site, stick it here ---- ====== Python + FoxyCart, Notes ====== //**Please note:**// The code on this page is submitted by members of the FoxyCart community, and may not verified by FoxyCart.com LLC in any way, shape, or form. Please double check the code before installing. If you need help with it please [[http://forum.foxycart.com/|post in our forum]], but if we cannot offer assistance (due to unfamiliarity with this particular system or language) we apologize in advance. //**Django**// If you're integrating FoxyCart into your [[https://www.djangoproject.com/|Django]] project please read a separate page about [[integration:django|Django integration]] ===== Description ===== These examples are a work in progress implementation of a datafeed parser in Python. This code is not intended to be used in a production environment, but is a useful starting point for your own integrations and for testing. There are examples for Python 2.7.9 and Python 3. ===== Installation ===== - Download the Python modules. - Use them for good, not evil. - Test ===== Requirements ===== * Make sure you're aware of quirks between different URL encoding and decoding options. [[http://forum.foxycart.com/comments.php?DiscussionID=965|This forum discussion]] has more info. ===== Python 2.7.9 Example Code ===== view.py """ Example Django view for receiving and decrypting datafeed. """ import urllib from django.http import * from utils.foxycart import FoxyData # I put foxycart.py in a 'utils' module import settings # Store datafeed key in your settings.py def foxyfeed(request): if request.POST and 'FoxyData' in request.POST: try: data = FoxyData.from_crypted_str(urllib.unquote_plus(request.POST['FoxyData']), settings.FOXYCART_DATAFEED_KEY) # IMPORTANT: unquote_plus is necessary for the non-ASCII binary that FoxyCart sends. for transaction in data.transactions: pass # Process each transaction here return HttpResponse('foxy') except Exception, e: # Something went wrong, handle the error... raise return HttpResponseForbidden('Unauthorized request.') # No FoxyData? Not a POST? We don't speak that. foxycart.py """ Utilities for decrypting and parsing a FoxyCart datafeed. """ from xml.dom.minidom import parseString from datetime import datetime # Thanks, Wikipedia: http://en.wikipedia.org/wiki/RC4#Implementation class ARC4: def __init__(self, key = None): self.state = range(256) # Initialize state array with values 0 .. 255 self.x = self.y = 0 # Our indexes. x, y instead of i, j if key is not None: self.init(key) # KSA def init(self, key): for i in range(256): self.x = (ord(key[i % len(key)]) + self.state[i] + self.x) & 0xFF self.state[i], self.state[self.x] = self.state[self.x], self.state[i] self.x = 0 # PRGA def crypt(self, input): output = [None]*len(input) for i in xrange(len(input)): self.x = (self.x + 1) & 0xFF self.y = (self.state[self.x] + self.y) & 0xFF self.state[self.x], self.state[self.y] = self.state[self.y], self.state[self.x] r = self.state[(self.state[self.x] + self.state[self.y]) & 0xFF] output[i] = chr(ord(input[i]) ^ r) return ''.join(output) class FoxyData: DateFmt = '%Y-%m-%d' DateTimeFmt = '%Y-%m-%d %H:%M:%S' class Transaction: def __init__(self, node): def extract_kv_node(node, key_name): el = node.getElementsByTagName(key_name) return len(el) > 0 and el[0].firstChild.data or '' self.id = extract_kv_node(node, 'id') self.date = datetime.strptime( extract_kv_node(node, 'transaction_date'), FoxyData.DateTimeFmt) self.customer_id = extract_kv_node(node, 'customer_id') self.attributes = attrs = {} self.items = items = attrs['items'] = [] self.custom_fields = attrs['custom_fields'] = {} for custom_field in node.getElementsByTagName('custom_field'): self.custom_fields[extract_kv_node(custom_field, 'custom_field_name')] = \ extract_kv_node(custom_field, 'custom_field_value') self.transaction_details = attrs['detail'] = [] for details in node.getElementsByTagName('transaction_detail'): item = {'product_code': extract_kv_node(details, 'product_code')} for key in ['subscription_startdate', 'next_transaction_date']: date_str = extract_kv_node(details, key) try: item[key] = datetime.strptime(date_str, FoxyData.DateFmt) except ValueError: item[key] = date_str detail = item['detail'] = {} for detail_opt in details.getElementsByTagName('transaction_detail_option'): detail[extract_kv_node(detail_opt, 'product_option_name')] = \ extract_kv_node(detail_opt, 'product_option_value') items.append(item) def __init__(self, markup): self.markup = markup self.doc = parseString(self.markup) self.transactions = [] for transaction in self.doc.getElementsByTagName('transaction'): self.transactions.append(FoxyData.Transaction(transaction)) def __str__(self): return str(self.markup) @classmethod def from_str(self, data_str): return FoxyData(data_str) """ Given a string containing RC4-crypted FoxyCart datafeed XML and the cryptographic key, decrypt the contents and create a FoxyData object containing all of the Transactions in the data feed. """ @classmethod def from_crypted_str(self, data_str, crypt_key): a = ARC4(crypt_key) return FoxyData.from_str(a.crypt(data_str)) def __len__(self): return len(self.transactions) foxycart_test.py """ Unit test for foxycart.py. """ import unittest from foxycart import * class Constants: pass class FoxyDataVectorTest(unittest.TestCase): def _test_it_hard(self, vector): self.assertEqual(1, len(vector), 'expected one transaction') tx = vector.transactions[0] self.assertEqual('616', tx.id) self.assert_(tx.date) self.assertEqual("2007-05-04 20:53:57", tx.date.strftime(FoxyData.DateTimeFmt)) self.assertEqual('122', tx.customer_id) self.assertEqual(1, len(tx.items), 'expected one item') self.assertEqual(2, len(tx.custom_fields), 'expected 2 custom fields') self.assert_('My_Cool_Text' in tx.custom_fields.keys(), 'missing custom field') self.assertEqual('Value123', tx.custom_fields['My_Cool_Text'], 'missing custom field value') self.assert_('Another_Custom_Field' in tx.custom_fields.keys(), 'missing custom field') self.assertEqual('10', tx.custom_fields['Another_Custom_Field'], 'missing custom field value') item = tx.items.pop() self.assertEqual('abc123', item['product_code']) detail = item['detail'] self.assertEqual(1, len(detail)) self.assertEqual('blue', detail['color']) self.assert_(item['subscription_start_date']) self.assertEqual('2007-07-07', item['subscription_start_date'].strftime(FoxyData.DateFmt)) self.assert_(item['next_transaction_date']) self.assertEqual('2007-08-07', item['next_transaction_date'].strftime(FoxyData.DateFmt)) self.assertEqual() def test_from_str(self): vector = FoxyData.from_str(Constants.subscription_xml) self._test_it_hard(vector) def test_from_crypted_str(self): crypted_str = ARC4(Constants.SECRET_KEY).crypt(Constants.subscription_xml) vector = FoxyData.from_crypted_str(crypted_str, Constants.SECRET_KEY) self._test_it_hard(vector) Constants.SECRET_KEY = 'abc123akp8ak7898a,.aoeueaouaoeuaoeu' Constants.subscription_xml = """ XML FoxyCart Version 0.7 616 2007-05-04 20:53:57 122 John Doe 12345 Any Street Any City TN 37013 US (123) 456-7890 someone@somewhere.com 71.228.237.177 John Doe 1234 Any Street Some City TN 37013 US UPS: Ground 20.00 0.00 4.38 24.38 24.38 1aab23051b24582c5dc8e23fc595d505 My_Cool_Text Value123 Another_Custom_Field 10 foo 20.00 1 0.10 abc123 1m 2007-07-07 2007-08-07 John Doe Default for all products DEFAULT shipped color blue John Doe John Doe 2345 Some Address Some City TN 37013 US DHL: Next Afternoon 52.15 6.31 15.76 74.22 My_Custom_Info john's stuff More_Custom_Info more of john's stuff """ if __name__ == '__main__': unittest.main() ===== Caveats ===== ==== XML Being Truncated ==== The following comes from a user, 2013-10-09:
I was able to figure out the issue and it wasn't an issue with foxy cart. It appears as though it was an issue with python where it would truncate a string that was too big when setting it to a variable. Ill go into a bit more detail incase you find other people are having the same issue: What I was doing was to basically add each section to a variable and log it: post_data = request.POST['FoxyData'] Log(post_data) unquoted = urllib.unquote_plus(post_data) Log(unquoted) # At this point, 'unquoted' is truncated because the response is too big data = FoxyData.from_crypted_str(unquoted, settings.FOXYCART_DATAFEED_KEY) Log(data) What ultimately worked was to put it all together like in the example: data = FoxyData.from_crypted_str(urllib.unquote_plus(request.POST['FoxyData']), settings.FOXYCART_DATAFEED_KEY)
===== Python 3 Example Code ===== """ Example Django view for receiving and decrypting datafeed. """ ## My View, I have a session variable I submit to foxy cart, that's my booking_id. @csrf_exempt def FoxyCartIPNView(request): if request.POST and 'FoxyData' in request.POST: try: # IMPORTANT: unquote_plus is necessary for the non-ASCII binary that # FoxyCart sends. # MKR als important to set the encoding when using python3! data = FoxyData.from_crypted_str((unquote_plus(request.POST['FoxyData'], encoding='latin-1')), FOXYCART_DATAFEED_KEY) for transaction in data.transactions: # get the booking id, and verify it so BMCM can see it in admin # also save foxy's xml for archive purposes, finally, we mail the customer. booking = transaction.booking_id # a custom field from foxycart booking = RetreatBooking.objects.get(pk=booking) booking.verified_transaction = True booking.foxycart_xml = data.markup booking.save() mail_customer(request, booking, transaction) print("transaction: {}, mail customer {}\n".format(transaction.id, transaction.customer_email) ) return HttpResponse('foxy') except: # Something went wrong, handle the error... right? :P raise return HttpResponseForbidden('Unauthorized request.') # No FoxyData? Not a POST? We don't speak that. ## my utils for the view - Python 3 compatible! class ARC4: def __init__(self, key = None): self.state = list(range(256)) # Initialize state array with values 0 .. 255 self.x = self.y = 0 # Our indexes. x, y instead of i, j if key is not None: self.init(key) # KSA def init(self, key): for i in list(range(256)): self.x = (ord(key[i % len(key)]) + self.state[i] + self.x) & 0xFF self.state[i], self.state[self.x] = self.state[self.x], self.state[i] self.x = 0 # PRGA def crypt(self, input): output = [None]*len(input) for i in range(len(input)): self.x = (self.x + 1) & 0xFF self.y = (self.state[self.x] + self.y) & 0xFF self.state[self.x], self.state[self.y] = self.state[self.y], self.state[self.x] r = self.state[(self.state[self.x] + self.state[self.y]) & 0xFF] output[i] = chr(ord(input[i]) ^ r) return ''.join(output) @classmethod def from_crypted_str(self, data_str, crypt_key): a = ARC4(crypt_key) return FoxyData.from_str(a.crypt(data_str)) class FoxyData: DateFmt = '%Y-%m-%d' DateTimeFmt = '%Y-%m-%d %H:%M:%S' class Transaction: def __init__(self, node): def extract_kv_node(node, key_name): # print('getting node {}\n{}'.format(key_name, node)) el = node.getElementsByTagName(key_name) return len(el) > 0 and el[0].firstChild.data or '' self.id = extract_kv_node(node, 'id') self.date = datetime.strptime( extract_kv_node(node, 'transaction_date'), FoxyData.DateTimeFmt) self.customer_id = extract_kv_node(node, 'customer_id') self.customer_email = extract_kv_node(node, 'customer_email') self.attributes = attrs = {} self.items = items = attrs['items'] = [] self.custom_fields = attrs['custom_fields'] = {} for custom_field in node.getElementsByTagName('custom_field'): self.custom_fields[extract_kv_node(custom_field, 'custom_field_name')] = \ extract_kv_node(custom_field, 'custom_field_value') # import pdb;pdb.set_trace() self.booking_id = self.custom_fields.get('booking_id') self.transaction_details = attrs['detail'] = [] def __init__(self, markup): self.markup = markup self.doc = parseString(self.markup) self.transactions = [] for transaction in self.doc.getElementsByTagName('transaction'): self.transactions.append(FoxyData.Transaction(transaction)) def __str__(self): return str(self.markup) @classmethod def from_str(self, data_str): return FoxyData(data_str) """ Given a string containing RC4-crypted FoxyCart datafeed XML and the cryptographic key, decrypt the contents and create a FoxyData object containing all of the Transactions in the data feed. """ @classmethod def from_crypted_str(self, data_str, crypt_key): a = ARC4(crypt_key) return FoxyData.from_str(a.crypt(data_str)) def __len__(self): return len(self.transactions)