[gen] First version of a Ogone Appy plug-in.

This commit is contained in:
Gaetan Delannay 2012-07-26 17:22:22 +02:00
parent 178059ba1b
commit b2e1e8c780
10 changed files with 197 additions and 11 deletions

View file

@ -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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -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):

View file

@ -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

View file

@ -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
View 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)
# ------------------------------------------------------------------------------

View file

@ -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.

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -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
View 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>