---- 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 ======
"""
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 = """
===== 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)