/[drupal]/contributions/modules/quickpay/quickpay.module
ViewVC logotype

Contents of /contributions/modules/quickpay/quickpay.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.6 - (show annotations) (download) (as text)
Wed Oct 14 19:46:57 2009 UTC (6 weeks, 3 days ago) by xen
Branch: MAIN
Changes since 1.5: +7 -1 lines
File MIME type: text/x-php
by Xen.dk: Improved error handling.
1 <?php
2 // $Id: quickpay.module,v 1.5 2009/10/03 09:21:27 xen Exp $
3
4 define('QUICKPAY_VERSION', '3');
5 /**
6 * Implementation of hook_menu().
7 */
8 function quickpay_menu() {
9 $items['quickpay'] = array(
10 'title' => 'QuickPay callback page',
11 'page callback' => 'quickpay_callback',
12 'page arguments' => array(1),
13 'access callback' => TRUE,
14 'type' => MENU_CALLBACK,
15 );
16
17 $items['quickpay_popdown'] = array(
18 'title' => 'QuickPay popdown page',
19 'page callback' => 'quickpay_popdown',
20 'page arguments' => array(1, 2),
21 'access callback' => 'variable_get',
22 'access arguments' => array('quickpay_hosted_popup', TRUE),
23 'type' => MENU_CALLBACK,
24 );
25
26 return $items;
27 }
28
29 /**
30 * Returns a list of all quickpay supported payment methods.
31 */
32 function quickpay_all_cards() {
33 static $cards;
34 if (!$cards) {
35 $cards = array();
36 $tmp = "dan:Dankort:dan.jpg
37 edan:eDankort:edan.jpg
38 visa:Visa:visa.jpg
39 visael:Visa Electron:visaelectron.jpg
40 mastercard:Mastercard:mastercard.jpg
41 maestro:Maestro:maestro.gif
42 jcb:JCB:jcb.jpg
43 din:Diners:diners.jpg
44 amex:American Express:amexpress.jpg
45 danskebank:Danske Netbetaling:danskebank.jpg
46 nordea:Nordea Netbetaling:nordea.jpg
47 ff:Forbrugsforeningen:forbrugsforeningen.gif
48 ikano:Ikano:ikano.jpg";
49 foreach (explode("\n", $tmp) as $card) {
50 list($key, $name, $img) = explode(':', $card);
51 $cards[$key] = array('name' => t($name),
52 'image' => drupal_get_path('module', 'quickpay') .
53 '/images/' . $img);
54 }
55 }
56 return $cards;
57 }
58
59 function quickpay_cards($list) {
60 return array_intersect_key(quickpay_all_cards(), array_flip($list));
61 }
62
63 function quickpay_settings_form() {
64 $form['quickpay_merchant'] = array(
65 '#type' => 'textfield',
66 '#title' => t('Merchant number'),
67 '#description' => t('Merchant id as shown in the QuickPay admin. NOTE: <em>not</em> PBS id'),
68 '#default_value' => variable_get('quickpay_merchant', ''),
69 '#required' => TRUE,
70 );
71
72 $form['quickpay_secret'] = array(
73 '#type' => 'textfield',
74 '#title' => t('MD5 secret'),
75 '#description' => t('MD5 secret as shown in the Quickpay admin.'),
76 '#default_value' => variable_get('quickpay_secret', ''),
77 '#required' => TRUE,
78 );
79
80 $form['quickpay_order_prefix'] = array(
81 '#type' => 'textfield',
82 '#title' => t('Order id prefix'),
83 '#description' => t('Prefix for order ids. Order ids must be uniqe when sent to QuickPay, use this to resolve clashes.'),
84 '#default_value' => variable_get('quickpay_order_prefix', ''),
85 '#element_validate' => array('quickpay_order_prefix_validate'),
86 );
87
88 $options = array();
89 foreach (quickpay_cards(array('dan', 'visa', 'visael', 'mastercard',
90 'maestro', 'jcb', 'din', 'amex', 'ff',
91 'ikano')) as $key => $card) {
92 $options[$key] = theme('image', $card['image']) . '&nbsp;' . $card['name'];
93 }
94
95 $form['quickpay_accepted_cards'] = array(
96 '#type' => 'checkboxes',
97 '#title' => t('Accepted payment methods'),
98 '#description' => t('Which cards or other payment methods to show as accepted in the block. NOTE: Some require special agreements.'),
99 '#default_value' => variable_get('quickpay_accepted_cards', array('dan')),
100 '#options' => $options,
101 );
102
103 $form['hosted'] = array(
104 '#type' => 'fieldset',
105 '#title' => t('Payment window options'),
106 );
107
108 $form['hosted']['quickpay_hosted_popup'] = array(
109 '#type' => 'checkbox',
110 '#title' => t('Use popup'),
111 '#default_value' => variable_get('quickpay_hosted_popup', TRUE),
112 '#description' => t('Whether to show the credit card from in a popup window.'),
113 );
114
115 // FIXME: Remove, not used anymore.
116 $form['hosted']['quickpay_hosted_link_message'] = array(
117 '#type' => 'textfield',
118 '#title' => t('Link page message'),
119 '#default_value' => variable_get('quickpay_hosted_link_message',
120 'In order to complete the payment, continue to QuickPay to enter your credit card information.'),
121 '#description' => t('The message displayed on the page before sending the customer to QuickPays credit card form. Subject to translation.')
122 );
123
124 $languages = array('da' => t('Danish'), 'en' => t('English'),
125 'no' => t('Norwegian'), 'se' => t('Swedish'));
126 // FIXME: work together with i18n
127 $form['hosted']['quickpay_hosted_language'] = array(
128 '#type' => 'select',
129 '#title' => t('Language'),
130 '#description' => t('The language for the credit card form.'),
131 '#options' => $languages,
132 '#default_value' => variable_get('quickpay_hosted_language', 'en'),
133 );
134
135 $form['hosted']['quickpay_hosted_link_button'] = array(
136 '#type' => 'textfield',
137 '#title' => t('Link button text'),
138 '#default_value' => variable_get('quickpay_hosted_link_button',
139 'Continue to QuickPay'),
140 '#description' => t('Text of the button to open the credit card form. Subject to translation.')
141 );
142
143 return $form;
144 }
145
146 function quickpay_order_prefix_validate($element, &$form_state) {
147 if (!preg_match('/^[a-zA-Z0-9]{0,15}$/', $element['#value'])) {
148 form_error($element, t('Order prefix must only contain alphanumerics and no longer than 15 characters.'));
149 }
150 }
151
152 function quickpay_api_success_message($txn) {
153 if ($txn->uid == 0) {
154 $message = variable_get('quickpay_api_success_message', 'Payment completed, we will process your order as soon as possible. Your order number: %id');
155 if ($message != '<none>')
156 return t($message);
157 } else {
158 $message = variable_get('quickpay_api_success_message', 'Payment completed, we will process your order as soon as possible. Your order number: %id');
159 if ($message != '<none>')
160 return t($message, array('!uid' => $txn->uid,
161 '!transaction_id' => $txn->txnid));
162 }
163 return NULL;
164 }
165
166 /**
167 * Implementation of hook_simpletest().
168 */
169 function quickpay_simpletest() {
170 $dir = drupal_get_path('module', 'quickpay'). '/tests';
171 $tests = file_scan_directory($dir, '\.test$');
172 return array_keys($tests);
173 }
174
175 function quickpay_block($op = 'list', $delta = 0, $edit = array()) {
176 switch($op) {
177 case 'list':
178 $blocks[0] = array('info' => t('Shows QuickPay supported payment methods.'));
179 return $blocks;
180 case 'configure':
181 case 'save':
182 return;
183 case 'view':
184 $block = array('subject' => t('We accept'),
185 'content' => theme('quickpay_cards', quickpay_supported_cards()));
186 return $block;
187 }
188 }
189
190 /**
191 * Returns a list of the cards that the shop is configured for.
192 */
193 function quickpay_supported_cards() {
194 return quickpay_cards(variable_get('quickpay_accepted_cards', array('dan')));
195 }
196
197 function quickpay_theme() {
198 return array(
199 'quickpay_cards' => array(
200 'arguments' => array('cards' => array()),
201 ),
202 );
203 }
204
205
206 function theme_quickpay_cards($cards) {
207 drupal_add_css(drupal_get_path('module', 'quickpay') . '/quickpay.css');
208 $output = "<div class='quickpay-cards'>";
209 $i = 1;
210 foreach ($cards as $card) {
211 $output .= theme('image', $card['image'], $card['name'], $card['name'],
212 array('class' => 'card-' . $i++), TRUE);
213 }
214 $output .= '</div>';
215 return $output;
216 }
217
218 /**
219 * Callback page.
220 * Modules implementing hook_quickpay_callback should return NULL if not
221 * handled, and TRUE or FALSE if the transaction was handled successfully
222 * or not.
223 *
224 * In most cases there should only be one module implementing
225 * hook_quickpay_callback, but implementations should never the less
226 * check whether the transaction is one they created by checking
227 * whether the id corresponds to an active transaction.
228 */
229 function quickpay_callback($id) {
230 $args = func_get_args();
231 $transaction = quickpay_from_callback();
232 if (!$transaction) {
233 // quickpay_from_callback already logged it.
234 drupal_access_denied();
235 return;
236 }
237
238 // Let the first module that returns !NULL handle it.
239 foreach (module_implements('quickpay_callback') as $module) {
240 if (module_invoke($module, 'quickpay_callback', $id, $transaction, $args)
241 !== NULL)
242 break;
243 }
244 }
245
246 /**
247 * The popup closer.
248 *
249 * Modules implementing hook_quickpay_popdown should return an url to
250 * redirect to if they accepted handling the transaction, or
251 * NULL/FALSE if not.
252 *
253 * In most cases there should only be one module implementing this
254 * hook, but implementations should never the less check whether the
255 * transaction is one they created by checking whether the id
256 * corresponds to an active transaction.
257 */
258 function quickpay_popdown($id, $status) {
259 if ($status != 'success') {
260 $status == 'cancel';
261 }
262
263 // Let the first module that returns an url handle it.
264 foreach (module_implements('quickpay_popdown') as $module) {
265 if ($url = module_invoke($module, 'quickpay_popdown', $id, $status))
266 break;
267 }
268 if ($url) {
269 echo '<html><head>
270 <script type="text/javascript">
271 <!--
272 opener.location = "' . $url . '";
273 self.close();
274 // -->
275 </script></head><body></body></html>';
276 return NULL;
277 } else
278 drupal_access_denied();
279 }
280
281 /**
282 * Returns the form for redirecting users to quickpay.
283 */
284 function quickpay_hosted_form($amount, $currency, $order_id, $form_id, $continue_url,
285 $cancel_url, $callback_url) {
286 $md5_order = array(
287 'protocol',
288 'msgtype',
289 'merchant',
290 'language',
291 'ordernumber',
292 'amount',
293 'currency',
294 'continueurl',
295 'cancelurl',
296 'callbackurl',
297 'autocapture',
298 'cardtypelock',
299 'description',
300 'ipaddress',
301 'testmode',
302 );
303
304 // Required variables.
305 $data['protocol'] = "3";
306 $data['msgtype'] = "authorize"; // TODO: support subscribe.
307 $data['merchant'] = variable_get('quickpay_merchant', '');
308 // FIXME: work together with i18n/locale
309 $data['language'] = variable_get('quickpay_hosted_language', 'en');
310 $prefix = variable_get('quickpay_order_prefix', '');
311 $data['ordernumber'] = $prefix . $order_id;
312 // Ensure that Order number is at least 4 characters.
313 if (strlen($data['ordernumber']) < 4) {
314 $data['ordernumber'] = $prefix . substr('0000' . $order_id,
315 -4+strlen($prefix));
316 }
317 list($amount, $currency) =
318 _quickpay_validate_amount($amount, $currency);
319 if (!$currency) {
320 // FIXME: better error handling.
321 drupal_set_message(t('Internal error.'), 'error');
322 drupal_not_found();
323 return;
324 }
325 $data['amount'] = $amount;
326 // FIXME: any other option than using default currency?
327 $data['currency'] = $currency;
328 $data['continueurl'] = $continue_url;
329 $data['cancelurl'] = $cancel_url;
330 // End of required variables.
331 $data['callbackurl'] = $callback_url;
332
333 // FIXME: should depend on whether order needs shipping, which means that
334 // the caller need to specify it.
335 $data['autocapture'] = '0';
336
337 // Other possible variables:
338 // $data['cardtypelock'];
339 // $data['description'];
340 // $data['ipaddress'];
341 // $data['testmode'];
342 // $data['CUSTOM_*'];
343
344 $md5_string = "";
345 foreach ($md5_order as $field) {
346 $md5_string .= $data[$field];
347 }
348 $data['md5check'] = md5($md5_string . variable_get('quickpay_secret', ''));
349
350 $form['#method'] = 'POST';
351 // FIXME: Make quickpay.php url settable?
352 $form['#action'] = 'https://secure.quickpay.dk/form/';
353
354 foreach ($data as $name => $value) {
355 $form[$name] = array('#type' => 'hidden', '#value' => $value);
356 }
357
358 $form['submit'] =
359 array('#type' => 'submit',
360 '#value' => t(variable_get('quickpay_hosted_link_button',
361 'Continue to QuickPay')),
362 );
363 // The oddity of setting the return urls by JavaScript, ensures that
364 // we're only using the JavaScript requiring popdown page if JavaScript is
365 // enabled.
366 // This assumes that the module implements hook_quickpay_popdown.
367 // And we need to recalculate the md5 sum as the data changed.
368 $data['continueurl'] = url('quickpay_popdown/' . $order_id . '/success', array( 'absolute' => TRUE));
369 $data['cancelurl'] = url('quickpay_popdown/' . $order_id . '/cancel', array('absolute' => TRUE));
370 $md5_string = "";
371 foreach ($md5_order as $field) {
372 $md5_string .= $data[$field];
373 }
374 $data['md5check'] = md5($md5_string . variable_get('quickpay_secret', ''));
375 // Interpolation of arrays in strings is iffy. Lets just use regular scalars.
376 $continueurl = $data['continueurl'];
377 $cancelurl = $data['cancelurl'];
378 $md5check = $data['md5check'];
379 if (variable_get('quickpay_hosted_popup', TRUE)) {
380 $js = <<<EOF
381 $(document).ready(function() {
382 $('#$form_id').submit(function() {
383 var left = (screen.width) ? (screen.width-670)/2 : 0;
384 var top = (screen.height) ? (screen.height-500)/2 : 0;
385 $(this).find('#edit-continueurl').val('$continueurl');
386 $(this).find('#edit-cancelurl').val('$cancelurl');
387 $(this).find('#edit-md5check').val('$md5check');
388 window.open('','quickpay_payment', 'top=' + top + ',left=' + left + ',height=500,width=670,scrollbars=yes,toolbars=no,statusbar=yes,location=0');
389 $(this).attr('target', 'quickpay_payment');
390 return true;
391 });
392 });
393 EOF;
394 drupal_add_js($js, 'inline');
395 }
396 return $form;
397 }
398
399 /**
400 * Helper function returns all supported currencies.
401 */
402 function quickpay_supported_currencies() {
403 return array_keys(_quickpay_currencies());
404 }
405
406
407 // --- Core API functions ---
408
409 function quickpay_successful($txn) {
410 if ($txn === FALSE) {
411 return FALSE;
412 }
413 return $txn['qpstat'] == '000';
414 }
415 /**
416 * Returns whether a given transaction was successful or not.
417 * Use this to check a transaction, it returns 'success' when successful,
418 * 'failed' if the transaction was rejected, 'error' on errors and 'unknown'
419 * if the transaction had an unknown return code.
420 */
421 function quickpay_result($txn) {
422 if ($txn === FALSE) {
423 return 'error';
424 }
425 switch ($txn['qpstat']) {
426 case '000': // Accepted
427 return 'success';
428 break;
429 case '001': // Rejected
430 case '003': // Expired
431 case '008': // Bad parameters sent to quickpay (could be user error)
432 // Handled as failed.
433 return 'failed';
434 break;
435 case '002': // Communication error
436 case '004': // Wrong status (not authorized)
437 case '005': // Authorization expired
438 case '006': // Error at PBS
439 case '007': // Error at QuickPay
440 // All these are handled as internal error.
441 return 'error';
442 default:
443 return 'unknown';
444 }
445 }
446
447 /**
448 * Fetches a transaction from $_POST, in the way callbackurl is called.
449 *
450 * Primarily for internal use.
451 *
452 * @return array the transaction.
453 */
454 function quickpay_from_callback() {
455 static $md5_order = array(
456 'msgtype',
457 'ordernumber',
458 'amount',
459 'currency',
460 'time',
461 'state',
462 'qpstat',
463 'qpstatmsg',
464 'chstat',
465 'chstatmsg',
466 'merchant',
467 'merchantemail',
468 'transaction',
469 'cardtype',
470 'cardnumber',
471 );
472
473 // Check that it validates.
474 $md5_string = "";
475 foreach ($md5_order as $field) {
476 $md5_string .= $_POST[$field];
477 }
478 if (md5($md5_string . variable_get('quickpay_secret', '')) !=
479 $_POST['md5check']) {
480 watchdog('quickpay', 'Transaction callback md5 didn\'t verify.',
481 array(), WATCHDOG_ERROR);
482 return NULL;
483 }
484
485 $txn = array();
486 $fields = array('amount', 'time', 'ordernumber', 'pbsstat', 'qpstat',
487 'qpstatmsg', 'merchantemail', 'merchant', 'currency',
488 'cardtype', 'transaction', 'cardnumber');
489 foreach ($fields as $field) {
490 $txn[$field] = $_POST[$field];
491 }
492
493 // Reverse amount
494 if ($txn['amount'] and $txn['currency']) {
495 list($txn['amount'], $txn['currency']) =
496 _quickpay_reverse_currency($txn['amount'], $txn['currency']);
497 if (!$txn['amount'])
498 return NULL;
499 }
500 return $txn;
501 }
502
503 /**
504 * Attempt payment authorization.
505 *
506 * When a payment is authorized, it is reserved in the customers
507 * account for later withdrawal using quickpay_capture(). Payment can
508 * be cancelled with quickpay_cancel().
509 *
510 * Autocapture allows for transferring the money immediately, but is
511 * only allowed for merchants where the customer gets the goods strait
512 * away, like music downloads and online services. Goods that will be
513 * shipped to the customer must first be captured when the goods are
514 * shipped.
515 *
516 * For subscriptions use quickpay_recurring() instead.
517 *
518 * @see quickpay_capture()
519 * @see quickpay_cancel()
520 * @see quickpay_recurring()
521 * @see _quickpay_request()
522 * @see _quickpay_carddata()
523 *
524 * @param array $carddata
525 * Customer card data, as expected by _quickpay_carddata()
526 * @param mixed $orderid
527 * order id, must be unique alphanummeric and underscore only.
528 * @param mixed $amount
529 * the amount to charge the customer
530 * @param string $currency
531 * the currency, NULL for default
532 * @param boolean $autocapture
533 * whether to capture the transaction immediately
534 * @return
535 * result from _quickpay_request()
536 */
537 function quickpay_authorize($carddata, $orderid, $amount, $currency = NULL,
538 $autocapture = FALSE) {
539 $request_data = _quickpay_carddata($carddata);
540 list($request_data['amount'], $request_data['currency']) =
541 _quickpay_validate_amount($amount, $currency);
542 if (!$request_data['amount'])
543 return FALSE;
544 $request_data['autocapture'] = $autocapture ? '1' : '0';
545 $request_data['ordernumber'] = variable_get('quickpay_order_prefix', '') .
546 $orderid;
547 return _quickpay_request('authorize', $request_data);
548 }
549
550 /**
551 * Attempt authorization on subscription payment.
552 *
553 * As quickpay_authorize, but for payments set up using
554 * quickpay_subscribe(). Payment still needs to be withdrawn using
555 * quickpay_capture() or cancelled with quickpay_cancel().
556 *
557 * See quickpay_authorize() for details on common parameters.
558 *
559 * @see quickpay_subscribe()
560 * @see quickpay_capture()
561 * @see quickpay_cancel()
562 * @see quickpay_authorize()
563 *
564 * @param array $txn
565 * the result of a previous subscribe transaction. Only
566 * the 'transaction' key is used
567 *
568 * @param string $orderid
569 * the order id
570 * @param mixed $amount
571 * the amount to charge
572 * @param string $currency
573 * the currency to charge in
574 * @param boolean $autocapture
575 * whether to capture immediately
576 * @return
577 * result from _quickpay_request()
578 */
579 function quickpay_recurring($txn, $orderid, $amount, $currency = NULL, $autocapture = FALSE) {
580 $request_data = array('transaction' => $txn['transaction']);
581 list($request_data['amount'], $request_data['currency']) =
582 _quickpay_validate_amount($amount, $currency);
583 if (!$request_data['amount'])
584 return FALSE;
585 $request_data['autocapture'] = $autocapture ? '1' : '0';
586 $request_data['ordernumber'] = variable_get('quickpay_order_prefix', '') .
587 $orderid;
588 return _quickpay_request('recurring', $request_data);
589 }
590
591 /**
592 * Capture an authorized payment.
593 *
594 * Attempts to transfer the meoney from the customers account to the
595 * merchants. Can charge less that specified in the authorization,
596 * but not more.
597 *
598 * @param $txn
599 * the previously authorized transaction
600 * @param
601 * $amount the amount to transfer
602 * @return
603 * result from _quickpay_request()
604 */
605 function quickpay_capture($txn, $amount) {
606 $request_data = array('transaction' => $txn['transaction']);
607 list($request_data['amount'], $dummy) =
608 _quickpay_validate_amount($amount, $txn['currency']);
609 return _quickpay_request('capture', $request_data);
610 }
611
612 function quickpay_subscribe($carddata, $orderid, $description) {
613 $request_data = _quickpay_carddata($carddata);
614 $request_data['description'] = $description;
615 $request_data['ordernumber'] = variable_get('quickpay_order_prefix', '') .
616 $orderid;
617 return _quickpay_request('subscribe', $request_data);
618 }
619
620 /**
621 * Cancel a transaction.
622 * Cancels the authorization and erases the transaction without transferring
623 * any money,
624 * @param $txn the transaction to be cancelled
625 */
626 function quickpay_cancel($txn) {
627 $request_data = array('transaction' => $txn['transaction']);
628 return _quickpay_request('cancel', $request_data);
629 }
630
631 function quickpay_refund($txn, $amount) {
632 $request_data = array('transaction' => $txn['transaction']);
633 list($request_data['amount'], $dummy) =
634 _quickpay_validate_amount($amount, $txn['currency']);
635 return _quickpay_request('refund', $request_data);
636 }
637
638 function quickpay_status($txn) {
639 $request_data = array('transaction' => $txn['transaction']);
640 return _quickpay_request('status', $request_data);
641 }
642
643 // --- Internal functions ---
644
645 /**
646 * Currencies and their multipliers. Internal use only. Used by other
647 * functions as a central place for currency information.
648 */
649 function _quickpay_currencies() {
650 static $currencies = array('DKK' => 100, 'USD' => 100, 'EUR' => 100,
651 'GBP' => 100);
652 return $currencies;
653 }
654
655 /**
656 * Utility function to convert a $carddata object/array to a proper
657 * request array. Internal use only. Card data is an array/object
658 * with the following keys/properties:
659 *
660 * * number: the card number
661 * * exp_month: expiration month
662 * * exp_year: expriration year
663 * * cvd: card verification number
664 */
665 function _quickpay_carddata($carddata) {
666 $carddata = (array) $carddata;
667 return array('cardnumber' => $carddata['number'],
668 'expirationdate' => sprintf('%02d%02d', $carddata['exp_year'],
669 $carddata['exp_month']),
670 'cvd' => $carddata['cvd']);
671 }
672
673 /**
674 * Executes a request to QuickPay. Internal use only.
675 * @param $msg_type the type of message.
676 * @param $request_data the contents of the request.
677 * @return mixed a response array or FALSE on serious errors.
678 */
679 function _quickpay_request($msg_type, $request_data) {
680 if (!is_array($request_data))
681 $request_data = (array) $request_data;
682
683 $message = array();
684 // Order is impotant here.
685 $md5fields =
686 array(
687 'protocol' => NULL,
688 'msgtype' => NULL,
689 'merchant' => NULL,
690 'ordernumber' => NULL,
691 'amount' => NULL,
692 'currency' => NULL,
693 'autocapture' => NULL,
694 'cardnumber' => NULL,
695 'expirationdate' => NULL,
696 'cvd' => NULL,
697 'cardtypelock' => NULL,
698 'transaction' => NULL,
699 'description' => NULL,
700 );
701
702 $merchant = variable_get('quickpay_merchant', NULL);
703 $secret = variable_get('quickpay_secret', NULL);
704 if (!$merchant or !$secret) {
705 if (!$merchant)
706 _quickpay_error(t('Merchant number not set, transaction failed.'));
707 if (!$secret)
708 _quickpay_error(t('MD5 secret not set, transaction failed.'));
709 return FALSE;
710 }
711
712 $request_data['protocol'] = QUICKPAY_VERSION;
713 $request_data['msgtype'] = $msg_type;
714 $request_data['merchant'] = $merchant;
715 foreach ($request_data as $k => $v) {
716 $message[$k] = $v;
717 if (array_key_exists($k, $md5fields)) {
718 $md5fields[$k] = $v;
719 }
720 }
721 $md5fields['secret'] = $secret;
722 $message['md5check'] = md5(implode('', $md5fields));
723 if (!_quickpay_validate($message)) {
724 _quickpay_error(t('Request message didn\'t pass validation.'));
725 return FALSE;
726 }
727 $response = drupal_http_request('https://secure.quickpay.dk/api',
728 array('Content-Type' =>
729 'application/x-www-form-urlencoded'),
730 'POST',
731 http_build_query($message, FALSE, '&'), 0);
732 if ($response->code != 200 or empty($response->data)) {
733 _quickpay_error(t('Server returned non-success code or empty result'));
734 return FALSE;
735 }
736 return _quickpay_response($response->data);
737 }
738
739 /**
740 * Validates that the request fields is formatted as expected by QuickPay.
741 * @param $data Associative array of params.
742 * @returns boolean TRUE if the data is valid.
743 */
744 function _quickpay_validate($data) {
745 static $fields =
746 array(
747 'protocol' => '/^3$/',
748 'msgtype' => '/^[a-z]+$/',
749 'merchant' => '/^[0-9]{8}$/',
750 'ordernumber' => '/^[\w_]{4,20}$/',
751 'amount' => '/^[0-9]{1,10}$/',
752 'currency' => '/^[A-Z]{3}$/',
753 'autocapture' => '/^[0-1]{1}$/',
754 'cardnumber' => '/^[0-9]{13,19}$/',
755 'expirationdate' => '/^[0-9]{4}$/',
756 'cvd' => '/^[0-9]{0,4}$/',
757 'cardtypelock' => '/^[a-zA-Z,]{0,128}$/',
758 'transaction' => '/^[0-9]{1,32}$/',
759 'description' => '/^[\w _\-\.]{0,20}$/',
760 'md5check' => '/^[a-z0-9]{32}$/',
761 'CUSTOM_' => '/^[\w _\-\.]{0,20}$/'
762 );
763
764 foreach ($data as $field => $value) {
765 // No NULL values please
766 if (is_NULL($value)) {
767 _quickpay_error(t('%field cannot be NULL', array('%field' => $field)));
768 return FALSE;
769 } elseif ($fields[$field]) {
770 if (!preg_match($fields[$field], $value)) {
771 // We're not logging the actual value, as that might be
772 // sensitive information
773 _quickpay_error(t('%field didn\'t pass validation.',
774 array('%field' => $field)));
775 return FALSE;
776 }
777 } elseif (preg_match('/^CUSTOM_/', $field)) {
778 if (!preg_match($fields['CUSTOM_'], $value)) {
779 _quickpay_error(t('%field didn\'t pass validation.',
780 array('%field' => $field)));
781 return FALSE;
782 }
783 } else {
784 _quickpay_error(t('Unknown %field.',
785 array('%field' => $field)));
786 return FALSE;
787 }
788 }
789 return TRUE;
790 }
791
792 /**
793 * Parses the XML response from QuickPay into an associative
794 * array. Internal use only.
795 *
796 * The success or failure of the request can be determined with
797 * quickpay_successful() or quickpay_result().
798 *
799 * The array contains all the data from QuickPays response, but their
800 * use is discouraged. If you find yourself needing something from the
801 * response, please contact the maintainers of this module and tell
802 * them what you need and why, so they can implement a proper way to
803 * get that data.
804 *
805 * @param string $response the XML response.
806 * @return array associative array
807 */
808 function _quickpay_response($response) {
809 // TODO: PHP4 support?
810 // Load XML in response into DOM
811 $result = array();
812 $dom = new DOMDocument;
813 $dom->loadXML($response);
814 // Find elements en response and put them in an associative array
815 $xpath = new DOMXPath($dom);
816 $elements = $xpath->query('/response/*');
817 foreach ($elements as $cn) {
818 // If the element has (real) children - this is the case for status->history and chstatus->entry
819 if ($cn->childNodes->length > 1) {
820 foreach ($cn->childNodes as $hn) {
821 $result[$cn->nodeName][intval($i)][$hn->nodeName] = $hn->nodeValue;
822 }
823 $i++;
824 } else {
825 $result[$cn->nodeName] = $cn->nodeValue;
826 }
827 }
828
829 // Reverse amount
830 if ($result['amount'] and $result['currency']) {
831 list($result['amount'], $result['currency']) =
832 _quickpay_reverse_currency($result['amount'], $result['currency']);
833 if (!$result['amount'])
834 return NULL;
835 }
836 return $result;
837 }
838
839 /**
840 * Validate currency. Internal use only.
841 *
842 * Returns the multiplier for the currency or NULL for non-valid
843 * currencies.
844 *
845 * @todo Check up against http://www.iso.org/iso/support/faqs/faqs_widely_used_standards/widely_used_standards_other/currency_codes/currency_codes_list-1.htm
846 */
847 function _quickpay_validate_currency($currency) {
848 $currencies = _quickpay_currencies();
849 return $currencies[$currency];
850 }
851
852 /**
853 * Validates an amount. Internal use only.
854 *
855 * Returns an array of the amount multiplied to integer, if the currency
856 * demands it, and the currency itself.
857 * Uses default currency if $currency is NULL.
858 * Uses arbitrary precision if available.
859 */
860 function _quickpay_validate_amount($amount, $currency) {
861 if (!$currency)
862 $currency = variable_get('quickpay_default_currency', NULL);
863 $multiplyer = _quickpay_validate_currency($currency);
864 if (!$multiplyer)
865 return array(FALSE, FALSE);
866 return array((function_exists('bcmul') ?
867 bcmul($amount, $multiplyer, 0) :
868 $amount * $multiplyer), $currency);
869 }
870
871 /**
872 * Reverses _quickpay_validate_currency().
873 *
874 * Used to revert the amount from integer to decimal if the currency
875 * requires it. Used to get the original floating point amount from
876 * the integer returned by QuickPay.
877 */
878 function _quickpay_reverse_currency($amount, $currency) {
879 $multiplyer = _quickpay_validate_currency($currency);
880 if (!$multiplyer)
881 return array(FALSE, FALSE);
882 return array((function_exists('bcdiv') ?
883 bcdiv($amount, $multiplyer, 2) :
884 $amount / $multiplyer), $currency);
885 }
886
887 /**
888 * Log an error, and show it to the user if it's user 1.
889 */
890 function _quickpay_error($string) {
891 global $user;
892 watchdog('quickpay', $string, array(), WATCHDOG_ERROR);
893 // TODO: check user perms instead.
894 if ($user->uid == 1)
895 drupal_set_message($string, 'error');
896 }
897
898 /**
899 * Returns an array that maps state codes to human readable strings.
900 */
901 function _quickpay_state_codes() {
902 static $codes = array(
903 1 => 'Authorized',
904 2 => 'Authorize failed',
905 3 => 'Captured',
906 4 => 'Capture failed',
907 5 => 'Cancelled',
908 6 => 'Cancel failed',
909 7 => 'Refunded',
910 8 => 'Refund failed',
911 9 => 'Subscribed',
912 10 => 'Subscription failed',
913 );
914 return $codes;
915 }
916
917 /**
918 * Maps qpstat status codes to human readable strings.
919 *
920 * Returns the string for the given code, or all known state codes if
921 * no code was given.
922 */
923 function _quickpay_qpstat_codes($code = NULL) {
924 static $codes;
925 if (!$codes)
926 $codes = array(
927 '000' => t('Approved'),
928 '001' => t('Rejected by PBS'),
929 '002' => t('Communication error'),
930 '003' => t('Card expired'),
931 '004' => t('Wrong status (not authorized)'),
932 '005' => t('Authorization expired'),
933 '006' => t('Error at PBS'),
934 '007' => t('Error at QuickPay'),
935 '008' => t('Errors in parameteres sent to QuickPay'),
936 );
937 if ($code)
938 return $codes[$code];
939 else
940 return $codes;
941 }

  ViewVC Help
Powered by ViewVC 1.1.2