Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

executable file 665 lines (604 sloc) 23.441 kb
<?php
// $Id: q_menu.module,v 6-x-0.1 2008/07/08 15:31:52 qrios Exp $
// $Id$
/**
* @file
* Generic iDEAL payment API module for iDEAL payment processing. No shoppingcart module needed.
* For iDEAL ING/PB Advanced & RABO Professional
*
* Development by Qrios | http://www.qrios.nl | c.kodde {at} qrios {dot} nl
*
* @TODO: harmonise the error messages to WATCHDOG and to SET_MESSAGE.
*/
define('IPAPI_STATUS_OPEN', 0);
define('IPAPI_STATUS_SUCCESS', 1);
define('IPAPI_STATUS_CANCELLED', 2);
define('IPAPI_STATUS_EXPIRED', 3);
define('IPAPI_STATUS_FAILURE', 4);
/**
* Implementation of hook_cron().
*/
function ideal_payment_api_cron() {
_ideal_payment_api_include_lib();
// @TODO: insert time-based action firing here.
//@TODO: refresh-issuers should be called far less often then recheck.
ideal_payment_api_refresh_issuerlist();
ideal_payment_api_auto_recheck();
}
/**
* Implementation of hook_menu().
*/
function ideal_payment_api_menu($may_cache) {
$items = array();
if ($may_cache) {
$items[] = array(
'path' => 'admin/settings/ideal',
'title' => t('iDEAL Payment API'),
'description' => t('iDEAL Payment API settings.'),
'callback' => 'drupal_get_form',
'callback arguments' => array('ideal_payment_api_admin_settings'),
'access' => user_access('administer iPAPI'),
'type' => MENU_NORMAL_ITEM,
);
$items[] = array(
'path' => 'ideal/payment_test',
'title' => t('iDEAL Payment API Test'),
'description' => t('iDEAL Payment API Test.'),
'callback' => 'ideal_payment_api_test',
'access' => user_access('administer iPAPI'),
'type' => MENU_NORMAL_ITEM,
);
$items[] = array(
'path' => 'ideal/payment_statreq',
'title' => t('IDEAL Payment status request'),
'callback' => 'ideal_payment_api_statreq_confirm',
'callback arguments' => array(FALSE, FALSE),
'access' => user_access('access content'),
'type' => MENU_CALLBACK,
);
$items[] = array(
'path' => 'ideal/payment_statreq_recheck',
'title' => t('iDEAL Statusrequest recheck'),
'description' => t('Manual Statusrequest for iDEAL payments.'),
'callback' => 'ideal_payment_api_statreq_recheck',
'access' => user_access('access content'),
'type' => MENU_CALLBACK,
);
}
else {
_ideal_payment_api_include_lib();
}
return $items;
}
/**
* Implementation of hook_perm().
*/
function ideal_payment_api_perm() {
return array('administer iPAPI', 'make iPAPI payments');
}
/**
* Menu callback; presents the sitemap settings page.
*/
function ideal_payment_api_admin_settings() {
$form['ideal_payment_api_privatekey'] = array(
'#type' => 'textfield',
'#title' => t('privatekey'),
'#default_value' => variable_get('ideal_payment_api_privatekey', 'priv.pem'),
'#description' => t('The private key .pem file located in */security.'),
);
$form['ideal_payment_api_privatekeypass'] = array(
'#type' => 'textfield',
'#title' => t('privatekeypass'),
'#default_value' => variable_get('ideal_payment_api_privatekeypass', ''),
'#description' => t('The private key password used for generating the key file.'),
'#required' => TRUE,
);
$form['ideal_payment_api_privatecert'] = array(
'#type' => 'textfield',
'#title' => t('privatecert'),
'#default_value' => variable_get('ideal_payment_api_privatecert', 'cert.cer'),
'#description' => t('The private certificate .cer file located in */security.'),
'#required' => TRUE,
);
$form['ideal_payment_api_authenticationtype'] = array(
'#type' => 'textfield',
'#title' => t('authenticationtype'),
'#default_value' => variable_get('ideal_payment_api_authenticationtype', 'SHA1_RSA'),
'#description' => t('Authentication type protocol. Leave SHA1_RSA default if unsure.'),
'#required' => TRUE,
);
$form['ideal_payment_api_certificate0'] = array(
'#type' => 'textfield',
'#title' => t('certificate0'),
'#default_value' => variable_get('ideal_payment_api_certificate0', 'ideal.cer'),
'#description' => t('Certificate0 contains the signing certificate of your acquirer. This would probably be ideal.cer or webserver.cer.'),
'#required' => TRUE,
);
$form['ideal_payment_api_acquirerurl'] = array(
'#type' => 'textfield',
'#title' => t('acquirerurl'),
'#default_value' => variable_get('ideal_payment_api_acquirerurl', 'ssl://idealtest.secure-ing.com:443/ideal/iDeal'),
'#description' => t('Address of the iDEAL acquiring server. Use ssl://idealtest.secure-ing.com:443/ideal/iDeal during integration/test. Use ssl://ideal.secure-ing.com:443/ideal/iDeal only for production. Look into integration documents for Rabo equivalents.'),
'#required' => TRUE,
);
$form['ideal_payment_api_acquirertimeout'] = array(
'#type' => 'textfield',
'#title' => t('acquirertimeout'),
'#default_value' => variable_get('ideal_payment_api_acquirertimeout', '10'),
'#description' => t('Do not change AcquirerTimeout unless you have specific reasons to do so.'),
'#required' => TRUE,
);
$form['ideal_payment_api_merchantid'] = array(
'#type' => 'textfield',
'#title' => t('merchantid'),
'#default_value' => variable_get('ideal_payment_api_merchantid', '000000000'),
'#description' => t('Default MerchantID enables you to test the example demoshop. Your own Merchant ID can be retrieved via the iDEAL Dashboard.'),
'#required' => TRUE,
);
$form['ideal_payment_api_subid'] = array(
'#type' => 'textfield',
'#title' => t('subid'),
'#default_value' => variable_get('ideal_payment_api_subid', '0'),
'#description' => t('Do not change subID unless you have specific reasons to do so.'),
);
$form['ideal_payment_api_currency'] = array(
'#type' => 'textfield',
'#title' => t('currency'),
'#default_value' => variable_get('ideal_payment_api_currency', 'EUR'),
'#description' => t('Do not change currency unless you have specific reasons to do so'),
'#required' => TRUE,
);
$form['ideal_payment_api_expirationperiod'] = array(
'#type' => 'textfield',
'#title' => t('expirationperiod'),
'#default_value' => variable_get('ideal_payment_api_expirationperiod', 'PT10M'),
'#description' => t('ExpirationPeriod is the timeframe during which the transaction is allowed to take place. Maximum is PT1H (1 hour).'),
'#required' => TRUE,
);
$form['ideal_payment_api_language'] = array(
'#type' => 'textfield',
'#title' => t('language'),
'#default_value' => variable_get('ideal_payment_api_language', 'nl'),
'#description' => t('Language is only used for showing errormessages in the prefered language. nl/en.'),
'#required' => TRUE,
);
$form['ideal_payment_api_sessions'] = array(
'#type' => 'checkbox',
'#title' => t('Use Cookie sessions'),
'#default_value' => variable_get('ideal_payment_api_sessions', 1),
'#description' => t('Whether or not to use sessions on top of the usual validation. If set, the API will additionally use cookies to track the transaction_id and order_id. This lowers chances of breaking your database/payments trough url-hacking.'),
'#required' => FALSE,
);
return system_settings_form($form);
}
/**
* MAIN API: this is the function to call, in order to present a page.
* @argument $order_desc, string
* @argument $order_total, decimal number ###,##
* @argument $path_back_error, string, drupal path to redirect to on unsuccesfull payment. Probably the originating product/order form.
* @argument $path_back_succes, string, drupal path to redirect to on succesfull payment. Probably the home page or a "thank you" page.
* All arguments are required
* @TODO: insert a user_id in the order, to allow orders for other $users then global $user (= you)
*/
function ideal_payment_api_payment_page($order_desc, $order_total, $path_back_error, $path_back_succes) {
if (user_access('make iPAPI payments')) {
_ideal_payment_api_include_lib();
$order = array(
'header' => $order_desc,
'amount' => $order_total * 100, //amount *100, in cents.
'path_back_error' => $path_back_error,
'path_back_succes' => $path_back_succes,
);
$form_html = drupal_get_form('ideal_payment_api_issuer_form', $order);
return $form_html;
}
else{
drupal_set_message(t('Sorry, you have no privileges to make payments.') . $order_id, 'warning');
return FALSE;
}
}
/**
* Refresh issuer list
*/
function ideal_payment_api_refresh_issuerlist() {
$arr_issuers = ideal_payment_api_dirreq_call();
if (is_array($arr_issuers) && count($arr_issuers) > 0) {
db_query("TRUNCATE TABLE {ideal_payment_api_issuers}");
foreach ($arr_issuers as $issuer) {
db_query("INSERT INTO {ideal_payment_api_issuers} (iss_id, iss_name) VALUES (%d, '%s')", $issuer->issuerID, $issuer->issuerName );
}
}
else {
//@TODO: move into watchdog. And present error in hook_requirements.
print_r($arr_issuers); //Contains error message
}
}
/**
* Load issuer form
* When using this form, embedded in another form, make sure you do not override the validate and submit handlers.
* These are needed to process the form.
*/
function ideal_payment_api_issuer_form($order) {
$form['order'] = array(
'#type' => 'value',
'#value' => $order,
);
//Load issuers
$options['choose'] = t('Choose your bank...');
$result = db_query('SELECT * FROM {ideal_payment_api_issuers} ORDER BY iss_id ASC');
while ($row = db_fetch_object($result)) {
$options[$row->iss_id] = $row->iss_name;
}
$form['ideal_dirreq_form'] = array(
'#type' => 'fieldset',
'#title' => t('Please choose your bank'),
'#collapsible' => FALSE,
'#description' => t('You will be returned to this website after completing your IDEAL payment transaction.'),
);
$form['ideal_dirreq_form']['issuer'] = array(
'#type' => 'select',
'#default_value' => NULL,
'#options' => $options,
'#disabled' => empty($options) ? TRUE : FALSE,
'#required' => TRUE,
'#weight' => 0,
);
$form['ideal_dirreq_form']['submit'] = array(
'#type' => 'submit',
'#value' => t('Go to my bank »'),
'#weight' => 1,
);
//Set the form_handlers by hand, so that our own validate and submit function
// will be called, even if this form is embedded in another form.
$form['#submit'] = array('ideal_payment_api_issuer_form_submit' => array());
$form['#validate'] = array('ideal_payment_api_issuer_form_validate' => array());
return $form;
}
/**
* Validate issuer form
*/
function ideal_payment_api_issuer_form_validate($form, $form_state) {
$issuer = $form_state['issuer'];
if ($issuer == 'choose') {
form_set_error('issuer', t('Please select your bank.'));
}
if (empty($form_state['order'])) {
form_set_error('order', t('You did not provide an order, probably due to a technical issue.'));
}
}
/**
* Submit issuer form
*/
function ideal_payment_api_issuer_form_submit($form, &$form_state) {
$order = $form_state['order'];
$order['issuer_id'] = $form_state['issuer'];
# Saving the order will create a unique $order['order_id'] and update the $order.
if (!ideal_payment_api_order_save($order)) {
drupal_set_message(t('Order could not be saved.'));
}
else {
_ideal_payment_api_session_set($order);
$response = ideal_payment_api_transreq_call($order);
if (!$response->errCode) {
//Get IssuerURL and decode it
$issuer_url = $response->issuerAuthenticationURL;
$order['transaction_id'] = $response->transactionID;
ideal_payment_api_order_update($order['order_id'], IPAPI_STATUS_OPEN, $order['transaction_id']);
_ideal_payment_api_session_set($order);
drupal_goto($issuer_url);
}
else {//TransactionRequest failed, inform the consumer
$msg = $response->consumerMsg;
drupal_set_message(t('Something went wrong in processing your IDEAL payment. IDEAL error: %msg', array($msg)), 'error');
ideal_payment_api_order_update($order['order_id'], IPAPI_STATUS_OPEN, 0);
//@TODO: or should we set to CANCELLED HERE; can we recheck at this phase, at all?
return 'error'; //@TODO: allow path-back-on-error setting!
}
}
}
/**
* Theme issuer form/
* @ingroup theme
*/
function theme_ideal_payment_api_issuer_form($form) {
$output = '';
$output .= '<div class="order">';
$output .= $form['order']['#value']['header']; //no filter_xss, because often we get a table. Passer should escape XSS!
$output .= '</div>';
$output .= drupal_render($form);
return $output;
}
/**
* Theme function to build an administrative description that is passed along
* to the ideal environment.
* @ingroup theme
* @param $order
* @TODO: collect sensible information and turn into a short string.
*/
function theme_ideal_payment_api_description($order) {
return '['. $order['order_id'] .'] foo';
}
/**
* Recheck payment status manually
*/
function ideal_payment_api_statreq_recheck() {
$form_html = drupal_get_form('ideal_payment_api_statreq_recheck_form', $form);
}
/*
* Load status recheck form
*/
function ideal_payment_api_statreq_recheck_form() {
$form['ideal_dirreq_form']['submit'] = array(
'#type' => 'submit',
'#value' => t('Recheck my payment status »'),
'#weight' => 1,
);
return $form;
}
/*
* Load status recheck form
*/
function ideal_payment_api_statreq_recheck_form_submit($form, &$form_state) {
$order = ideal_payment_api_order_load($form_state['order']['order_id']);
ideal_payment_api_statreq_call($order);
}
/*
* Automated status request
*/
function ideal_payment_api_auto_recheck() {
$result = db_query('SELECT * FROM {ideal_payment_api_orders} WHERE payment_status = 0');
while ($order = db_fetch_array($result)) {
$order['order_id'] = $order['oid']; //To be compatible with order session var
ideal_payment_api_statreq_call($order, TRUE);
}
}
/**
* Confirm a payment, either when called as return-page, or else from cron.
* iDEAL returns to the return url and passes these vars along: ?trxid=0050000020612432&ec=37
* @param $txrid
* Description of param $param1
* @param $ec
* Passed
*
* @return
* Nothing.
*/
function ideal_payment_api_statreq_confirm() {
$has_error = TRUE;
if (_ideal_payment_api_validate_merchant_return_url($_GET['trxid'], $_GET['ec'])) {
$order = ideal_payment_api_order_load($_GET['ec']);
$response = ideal_payment_api_statreq_call($order);
if (_ideal_payment_api_validate_acquire_status_req($response)) {
$fatal = _ideal_payment_api_set_error_message($response);
if (!$fatal) {
$has_error = FALSE;
}
ideal_payment_api_order_update($order['order_id'], $response->status, $order['transaction_id']);
//Allow other modules to update the transaction or order.
module_invoke_all('ideal_payment_api_order_payed', $order, $response);
$fatal = _ideal_payment_api_set_status_message($response);
if (!$fatal) {
$has_error = FALSE;
}
}
}
if ($has_error) {
$path_back = $order_data['path_back_error'] ? $order_data['path_back_error'] : '';//@TODO replace with proper path-code, such as variable_get.
}
else {
$path_back = $order_data['path_back_succes'] ? $order_data['path_back_succes'] : '';//@TODO replace with proper path-code, such as variable_get.
_ideal_payment_api_session_del();
}
if ($fatal) {
//no use to keep the session if it can never be used again.
_ideal_payment_api_session_del();
}
//redirect the user to the landing-page.
drupal_goto($path_back);
}
/**
*----------------
*Helper functions
*/
/**
* Get configuration
*/
function LoadConfiguration() {
$url_base = url(NULL, NULL, NULL, TRUE);
//Load configuration in array
$arr_conf = array(
'PRIVATEKEY' => filter_xss(variable_get('ideal_payment_api_privatekey', 'priv.pem')),
'PRIVATEKEYPASS' => filter_xss(variable_get('ideal_payment_api_privatekeypass', FALSE)),
'PRIVATECERT' => filter_xss(variable_get('ideal_payment_api_privatecert', 'cert.cer')),
'AUTHENTICATIONTYPE' => filter_xss(variable_get('ideal_payment_api_authenticationtype', 'SHA1_RSA')),
'CERTIFICATE0' => filter_xss(variable_get('ideal_payment_api_certificate0', 'ideal.cer')),
'ACQUIRERURL' => filter_xss(variable_get('ideal_payment_api_acquirerurl', 'ssl://idealtest.secure-ing.com:443/ideal/iDeal')),
'ACQUIRERTIMEOUT' => filter_xss(variable_get('ideal_payment_api_acquirertimeout', 10)),
'MERCHANTID' => check_plain(variable_get('ideal_payment_api_merchantid', '000000000')),
'SUBID' => filter_xss(variable_get('ideal_payment_api_subid', 0)),
'MERCHANTRETURNURL' => $url_base.'ideal/payment_statreq',
'CURRENCY' => filter_xss(variable_get('ideal_payment_api_currency', 'EUR')),
'EXPIRATIONPERIOD' => filter_xss(variable_get('ideal_payment_api_expirationperiod', 'PT10M')),
'LANGUAGE' => filter_xss(variable_get('ideal_payment_api_language', 'nl')),
//debug
//'LOGFILE' => 'Connector_log.txt',
//'TraceLevel' => 'DEBUG,ERROR',
);
//IMPORTANT, 'DESCRIPTION' is bound to max. 32 chars!!
$arr_conf['DESCRIPTION'] = t('order# ') . $_SESSION['ideal_payment_api_order_data']['order_id'];
return $arr_conf;
}
/**
* Lookup installed PHP iDEAL lib
*/
function _ideal_payment_api_get_lib() {
$path_module = drupal_get_path('module', 'ideal_payment_api');
if (file_exists($path_module .'/lib/ThinMPI.php')) {
$lib = 'thinmpi'; //Rabo + old ING
}
elseif (file_exists($path_module .'/lib/iDEALConnector.php')) {
$lib = 'connector'; //New ING
}
return $lib;
}
function _ideal_payment_api_include_lib() {
$lib = _ideal_payment_api_get_lib();
$path_module = drupal_get_path('module', 'ideal_payment_api');
if ($lib == 'thinmpi') {
require_once($path_module .'/ideal_payment_api_thinmpi.inc.php');
}
elseif ($lib == 'connector') {
require_once($path_module .'/ideal_payment_api_connector.inc.php');
}
# Include the database lib too.
require_once($path_module .'/ideal_payment_api_db.inc');
}
/** .
*
* Validates the parameters in the return-url, matches against our database
* and the status of various settings.
*
* @param $txrid
* Description of param $txrid
* @param $ec
* Description of param $ec
* @param $order
* Description of param $order
*
* @return
* Boolean TRUE on valid, FALSE on invalid
*/
function _ideal_payment_api_validate_merchant_return_url($txrid, $ec) {
$valid = FALSE;
if (!empty($txrid)) { //txrid must be valid
if (!empty($ec) && ($ec = (int)$ec)) { //$ec must be valid
$order = ideal_payment_api_order_load($ec);
if ($order['order_id'] && ($txrid == $order['transaction_id']) &&
_ideal_payment_api_session_order_is_valid($order)) { //Order must be valid, must exist and the transaction_id must match $txrid.
//Allow other modules to jump in and confirm that this order is valid.
$statii = module_invoke_all('ideal_payment_api_order_validate', $order);
if (!in_array(FALSE, $statii, TRUE)) { //We got no Veto
$valid = TRUE;
}
else {
watchdog('ideal_payment_api', t('Order validation halted by a module implementation of hook_ideal_payment_api_order_validate. OrderID was $order_id', array('%order_id' => $order_id)));
}
}
else {
watchdog('ideal_payment_api', t('Order not found in the database with this order_id and transaction_id. OrderID was %order_id, TransactionID was %transaction_id ', array('%order_id' => $order_id, '%transaction_id' => $transaction_id)));
}
}
}
return $valid;
}
/**
* Validates the aquire status request, and sees if there are no errors
* @param $response
* Description of param $response
*
* @return
* boolean TRUE on succes, FALSE on fault or error.
* @TODO: create proper validation here.
*/
function _ideal_payment_api_validate_acquire_status_req($response) {
$valid = FALSE;
if (!empty($response)) {
if (!$response->errCode) {
$valid = TRUE;
}
}
return $valid;
}
/**
* Sets a watchdog and, if needed, a drupal_set_message.
*
* @param $response
* Description of param $response
*
* @return
* bool FALSE on a fatal error TRUE if none-fatal.
*/
function _ideal_payment_api_set_error_message($response) {
if ($response->errCode) {
watchdog('ideal_api', $response->errCode .': '. $response->errMsg, WATCHDOG_ERROR);
switch ($response->errCode) {
case '1': //@TODO insert all possible errors in here.
$msg = t('We could not verify the payment status automaticaly, You can check your payment manualy with the button below. Please contact us if you keep getting this message. IDEAL error: %consumer_msg', array('%msg' => $response->consumerMsg));
$fatal = FALSE;
break;
default:
$fatal = TRUE;
break;
}
}
if ($msg) {
drupal_set_message($msg);
}
return $fatal;
}
/**
* Sets the status message in watchdog and flash-messages.
*
* @param $response
* Description of param $response
*
* @return
* boolean TRUE on fatal, FALSE on none-fatal.
*/
function _ideal_payment_api_set_status_message($response) {
$fatal = FALSE;
//@TODO: insert all statii in here with proper user-messages,
switch ($response->status) {
case IPAPI_STATUS_OPEN:
break;
case IPAPI_STATUS_SUCCESS:
drupal_set_message(t('Thank you for shopping with us.')); //@TODO: write a much better tekst for this.
break;
case IPAPI_STATUS_CANCELLED:
//Transaction failed
watchdog('ideal_api', 'Your IDEAL payment has been canceled by you or by the IDEAL process. Please try again. Contact us if you have problems.', NULL, WATCHDOG_WARNING);
//inform the consumer
drupal_set_message(t('Your IDEAL payment has been canceled by you or by the IDEAL process. Please try again. Contact us if you have problems.'), 'warning');
$fatal = TRUE;
break;
case IPAPI_STATUS_EXPIRED:
$fatal = TRUE;
break;
case IPAPI_STATUS_FAILURE:
$fatal = TRUE;
break;
}
return $fatal;
}
//@TODO: harden and document.
//@TODO: add issets to only CRUD session when (not) already set.
function _ideal_payment_api_session_set($order) {
if (variable_get('ideal_payment_api_sessions', 1)) {
$_SESSION['ideal_payment_api_order'] = $order;
}
}
function _ideal_payment_api_session_del() {
if (variable_get('ideal_payment_api_sessions', 1)) {
unset($_SESSION['ideal_payment_api_order']);
}
}
function _ideal_payment_api_session_order_is_valid($order) {
if (variable_get('ideal_payment_api_sessions', 1)) {
if ($_SESSION['ideal_payment_api_order']['order_id'] == $order['order_id']) {
if ($_SESSION['ideal_payment_api_order']['transaction_id'] == $order['transaction_id']) {
return TRUE;
}
}
}
return TRUE; //in case we don't want session tracking.
}
/**
* Test this API
*/
function ideal_payment_api_test() {
$order_desc = 'test opmerking';
$order_total = '3.15';
$path_back_error = 'ideal/payment_test';
$path_back_succes = 'node'; // = home
return ideal_payment_api_payment_page($order_desc, $order_total, $path_back_error, $path_back_succes);
}
Jump to Line
Something went wrong with that request. Please try again.