[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).
|
||||
if '<script' in value:
|
||||
obj.log('Detected Javascript in user input.', type='error')
|
||||
raise 'Your behaviour is considered a security attack. System ' \
|
||||
'administrator has been warned.'
|
||||
raise Exception('Your behaviour is considered a security ' \
|
||||
'attack. System administrator has been warned.')
|
||||
|
||||
def validate(self, obj, value):
|
||||
'''This method checks that p_value, coming from the request (p_obj is
|
||||
|
@ -968,6 +968,12 @@ class Type:
|
|||
if raiseOnError: raise 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):
|
||||
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
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
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!>]
|
||||
languageSelector = <!languageSelector!>
|
||||
sourceLanguage = '<!sourceLanguage!>'
|
||||
ogone = <!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.
|
||||
|
|
|
@ -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% }
|
||||
|
|
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/>
|
||||
<tal:previous condition="python: previousPage and pageInfo['showPrevious']">
|
||||
<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;
|
||||
title python: _('page_previous')"/>
|
||||
<input type="hidden" name="previousPage" tal:attributes="value previousPage"/>
|
||||
|
@ -277,13 +277,13 @@
|
|||
</tal:previous>
|
||||
|
||||
<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;
|
||||
title python: _('object_save')"/>
|
||||
</tal:save>
|
||||
|
||||
<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;
|
||||
title python: _('object_cancel')"/>
|
||||
</tal:cancel>
|
||||
|
@ -304,7 +304,7 @@
|
|||
|
||||
<tal:next condition="python: nextPage and pageInfo['showNext']">
|
||||
<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;
|
||||
title python: _('page_next')"/>
|
||||
<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