Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Fix encoding #22

Merged
merged 4 commits into from

1 participant

@spookylukey
Collaborator

Fixes two problems with non ASCII characters.

Both of them were caused by the fact that request.POST was not taking into account the declared charset in the data posted by PayPal. The two problems are:

1) the IPN object was being populated incorrectly, with things like "J�rg" instead of "Jörg". The fix for this has full tests - which mainly involved changing the IPN tests to match the way that Paypal does it.

2) the use of request.POST.urlencode() caused the wrong data to be stored in PayPalIPN.query, resulting in the IPN being marked as invalid when checked against PayPal. This can be avoided using request.raw_post_data. There isn't specific test for this, because it depends largely on the behaviour of PayPal.

@spookylukey
Collaborator

I meant to add - I did thoroughly check out the two fixes using PayPal sandbox, and all the changes to tests were based on dumps of the raw HTTP requests I was getting from PayPal.

@spookylukey spookylukey merged commit 94dae3d into dcramer:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
34 paypal/standard/ipn/tests/test_ipn.py
@@ -1,3 +1,5 @@
+import urllib
+
from django.conf import settings
from django.http import HttpResponse
from django.test import TestCase
@@ -10,6 +12,9 @@
recurring_create, recurring_payment, recurring_cancel)
+# Parameters are all bytestrings, so we can construct a bytestring
+# request the same way that Paypal does.
+
IPN_POST_PARAMS = {
"protection_eligibility": "Ineligible",
"last_name": "User",
@@ -24,7 +29,7 @@
"txn_type": "express_checkout",
"handling_amount": "0.00",
"payment_date": "23:04:06 Feb 02, 2009 PST",
- "first_name": "Test",
+ "first_name": "J\xF6rg",
"item_name": "",
"charset": "windows-1252",
"custom": "website_id=13&user_id=21",
@@ -85,7 +90,15 @@ def tearDown(self):
recurring_create.receivers = self.recurring_create_receivers
recurring_payment.receivers = self.recurring_payment_receivers
recurring_cancel.receivers = self.recurring_cancel_receivers
-
+
+ def paypal_post(self, params):
+ """
+ Does an HTTP POST the way that PayPal does, using the params given.
+ """
+ # We build params into a bytestring ourselves, to avoid some encoding
+ # processing that is done by the test client.
+ post_data = urllib.urlencode(params)
+ return self.client.post("/ipn/", post_data, content_type='application/x-www-form-urlencoded')
def assertGotSignal(self, signal, flagged, params=IPN_POST_PARAMS):
# Check the signal was sent. These get lost if they don't reference self.
@@ -97,7 +110,7 @@ def handle_signal(sender, **kwargs):
self.signal_obj = sender
signal.connect(handle_signal)
- response = self.client.post("/ipn/", params)
+ response = self.paypal_post(params)
self.assertEqual(response.status_code, 200)
ipns = PayPalIPN.objects.all()
self.assertEqual(len(ipns), 1)
@@ -106,9 +119,12 @@ def handle_signal(sender, **kwargs):
self.assertTrue(self.got_signal)
self.assertEqual(self.signal_obj, ipn_obj)
+ return ipn_obj
def test_correct_ipn(self):
- self.assertGotSignal(payment_was_successful, False)
+ ipn_obj = self.assertGotSignal(payment_was_successful, False)
+ # Check some encoding issues:
+ self.assertEqual(ipn_obj.first_name, u"J\u00f6rg")
def test_failed_ipn(self):
PayPalIPN._postback = lambda self: "INVALID"
@@ -117,7 +133,7 @@ def test_failed_ipn(self):
def assertFlagged(self, updates, flag_info):
params = IPN_POST_PARAMS.copy()
params.update(updates)
- response = self.client.post("/ipn/", params)
+ response = self.paypal_post(params)
self.assertEqual(response.status_code, 200)
ipn_obj = PayPalIPN.objects.all()[0]
self.assertEqual(ipn_obj.flag, True)
@@ -137,15 +153,15 @@ def test_vaid_payment_status_cancelled(self):
update = {"payment_status": ST_PP_CANCELLED}
params = IPN_POST_PARAMS.copy()
params.update(update)
- response = self.client.post("/ipn/", params)
+ response = self.paypal_post(params)
self.assertEqual(response.status_code, 200)
ipn_obj = PayPalIPN.objects.all()[0]
self.assertEqual(ipn_obj.flag, False)
def test_duplicate_txn_id(self):
- self.client.post("/ipn/", IPN_POST_PARAMS)
- self.client.post("/ipn/", IPN_POST_PARAMS)
+ self.paypal_post(IPN_POST_PARAMS)
+ self.paypal_post(IPN_POST_PARAMS)
self.assertEqual(len(PayPalIPN.objects.all()), 2)
ipn_obj = PayPalIPN.objects.order_by('-created_at', '-pk')[0]
self.assertEqual(ipn_obj.flag, True)
@@ -218,7 +234,7 @@ def handle_signal(sender, **kwargs):
self.signal_obj = sender
recurring_payment.connect(handle_signal)
- response = self.client.post("/ipn/", params)
+ response = self.paypal_post(params)
self.assertEqual(response.status_code, 200)
ipns = PayPalIPN.objects.all()
self.assertEqual(len(ipns), 1)
View
48 paypal/standard/ipn/views.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-from django.http import HttpResponse
+from django.http import HttpResponse, QueryDict
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from paypal.standard.ipn.forms import PayPalIPNForm
@@ -24,23 +24,39 @@ def ipn(request, item_check_callable=None):
ipn_obj = None
# Clean up the data as PayPal sends some weird values such as "N/A"
- data = request.POST.copy()
- date_fields = ('time_created', 'payment_date', 'next_payment_date',
- 'subscr_date', 'subscr_effective')
- for date_field in date_fields:
- if data.get(date_field) == 'N/A':
- del data[date_field]
+ # Also, need to cope with custom encoding, which is stored in the body (!).
+ # Assuming the tolerate parsing of QueryDict and an ASCII-like encoding,
+ # such as windows-1252, latin1 or UTF8, the following will work:
- form = PayPalIPNForm(data)
- if form.is_valid():
- try:
- #When commit = False, object is returned without saving to DB.
- ipn_obj = form.save(commit = False)
- except Exception, e:
- flag = "Exception while processing. (%s)" % e
+ encoding = request.POST.get('charset', None)
+
+ if encoding is None:
+ flag = "Invalid form - no charset passed, can't decode"
+ data = None
else:
- flag = "Invalid form. (%s)" % form.errors
-
+ try:
+ data = QueryDict(request.raw_post_data, encoding=encoding)
+ except LookupError:
+ data = None
+ flag = "Invalid form - invalid charset"
+
+ if data is not None:
+ date_fields = ('time_created', 'payment_date', 'next_payment_date',
+ 'subscr_date', 'subscr_effective')
+ for date_field in date_fields:
+ if data.get(date_field) == 'N/A':
+ del data[date_field]
+
+ form = PayPalIPNForm(data)
+ if form.is_valid():
+ try:
+ #When commit = False, object is returned without saving to DB.
+ ipn_obj = form.save(commit = False)
+ except Exception, e:
+ flag = "Exception while processing. (%s)" % e
+ else:
+ flag = "Invalid form. (%s)" % form.errors
+
if ipn_obj is None:
ipn_obj = PayPalIPN()
View
11 paypal/standard/models.py
@@ -176,8 +176,8 @@ class PayPalStandardBase(Model):
flag = models.BooleanField(default=False, blank=True)
flag_code = models.CharField(max_length=16, blank=True)
flag_info = models.TextField(blank=True)
- query = models.TextField(blank=True) # What we sent to PayPal.
- response = models.TextField(blank=True) # What we got back.
+ query = models.TextField(blank=True) # What Paypal sent to us initially
+ response = models.TextField(blank=True) # What we got back from our request
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -306,7 +306,12 @@ def send_signals(self):
def initialize(self, request):
"""Store the data we'll need to make the postback from the request object."""
- self.query = getattr(request, request.method).urlencode()
+ if request.method == 'GET':
+ # PDT only - this data is currently unused
+ self.query = request.META.get('QUERY_STRING', '')
+ elif request.method == 'POST':
+ # The following works if paypal sends an ASCII bytestring, which it does.
+ self.query = request.raw_post_data
self.ipaddress = request.META.get('REMOTE_ADDR', '')
def _postback(self):
Something went wrong with that request. Please try again.