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 0000000..9bcbfbc Binary files /dev/null and b/gen/ui/ogone.gif differ 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) +