[gen] First version of a Ogone Appy plug-in.
This commit is contained in:
parent
178059ba1b
commit
b2e1e8c780
|
@ -862,8 +862,8 @@ class Type:
|
||||||
# Search Javascript code in the value (prevent XSS attacks).
|
# Search Javascript code in the value (prevent XSS attacks).
|
||||||
if '<script' in value:
|
if '<script' in value:
|
||||||
obj.log('Detected Javascript in user input.', type='error')
|
obj.log('Detected Javascript in user input.', type='error')
|
||||||
raise 'Your behaviour is considered a security attack. System ' \
|
raise Exception('Your behaviour is considered a security ' \
|
||||||
'administrator has been warned.'
|
'attack. System administrator has been warned.')
|
||||||
|
|
||||||
def validate(self, obj, value):
|
def validate(self, obj, value):
|
||||||
'''This method checks that p_value, coming from the request (p_obj is
|
'''This method checks that p_value, coming from the request (p_obj is
|
||||||
|
@ -968,6 +968,12 @@ class Type:
|
||||||
if raiseOnError: raise e
|
if raiseOnError: raise e
|
||||||
else: return str(e)
|
else: return str(e)
|
||||||
|
|
||||||
|
def process(self, obj):
|
||||||
|
'''This method is a general hook allowing a field to perform some
|
||||||
|
processing after an URL on an object has been called, of the form
|
||||||
|
<objUrl>/onProcess.'''
|
||||||
|
return obj.goto(obj.absolute_url())
|
||||||
|
|
||||||
class Integer(Type):
|
class Integer(Type):
|
||||||
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
||||||
default=None, optional=False, editDefault=False, show=True,
|
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
|
# Language that will be used as a basis for translating to other
|
||||||
# languages.
|
# languages.
|
||||||
self.sourceLanguage = 'en'
|
self.sourceLanguage = 'en'
|
||||||
|
# When using Ogone, place an instance of appy.gen.ogone.OgoneConfig in
|
||||||
|
# the field below.
|
||||||
|
self.ogone = None
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -727,6 +727,7 @@ class ZopeGenerator(Generator):
|
||||||
repls['languages'] = ','.join('"%s"' % l for l in self.config.languages)
|
repls['languages'] = ','.join('"%s"' % l for l in self.config.languages)
|
||||||
repls['languageSelector'] = self.config.languageSelector
|
repls['languageSelector'] = self.config.languageSelector
|
||||||
repls['sourceLanguage'] = self.config.sourceLanguage
|
repls['sourceLanguage'] = self.config.sourceLanguage
|
||||||
|
repls['ogone'] = repr(self.config.ogone)
|
||||||
self.copyFile('config.pyt', repls, destName='config.py')
|
self.copyFile('config.pyt', repls, destName='config.py')
|
||||||
|
|
||||||
def generateInit(self):
|
def generateInit(self):
|
||||||
|
|
|
@ -1020,10 +1020,13 @@ class ToolMixin(BaseMixin):
|
||||||
url = appyUser.o.getUrl(mode='edit', page='main', nav='')
|
url = appyUser.o.getUrl(mode='edit', page='main', nav='')
|
||||||
return (' | '.join(info), url)
|
return (' | '.join(info), url)
|
||||||
|
|
||||||
def getUserName(self, login):
|
def getUserName(self, login=None):
|
||||||
'''Gets the user name corresponding to p_login, or the p_login itself
|
'''Gets the user name corresponding to p_login (or the currently logged
|
||||||
if the user does not exist anymore.'''
|
login if None), or the p_login itself if the user does not exist
|
||||||
user = self.appy().search1('User', noSecurity=True, login=login)
|
anymore.'''
|
||||||
|
tool = self.appy()
|
||||||
|
if not login: login = tool.user.getId()
|
||||||
|
user = tool.search1('User', noSecurity=True, login=login)
|
||||||
if not user: return login
|
if not user: return login
|
||||||
firstName = user.firstName
|
firstName = user.firstName
|
||||||
name = user.name
|
name = user.name
|
||||||
|
|
|
@ -1594,4 +1594,9 @@ class BaseMixin:
|
||||||
if not parent: # Is propably being created through code
|
if not parent: # Is propably being created through code
|
||||||
return False
|
return False
|
||||||
return parent.getId() == 'temp_folder'
|
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)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
139
gen/ogone.py
Normal file
139
gen/ogone.py
Normal file
|
@ -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)
|
||||||
|
# ------------------------------------------------------------------------------
|
|
@ -47,6 +47,7 @@ grantableRoles = [<!grRoles!>]
|
||||||
languages = [<!languages!>]
|
languages = [<!languages!>]
|
||||||
languageSelector = <!languageSelector!>
|
languageSelector = <!languageSelector!>
|
||||||
sourceLanguage = '<!sourceLanguage!>'
|
sourceLanguage = '<!sourceLanguage!>'
|
||||||
|
ogone = <!ogone!>
|
||||||
|
|
||||||
# When Zope is starting or runs in test mode, there is no request object. We
|
# When Zope is starting or runs in test mode, there is no request object. We
|
||||||
# create here a fake one for storing Appy wrappers.
|
# create here a fake one for storing Appy wrappers.
|
||||||
|
|
|
@ -16,7 +16,7 @@ form { margin: 0; padding: 0;}
|
||||||
p { margin: 0;}
|
p { margin: 0;}
|
||||||
acronym {cursor: help;}
|
acronym {cursor: help;}
|
||||||
input { font: 92% Helvetica,Arial,sans-serif }
|
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=checkbox] { border: 0; background: none; cursor: pointer;}
|
||||||
input[type=radio] { border: 0; background: none; cursor: pointer;}
|
input[type=radio] { border: 0; background: none; cursor: pointer;}
|
||||||
input[type=file] { border: 0px solid #D7DEE4;
|
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 td { border-top: 1px solid grey;}
|
||||||
.history th { font-style: italic; text-align; left;}
|
.history th { font-style: italic; text-align; left;}
|
||||||
.topSpace { margin-top: 15px;}
|
.topSpace { margin-top: 15px;}
|
||||||
|
.bottomSpace { margin-bottom: 15px;}
|
||||||
.discreet { color: grey}
|
.discreet { color: grey}
|
||||||
.pageLink { padding-left: 6px; font-style: italic}
|
.pageLink { padding-left: 6px; font-style: italic}
|
||||||
.footer { font-size: 95% }
|
.footer { font-size: 95% }
|
||||||
|
|
BIN
gen/ui/ogone.gif
Normal file
BIN
gen/ui/ogone.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
|
@ -263,7 +263,7 @@
|
||||||
<br/>
|
<br/>
|
||||||
<tal:previous condition="python: previousPage and pageInfo['showPrevious']">
|
<tal:previous condition="python: previousPage and pageInfo['showPrevious']">
|
||||||
<tal:button condition="isEdit">
|
<tal:button condition="isEdit">
|
||||||
<input type="image" class="imageInput" style="cursor:pointer" name="buttonPrevious"
|
<input type="image" name="buttonPrevious"
|
||||||
tal:attributes="src string:$appUrl/ui/previous.png;
|
tal:attributes="src string:$appUrl/ui/previous.png;
|
||||||
title python: _('page_previous')"/>
|
title python: _('page_previous')"/>
|
||||||
<input type="hidden" name="previousPage" tal:attributes="value previousPage"/>
|
<input type="hidden" name="previousPage" tal:attributes="value previousPage"/>
|
||||||
|
@ -277,13 +277,13 @@
|
||||||
</tal:previous>
|
</tal:previous>
|
||||||
|
|
||||||
<tal:save condition="python: isEdit and pageInfo['showSave']">
|
<tal:save condition="python: isEdit and pageInfo['showSave']">
|
||||||
<input type="image" class="imageInput" style="cursor:pointer" name="buttonOk"
|
<input type="image" name="buttonOk"
|
||||||
tal:attributes="src string:$appUrl/ui/save.png;
|
tal:attributes="src string:$appUrl/ui/save.png;
|
||||||
title python: _('object_save')"/>
|
title python: _('object_save')"/>
|
||||||
</tal:save>
|
</tal:save>
|
||||||
|
|
||||||
<tal:cancel condition="python: isEdit and pageInfo['showCancel']">
|
<tal:cancel condition="python: isEdit and pageInfo['showCancel']">
|
||||||
<input type="image" class="imageInput" style="cursor:pointer" name="buttonCancel"
|
<input type="image" name="buttonCancel"
|
||||||
tal:attributes="src string:$appUrl/ui/cancel.png;
|
tal:attributes="src string:$appUrl/ui/cancel.png;
|
||||||
title python: _('object_cancel')"/>
|
title python: _('object_cancel')"/>
|
||||||
</tal:cancel>
|
</tal:cancel>
|
||||||
|
@ -304,7 +304,7 @@
|
||||||
|
|
||||||
<tal:next condition="python: nextPage and pageInfo['showNext']">
|
<tal:next condition="python: nextPage and pageInfo['showNext']">
|
||||||
<tal:button condition="isEdit">
|
<tal:button condition="isEdit">
|
||||||
<input type="image" class="imageInput" style="cursor:pointer" name="buttonNext"
|
<input type="image" name="buttonNext"
|
||||||
tal:attributes="src string:$appUrl/ui/next.png;
|
tal:attributes="src string:$appUrl/ui/next.png;
|
||||||
title python: _('page_next')"/>
|
title python: _('page_next')"/>
|
||||||
<input type="hidden" name="nextPage" tal:attributes="value nextPage"/>
|
<input type="hidden" name="nextPage" tal:attributes="value nextPage"/>
|
||||||
|
|
27
gen/ui/widgets/ogone.pt
Normal file
27
gen/ui/widgets/ogone.pt
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<tal:comment replace="nothing">View macro</tal:comment>
|
||||||
|
<metal:view define-macro="view">
|
||||||
|
<tal:comment replace="nothing">var "value" is misused and contains the contact params for Ogone.</tal:comment>
|
||||||
|
<tal:comment replace="nothing">The form for sending the payment request to Ogone.</tal:comment>
|
||||||
|
<form method="post" id="form1" name="form1"
|
||||||
|
tal:define="env value/env"
|
||||||
|
tal:attributes="action string: https://secure.ogone.com/ncol/$env/orderstandard.asp">
|
||||||
|
<tal:fields repeat="item value/items">
|
||||||
|
<input type="hidden" tal:condition="python: item[0] != 'env'"
|
||||||
|
tal:attributes="id python: item[0]; name python: item[0]; value python: item[1]"/>
|
||||||
|
</tal:fields>
|
||||||
|
<tal:comment replace="nothing">Submit image</tal:comment>
|
||||||
|
<input type="image" id="submit2" name="submit2"
|
||||||
|
tal:attributes="src string: $appUrl/ui/ogone.gif; title python: _('custom_pay')"/>
|
||||||
|
</form>
|
||||||
|
</metal:view>
|
||||||
|
|
||||||
|
<tal:comment replace="nothing">Edit macro (none)</tal:comment>
|
||||||
|
<metal:edit define-macro="edit"></metal:edit>
|
||||||
|
|
||||||
|
<tal:comment replace="nothing">Cell macro (=view)</tal:comment>
|
||||||
|
<metal:cell define-macro="cell">
|
||||||
|
<metal:call use-macro="app/ui/widgets/ogone/macros/view"/>
|
||||||
|
</metal:cell>
|
||||||
|
|
||||||
|
<tal:comment replace="nothing">Search macro (none)</tal:comment>
|
||||||
|
<metal:search define-macro="search"></metal:search>
|
Loading…
Reference in a new issue