? contrib/paypalpro/ppp.patch Index: contrib/paypalpro/paypalpro.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/contrib/paypalpro/paypalpro.module,v retrieving revision 1.12 diff -u -F^f -r1.12 paypalpro.module --- contrib/paypalpro/paypalpro.module 7 Apr 2006 05:21:16 -0000 1.12 +++ contrib/paypalpro/paypalpro.module 19 May 2006 20:34:58 -0000 @@ -27,15 +27,7 @@ function paypalpro_menu($may_cache) { if ($may_cache) { $items[] = array( 'path' => 'paypalpro/form', 'title' => t('Credit card payment'), - 'callback' => 'paypalpro_page', 'access' => TRUE, - 'type' => MENU_CALLBACK); - $items[] = array( - 'path' => 'paypalpro/redirect', 'title' => t('Express checkout redirect'), - 'callback' => 'paypalpro_express_checkout_redirect', 'access' => TRUE, - 'type' => MENU_CALLBACK); - $items[] = array( - 'path' => 'paypalpro/express', 'title' => t('PayPal Express Checkout'), - 'callback' => 'paypalpro_express_checkout', 'access' => TRUE, + 'callback' => 'paypalpro_form', 'access' => TRUE, 'type' => MENU_CALLBACK); } @@ -43,7 +35,7 @@ function paypalpro_menu($may_cache) { } /** - * Implementation of Drupal _settings() hook. + * Implementation of E-Commerce _settings() hook. * * @return form Form used to configure the paypalpro module. */ @@ -141,200 +133,297 @@ function paypalpro_ec_settings() { } /** - * Implementation of Drupal _page() hook. + * Implementation of _form() hook. This form is used to collect credit card + * information. * - * @param $txnid Optional transaction id. + * @param $txnid Transaction id. + * @param form Credit card form. */ -function paypalpro_page($txnid = NULL) { - $edit = $_POST['edit']; - $op = $_POST['op']; +function paypalpro_form($txnid = NULL) { + global $user; - switch ($op) { - case t('Place your order'): - if (paypalpro_validate($edit)) { - paypalpro_process($edit); - } - else { - $output = paypalpro_form($edit['txnid']); - } - break; + $t = store_transaction_load($txnid); + // make sure the current users owns this transaction (or is the site admin) + // if configured, require that the user access this page via https:// + if (($user->uid != $t->uid && $user->uid != 1) || + (variable_get('paypalpro_secure', 1) && !$_SERVER['HTTPS'])) { + drupal_access_denied(); + return; /* make sure no more output is returned */ + } - default: - $output = paypalpro_form($txnid); + + // prepare the values of the form fields + $years = drupal_map_assoc(range(2004, 2020)); + $months = drupal_map_assoc(array('01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12')); + // array includes credit card images + $paypalpro_cc_types = array( + 'Visa '. t('Visa'), + ' Mastercard '. t('MasterCard'), + ' Discover '. t('Discover'), + ' American Express '. t('American Express') + ); + + + $form['#method'] = 'POST'; + // display optional form help text + $form['help'] = array( + '#type' => 'markup', + '#prefix' => '
', + '#suffix' => '
', + '#value' => variable_get('paypalpro_form_help', ''), + ); + + // display all items being purchased + if ($t->items) { + foreach ($t->items as $p) { + $product = product_load((object)$p); + $subtotal += $p->qty * $p->price; + $items[] = t('%order of %title at %price each', array('%order' => format_plural($p->qty, '1 order', '%count orders'), '%title' => $p->title, '%price' => payment_format($product->price))). "\n"; + } } + $form['items'] = array( + '#prefix' => '

', + '#suffix' => '

', + '#value' => theme('item_list', $items, t('Your items')), + ); + + $form['cc'] = array( + '#type' => 'fieldset', + '#title' => t('Credit card details'), + ); + + // Credit card information + $form['cc']['name'] = array( + '#type' => 'fieldset', + '#title' => 'Enter your name as it appears on the card', + ); + $form['cc']['name']['cc_first_name'] = array( + '#type' => 'textfield', + '#title' => t('First name'), + '#default_value' => $t->billing_firstname, + '#size' => 21, + '#maxlength' => 42, + '#description' => '', + '#attributes' => NULL, + '#required' => TRUE, + ); + $form['cc']['name']['cc_middle_name'] = array( + '#type' => 'textfield', + '#title' => t('Middle name or initial'), + '#size' => 21, + '#maxlength' => 42, + '#description' => '', + ); + $form['cc']['name']['cc_last_name'] = array( + '#type' => 'textfield', + '#title' => t('Last name'), + '#default_value' => $t->billing_lastname, + '#size' => 21, + '#maxlength' => 42, + '#description' => '', + '#attributes' => NULL, + '#required' => TRUE, + ); + + + // the card type and card numbers + $form['cc']['number'] = array( + '#type' => 'fieldset', + '#title' => 'Select a credit card type and enter your card number', + ); + $form['cc']['number']['cc_type'] = array( + '#type' => 'radios', + '#title' => t('Card type'), + '#options' => $paypalpro_cc_types, + '#description' => t('Select the type of credit card you would like to use to make your payment.'), + '#required' => NULL, + '#attributes' => NULL, + ); + // todo: allow numbers with spaces and dashes (convert on-the-fly) + $form['cc']['number']['cc_number'] = array( + '#type' => 'textfield', + '#title' => t('Card number'), + '#size' => 21, + '#maxlength' => 21, + '#description' => t('Please enter your credit card number without any spaces or dashes.'), + '#attributes' => NULL, + '#required' => TRUE, + ); + $form['cc']['number']['cvv2'] = array( + '#type' => 'textfield', + '#title' => t('CCV2'), + '#size' => 3, + '#maxlength' => 4, + '#description' => t('The CCV2 is a 3 digit number located on the back of Visa, MasterCard and Discover credit cards in the signature panel, and a 4 digit number located on the front of an American Express card above and to the right of the imprinted card number. This number is used to provide additional security to internet orders.'), + '#attributes' => NULL, + '#required' => TRUE, + ); + + // the expiration date + $form['cc']['date'] = array( + '#type' => 'fieldset', + '#title' => t('Select your credit card\'s expiration date'), + ); + $form['cc']['date']['cc_month'] = array( + '#type' => 'select', + '#title' => t('Month'), + '#default_value' => date('m'), + '#options' => $months, + '#description' => NULL, + '#extra' => 0, + '#multiple' => false, + '#required' => TRUE, + ); + $form['cc']['date']['cc_year'] = array( + '#type' => 'select', + '#title' => t('Year'), + '#default_value' => date('Y'), + '#options' => $years, + '#description' => NULL, + '#extra' => 0, + '#multiple' => false, + '#required' => TRUE, + ); - print theme('page', $output); + $form['txnid'] = array( + '#type' => 'hidden', + '#value' => $txnid, + ); + $form[] = array( + '#type' => 'submit', + '#value' => t('Place your order'), + ); + + return drupal_get_form('paypalpro', $form); } /** * Implementation of Drupal _validate() hook. * + * @param $form_id Our form id * @param $edit Form array to validate. * @return boolean True if form validates, false if not. */ -function paypalpro_validate($edit) { - $valid = TRUE; +function paypalpro_validate($form_id, $form_values) { $paypalpro_cc_types = array(t('Visa'), t('MasterCard'), t('Discover'), t('American Express')); $type = 'invalid'; - if (!$edit['cc_first_name']) { - form_set_error('cc_first_name', t('Please enter your first name how it appear on your credit card.')); - $valid = FALSE; - } - if (!$edit['cc_last_name']) { - form_set_error('cc_last_name', t('Please enter your last name how it appear on your credit card.')); - $valid = FALSE; - } - if (!$edit['cc_number']) { - form_set_error('cc_number', t('Please enter a credit card number.')); - $valid = FALSE; - } - elseif (!is_numeric($edit['cc_number'])) { + if (!is_numeric($form_values['cc_number'])) { form_set_error('cc_number', t('Please enter a valid credit card number.')); - $valid = FALSE; } - elseif (($edit['cc_year'] < date('Y')) || ($edit['cc_year'] <= date('Y')) && - ($edit['cc_month'] < date('m'))) { + if (($form_values['cc_year'] < date('Y')) || ($form_values['cc_year'] <= date('Y')) && + ($form_values['cc_month'] < date('m'))) { form_set_error('cc_month', t('Your credit card has expired. Please try another card.')); - $valid = FALSE; } - else { - // Verify that the credit card type matches the number of digits in the - // credit card. - $length = strlen($edit['cc_number']); - if ($length == 13) { - if (substr($edit['cc_number'], 0, 1) == '4') { - $type = t('Visa'); - } - } - elseif ($length == 16) { - if (substr($edit['cc_number'], 0, 1) == '4') { - $type = t('Visa'); - } - if (substr($edit['cc_number'], 0, 1) == '5') { - $type = t('MasterCard'); - } - elseif (substr($edit['cc_number'], 0, 4) == '6011') { - $type = t('Discover'); - } - } - elseif ($length == 15) { - if (substr($edit['cc_number'], 0, 1) == '3') { - $type = t('American Express'); - } - } - if ($type != $paypalpro_cc_types[$edit['cc_type']]) { - form_set_error('cc_number', t('The credit card number you have entered is not a valid %type credit card number. Please fix the credit card type, or re-enter the credit card number.', array('%type' => $paypalpro_cc_types[$edit['cc_type']]))); - $valid = FALSE; + + // Verify that the credit card type matches the number of digits in the + // credit card. + $length = strlen($form_values['cc_number']); + if ($length == 13) { + if (substr($form_values['cc_number'], 0, 1) == '4') { + $type = t('Visa'); } - // TODO: Different cards refer to this number with a different term. - // Visa = CVV2 (card verification value) - // MasterCard = CVC2 (card validation code) - // Discover = Cardmember ID - // American Express = CID (Card Identification Number) - elseif (!$edit['cvv2']) { - form_set_error('cvv2', t('Please enter a CCV2 number.')); - $valid = FALSE; + } + elseif ($length == 16) { + if (substr($form_values['cc_number'], 0, 1) == '4') { + $type = t('Visa'); } - elseif (!is_numeric($edit['cvv2'])) { - form_set_error('cvv2', t('Please enter a valid CCV2 number. Non-numeric characters are not allowed.')); - $valid = FALSE; + if (substr($form_values['cc_number'], 0, 1) == '5') { + $type = t('MasterCard'); } - elseif (($edit['cc_type'] == 3) && (strlen($edit['cvv2']) != 4)) { - form_set_error('cvv2', t('Please enter a valid 4 digit CCV2 number. The CCV2 number on your %type credit card is located on the front above and to the right of the imprinted card number.', array('%type' => $paypalpro_cc_types[$edit['cc_type']]))); - $valid = FALSE; + elseif (substr($form_values['cc_number'], 0, 4) == '6011') { + $type = t('Discover'); } - elseif (($edit['cc_type'] != 3) && (strlen($edit['cvv2']) != 3)) { - form_set_error('cvv2', t('Please enter a valid 3 digit CCV2 number. The CCV2 number on your %type credit card is located on the back in the signature panel after the credit card number.', array('%type' => $paypalpro_cc_types[$edit['cc_type']]))); - $valid = FALSE; + } + elseif ($length == 15) { + if (substr($form_values['cc_number'], 0, 1) == '3') { + $type = t('American Express'); } } + if ($type != $paypalpro_cc_types[$form_values['cc_type']]) { + form_set_error('cc_number', t('The credit card number you have entered is not a valid %type credit card number. Please fix the credit card type, or re-enter the credit card number.', array('%type' => $paypalpro_cc_types[$form_values['cc_type']]))); + } - return $valid; + // TODO: Different cards refer to this number with a different term. + // Visa = CVV2 (card verification value) + // MasterCard = CVC2 (card validation code) + // Discover = Cardmember ID + // American Express = CID (Card Identification Number) + if (!is_numeric($form_values['cvv2'])) { + form_set_error('cvv2', t('Please enter a valid CCV2 number. Non-numeric characters are not allowed.')); + } + elseif (($form_values['cc_type'] == 3) && (strlen($form_values['cvv2']) != 4)) { + form_set_error('cvv2', t('Please enter a valid 4 digit CCV2 number. The CCV2 number on your %type credit card is located on the front above and to the right of the imprinted card number.', array('%type' => $paypalpro_cc_types[$form_values['cc_type']]))); + } + elseif (($form_values['cc_type'] != 3) && (strlen($form_values['cvv2']) != 3)) { + form_set_error('cvv2', t('Please enter a valid 3 digit CCV2 number. The CCV2 number on your %type credit card is located on the back in the signature panel after the credit card number.', array('%type' => $paypalpro_cc_types[$form_values['cc_type']]))); + } } /** - * Implementation of e-commerce _form() hook. This form is used to collect - * credit card information. + * Process a credit card transaction. Makes a curl connection to PayPal's API + * server to validate the credit card. We manually process the returned SOAP + * string rather than using PEAR and PayPal's PHP API. After a succesful + * transaction, the transaction information is stored in the local database. + * After a failed transaction, the user is redirected back to the credit card + * form and provided a helpful error to explain what is wrong. * - * @param $txnid Transaction id. - * @param form Credit card form. + * @param $edit The $edit array. */ -function paypalpro_form($txnid) { +function paypalpro_submit($form_id, $form_values) { global $user, $base_url; - if ($_POST['edit']) { - $edit = $_POST['edit']; - } - else if ($_SESSION['edit']) { - // paypalpro_goto saves the edit array in the session - $edit = $_SESSION['edit']; - } - // it doesn't hurt to unset this even if it's not set - unset ($_SESSION['edit']); - - // array includes credit card images - $paypalpro_cc_types = array('Visa '. t('Visa'), ' Mastercard '. t('MasterCard'), ' Discover '. t('Discover'), ' American Express '. t('American Express')); - - $t = store_transaction_load($txnid); - - // make sure the current users owns this transaction (or is the site admin) - if ($user->uid != $t->uid && $user->uid != 1) { - drupal_access_denied(); - } + $t = store_transaction_load($form_values['txnid']); - // if configured, require that the user access this page via https:// - if (variable_get('paypalpro_secure', 1) && !$_SERVER['HTTPS']) { - drupal_access_denied(); - } + // TODO: validate url, cert, etc... - // display optional form help text - $output = t('
%paypalpro_form_help
', array('%paypalpro_form_help' => variable_get('paypalpro_form_help', ''))); + // submit a SOAP DoDirectPaymentRequest message with curl + $errno = $error = 0; + $exec_return = paypalpro_make_SOAP_request(paypalpro_paymentrequest($t, $form_values), $errno, $error); - // display all items being purchased - if ($t->items) { - foreach ($t->items as $p) { - $product = product_load((object)$p); - $subtotal += $p->qty * $p->price; - $items[] = t('%order of %title at %price each', array('%order' => format_plural($p->qty, '1 order', '%count orders'), '%title' => $p->title, '%price' => payment_format($product->price))). "\n"; - } + // an error here indicates that the module isn't properly configured + if ($errno) { + drupal_set_message(t('PayPalPro configuration error: '). $error, 'error'); } - $output .= '

'. theme('item_list', $items, t('Your items')). '

'; - // prepare the values of the form fields - $years = drupal_map_assoc(range(2004, 2020)); - $months = drupal_map_assoc(array('01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12')); + // parse the paymentreply SOAP message, look for 'Success' or 'Failure' + if (strpos($exec_return, 'Success')) { + drupal_set_message(t('Transaction approved. Thank you for your order!')); + $t->proid = paypalpro_parse_xml($exec_return, '', ''); + // TODO: fix this when non-USD are supported + $t->amount = paypalpro_parse_xml($exec_return, '', ''); - // the name as it appears on the card - $group = form_textfield(t('First name'), 'cc_first_name', $edit['cc_first_name'], 21, 42, '', NULL, TRUE); - $group .= form_textfield(t('Middle name or initial'), 'cc_middle_name', $edit['cc_middle_name'], 21, 42, ''); - $group .= form_textfield(t('Last name'), 'cc_last_name', $edit['cc_last_name'], 21, 42, '', NULL, TRUE); - $form = form_group('Enter your name as it appears on the card', $group); + // set e-commerce API transaction payment status to 'completed'. + $t->payment_status = payment_get_status_id('completed'); + // transaction handled by paypalpro module + $t->payment_method = 'paypalpro'; - // the card type and card numbers - $group = form_radios(t('Card type'), 'cc_type', $edit['cc_type'], $paypalpro_cc_types, t('Select the type of credit card you would like to use to make your payment.'), NULL, NULL, TRUE); - // todo: allow numbers with spaces and dashes (convert on-the-fly) - $group .= form_textfield(t('Card number'), 'cc_number', $edit['cc_number'], 21, 21, t('Please enter your credit card number without any spaces or dashes.'), NULL, TRUE); - $group .= form_textfield(t('CCV2'), 'cvv2', $edit['cvv2'], 3, 4, t('The CCV2 is a 3 digit number located on the back of Visa, MasterCard and Discover credit cards in the signature panel, and a 4 digit number located on the front of an American Express card above and to the right of the imprinted card number. This number is used to provide additional security to internet orders.'), NULL, TRUE); - $form .= form_group('Select a credit card type and enter your card number', $group); + $is_new = (db_result(db_query('SELECT COUNT(txnid) FROM {ec_paypalpro} WHERE txnid = %d', $t->txnid))) ? FALSE : TRUE; + $txnid = store_transaction_save((array)$t); - // the expiration date - $group = form_select(t('Month'), 'cc_month', ($edit['cc_month'] ? $edit['cc_month'] : date('m')), $months, NULL, 0, false, TRUE); - $group .= form_select(t('Year'), 'cc_year', ($edit['cc_year'] ? $edit['cc_year'] : date('Y')), $years, NULL, 0, false, TRUE); - $form .= form_group(t('Select your credit card\'s expiration date'), $group); - - $output .= form_group(t('Credit card details'), $form); - $output .= form_hidden('txnid', $txnid); - $output .= form_submit(t('Place your order')); + if ($is_new && $txnid) { + // compose and send confirmation email to the user + store_send_invoice_email($txnid); + } - if (variable_get('paypalpro_secure', 1)) { - $base = str_replace('http://', 'https://', $base_url); + // transaction complete, return to http:// + $goto = str_replace('https://', 'http://', $base_url); + header("Location: $goto/" . url(strtr(variable_get('paypalpro_success_url', 'store/transaction/view/%txnid'), array('%txnid' => $txnid)), NULL, NULL, TRUE)); + exit(); } else { - $base = $base_url; + // transaction failed + if (strpos($exec_return, 'Failure')) { + $errors = paypalpro_get_errors($exec_return); + foreach ($errors as $error) { + paypalpro_set_error($error); + } + } + else { + $output .= drupal_set_message('Communication error. Failed to connect to the authentication server. Please try again later.', 'error'); + } + paypalpro_goto($t); } - - return form($output, 'POST', $base .'/'. url("paypalpro/form/". $txnid)); } /** @@ -392,221 +481,15 @@ function paypalpro_ec_transactionapi(&$t } /** - * Redirects the user to PayPal's secure site. The goal is to have them log - * in, select an address, and to obtain a token from PayPal used to actually - * charge the user's account. There is no return from this function, we - * either redirect to PayPal's website, or on an error we go back to the - * shopping cart. - * - * TODO: the redirect is ugly, it would be nice to display a more Drupal-ish - * page. - */ -function paypalpro_express_checkout_redirect() { - $txn = ec_checkout_get_data(); - - // submit a SOAP setexpresscheckoutrequest message with curl - $SOAPrequest = paypalpro_setExpressCheckoutRequest($txn->gross); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, variable_get('paypalpro_url', 'https://api.sandbox.paypal.com/2.0/')); - curl_setopt($ch, CURLOPT_SSLCERT, variable_get('paypalpro_sslcert', '')); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_POSTFIELDS, $SOAPrequest); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - $exec_return = curl_exec($ch); - if ($errno = curl_errno($ch)) { - $error = curl_error($ch); - } - curl_close($ch); - - if (strpos($exec_return, 'Success')) { - $token = paypalpro_parse_xml($exec_return, '', ' - - - - -

- ". t('One moment please, contacting PayPal.') ." - -

- -
- - - - -
- -"; - } - else { - if (strpos($exec_return, 'Failure')) { - $errors = paypalpro_get_errors($exec_return); - foreach ($errors as $error) { - paypalpro_set_error($error); - } - } - else { - $output .= drupal_set_message('Communication error. Failed to connect to the authentication server. Please try again later.', 'error'); - if ($error) { - // Curl errors: http://curl.haxx.se/libcurl/c/libcurl-errors.html - $output .= drupal_set_message("(libcurl error #$errno: $error)", 'error'); - } - } - drupal_goto('cart/checkout'); - } -} - -/** - * This is a replacement for cart_checkout_form() when checking out with - * PayPal Express Checkout. - */ -function paypalpro_express_checkout() { - global $user; - - $token = array_key_exists('token', $_GET) ? $_GET['token'] : ''; - - if ($token) { - // submit a SOAP getexpresscheckoutdetails message with curl - $SOAPrequest = paypalpro_getExpressCheckoutDetails($token); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, variable_get('paypalpro_url', 'https://api.sandbox.paypal.com/2.0/')); - curl_setopt($ch, CURLOPT_SSLCERT, variable_get('paypalpro_sslcert', '')); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_POSTFIELDS, $SOAPrequest); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - $exec_return = curl_exec($ch); - if ($errno = curl_errno($ch)) { - $error = curl_error($ch); - } - curl_close($ch); - - if (strpos($exec_return, 'Success')) { - $edit = ec_checkout_get_data(); - $shippable = FALSE; - foreach ($edit->items as $item) { - if (product_is_shippable($item->nid)) { - $shippable = TRUE; - break; - } - } - - // prepare checkout form - $edit->firstname = paypalpro_parse_xml($exec_return, '', ''); - $edit->lastname = paypalpro_parse_xml($exec_return, '', ''); - $edit->street1 = paypalpro_parse_xml($exec_return, '', ''); - $edit->street2 = paypalpro_parse_xml($exec_return, '', ''); - $edit->city = paypalpro_parse_xml($exec_return, '', ''); - $edit->state = paypalpro_parse_xml($exec_return, '', ''); - $edit->zip = paypalpro_parse_xml($exec_return, '', ''); - $edit->country = paypalpro_parse_xml($exec_return, '', ''); - $edit->mail = paypalpro_parse_xml($exec_return, '', ''); - //$business = paypalpro_parse_xml($exec_return, '', ''); - //$address_type = paypalpro_parse_xml($exec_return, '
', '', ''); - //$countryCode = paypalpro_parse_xml($exec_return, '', ''); - - $edit->paypalpro_token = paypalpro_parse_xml($exec_return, '', 'paypalpro_payerid = paypalpro_parse_xml($exec_return, '', ''); - $payeremail = paypalpro_parse_xml($exec_return, '', '', ''); - - $edit->screen+= 2; - ec_checkout_hide_data($edit); - drupal_goto('cart/checkout', 'op=next'); - } - } - drupal_goto('cart/checkout'); -} - -/** - * This is the final step in Express Checkout where we actually bill the - * user's PayPal account. - * - * @param $txn The current transaction object. - */ -function paypalpro_express_checkout_process($txn) { - global $user, $base_url; - - // submit a SOAP doexpresscheckoutpayment message with curl - $SOAPrequest = paypalpro_doExpressCheckoutPayment($txn); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, variable_get('paypalpro_url', 'https://api.sandbox.paypal.com/2.0/')); - curl_setopt($ch, CURLOPT_SSLCERT, variable_get('paypalpro_sslcert', '')); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_POSTFIELDS, $SOAPrequest); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - $exec_return = curl_exec($ch); - if ($errno = curl_errno($ch)) { - $error = curl_error($ch); - } - curl_close($ch); - - if (strpos($exec_return, 'Success')) { - $txn->proid = paypalpro_parse_xml($exec_return, '', ''); - // TODO: fix this when non-USD are supported - $txn->amount = paypalpro_parse_xml($exec_return, '', ''); - - // set e-commerce API transaction payment status to 'completed'. - $txn->payment_status = payment_get_status_id('completed'); - // transaction handled by paypalpro module - $txn->payment_method = 'paypalpro'; - - $is_new = (db_result(db_query('SELECT COUNT(txnid) FROM {ec_paypalpro} WHERE txnid = %d', $txn->txnid))) ? FALSE : TRUE; - $txnid = store_transaction_save($txn); - - if ($is_new && $txnid) { - // compose and send confirmation email to the user - store_send_invoice_email($txnid); - } - - // transaction complete, return to http:// - $goto = str_replace('https://', 'http://', $base_url); - header("Location: $goto/" . url(strtr(variable_get('paypalpro_success_url', 'store/transaction/view/%txnid'), array('%txnid' => $txnid)))); - exit(); - } - else { - if (strpos($exec_return, 'Failure')) { - $errors = paypalpro_get_errors($exec_return); - foreach ($errors as $error) { - paypalpro_set_error($error); - } - } - else { - $output .= drupal_set_message('Communication error. Failed to connect to the authentication server. Please try again later.', 'error'); - } - } -} - - -/** * Save or update a transaction, called from paypalpro_paymentapi. * * @param $txn The transaction object. */ function paypalpro_save($txn) { if (is_numeric($txn->txnid) && $txn->proid) { - db_query("UPDATE {ec_paypalpro} SET proid = '%s', amount = '%f' WHERE txnid = %d", $txn->proid, $txn->amount, $txn->txnid); + db_query('UPDATE {ec_paypalpro} SET proid = \'%s\', amount = %f WHERE txnid = %d', $txn->proid, $txn->amount, $txn->txnid); if (!db_affected_rows()) { - db_query("INSERT INTO {ec_paypalpro} (txnid, proid, amount) VALUES(%d, '%s', '%f')", $txn->txnid, $txn->proid, $txn->amount); + db_query('INSERT INTO {ec_paypalpro} (txnid, proid, amount) VALUES(%d, \'%s\', %f)', $txn->txnid, $txn->proid, $txn->amount); } } } @@ -629,23 +512,14 @@ function paypalpro_delete($txn) { */ function paypalpro_goto($txn) { global $base_url; - $txn = (object)$txn; + $base = ''; // if configured for secure connections, rewrite http:// a http:// if (variable_get('paypalpro_secure', 1)) { - $base = str_replace('http://', 'https://', $base_url); - } - else { - $base = $base_url; + $base = str_replace('http://', 'https://', $base_url) . '/'; } - // Save the transaction object in the current session to be restored when - // displaying the form. This way the user doesn't have to re-enter - // all their credit card information. - $_SESSION['edit'] = (array)$txn; - - header("Location: $base". '/'. url('paypalpro/form/'. $txn->txnid)); - exit(); + drupal_goto($base . 'paypalpro/form/'. $txn->txnid); } function paypalpro_set_error($error) { @@ -658,319 +532,108 @@ function paypalpro_set_error($error) { form_set_error('cc_number', ''. $error['short'] .' '. t('Please re-enter your credit card number.')); break; default: - drupal_set_message(''. $error['short'] .' '. $error['long'] .'.', 'error'); + watchdog('paypalpro', ''. $error['short'] .' '. $error['long'] .'.', WATCHDOG_ERROR); + drupal_set_message(t('There was an error processing your transaction with PayPal. A message has been logged. We are sorry for the inconvenience')); break; } } - /** - * Process a credit card transaction. Makes a curl connection to PayPal's API - * server to validate the credit card. We manually process the returned SOAP - * string rather than using PEAR and PayPal's PHP API. After a succesful - * transaction, the transaction information is stored in the local database. - * After a failed transaction, the user is redirected back to the credit card - * form and provided a helpful error to explain what is wrong. + * A simple xml parsing function. * - * @param $edit The $edit array. - */ -function paypalpro_process($edit) { - global $user, $base_url; - - $t = store_transaction_load($edit['txnid']); - - // make sure the current users owns this transaction (or is the site admin) - if ($user->uid != $t->uid && $user->uid != 1) { - drupal_access_denied(); - } - - // if configured, require https:// - if (variable_get('paypalpro_secure', 1) && !$_SERVER['HTTPS']) { - drupal_access_denied(); - } - - // TODO: validate url, cert, etc... - - // submit a SOAP paymentrequest message with curl - $SOAPrequest = paypalpro_paymentrequest($t, $edit); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, variable_get('paypalpro_url', 'https://api.sandbox.paypal.com/2.0/')); - curl_setopt($ch, CURLOPT_SSLCERT, variable_get('paypalpro_sslcert', '')); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_POSTFIELDS, $SOAPrequest); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - $exec_return = curl_exec($ch); - if ($errno = curl_errno($ch)) { - $error = curl_error($ch); - } - curl_close($ch); - - // an error here indicates that the module isn't properly configured - if ($errno) { - drupal_set_message(t('PayPalPro configuration error: '). $error, 'error'); - } - - // parse the paymentreply SOAP message, look for 'Success' or 'Failure' - if (strpos($exec_return, 'Success')) { - drupal_set_message(t('Transaction approved. Thank you for your order!')); - $edit['proid'] = paypalpro_parse_xml($exec_return, '', ''); - // TODO: fix this when non-USD are supported - $edit['amount'] = paypalpro_parse_xml($exec_return, '', ''); - - // set e-commerce API transaction payment status to 'completed'. - $edit['payment_status'] = payment_get_status_id('completed'); - // transaction handled by paypalpro module - $edit['payment_method'] = 'paypalpro'; - - $is_new = (db_result(db_query('SELECT COUNT(txnid) FROM {ec_paypalpro} WHERE txnid = %d', $edit['txnid']))) ? FALSE : TRUE; - $txnid = store_transaction_save($edit); - - if ($is_new && $txnid) { - // compose and send confirmation email to the user - store_send_invoice_email($txnid); - } + * @param $xml A text string that contains the xml to be parsed. + * @param $open_tag The opening xml tag to search for. + * @param $close_tag The closing xml tag to search for. + * @return string The string between $open_tag and $close_tag + */ +function paypalpro_parse_xml($xml, $open_tag, $close_tag) { + $pos1 = strpos($xml, $open_tag); + $pos2 = strpos($xml, $close_tag); + return substr($xml, $pos1 + strlen($open_tag), $pos2 - ($pos1 + strlen($open_tag))); +} - // transaction complete, return to http:// - $goto = str_replace('https://', 'http://', $base_url); - header("Location: $goto/" . url(strtr(variable_get('paypalpro_success_url', 'store/transaction/view/%txnid'), array('%txnid' => $txnid)))); - exit(); - } - else { - // transaction failed - if (strpos($exec_return, 'Failure')) { - $errors = paypalpro_get_errors($exec_return); - foreach ($errors as $error) { - paypalpro_set_error($error); +/** + * A recursive xml parsing function for obtaining multiple error messages. + * @param $xml A text string that contains the xml to be parsed. + * @return array All error messages found in the xml. + */ +function paypalpro_get_errors($xml) { + // errors in returned xml are comprised of short and long messages + define(SHORT_OPEN, ''); + define(SHORT_CLOSE, ''); + define(LONG_OPEN, ''); + define(LONG_CLOSE, ''); + $errors = array(); + // loop through xml to find all error messages + $loop = TRUE; + while ($loop) { + // test if there are any more errors by looking for the SHORT_OPEN string + if (strpos($xml, SHORT_OPEN)) { + $error['short'] = paypalpro_parse_xml($xml, SHORT_OPEN, SHORT_CLOSE); + // there's no need to report the generic "Internal Error" + if ($error['short'] != 'Internal Error') { + $error['long'] = paypalpro_parse_xml($xml, LONG_OPEN, LONG_CLOSE); + $errors[] = $error; } + $xml = substr($xml, strpos($xml, LONG_CLOSE) + strlen(LONG_CLOSE)); } else { - $output .= drupal_set_message('Communication error. Failed to connect to the authentication server. Please try again later.', 'error'); + // no more error messages found + $loop = FALSE; } - paypalpro_goto($edit); } + return $errors; } /** - * Manaually generate a SOAP request (this is perhaps ugly, but it greatly - * simplifies the installation process as we don't have to require PEAR or - * the PayPal PHP API). - * - * This SOAP request begins the Express Checkout process. + * Look for an adress in the user's address book that exactly matches the + * address returned by PayPal. If found, return the aid. * - * @param $total The order total, including shipping/handling/tax. - * @return SOAP The SOAP expresscheckoutrequest message. + * @param $edit The $edit array, with address info returned from PayPal. + * @return $aid The id of the address entry in the database, if match found. */ -function paypalpro_setExpressCheckoutRequest($total) { - global $base_url; - - $username = variable_get('paypalpro_username', ''); - $password = variable_get('paypalpro_password', ''); - $OrderDescription = ''; // TODO: description? - $OrderTotal = $total; - - // if configured for secure connections, rewrite http:// a http:// - if (variable_get('paypalpro_secure', 1)) { - $base = str_replace('http://', 'https://', $base_url); - } - else { - $base = $base_url; - } - - // succesful payment at PayPal's website - $ReturnURL = $base .'/'. url('paypalpro/express'); - // cancelled or otherwise unsuccessful payment at Paypal's website, - // return to checkout start - $CancelURL = $base .'/'. url('cart/checkout'); - - -$SOAPrequest = <<< End_Of_Quote - - - - - - $username - $password - - - - - - - 1.0 - - $OrderTotal - $OrderDescription - $ReturnURL - $CancelURL - - - - - -End_Of_Quote; - - return $SOAPrequest; +function paypalpro_address_get_aid($edit) { + $address = db_fetch_object(db_query("SELECT aid FROM {ec_address} WHERE uid = %d AND firstname = '%s' AND lastname = '%s' AND street1 = '%s' AND street2 = '%s' AND zip = '%s' AND city = '%s' AND state = '%s' AND country = '%s'", $edit->uid, $edit->firstname, $edit->lastname, $edit->street1, $edit->street2, $edit->zip, $edit->city, $edit->state, $edit->country)); + return $address->aid; } /** - * Manaually generate a SOAP request (this is perhaps ugly, but it greatly - * simplifies the installation process as we don't have to require PEAR or - * the PayPal PHP API). - * - * This SOAP request gets the user's details from PayPal. + * Save the address returned from PayPal in the local address book, then return + * the aid. If the address already exists, simply return the aid. * - * @param $token A token provided by PayPal, used to track the order to completion. - * @return SOAP The SOAP expresscheckoutrequest message. + * @param $edit The $edit array with address info returned from PayPal. + * @return @aid The id of the address entry in the database. */ -function paypalpro_getExpressCheckoutDetails($token) { - $username = variable_get('paypalpro_username', ''); - $password = variable_get('paypalpro_password', ''); - -$SOAPrequest = <<< End_Of_Quote - - - - - - $username - $password - - - - - - - 1.00 - $token - - - - -End_Of_Quote; - - return $SOAPrequest; +function paypalpro_address_save($edit) { + $aid = paypalpro_address_get_aid($edit); + if (!$aid) { + db_query("INSERT INTO {ec_address} (uid, firstname, lastname, street1, street2, zip, city, state, country) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", $edit->uid, $edit->firstname, $edit->lastname, $edit->street1, $edit->street2, $edit->zip, $edit->city, $edit->state, $edit->country); + $aid = paypalpro_address_get_aid($edit); + } + return $aid; } /** - * Manaually generate a SOAP request (this is perhaps ugly, but it greatly - * simplifies the installation process as we don't have to require PEAR or - * the PayPal PHP API). - * - * This SOAP request actually submits the order and withdrawls the money - * from the user's PayPal account. - * - * @param $txn The current transaction object. + * + * @param $SOAPrequest string SOAP request ro post to PayPal + * @param $errno int option return variable to get errno from curl + * @param $error string option return variable to get error from curl */ -function paypalpro_doExpressCheckoutPayment($txn) { - $username = variable_get('paypalpro_username', ''); - $password = variable_get('paypalpro_password', ''); - - $token = $txn->paypalpro_token; - $payerid = $txn->paypalpro_payerid; - - $OrderTotal = $txn->gross; - $ItemTotal = $txn->subtotal; - $ShippingTotal = $txn->shipping_cost; - $HandlingTotal = 0; // not currently used - $TaxTotal = 0; // not currently used - $ShipName = $txn->shipping_firstname .' '. $txn->shipping_lastname; - $Street1 = $txn->shipping_street1; - $Street2 = $txn->shipping_street2; - $CityName = $txn->shipping_city; - $StateOrProvince = $txn->shipping_state; - $Country = strtoupper($txn->shipping_country); - $PostalCode = $txn->shipping_zip; - -$SOAPrequest = <<< End_Of_Quote - - - - - - $username - $password - - - - - - - - 1.0 - - Sale - $token - $payerid - - $OrderTotal - $ItemTotal - $ShippingTotal - $HandlingTotal - $TaxTotal - - - $ShipName - $Street1 - $Street2 - $CityName - $StateOrProvince - $Country - $PostalCode - -End_Of_Quote; - - foreach ($txn->items as $item) { - if ($item->title) { - $ItemName = $item->title; - } - elseif ($item->sku) { - $ItemName = $item->sku; - } - else { - $ItemName = $item->nid; - } - $ItemNumber = $item->nid; - $ItemQty = $item->qty; - $ItemPrice = $item->price; - $SOAPrequest .= <<< End_Of_Quote - - $ItemName - $ItemNumber - $ItemQty - $ItemPrice - -End_Of_Quote; +function paypalpro_make_SOAP_request($SOAPrequest, &$errno, &$error) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, variable_get('paypalpro_url', 'https://api.sandbox.paypal.com/2.0/')); + curl_setopt($ch, CURLOPT_SSLCERT, variable_get('paypalpro_sslcert', '')); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_POSTFIELDS, $SOAPrequest); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $exec_return = curl_exec($ch); + if ($errno = curl_errno($ch)) { + $error = curl_error($ch); } - - $SOAPrequest .= <<< End_Of_Quote - - - - - - -End_Of_Quote; - - return $SOAPrequest; + curl_close($ch); + + return $exec_return; } /** @@ -1006,36 +669,68 @@ function paypalpro_paymentrequest($t, $e $Custom = ''; // TODO: what is this? $InvoiceID = $edit->txnid; - $ShipToStreet1 = $t->shipping_street1; - $ShipToStreet2 = $t->shipping_street2; - $ShipToCityName = $t->shipping_city; - $ShipToStateOrProvince = $t->shipping_state; - $ShipToCountry = strtoupper($t->shipping_country); // TODO + $ShipToStreet1 = $t->address['shipping']->street1; + $ShipToStreet2 = $t->address['shipping']->street2; + $ShipToCityName = $t->address['shipping']->city; + $ShipToStateOrProvince = $t->address['shipping']->state; + $ShipToCountry = strtoupper($t->address['shipping']->country); $ShipToPhone = ''; // TODO: where do we find the phone number - $ShipToPostalCode = $t->shipping_zip; + $ShipToPostalCode = $t->address['shipping']->zip; + + $CreditCardType = $paypalpro_cc_types[$edit['cc_type']]; + $CreditCardNumber = $edit['cc_number']; + $ExpMonth = $edit['cc_month']; + $ExpYear = $edit['cc_year']; + $CVV2 = $edit['cvv2']; + + $Salutation = ''; + $FirstName = $edit['cc_first_name']; + $MiddleName = $edit['cc_middle_name']; + $LastName = $edit['cc_last_name']; + $Suffix = ''; + + $Name = $t->address['billing']->firstname .' '. $t->address['billing']->lastname; + $Street1 = $t->address['billing']->street1; + $Street2 = $t->address['billing']->street2; + $CityName = $t->address['billing']->city; + $StateOrProvince = $t->address['billing']->state; + $Country = strtoupper($t->address['billing']->country); + $CountryName = store_get_country($t->address['billing']->country); + $Phone = ''; // TODO update phone handling + $PostalCode = $t->address['billing']->zip; + + $AddressID = ''; + $AddressOwner = ''; + $ExternalAddressID = ''; + $InternationalName = ''; + $InternationalStateAndCity = ''; + $InternationalStreet = ''; + $AddressStatus = ''; + + $IPAddress = $_SERVER['REMOTE_ADDR']; $SOAPrequest = <<< End_Of_Quote - - + + - - $username - $password - - - - - - - 1.0 - + + $username + $password + + + + + + + 1.0 + Sale @@ -1076,38 +771,6 @@ function paypalpro_paymentrequest($t, $e End_Of_Quote; } - $CreditCardType = $paypalpro_cc_types[$edit['cc_type']]; - $CreditCardNumber = $edit['cc_number']; - $ExpMonth = $edit['cc_month']; - $ExpYear = $edit['cc_year']; - $CVV2 = $edit['cvv2']; - - $Salutation = ''; - $FirstName = $edit['cc_first_name']; - $MiddleName = $edit['cc_middle_name']; - $LastName = $edit['cc_last_name']; - $Suffix = ''; - - $Name = $t->billing_firstname .' '. $t->billing_lastname; - $Street1 = $t->billing_street1; - $Street2 = $t->billing_street2; - $CityName = $t->billing_city; - $StateOrProvince = $t->billing_state; - $Country = strtoupper($t->billing_country); // TODO - $CountryName = store_get_country($t->billing_country); - $Phone = ''; // TODO - $PostalCode = $t->billing_zip; - - $AddressID = ''; - $AddressOwner = ''; - $ExternalAddressID = ''; - $InternationalName = ''; - $InternationalStateAndCity = ''; - $InternationalStreet = ''; - $AddressStatus = ''; - - $IPAddress = $_SERVER['REMOTE_ADDR']; - $SOAPrequest .= <<< End_Of_Quote @@ -1152,93 +815,13 @@ function paypalpro_paymentrequest($t, $e $IPAddress $MerchantSessionId - - - + + + - + End_Of_Quote; return $SOAPrequest; } - -/** - * A simple xml parsing function. - * - * @param $xml A text string that contains the xml to be parsed. - * @param $open_tag The opening xml tag to search for. - * @param $close_tag The closing xml tag to search for. - * @return string The string between $open_tag and $close_tag - */ -function paypalpro_parse_xml($xml, $open_tag, $close_tag) { - $pos1 = strpos($xml, $open_tag); - $pos2 = strpos($xml, $close_tag); - return substr($xml, $pos1 + strlen($open_tag), $pos2 - ($pos1 + strlen($open_tag))); -} - -/** - * A recursive xml parsing function for obtaining multiple error messages. - * - * @param $xml A text string that contains the xml to be parsed. - * @param $open_tag The opening xml tag to search for. - * @param $close_tag The closing xml tag to search for. - * @return array All error messages found in the xml. - */ -function paypalpro_get_errors($xml) { - // errors in returned xml are comprised of short and long messages - define(SHORT_OPEN, ''); - define(SHORT_CLOSE, ''); - define(LONG_OPEN, ''); - define(LONG_CLOSE, ''); - $errors = array(); - // loop through xml to find all error messages - $loop = TRUE; - while ($loop) { - // test if there are any more errors by looking for the SHORT_OPEN string - if (strpos($xml, SHORT_OPEN)) { - $error['short'] = paypalpro_parse_xml($xml, SHORT_OPEN, SHORT_CLOSE); - // there's no need to report the generic "Internal Error" - if ($error['short'] != 'Internal Error') { - $error['long'] = paypalpro_parse_xml($xml, LONG_OPEN, LONG_CLOSE); - $errors[] = $error; - } - $xml = substr($xml, strpos($xml, LONG_CLOSE) + strlen(LONG_CLOSE)); - } - else { - // no more error messages found - $loop = FALSE; - } - } - return $errors; -} - -/** - * Look for an adress in the user's address book that exactly matches the - * address returned by PayPal. If found, return the aid. - * - * @param $edit The $edit array, with address info returned from PayPal. - * @return $aid The id of the address entry in the database, if match found. - */ -function paypalpro_address_get_aid($edit) { - $address = db_fetch_object(db_query("SELECT aid FROM {ec_address} WHERE uid = %d AND firstname = '%s' AND lastname = '%s' AND street1 = '%s' AND street2 = '%s' AND zip = '%s' AND city = '%s' AND state = '%s' AND country = '%s'", $edit->uid, $edit->firstname, $edit->lastname, $edit->street1, $edit->street2, $edit->zip, $edit->city, $edit->state, $edit->country)); - return $address->aid; -} - -/** - * Save the address returned from PayPal in the local address book, then return - * the aid. If the address already exists, simply return the aid. - * - * @param $edit The $edit array with address info returned from PayPal. - * @return @aid The id of the address entry in the database. - */ -function paypalpro_address_save($edit) { - $aid = paypalpro_address_get_aid($edit); - if (!$aid) { - db_query("INSERT INTO {ec_address} (uid, firstname, lastname, street1, street2, zip, city, state, country) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", $edit->uid, $edit->firstname, $edit->lastname, $edit->street1, $edit->street2, $edit->zip, $edit->city, $edit->state, $edit->country); - $aid = paypalpro_address_get_aid($edit); - } - return $aid; -} - -?> Index: contrib/paypalpro/paypalpro_express.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/contrib/paypalpro/paypalpro_express.module,v retrieving revision 1.1 diff -u -F^f -r1.1 paypalpro_express.module --- contrib/paypalpro/paypalpro_express.module 13 Mar 2006 00:36:16 -0000 1.1 +++ contrib/paypalpro/paypalpro_express.module 19 May 2006 20:34:58 -0000 @@ -13,6 +13,29 @@ function paypalpro_express_help($section } /** + * Implementation of Drupal _menu() hook. + * + * @param $may_cache Flag to indicate if menu items can be cached. + * @return array Menu items. + */ +function paypalpro_express_menu($may_cache) { + $items = array(); + + if ($may_cache) { + $items[] = array( + 'path' => 'paypalpro/redirect', 'title' => t('Express checkout redirect'), + 'callback' => 'paypalpro_express_checkout_redirect', 'access' => TRUE, + 'type' => MENU_CALLBACK); + $items[] = array( + 'path' => 'paypalpro/express', 'title' => t('PayPal Express Checkout'), + 'callback' => 'paypalpro_express_return', 'access' => TRUE, + 'type' => MENU_CALLBACK); + } + + return $items; +} + +/** * Implementation of hook_paymentapi() */ function paypalpro_express_paymentapi(&$txn, $op) { @@ -24,7 +47,7 @@ function paypalpro_express_paymentapi(&$ case 'form': $form[] = array( '#type' => 'markup', - '#value' => '', + '#value' => 'Save time. Checkout securely. Pay without sharing your financial information.' ); return $form; break; @@ -52,3 +75,396 @@ function paypalpro_express_checkoutapi(& break; } } + + +/** + * Redirects the user to PayPal's secure site. The goal is to have them log + * in, select an address, and to obtain a token from PayPal used to actually + * charge the user's account. There is no return from this function, we + * either redirect to PayPal's website, or on an error we go back to the + * shopping cart. + * + * TODO on error we should probably go back to payment select. + */ +function paypalpro_express_checkout_redirect() { + $txn = ec_checkout_get_data(); + + // submit a SOAP SetExpressCheckoutRequest message with curl + $SOAPrequest = paypalpro_setExpressCheckoutRequest(store_transaction_calc_gross($txn)); + $errno = $error = 0; + $exec_return = paypalpro_make_SOAP_request($SOAPrequest, $errno, $error); + + if (strpos($exec_return, 'Success')) { + $token = paypalpro_parse_xml($exec_return, '', 'Communication error. Failed to connect to the authentication server. Please try again later.', 'error'); + if ($error) { + // Curl errors: http://curl.haxx.se/libcurl/c/libcurl-errors.html + drupal_set_message("(libcurl error #$errno: $error)", 'error'); + } + } + drupal_goto('cart/checkout'); +} + +/** + * This is the return entry point for PayPal Express. It handles the + * information given by PayPal and returns the user to the correct location in + * the checkout process. + * + * @param $arg drupal path arg to let PayPal cancel using paypal express. + */ +function paypalpro_express_return($arg = NULL) { + global $user; + + $token = array_key_exists('token', $_GET) ? $_GET['token'] : ''; + $txn = ec_checkout_get_data(); + + if ($token && $arg != 'cancel') { + // submit a SOAP GetExpressCheckoutDetails message with curl + $SOAPrequest = paypalpro_getExpressCheckoutDetails($token); + $errno = $error = 0; + $exec_return = paypalpro_make_SOAP_request($SOAPrequest, $errno, $error); + + if (strpos($exec_return, 'Success')) { + $shippable = FALSE; + foreach ($txn->items as $item) { + if (product_is_shippable($item->nid)) { + $shippable = TRUE; + break; + } + } + + // If the address module is enabled we need to skip that screen + if ($address_key = array_search('address', $txn->screens)) { + unset($txn->screens[$address_key]); + } + + // Prepare shipping/billing fields for storage + $txn->address['shipping']->firstname = paypalpro_parse_xml($exec_return, '', ''); + $txn->address['shipping']->lastname = paypalpro_parse_xml($exec_return, '', ''); + $txn->address['shipping']->street1 = paypalpro_parse_xml($exec_return, '', ''); + $txn->address['shipping']->street2 = paypalpro_parse_xml($exec_return, '', ''); + $txn->address['shipping']->city = paypalpro_parse_xml($exec_return, '', ''); + $txn->address['shipping']->state = paypalpro_parse_xml($exec_return, '', ''); + $txn->address['shipping']->zip = paypalpro_parse_xml($exec_return, '', ''); + $txn->address['shipping']->country = strtolower(paypalpro_parse_xml($exec_return, '', '')); + + //paypalpro_address_save(); + // We don't have a billing address as that is stored with paypal. Just use + // shipping for now. We could add the paypal id in the future to be helpful. + $txn->address['billing']->firstname = $txn->address['shipping']->firstname; + $txn->address['billing']->lastname = $txn->address['shipping']->lastname; + $txn->address['billing']->street1 = $txn->address['shipping']->street1; + $txn->address['billing']->street2 = $txn->address['shipping']->street2; + $txn->address['billing']->city = $txn->address['shipping']->city; + $txn->address['billing']->state = $txn->address['shipping']->state; + $txn->address['billing']->zip = $txn->address['shipping']->zip; + $txn->address['billing']->country = $txn->address['shipping']->country; + + // TODO should we overwrite the payment email with the one we get from paypal? + $txn->mail = paypalpro_parse_xml($exec_return, '', ''); + + // Other Paypal info we might use + // TODO add support for address_status and spp http://www.paypal.com/spp + //$address_status = paypalpro_parse_xml($exec_return, '', ''); + //$account = paypalpro_parse_xml($exec_return, '', ''); + //$business = paypalpro_parse_xml($exec_return, '', ''); + //$address_type = paypalpro_parse_xml($exec_return, '
', '', ''); + //$countryCode = paypalpro_parse_xml($exec_return, '', ''); + + // Store PPP tokens for later + $txn->paypalpro_token = paypalpro_parse_xml($exec_return, '', 'paypalpro_payerid = paypalpro_parse_xml($exec_return, '', ''); + + // Cleanup and move on + $txn->screen++; // go to next screen + ec_checkout_hide_data($txn); + drupal_goto('cart/checkout', 'op=next'); + } + } + + // Try to get back to payment selection on failure. + $txn->screen--; + ec_checkout_hide_data($txn); + drupal_goto('cart/checkout', 'op=next'); +} + +/** + * This is the final step in Express Checkout where we actually bill the + * user's PayPal account. + * + * @param $txn The current transaction object. + */ +function paypalpro_express_checkout_process($txn) { + global $user, $base_url; + + // submit a SOAP DoExpressCheckoutPayment message with curl + $SOAPrequest = paypalpro_doExpressCheckoutPayment($txn); + $errno = $error = 0; + $exec_return = paypalpro_make_SOAP_request($SOAPrequest, $errno, $error); + + if (strpos($exec_return, 'Success')) { + $txn->proid = paypalpro_parse_xml($exec_return, '', ''); + // TODO: fix this when non-USD are supported + $txn->amount = paypalpro_parse_xml($exec_return, '', ''); + + // set e-commerce API transaction payment status to 'completed'. + $txn->payment_status = payment_get_status_id('completed'); + // transaction handled by paypalpro module + $txn->payment_method = 'paypalpro'; + + $is_new = (db_result(db_query('SELECT COUNT(txnid) FROM {ec_paypalpro} WHERE txnid = %d', $txn->txnid))) ? FALSE : TRUE; + $txnid = store_transaction_save($txn); + + if ($is_new && $txnid) { + // compose and send confirmation email to the user + store_send_invoice_email($txnid); + } + + // transaction complete, make sure we return to http:// + $goto = str_replace('https://', 'http://', $base_url); + drupal_goto("$goto/" . url(strtr(variable_get('paypalpro_success_url', 'store/transaction/view/%txnid'), array('%txnid' => $txnid)))); + } + elseif (strpos($exec_return, 'Failure')) { + $errors = paypalpro_get_errors($exec_return); + foreach ($errors as $error) { + paypalpro_set_error($error); + } + } + else { + drupal_set_message('Communication error. Failed to connect to the authentication server. Please try again later.', 'error'); + if ($error) { + // Curl errors: http://curl.haxx.se/libcurl/c/libcurl-errors.html + drupal_set_message("(libcurl error #$errno: $error)", 'error'); + } + } +} + +/** + * Manaually generate a SOAP request (this is perhaps ugly, but it greatly + * simplifies the installation process as we don't have to require PEAR or + * the PayPal PHP API). + * + * This SOAP request begins the Express Checkout process. + * + * @param $total The order total, including shipping/handling/tax. + * @return SOAP The SOAP expresscheckoutrequest message. + */ +function paypalpro_setExpressCheckoutRequest($total) { + $username = variable_get('paypalpro_username', ''); + $password = variable_get('paypalpro_password', ''); + $OrderDescription = ''; // TODO: description? + $OrderTotal = $total; + + // succesful payment at PayPal's website + $ReturnURL = url('paypalpro/express', NULL, NULL, TRUE); + // cancelled or otherwise unsuccessful payment at Paypal's website, + // return to checkout start + $CancelURL = url('paypalpro/express/cancel', NULL, NULL, TRUE); + //$CancelURL = url('cart/checkout', 'op=next', NULL, TRUE); + + // if configured for secure connections, rewrite http:// a http:// + if (variable_get('paypalpro_secure', 1)) { + $ReturnURL = str_replace('http://', 'https://', $ReturnURL); + $CancelURL = str_replace('http://', 'https://', $CancelURL); + } + +$SOAPrequest = <<< End_Of_Quote + + + + + + $username + $password + + + + + + + 1.0 + + $OrderTotal + $OrderDescription + $ReturnURL + $CancelURL + + + + + +End_Of_Quote; + + return $SOAPrequest; +} + +/** + * Manaually generate a SOAP request (this is perhaps ugly, but it greatly + * simplifies the installation process as we don't have to require PEAR or + * the PayPal PHP API). + * + * This SOAP request gets the user's details from PayPal. + * + * @param $token A token provided by PayPal, used to track the order to completion. + * @return SOAP The SOAP expresscheckoutrequest message. + */ +function paypalpro_getExpressCheckoutDetails($token) { + $username = variable_get('paypalpro_username', ''); + $password = variable_get('paypalpro_password', ''); + +$SOAPrequest = <<< End_Of_Quote + + + + + + $username + $password + + + + + + + 1.00 + $token + + + + +End_Of_Quote; + + return $SOAPrequest; +} + +/** + * Manaually generate a SOAP request (this is perhaps ugly, but it greatly + * simplifies the installation process as we don't have to require PEAR or + * the PayPal PHP API). + * + * This SOAP request actually submits the order and withdrawls the money + * from the user's PayPal account. + * + * @param $txn The current transaction object. + */ +function paypalpro_doExpressCheckoutPayment($txn) { + $username = variable_get('paypalpro_username', ''); + $password = variable_get('paypalpro_password', ''); + + $token = $txn->paypalpro_token; + $payerid = $txn->paypalpro_payerid; + + $OrderTotal = $txn->gross; + $ItemTotal = $txn->subtotal; + $ShippingTotal = $txn->shipping_cost; + $HandlingTotal = 0; // not currently used + $TaxTotal = 0; // not currently used + $ShipName = $txn->shipping_firstname .' '. $txn->shipping_lastname; + $Street1 = $txn->shipping_street1; + $Street2 = $txn->shipping_street2; + $CityName = $txn->shipping_city; + $StateOrProvince = $txn->shipping_state; + $Country = strtoupper($txn->shipping_country); + $PostalCode = $txn->shipping_zip; + +$SOAPrequest = <<< End_Of_Quote + + + + + + $username + $password + + + + + + + + 1.0 + + Sale + $token + $payerid + + $OrderTotal + $ItemTotal + $ShippingTotal + $HandlingTotal + $TaxTotal + + + $ShipName + $Street1 + $Street2 + $CityName + $StateOrProvince + $Country + $PostalCode + +End_Of_Quote; + + foreach ($txn->items as $item) { + if ($item->title) { + $ItemName = $item->title; + } + elseif ($item->sku) { + $ItemName = $item->sku; + } + else { + $ItemName = $item->nid; + } + $ItemNumber = $item->nid; + $ItemQty = $item->qty; + $ItemPrice = $item->price; + $SOAPrequest .= <<< End_Of_Quote + + $ItemName + $ItemNumber + $ItemQty + $ItemPrice + +End_Of_Quote; + } + + $SOAPrequest .= <<< End_Of_Quote + + + + + + +End_Of_Quote; + + return $SOAPrequest; +}