From b2e1e8c7808b7c7bba945b27b95d7b7e2337726e Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Thu, 26 Jul 2012 17:22:22 +0200 Subject: [PATCH] [gen] First version of a Ogone Appy plug-in. --- gen/__init__.py | 13 +++- gen/generator.py | 1 + gen/mixins/ToolMixin.py | 11 ++-- gen/mixins/__init__.py | 5 ++ gen/ogone.py | 139 +++++++++++++++++++++++++++++++++++++++ gen/templates/config.pyt | 1 + gen/ui/appy.css | 3 +- gen/ui/ogone.gif | Bin 0 -> 2403 bytes gen/ui/page.pt | 8 +-- gen/ui/widgets/ogone.pt | 27 ++++++++ 10 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 gen/ogone.py create mode 100644 gen/ui/ogone.gif create mode 100644 gen/ui/widgets/ogone.pt diff --git a/gen/__init__.py b/gen/__init__.py index 0a75efc..28c5d38 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -862,8 +862,8 @@ class Type: # Search Javascript code in the value (prevent XSS attacks). if '/onProcess.''' + return obj.goto(obj.absolute_url()) + class Integer(Type): def __init__(self, validator=None, multiplicity=(0,1), index=None, default=None, optional=False, editDefault=False, show=True, @@ -2888,4 +2894,7 @@ class Config: # Language that will be used as a basis for translating to other # languages. self.sourceLanguage = 'en' + # When using Ogone, place an instance of appy.gen.ogone.OgoneConfig in + # the field below. + self.ogone = None # ------------------------------------------------------------------------------ diff --git a/gen/generator.py b/gen/generator.py index b509f3d..1f56ee3 100644 --- a/gen/generator.py +++ b/gen/generator.py @@ -727,6 +727,7 @@ class ZopeGenerator(Generator): repls['languages'] = ','.join('"%s"' % l for l in self.config.languages) repls['languageSelector'] = self.config.languageSelector repls['sourceLanguage'] = self.config.sourceLanguage + repls['ogone'] = repr(self.config.ogone) self.copyFile('config.pyt', repls, destName='config.py') def generateInit(self): diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index 8f54612..1028d49 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -1020,10 +1020,13 @@ class ToolMixin(BaseMixin): url = appyUser.o.getUrl(mode='edit', page='main', nav='') return (' | '.join(info), url) - def getUserName(self, login): - '''Gets the user name corresponding to p_login, or the p_login itself - if the user does not exist anymore.''' - user = self.appy().search1('User', noSecurity=True, login=login) + def getUserName(self, login=None): + '''Gets the user name corresponding to p_login (or the currently logged + login if None), or the p_login itself if the user does not exist + anymore.''' + tool = self.appy() + if not login: login = tool.user.getId() + user = tool.search1('User', noSecurity=True, login=login) if not user: return login firstName = user.firstName name = user.name diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 12e93ed..cf37961 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -1594,4 +1594,9 @@ class BaseMixin: if not parent: # Is propably being created through code return False return parent.getId() == 'temp_folder' + + def onProcess(self): + '''This method is a general hook for transfering processing of a request + to a given field, whose name must be in the request.''' + return self.getAppyType(self.REQUEST['name']).process(self) # ------------------------------------------------------------------------------ diff --git a/gen/ogone.py b/gen/ogone.py new file mode 100644 index 0000000..4002eda --- /dev/null +++ b/gen/ogone.py @@ -0,0 +1,139 @@ +# ------------------------------------------------------------------------------ +import sha +from appy import Object +from appy.gen import Type +from appy.shared.utils import normalizeString + +# ------------------------------------------------------------------------------ +class OgoneConfig: + '''If you plan, in your app, to perform on-line payments via the Ogone (r) + system, create an instance of this class in your app and place it in the + 'ogone' attr of your appy.gen.Config instance.''' + def __init__(self): + # self.env refers to the Ogone environment and can be "test" or "prod". + self.env = 'test' + # You merchant Ogone ID + self.PSPID = None + # Default currency for transactions + self.currency = 'EUR' + # Default language + self.language = 'en_US' + # SHA-IN key (digest will be generated with the SHA-1 algorithm) + self.shaInKey = '' + # SHA-OUT key (digest will be generated with the SHA-1 algorithm) + self.shaOutKey = '' + + def __repr__(self): return str(self.__dict__) + +# ------------------------------------------------------------------------------ +class Ogone(Type): + '''This field allows to perform payments with the Ogone (r) system.''' + urlTypes = ('accept', 'decline', 'exception', 'cancel') + + def __init__(self, orderMethod, responseMethod, show='view', page='main', + group=None, layouts=None, move=0, specificReadPermission=False, + specificWritePermission=False, width=None, height=None, + colspan=1, master=None, masterValue=None, focus=False, + mapping=None, label=None): + Type.__init__(self, None, (0,1), None, None, False, False, show, page, + group, layouts, move, False, False,specificReadPermission, + specificWritePermission, width, height, None, colspan, + master, masterValue, focus, False, True, mapping, label) + # orderMethod must contain a method returning a dict containing info + # about the order. Following keys are mandatory: + # * orderID An identifier for the order. Tip: use the object uid, + # but the numeric part only, else, it could be too long. + # * amount An integer representing the price for this order, + # multiplied by 100 (no floating point value, no commas + # are tolerated. Dont't forget to multiply the amount by + # 100 !! + self.orderMethod = orderMethod + # responseMethod must contain a method accepting one param, let's call + # it "response". The response method will be called when we will get + # Ogone's response about the status of the payment. Param "response" is + # an object whose attributes correspond to all parameters that you have + # chosen to receive in your Ogone merchant account. + self.responseMethod = responseMethod + + noShaInKeys = ('env',) + noShaOutKeys = ('name', 'SHASIGN') + def createShaDigest(self, values, passphrase, keysToIgnore=()): + '''Creates an Ogone-compliant SHA-1 digest based on key-value pairs in + dict p_values and on some p_passphrase.''' + # Create a new dict by removing p_keysToIgnore from p_values, and by + # upperizing all keys. + shaRes = {} + for k, v in values.iteritems(): + if k in keysToIgnore: continue + shaRes[k.upper()] = v + # Create a sorted list of keys + keys = shaRes.keys() + keys.sort() + shaList = [] + for k in keys: + shaList.append('%s=%s' % (k, shaRes[k])) + shaObject = sha.new(passphrase.join(shaList) + passphrase) + res = shaObject.hexdigest() + print 'DIGEST', res + return res + + def getValue(self, obj): + '''The "value" of the Ogone field is a dict that collects all the + necessary info for making the payment.''' + tool = obj.getTool() + # Basic Ogone parameters were generated in the app config module. + res = obj.getProductConfig().ogone.copy() + shaKey = res['shaInKey'] + # Remove elements from the Ogone configu that we must not send in the + # payment request. + del res['shaInKey'] + del res['shaOutKey'] + res.update(self.callMethod(obj, self.orderMethod)) + # Add user-related information + res['CN'] = str(normalizeString(tool.getUserName())) + user = obj.appy().appyUser + res['EMAIL'] = user.email or user.login + # Add standard back URLs + siteUrl = tool.getSiteUrl() + res['catalogurl'] = siteUrl + res['homeurl'] = siteUrl + # Add redirect URLs + for t in self.urlTypes: + res['%surl' % t] = '%s/onProcess' % obj.absolute_url() + # Add additional parameter that we want Ogone to give use back in all + # of its responses: the name of this Appy Ogone field. This way, Appy + # will be able to call method m_process below, that will process + # Ogone's response. + res['paramplus'] = 'name=%s' % self.name + # Ensure every value is a str + for k in res.iterkeys(): + if not isinstance(res[k], str): + res[k] = str(res[k]) + # Compute a SHA-1 key as required by Ogone and add it to the res + res['SHASign'] = self.createShaDigest(res, shaKey, + keysToIgnore=self.noShaInKeys) + return res + + def ogoneResponseOk(self, obj): + '''Returns True if the SHA-1 signature from Ogone matches retrieved + params.''' + response = obj.REQUEST.form + shaKey = obj.getProductConfig().ogone['shaOutKey'] + digest = self.createShaDigest(response, shaKey, + keysToIgnore=self.noShaOutKeys) + return digest.lower() != response['SHASIGN'].lower() + + def process(self, obj): + '''Processes a response from Ogone.''' + # Call the response method defined in this Ogone field. + if not self.ogoneResponseOk(obj): + obj.log('Ogone response SHA failed. REQUEST: ' % \ + str(obj.REQUEST.form)) + raise Exception('Failure, possible fraud detection, an ' \ + 'administrator has been contacted.') + # Create a nice object from the form. + response = Object() + for k, v in obj.REQUEST.form.iterkeys(): + setattr(response, k, v) + self.responseMethod(obj.appy(), response) +# ------------------------------------------------------------------------------ diff --git a/gen/templates/config.pyt b/gen/templates/config.pyt index c8052ab..6bffade 100644 --- a/gen/templates/config.pyt +++ b/gen/templates/config.pyt @@ -47,6 +47,7 @@ grantableRoles = [] languages = [] languageSelector = sourceLanguage = '' +ogone = # When Zope is starting or runs in test mode, there is no request object. We # create here a fake one for storing Appy wrappers. diff --git a/gen/ui/appy.css b/gen/ui/appy.css index 1f20487..72ea462 100644 --- a/gen/ui/appy.css +++ b/gen/ui/appy.css @@ -16,7 +16,7 @@ form { margin: 0; padding: 0;} p { margin: 0;} acronym {cursor: help;} input { font: 92% Helvetica,Arial,sans-serif } -input[type=image] { border: 0; background: none; } +input[type=image] { border: 0; background: none; cursor: pointer; } input[type=checkbox] { border: 0; background: none; cursor: pointer;} input[type=radio] { border: 0; background: none; cursor: pointer;} input[type=file] { border: 0px solid #D7DEE4; @@ -122,6 +122,7 @@ img { border: 0; vertical-align: middle} .history td { border-top: 1px solid grey;} .history th { font-style: italic; text-align; left;} .topSpace { margin-top: 15px;} +.bottomSpace { margin-bottom: 15px;} .discreet { color: grey} .pageLink { padding-left: 6px; font-style: italic} .footer { font-size: 95% } diff --git a/gen/ui/ogone.gif b/gen/ui/ogone.gif new file mode 100644 index 0000000000000000000000000000000000000000..9bcbfbc3afd52f0de621c5cd5680a71dcb164a4f GIT binary patch literal 2403 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 J8Cw9b{4eH|Tblp? literal 0 HcmV?d00001 diff --git a/gen/ui/page.pt b/gen/ui/page.pt index 475b328..91aad7f 100644 --- a/gen/ui/page.pt +++ b/gen/ui/page.pt @@ -263,7 +263,7 @@
- @@ -277,13 +277,13 @@ - - @@ -304,7 +304,7 @@ - diff --git a/gen/ui/widgets/ogone.pt b/gen/ui/widgets/ogone.pt new file mode 100644 index 0000000..739c32c --- /dev/null +++ b/gen/ui/widgets/ogone.pt @@ -0,0 +1,27 @@ +View macro + + var "value" is misused and contains the contact params for Ogone. + The form for sending the payment request to Ogone. +
+ + + + Submit image + +
+
+ +Edit macro (none) + + +Cell macro (=view) + + + + +Search macro (none) +