summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Kinkade <nkinkade@creativecommons.org>2014-05-10 10:34:32 -0400
committerNathan Kinkade <nkinkade@creativecommons.org>2014-05-10 10:34:32 -0400
commit1e9a10741a05167ad72855fd9c45a2d1db844736 (patch)
tree8201d24886922e44313ade94fa1c283ce367cfec
Initial commit.HEADmaster
-rw-r--r--OneClick.class.php1692
-rw-r--r--OneClick.php52
2 files changed, 1744 insertions, 0 deletions
diff --git a/OneClick.class.php b/OneClick.class.php
new file mode 100644
index 0000000..55124f8
--- /dev/null
+++ b/OneClick.class.php
@@ -0,0 +1,1692 @@
+<?php
+
+/**
+ * This is a class that implements a "one-click" donation method. It currently
+ * works with PayPal and Google Checkout. While this class can be used in any
+ * manner one sees fit, it was designed to work with a small wrapper/router
+ * script that accepts incoming requests for donations or IPN/notifications
+ * from the payment processor and calls the appropriate member methods.
+ */
+
+class OneClick {
+
+ /**
+ * CONSTANTS
+ */
+
+ // Turn IPN debuggin on/off
+ const OC_DEBUG = TRUE;
+
+
+ /**
+ * VARIABLES
+ */
+
+ // This will host the POSTed or GET data.
+ public $_request;
+
+ // Valid groups that users can join. This is just a safeguard to prevent
+ // users from joining arbitrary groups
+ public $_valid_groups = array(
+ 'CC Newsletter',
+ 'CC Events'
+ );
+
+ // Google Checkout processor name in CiviCRM db.
+ public $_gc_pp_name = 'Google Checkout API Access';
+
+ // IPN callback URL for PayPal
+ public $_paypal_notify_url = 'https://creativecommons.net/sites/default/modules/civicrm/bin/OneClick.php?oc_action=paypalipn';
+
+ // Base URL for PayPal request (minus the query string)
+ public $_paypal_base_url = 'https://www.paypal.com/cgi-bin/webscr';
+
+ // Email address of site's PayPal account
+ public $_paypal_business = 'paypal@creativecommons.org';
+
+ // Base URL for Google Checkout request
+ public $_gc_base_url = 'https://checkout.google.com/api/checkout/v2/merchantCheckout/Donations';
+
+ // Minimum contribution amount, anything lower will be kicked back.
+ public $_min_amount = '5';
+
+ // An array of URLs that should be notified when an incoming
+ // contribution (via IPN) is successfully processed.
+ public $_notify_urls = array();
+
+ // Default name of item as contributor sees it and as recorded in CiviCRM.
+ public $_item_name = 'Online Contribution: Support Creative Commons';
+
+ // Where contributor goes if they cancel contribution at payment processor
+ public $_cancel_return = 'https://creativecommons.net/donate';
+
+ // Where user goes when they click "Return to Merchant Site"
+ public $_return = 'https://creativecommons.net/thanks';
+
+
+ // Class constructor
+ public function __construct($req) {
+
+ $this->_request = $req;
+
+ if ( ! empty($this->_request['source']) ) {
+ $this->_item_name = $this->_request['source'];
+ }
+
+ if ( ! empty($this->_request['cancel_return']) ) {
+ $this->_cancel_return = $this->_request['cancel_return'];
+ }
+
+ if ( ! empty($this->_request['return']) ) {
+ $this->_return = $this->_request['return'];
+ }
+
+ if ( self::OC_DEBUG ) {
+ include_once 'CRM/Core/Error.php';
+ }
+
+ }
+
+
+ /**
+ * This function accepts various arguments, processes them appropriately
+ * and then ships the user off to the payment processor.
+ */
+ function oc_donate() {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_donate()");
+ }
+
+ // If nojavascript is a query param then we'll need to manually assign
+ // the amount variable and also handle recurring donations.
+ if ( $this->_request['nojavascript'] == "true" ) {
+ if ( $this->_request['amount'] == "choose" ) {
+ $this->_request['amount'] = $this->_request['choose_amount'];
+ }
+ if ( $this->_request['split'] ) {
+ $this->_request['recur'] = $this->_request['split'];
+ $this->_request['amount'] = round($this->_request['amount']/12, 2);
+ }
+ }
+
+ // We don't do anything if the amount is less than the minimum amount.
+ if ( floor($this->_request['amount']) < $this->_min_amount ) {
+ $err_msg = "The minimum donation is \${$this->_min_amount}.";
+ $err_msg = urlencode($err_msg);
+ header("Location: $this->_cancel_return?{$_SERVER['QUERY_STRING']}&error=$err_msg");
+ exit;
+ }
+
+ if ( $this->_request['pp'] == "paypal") {
+ $paypal_query = $this->oc_make_paypal_url();
+ $paypal_url = "$this->_paypal_base_url?$paypal_query";
+ header("Location: $paypal_url");
+ exit;
+ } elseif ( $this->_request['pp'] == "gc" ) {
+ // Fetch the credential for GC, else fail
+ $payment_processor = $this->oc_get_paymentprocessor($this->_gc_pp_name, 'Google_Checkout');
+ if ( ! $payment_processor ) {
+ $err_msg = "Failed to determine Google Checkout credentials.";
+ $err_msg = urlencode($err_msg);
+ header("Location: $this->_cancel_return?{$_SERVER['QUERY_STRING']}&error=$err_msg");
+ exit;
+ }
+ // Retrieve the XML for this request
+ $gc_xml = $this->oc_make_gc_xml();
+ // Actually make the request
+ $gc_url = $this->oc_gc_request($gc_xml, $payment_processor);
+ header("Location: $gc_url");
+ exit;
+ } else {
+ $err_msg = "Unknown payment processor.";
+ $err_msg = urlencode($err_msg);
+ header("Location: $this->_cancel_return?{$_SERVER['QUERY_STRING']}&error=$err_msg");
+ exit;
+ }
+
+ }
+
+
+ /**
+ * This is a custom IPN/notification handler for the OneClick system.
+ */
+ function oc_ipn($pp) {
+
+ // Get request headers and raw POST data
+ $headers = apache_request_headers();
+ $raw_post_data = file_get_contents("php://input");
+
+ if ( self::OC_DEBUG ) {
+ $datetime = date('c');
+ $debug_output = "OneClick: Entering oc_ipn()\n";
+ $debug_output = "IPN START $datetime\nHEADERS:\n";
+ $debug_output .= print_r($headers, TRUE);
+ $debug_output .= "POST DATA:\n$raw_post_data\n";
+ CRM_Core_Error::debug_log_message($debug_output);
+ }
+
+ if ( $pp == 'google' ) {
+
+ require_once 'Google/library/googleresponse.php';
+ require_once 'Google/library/xml-processing/gc_xmlparser.php';
+
+ $payment_processor = $this->oc_get_paymentprocessor($this->_gc_pp_name, 'Google_Checkout');
+
+ $server_type = preg_match('/sandbox/', $this->_gc_base_url) ? 'sandbox' : '';
+ $response = new GoogleResponse($payment_processor['user_name'], $payment_processor['password'], $raw_post_data, $server_type);
+
+ // Do the notification's credentials match those in our DB?
+ if ( ! $status = $response->HttpAuthentication($headers) ) {
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Google Checkout IPN unauthorized.");
+ }
+ header("HTTP/1.1 401 Unauthorized");
+ exit;
+ }
+
+ // Retrieve the root element name and data from the notification XML
+ $xmlParser = new gc_XmlParser($raw_post_data);
+ // The root element of the POSTed XML
+ $root = $xmlParser->GetRoot();
+ $data = $xmlParser->GetData();
+
+ if ( self::OC_DEBUG ) {
+ $debug_output = "PAYMENT PROCESSOR: Google Checkout\n";
+ $debug_output .= "ROOT: $root\n";
+ $debug_output .= "DATA:\n" . print_r($data, true);
+ CRM_Core_Error::debug_log_message($debug_output);
+ }
+
+ // Check to see if this contribution already exists in the db, and
+ // if it does then this is just another notification for the same
+ // order, so we need to update the existing contribution rather
+ // than create a new one.
+ require_once 'CRM/Contribute/BAO/Contribution.php';
+ $find_params = array('trxn_id' => $data[$root]['google-order-number']['VALUE']);
+ $existing_contr = CRM_Contribute_BAO_Contribution::retrieve( $find_params,
+ CRM_Core_DAO::$_nullArray,
+ CRM_Core_DAO::$_nullArray);
+
+ switch ( $root ) {
+ case 'new-order-notification':
+ $params = $this->oc_map_ipn_fields('google', $data[$root], $existing_contr);
+ $contribution = $this->oc_record_contribution($params);
+ break;
+ case 'order-state-change-notification':
+ switch ( $data[$root]['new-financial-order-state']['VALUE'] ) {
+ case 'PAYMENT_DECLINED':
+ $status = 4; // Failed
+ break;
+ case 'CANCELLED':
+ case 'CANCELLED_BY_GOOGLE':
+ $status = 3; // Cancelled
+ break;
+ default:
+ $status = 0;
+ }
+ // The payment was cancelled or declined.
+ if ( $status ) {
+ $params = array(
+ 'transaction' => array(
+ 'id' => $existing_contr->id,
+ 'contact_id' => $existing_contr->contact_id,
+ 'contribution_status_id' => $status
+ )
+ );
+ $contribution = $this->oc_record_contribution($params);
+ $notify_callbacks = TRUE;
+ } else {
+ // Trigger an ACK to Google
+ $contribution = TRUE;
+ }
+ break;
+ case 'risk-information-notification':
+ // We don't need anything from this notification, so trigger an ACK
+ // to Google.
+ $contribution = TRUE;
+ break;
+ case 'cancelled-subscription-notification':
+ $params = array(
+ 'transaction' => array(
+ 'id' => $existing_contr->id,
+ 'contact_id' => $existing_contr->contact_id,
+ 'contribution_recur_id' => $existing_contr->contribution_recur_id,
+ 'is_recur' => '1'
+ ),
+ 'recur' => array(
+ 'contribution_status_id' => '3',
+ 'cancel_date' => $this->oc_iso8601_to_mysql($data[$root]['timestamp']['VALUE']),
+ )
+ );
+ $contribution = $this->oc_record_contribution($params);
+ $notify_callbacks = TRUE;
+ break;
+ case 'charge-amount-notification':
+ $params = array(
+ 'transaction' => array(
+ 'id' => $existing_contr->id,
+ 'contact_id' => $existing_contr->contact_id,
+ 'contribution_status_id' => 1
+ )
+ );
+ $contribution = $this->oc_record_contribution($params);
+ if ( $contribution ) {
+ $notify_callbacks = TRUE;
+ }
+ break;
+ case 'chargeback-amount-notification':
+ // CiviCRM has no handling for the concept of a chargeback,
+ // so we just mark the contribution as cancelled.
+ case 'refund-amount-notification':
+ // CiviCRM has no "refunded" contribution status, so for
+ // these we just use 3, which is "cancelled".
+ $params = array(
+ 'transaction' => array(
+ 'id' => $existing_contr->id,
+ 'contact_id' => $existing_contr->contact_id,
+ 'contribution_status_id' => 3
+ )
+ );
+ $contribution = $this->oc_record_contribution($params);
+ if ( $contribution ) {
+ $notify_callbacks = TRUE;
+ }
+ break;
+ }
+
+ // If we couldn't process the contribution for some reason then exit here
+ // and don't ACK Google so that Google will keep trying to notify us.
+ if ( $contribution ) {
+ $ack = $response->SendAck($data[$root]['serial-number']);
+ if ( self::OC_DEBUG ) {
+ $debug_output = "OneClick:\nFORMATTED DATA: \n" . print_r($params, TRUE);
+ $debug_output .= "CONTRIBUTION: \n" . print_r($contribution, TRUE);
+ $debug_output .= "GOOGLE ACK: $ack\n";
+ CRM_Core_Error::debug_log_message($debug_output);
+ }
+ } else {
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Google Checkout - failed to add contribution.");
+ }
+ header("HTTP/1.1 503 Service Unavailable");
+ exit;
+ }
+
+ } elseif ( $pp == 'paypal' ) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: PAYMENT PROCESSOR - PayPal");
+ }
+
+ // Verify this IPN with PayPal
+ $verification = 'cmd=_notify-validate&' . $raw_post_data;
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_POST, TRUE);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $verification);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
+ curl_setopt($ch, CURLOPT_URL, $this->_paypal_base_url);
+ $response = curl_exec($ch);
+ $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: PayPal IPN Verification: $response");
+ }
+
+ // If verification checks out, try to process the IPN
+ if ( $response == 'VERIFIED' && $response_code == '200' ) {
+ //if ( $response != 'VERIFIED' && $response_code == '200' ) {
+
+ // If this is a subscr IPN, then go ahead and look up any
+ // existing contributions that may be a part of this subscr
+ // because we'll need this data to determine a few things like
+ // whether to record a new contribution or to update an
+ // existing one, or whether to record a premium for this
+ // contribution. We don't need to worry about this for regular
+ // web_accept payments i.e. non-recurring.
+ if ( preg_match('/subscr_/', $this->_request['txn_type']) ) {
+ $subscr_query = "
+ SELECT c.id, c.contact_id, c.trxn_id, c.invoice_id, c.contribution_recur_id
+ FROM civicrm_contribution c LEFT JOIN civicrm_contribution_recur r
+ ON c.contribution_recur_id = r.id
+ WHERE r.processor_id = '{$this->_request['subscr_id']}'
+ ORDER BY c.id DESC
+ ";
+ $subscr_dao = CRM_Core_DAO::executeQuery( $subscr_query );
+ $subscr_dao->fetch();
+ }
+
+ switch ( $this->_request['txn_type'] ) {
+ case 'subscr_signup':
+ case 'subscr_payment':
+ case 'web_accept':
+ $existing_contr = isset($subscr_dao) ? $subscr_dao : NULL;
+ $params = $this->oc_map_ipn_fields('paypal', $this->_request, $existing_contr);
+ $contribution = $this->oc_record_contribution($params);
+ if ( $contribution && $contribution->contribution_status_id == '1' ) {
+ // subscr_signup can create a contribution record, but
+ // since it comes in at the same time as the subscr_payment
+ // and isn't really a *true* payment notice, then only send
+ // notifications for subscr_payment and web_accept.
+ if ( ! $this->_request['txn_type'] == 'subscr_signup' ) {
+ $notify_callbacks = TRUE;
+ }
+ }
+ if ( self::OC_DEBUG ) {
+ $debug_output = "OneClick: FORMATTED DATA: \n" . print_r($params, TRUE);
+ $debug_output .= "CONTRIBUTION: \n" . print_r($contribution, TRUE);
+ CRM_Core_Error::debug_log_message($debug_output);
+ }
+ break;
+ case 'subscr_eot':
+ $recur_update = array(
+ 'contribution_status_id' => 1,
+ 'end_date' => date('YmdHis'),
+ );
+ break;
+ case 'subscr_cancel':
+ $recur_update = array(
+ 'contribution_status_id' => 3,
+ 'cancel_date' => date('YmdHis'),
+ );
+ break;
+ case 'subscr_failed':
+ // This IPN type comes in when a subscr payments fails
+ // for any given reason. PayPal will try to make the
+ // payment two more times, and after the 3rd failure
+ // will give up and send a subscr_eot. Even though
+ // CiviCRM doesn't expose it, just set that status to
+ // failed in the recur table.
+ $recur_update = array(
+ 'contribution_status_id' => 4,
+ 'modified_date' => date('YmdHis'),
+ );
+ }
+
+ if ( $recur_update ) {
+ $recur_update['id'] = $subscr_dao->contribution_recur_id;
+ // Even though $subscr_dao could represent more than one
+ // subscr payment we don't really care because
+ // $recur_update will only exist for IPN that have to do
+ // with the subscription, and not any one payment, so we
+ // just set $transaction['id'] to that of the first record,
+ // as nothing will be updated anyway, and if we don't then
+ // oc_record_contribution will try to create a new
+ // contribution record which we don't want.
+ $params = array(
+ 'transaction' => array(
+ 'id' => $subscr_dao->id,
+ 'contact_id' => $subscr_dao->contact_id,
+ 'contribution_recur_id' => $subscr_dao->contribution_recur_id,
+ 'is_recur' => '1'
+ ),
+ 'recur' => $recur_update
+ );
+ $contribution = $this->oc_record_contribution($params);
+ }
+
+ } else {
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: PayPal IPN unverified.");
+ }
+ exit;
+ }
+
+ } else {
+ // We don't know what payment processor this is.
+ return FALSE;
+ }
+
+ // Notify any URLs configured to receive notifications. A notification will go
+ // out when a contribution is successfully recorded, and one will also go out
+ // if the the payment processor later denies the charge or if the user cancels
+ // the order.
+ if ( $notify_callbacks ) {
+ $this->oc_notify_urls($contribution);
+ }
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: IPN END");
+ }
+
+ }
+
+
+ /**
+ * Sends a contributor a custom receipt.
+ */
+ function oc_send_receipt ( $contribution, $receipt_id ) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_send_receipt()");
+ }
+
+ require_once 'CRM/Core/BAO/MessageTemplates.php';
+
+ $msg_params = array('id' => $receipt_id);
+ $template = CRM_Core_BAO_MessageTemplates::retrieve($msg_params, CRM_Core_DAO::$_nullArray);
+
+ $query = "SELECT label FROM civicrm_option_value WHERE description LIKE '%OneClick Email Receipts%'";
+ $mail_from = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray );
+ if ( ! $mail_from ) {
+ return FALSE;
+ }
+
+ $email_params = array();
+ $email_params['from'] = $mail_from;
+ $email_params['toName'] = "$contribution->first_name $contribution->last_name";
+ $email_params['bcc'] = $mail_from;
+ $email_params['replyTo'] = $mail_from;
+ $email_params['subject'] = $template->msg_subject;
+ $email_params['text'] = $template->msg_text;
+
+ // get the billing location type
+ $location_types =& CRM_Core_PseudoConstant::locationType( );
+ $billing_location_type_id = array_search( 'Billing', $location_types );
+
+ require_once 'CRM/Contact/BAO/Contact/Location.php';
+ list( $name, $email_params['toEmail'] ) = CRM_Contact_BAO_Contact_Location::getEmailDetails( $contribution->contact_id, FALSE, $billing_location_type_id );
+
+ // Make a pretty date
+ $unix_timestamp = strtotime($contribution->receive_date);
+ $contribution_date = date('l, F j, Y g:iA', $unix_timestamp);
+
+ // Poor man's templating with preg_replace
+ $msg_tokens = array(
+ '%{amount}' => $contribution->total_amount,
+ '%{date}' => $contribution_date,
+ '%{trxn_id}' => $contribution->trxn_id,
+ '%{first_name}' => $contribution->first_name,
+ '%{last_name}' => $contribution->last_name
+ );
+ foreach ( $msg_tokens as $msg_token => $token_value ) {
+ $email_params['text'] = preg_replace("/$msg_token/", $token_value, $email_params['text']);
+ }
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Sending receipt:\n{$email_params['text']}");
+ }
+
+ require_once 'CRM/Utils/Mail.php';
+ CRM_Utils_Mail::send( $email_params );
+
+ // Now update receipt_date in the contribution
+ $rcpt_params = array(
+ 'receipt_date' => date('YmdHis')
+ );
+ $rcpt_ids = array('contribution' => $contribution->id);
+ require_once 'CRM/Contribute/BAO/Contribution.php';
+ CRM_Contribute_BAO_Contribution::create( $rcpt_params, $rcpt_ids );
+
+ }
+
+
+ /**
+ * Adds a premium to a contribution.
+ */
+ function oc_set_premium( $contribution, $custom_data ) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_set_premium()");
+ }
+
+ // If the user wanted gifts then we need to create a new record
+ // in the civicrm_contribution_product table. Code from here:
+ // CRM/Contribute/Form/Contribution/Confirm.php:583-603
+ $gift_params = array(
+ 'product_id' => $custom_data['premium'],
+ 'contribution_id' => $contribution->id,
+ 'product_option' => $custom_data['shirt_size'],
+ 'quantity' => 1
+ );
+
+ //Fixed For CRM-3901
+ require_once 'CRM/Contribute/DAO/ContributionProduct.php';
+ $dao_contr_prod = & new CRM_Contribute_DAO_ContributionProduct();
+ $dao_contr_prod->contribution_id = $contribution->id;
+ if ( $dao_contr_prod->find(true) ) {
+ $gift_params['id'] = $dao_contr_prod->id;
+ }
+
+ require_once 'CRM/Contribute/BAO/Contribution.php';
+ CRM_Contribute_BAO_Contribution::addPremium( $gift_params );
+
+ }
+
+
+ /**
+ * Creates a soft contribution if the contribution originated on a PCP.
+ */
+ function oc_add_soft_contribution ( $contribution, $custom_data ) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_add_soft_contribution()");
+ }
+
+ $cs_params = array();
+ $cs_params['contribution_id'] = $contribution->id;
+ $cs_params['contact_id'] = $contribution->contact_id;
+ $cs_params['amount'] = $contribution->total_amount;
+ $cs_params['pcp_id'] = $custom_data['pcpid'];
+ // Add user to Honor Roll unless they chose to opt-out.
+ if ( ! isset($custom_data['sloptout']) ) {
+ $cs_params['pcp_display_in_roll'] = '1';
+ $cs_params['pcp_roll_nickname'] = "$contribution->first_name $contribution->last_name";
+ }
+
+ require_once 'CRM/Contribute/BAO/Contribution.php';
+ CRM_Contribute_BAO_Contribution::addSoftContribution($cs_params);
+
+ }
+
+
+ /**
+ * Adds user to any specified groups.
+ */
+ function oc_set_groups ( $contribution, $custom_data ) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_set_groups()");
+ }
+
+ require_once 'CRM/Contact/BAO/Group.php';
+ require_once 'CRM/Contact/BAO/GroupContact.php';
+ foreach ( $custom_data['groups'] as $group ) {
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: User to be added to group '$group'");
+ }
+ // CiviCRM replaces any spaces in group names with an underscore
+ $group_name = preg_replace('/ /', '_', $group);
+ $ml_params = array(
+ "name" => $group_name,
+ "is_active" => "1"
+ );
+ $group_values = array();
+ CRM_Contact_BAO_Group::retrieve( $ml_params, $group_values );
+ $contact_ids = array($contribution->contact_id);
+ $group_id = $group_values['id'];
+ CRM_Contact_BAO_GroupContact::addContactsToGroup( $contact_ids, $group_id );
+ }
+
+ }
+
+
+ /**
+ * Sets a db custom value so contributor will show up on a public supporter list.
+ * WARNING: This function is CC-specific, but generally useful.
+ */
+ function oc_set_supporter_list ( $contribution ) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_set_supporter_list()");
+ }
+
+ $query = "SELECT id FROM civicrm_custom_field WHERE label = 'OneClick Supporter List'";
+ $custom_id = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray );
+ if ( ! $custom_id ) {
+ return FALSE;
+ }
+
+ require_once 'CRM/Core/BAO/CustomValueTable.php';
+ $sl_params = array(
+ 'entityID' => $contribution->id,
+ "custom_{$custom_id}" => "Yes"
+ );
+ CRM_Core_BAO_CustomValueTable::setValues($sl_params);
+
+ }
+
+ /**
+ * Sets a contributor source, so we can more easily track mailing donation totals, etc
+ */
+ function oc_set_contributor_source ( $contribution, $custom_data ) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_set_contributor_source()");
+ }
+
+ $query = "SELECT id FROM civicrm_custom_field WHERE label = 'Contributor Source'";
+ $custom_id = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray );
+ if ( ! $custom_id ) {
+ return FALSE;
+ }
+
+ require_once 'CRM/Core/BAO/CustomValueTable.php';
+ $sl_params = array(
+ 'entityID' => $contribution->id,
+ "custom_$custom_id" => $custom_data['contrib_source']
+ );
+ CRM_Core_BAO_CustomValueTable::setValues($sl_params);
+ }
+
+ /**
+ * Stuff custom data into serialized array.
+ */
+ function oc_create_custom_data($additional_data = NULL) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_create_custom_data()");
+ }
+
+ $custom_data = array();
+
+ # Is this a contribution through a Personal Campaign Page?
+ if ( isset($this->_request['pcpid']) && is_numeric($this->_request['pcpid']) ) {
+ $custom_data['pcpid'] = trim($this->_request['pcpid']);
+ }
+
+ # Does the user want to opt-out of public supporter lists? This includes
+ # both the public supporter page and also the "honor roll" list for PCPs.
+ if ( isset($this->_request['sloptout']) ) {
+ $custom_data['sloptout'] = '1';
+ }
+
+ // Does the user want to join some mail lists?
+ if ( isset($this->_request['groups']) ) {
+ $groups = split(':', urldecode($this->_request['groups']));
+ // Make sure they are permitted groups
+ for ( $i = 0; $i < count($groups); $i++ ) {
+ if ( ! in_array($groups[$i], $this->_valid_groups) ) {
+ unset($groups[$i]);
+ }
+ $groups[$i] = trim($groups[$i]);
+ }
+ $custom_data['groups'] = $groups;
+ }
+
+ // The contributor wants gifts
+ if ( isset($this->_request['premium']) && is_numeric($this->_request['premium']) ) {
+ $custom_data['shirt_size'] = $this->_request['size'];
+
+ // This is the db ID of the premium
+ $custom_data['premium'] = $this->_request['premium'];
+ }
+
+ // Send the contributor a receipt
+ if ( isset($this->_request['receipt']) && is_numeric($this->_request['receipt']) ) {
+ $custom_data['receipt'] = $this->_request['receipt'];
+ }
+
+ // If this is a recurring contribution, the ID of a final/reminder receipt
+ if ( isset($this->_request['final_receipt']) && is_numeric($this->_request['final_receipt']) ) {
+ $custom_data['final_receipt'] = $this->_request['final_receipt'];
+ }
+
+ // Is there a contributor source defined?
+ if ( isset($this->_request['contrib_source']) ) {
+ $custom_data['contrib_source'] = htmlspecialchars($this->_request['contrib_source']);
+ }
+
+ // Generally all custom data is gleaned from request variables, but
+ // this function also accepts an argument with some additional data,
+ // when necessary. The argument must be an array.
+ if ( $additional_data && is_array($additional_data) ) {
+ foreach ( $additional_data as $key => $data ) {
+ $custom_data[$key] = $data;
+ }
+ }
+
+ // Return serialized custom data
+ if ( $custom_data ) {
+ return serialize($custom_data);
+ } else {
+ return FALSE;
+ }
+
+ }
+
+
+ /**
+ * Returns a query string suitable for PayPal.
+ */
+ function oc_make_paypal_url() {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_make_paypal_url()");
+ }
+
+ // See this URL for more information on these fields
+ // https://www.paypal.com/IntegrationCenter/ic_std-variable-ref-buy-now.html
+ // Preset common/default PayPal variables:
+ $paypal_fields = array(
+ "business" => $this->_paypal_business,
+ "cancel_return" => $this->_cancel_return,
+ "notify_url" => $this->_paypal_notify_url,
+ "cmd" => "_xclick",
+ "currency_code" => "USD",
+ "invoice" => "",
+ "item_name" => $this->_item_name,
+ "no_note" => "1",
+ "no_shipping" => "1",
+ "quantity" => "1",
+ "return" => $this->_return,
+ "invoice" => $this->oc_create_invoice_id(),
+ "rm" => "2"
+ );
+
+ // Is this a recurring contribution of some sort?
+ // https://www.paypal.com/en_US/ebook/subscriptions/html.html
+ if ( isset($this->_request['recur']) ) {
+ switch ( $this->_request['recur'] ) {
+ case '1':
+ // Payments will recur for 12 months
+ $paypal_fields['srt'] = "12";
+ case '2':
+ $paypal_fields['cmd'] = "_xclick-subscriptions";
+ $paypal_fields['a3'] = round($this->_request['amount'], 2);
+ $paypal_fields['p3'] = "1";
+ $paypal_fields['t3'] = "M";
+ $paypal_fields['src'] = "1";
+ $paypal_fields['sra'] = "1";
+ break;
+ }
+ } else {
+ // Field "amount" is used in non-recurring payments
+ $paypal_fields['amount'] = round($this->_request['amount'], 2);
+ }
+
+ // If the contributor wanted a premium then we pass this info on to PayPal.
+ // And we also tell PayPal that we need a shipping address.
+ // pretty and nice.
+ if ( isset($this->_request['premium']) && is_numeric($this->_request['premium']) ) {
+ $paypal_fields['on0'] = "Gift";
+ $paypal_fields['os0'] = "Yes";
+ $paypal_fields['on1'] = "Shirt size";
+ $paypal_fields['os1'] = $this->_request['size'];
+ $paypal_fields['no_shipping'] = "2";
+ } else {
+ $paypal_fields['on0'] = "Gift";
+ $paypal_fields['os0'] = "No";
+ }
+
+ // Create a custom data field to be submitted to the payment processor.
+ if ( $custom_data = $this->oc_create_custom_data() ) {
+ $paypal_fields['custom'] = $custom_data;
+ }
+
+ // Concatenate the field with &
+ $paypal_query_string = '';
+ foreach ( $paypal_fields as $key => $value ) {
+ $value = urlencode($value);
+ $paypal_query_string .= "$key=$value&";
+ }
+ $paypal_query_string = rtrim($paypal_query_string, '&');
+
+ return $paypal_query_string;
+
+ }
+
+
+ /**
+ * Generate XML suitable to send to Google Checkout's API.
+ */
+ function oc_make_gc_xml() {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_make_gc_xml()");
+ }
+
+ // Base XML template for Google API request
+ if ( isset($this->_request['recur']) ) {
+ // Template for subscription (recurring) payments
+ $template = <<<XML
+<checkout-shopping-cart xmlns="http://checkout.google.com/schema/2">
+ <shopping-cart>
+ <items>
+ <item>
+ <item-name></item-name>
+ <item-description></item-description>
+ <quantity>1</quantity>
+ <unit-price currency="USD"></unit-price>
+ <subscription type="google" period="MONTHLY">
+ <payments>
+ <subscription-payment>
+ <maximum-charge currency="USD"></maximum-charge>
+ </subscription-payment>
+ </payments>
+ <recurrent-item>
+ <item-name></item-name>
+ <item-description></item-description>
+ <quantity>1</quantity>
+ <unit-price currency="USD"></unit-price>
+ <merchant-private-item-data>
+ <recurrent-custom-data></recurrent-custom-data>
+ </merchant-private-item-data>
+ </recurrent-item>
+ </subscription>
+ </item>
+ </items>
+ <merchant-private-data></merchant-private-data>
+ </shopping-cart>
+ <checkout-flow-support>
+ <merchant-checkout-flow-support>
+ <continue-shopping-url></continue-shopping-url>
+ <edit-cart-url></edit-cart-url>
+ </merchant-checkout-flow-support>
+ </checkout-flow-support>
+</checkout-shopping-cart>
+XML;
+ } else {
+ // Template for single-time donations
+ $template = <<<XML
+<checkout-shopping-cart xmlns="http://checkout.google.com/schema/2">
+ <shopping-cart>
+ <items>
+ <item>
+ <item-name></item-name>
+ <item-description></item-description>
+ <quantity>1</quantity>
+ <unit-price currency="USD"></unit-price>
+ </item>
+ </items>
+ <merchant-private-data></merchant-private-data>
+ </shopping-cart>
+ <checkout-flow-support>
+ <merchant-checkout-flow-support>
+ <continue-shopping-url></continue-shopping-url>
+ <edit-cart-url></edit-cart-url>
+ </merchant-checkout-flow-support>
+ </checkout-flow-support>
+</checkout-shopping-cart>
+XML;
+ }
+
+ // A place to stick additional merchant-private-data ($custom_data).
+ $add_custom_data = array();
+
+ $gc_xml = new DomDocument('1.0');
+ $gc_xml->loadXML($template);
+ $gc_xml->encoding = 'UTF-8';
+
+ $node_item_name = $gc_xml->createTextNode($this->_item_name);
+ $node_amount = $gc_xml->createTextNode( round($this->_request['amount'], 2) );
+
+ $gc_xml->getElementsByTagName('item-name')->item(0)->appendChild($node_item_name);
+ $gc_xml->getElementsByTagName('unit-price')->item(0)->appendChild($node_amount);
+
+ if ( isset($this->_request['recur']) ) {
+ $node_recur_item_name = $gc_xml->createTextNode($this->_item_name);
+ $node_recur_amount = $gc_xml->createTextNode( round($this->_request['amount'], 2) );
+ $node_maximum_charge = $gc_xml->createTextNode( round($this->_request['amount'], 2) );
+ $gc_xml->getElementsByTagName('item-name')->item(1)->appendChild($node_recur_item_name);
+ $gc_xml->getElementsByTagName('unit-price')->item(1)->appendChild($node_recur_amount);
+ $gc_xml->getElementsByTagName('maximum-charge')->item(0)->appendChild($node_maximum_charge);
+ // Is this subscription just for 12 months?
+ if ( $this->_request['recur'] == '1' ) {
+ $attribute = $gc_xml->createAttribute('times');
+ $text_node = $gc_xml->createTextNode('12');
+ $attribute->appendChild($text_node);
+ $gc_xml->getElementsByTagName('subscription-payment')->item(0)->appendChild($attribute);
+ }
+
+ // Unfortunately, Google doesn't provide a unique ID to indentify a
+ // payment as belonging to a certain subscription, so we generate a
+ // (hopefully) unique ID here and add it to our $custom_data for
+ // later reference.
+ $add_custom_data['subscription_id'] = md5( time() . rand() );
+ }
+
+ // Set where user goes when order is complete at GC
+ $node_return = $gc_xml->createTextNode($this->_return);
+ $gc_xml->getElementsByTagName('continue-shopping-url')->item(0)->appendChild($node_return);
+
+ // Set where user goes if they leave GC before finishing
+ $node_cancel = $gc_xml->createTextNode($this->_cancel_return);
+ $gc_xml->getElementsByTagName('edit-cart-url')->item(0)->appendChild($node_cancel);
+
+ // Set up custom OneClick custom_data.
+ if ( $custom_data = $this->oc_create_custom_data($add_custom_data) ) {
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: custom_data = $custom_data");
+ }
+ $text_node = $gc_xml->createTextNode($custom_data);
+ $gc_xml->getElementsByTagName('merchant-private-data')->item(0)->appendChild($text_node);
+ if ( isset($this->_request['recur']) ) {
+ $text_node = $gc_xml->createTextNode($custom_data);
+ $gc_xml->getElementsByTagName('recurrent-custom-data')->item(0)->appendChild($text_node);
+ }
+ }
+
+ $xml_request = $gc_xml->saveXML();
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: GC XML request:\n$xml_request");
+ }
+
+ return $xml_request;
+
+ }
+
+
+ /**
+ * Make a CURL request to the GC API. If it's successful return the redirect
+ * URL, else echo the error.
+ */
+ function oc_gc_request($gc_xml, $payment_processor) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_gc_request()");
+ }
+
+ $gc_url = "{$this->_gc_base_url}/{$payment_processor['user_name']}";
+ $gc_auth = base64_encode("{$payment_processor['user_name']}:{$payment_processor['password']}");
+
+ $header = array();
+ $header[] = "Authorization: Basic $gc_auth";
+ $header[] = "Content-type: application/xml;charset=UTF-8";
+ $header[] = "Accept: application/xml;charset=UTF-8";
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $gc_url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $gc_xml);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
+
+ // Execute the API request.
+ $gc_response = curl_exec($ch);
+
+ $response_xml = new DomDocument('1.0');
+ $response_xml->loadXML($gc_response);
+ $response_type = $response_xml->documentElement->nodeName;
+
+ switch ($response_type) {
+ case "checkout-redirect":
+ return $response_xml->getElementsByTagName('redirect-url')->item(0)->nodeValue;
+ break;
+ case "error":
+ $error = $response_xml->getElementsByTagName('error-message')->item(0)->nodeValue;
+ echo "Google Checkout returned error:\n\n$error";
+ exit;
+ break;
+ default:
+ $data = $response_xml->saveXML();
+ echo "Unknown response from Google Checkout:\n\n$data";
+ exit;
+ }
+
+ }
+
+
+ /**
+ * Generate a unique CiviCRM invoice number.
+ */
+ function oc_create_invoice_id() {
+ // CRM/Contribute/Form/Contribution/Main.php:849
+ return md5(uniqid(rand(), TRUE));
+ }
+
+
+ /**
+ * Post contribution and contributor details to each of an
+ * array of URLs.
+ */
+ function oc_notify_urls($notification) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_notify_urls()");
+ }
+
+ if ( ! $this->_notify_urls ) {
+ return false;
+ }
+
+ $json_data = json_encode($notification);
+ $hash = sha1(CIVICRM_SITE_KEY . $json_data);
+
+ $post_data = "data=$json_data";
+ $post_data .= "&hash=$hash";
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_POST, TRUE);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
+
+ foreach ( $this->_notify_urls as $notify_url ) {
+ curl_setopt($ch, CURLOPT_URL, $notify_url);
+ $response = curl_exec($ch);
+ $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ if ( $status_code != '200' ) {
+ if ( self::OC_DEBUG ) {
+ $debug_output = "Notify URL '$notify_url' returned HTTP status code: '$status_code'\n";
+ $debug_output .= "The response was:\n$response";
+ CRM_Core_Error::debug_log_message("OneClick: $debug_output");
+ }
+ }
+ }
+
+ }
+
+
+ /**
+ * Fetch payment processor object from CiviCRM database.
+ */
+ function oc_get_paymentprocessor($pp_name, $pp_type) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_get_paymentprocessor()");
+ }
+
+ require_once 'CRM/Core/BAO/PaymentProcessor.php';
+
+ // This means that we have to be consistent about naming our GC payment
+ // processor in CiviCRM
+ $params = array(
+ 'name' => $pp_name,
+ 'payment_processor_type' => $pp_type,
+ 'is_active' => 1,
+ 'is_test' => 0
+ );
+ $payment_processor = array();
+ CRM_Core_BAO_PaymentProcessor::retrieve($params, $payment_processor);
+
+ if ( $payment_processor ) {
+ return $payment_processor;
+ } else {
+ return FALSE;
+ }
+
+ }
+
+
+ /**
+ * Stores or fetches OneClick custom data in/from a CiviCRM custom data field.
+ * This could be useful to record, and we can't be guaranteed that every IPN
+ * will contain this data, but we may need it. This is somewhat of a hack, but
+ * to stay away from coding for a database ID we'll just require that the custom
+ * data field label is "OneClick Custom Data" so that we can find it in the DB.
+ */
+ function oc_store_fetch_custom_data($operation, $contribution_id, $custom_data = NULL) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_store_fetch_custom_data()");
+ }
+
+ if ( ! $operation || ! $contribution_id ) {
+ return FALSE;
+ }
+
+ $return = FALSE;
+
+ $query = "SELECT id FROM civicrm_custom_field WHERE label = 'OneClick Custom Data'";
+ $custom_id = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray );
+
+ if ( $custom_id ) {
+ require_once 'CRM/Core/BAO/CustomValueTable.php';
+ if ( $operation == 'setValues' ) {
+ $custom_value = serialize($custom_data);
+ } else {
+ $custom_value = 1;
+ }
+
+ $custom_params = array(
+ 'entityID' => $contribution_id,
+ "custom_{$custom_id}" => $custom_value
+ );
+
+ $custom_value = CRM_Core_BAO_CustomValueTable::$operation($custom_params);
+ if ( ! $custom_value['is_error'] ) {
+ if ( $operation == 'getValues' ) {
+ $return = unserialize($custom_value["custom_{$custom_id}"]);
+ } elseif ( $operation == 'setValues' ) {
+ $return = TRUE;
+ }
+ }
+ }
+
+ return $return;
+
+ }
+
+
+ /**
+ * Stores the payment processor in a custom field for the contribution.
+ */
+ function oc_set_payment_processor($payment_processor, $contribution_id) {
+
+ if ( ! $payment_processor || ! $contribution_id ) {
+ return FALSE;
+ }
+
+ $return = FALSE;
+
+ $query = "SELECT id FROM civicrm_custom_field WHERE label = 'Payment Processor'";
+ $custom_id = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray );
+
+ if ( $custom_id ) {
+ require_once 'CRM/Core/BAO/CustomValueTable.php';
+
+ $custom_params = array(
+ 'entityID' => $contribution_id,
+ "custom_{$custom_id}" => $payment_processor
+ );
+
+ $custom_value = CRM_Core_BAO_CustomValueTable::setValues($custom_params);
+ if ( ! $custom_value['is_error'] ) {
+ $return = TRUE;
+ }
+ }
+
+ return $return;
+
+ }
+
+
+ /**
+ * Converts ISO-8601 dates to MySQL suitable dates.
+ */
+ function oc_iso8601_to_mysql($date) {
+ $unix_timestamp = strtotime($date);
+ $date_field = date('YmdHis', $unix_timestamp);
+ return $date_field;
+ }
+
+
+ /**
+ * Takes the POSTed input from the payment processor IPN and normalizes the
+ * data.
+ */
+ function oc_map_ipn_fields($payment_processor, $ipn_data, &$existing_contr) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_map_ipn_fields()");
+ }
+
+ $contact = array();
+ $email = array();
+ $address = array();
+ $recur = array();
+ $contribution = array();
+
+ // This value gets stored in a custom field in the contribution
+ // making searching based on payment processor easy
+ $contribution['payment_processor'] = $payment_processor;
+
+ if ( $payment_processor == 'paypal' ) {
+
+ // Contact information
+ $contact['first_name'] = $ipn_data['first_name'];
+ $contact['last_name'] = $ipn_data['last_name'];
+
+ // Email address
+ $email['email'] = $ipn_data['payer_email'];
+
+ // Address information
+ if ( $ipn_data['address_name'] ) {
+ $address['address_name'] = $ipn_data['address_name'];
+ $address['street_address'] = $ipn_data['address_street'];
+ $address['city'] = $ipn_data['address_city'];
+ $address['state_province'] = $ipn_data['address_state'];
+ $address['postal_code'] = $ipn_data['address_zip'];
+ $address['country'] = $ipn_data['address_country_code'];
+ }
+
+ // Common contribution information
+ $contribution['source'] = $ipn_data['item_name'];
+ if ( $ipn_data['memo'] ) {
+ $contribution['note'] = $ipn_data['memo'];
+ }
+
+ // The fields in the IPN change depending on the transaction type.
+ // web_accept and subscr_payment are mostly identical.
+ switch ( $ipn_data['txn_type'] ) {
+ case 'subscr_signup':
+ // If N==1, then the subscr_payment IPN already arrived, so
+ // we set these fields so that this IPN updates the
+ // existing record rather recording a new one.
+ if ( $existing_contr->N == 1 ) {
+ $contribution['id'] = $existing_contr->id;
+ $contribution['contact_id'] = $existing_contr->contact_id;
+ $contribution['contribution_recur_id'] = $existing_contr->contribution_recur_id;
+ } else {
+ // subscr_signup came in first, so mark contr. as
+ // pending and set the invoice_id temporarily (later it
+ // will be changed to be the same as the trxn_id when
+ // the subscr_payment IPN arrives)
+ $contribution['contribution_status_id'] = '2';
+ $contribution['invoice_id'] = $ipn_data['invoice'];
+ }
+
+ $contribution['total_amount'] = $ipn_data['mc_amount3'];
+ $contribution['currency'] = $ipn_data['mc_currency'];
+ $recur['amount'] = $ipn_data['mc_amount3'];
+ $recur['start_date'] = $this->oc_iso8601_to_mysql($ipn_data['subscr_date']);
+ $recur['processor_id'] = $ipn_data['subscr_id'];
+
+ // PayPal sends two distinct data elements in a single field,
+ // so we break them out here.
+ $freq_units = array (
+ 'D' => 'day',
+ 'W' => 'week',
+ 'M' => 'month',
+ 'Y' => 'year'
+ );
+ list($frequency_interval, $frequency_unit) = split(' ', $ipn_data['period3']);
+ $recur['frequency_interval'] = $frequency_interval;
+ $recur['frequency_unit'] = $freq_units[$frequency_unit];
+ $recur['installments'] = $ipn_data['recur_times'];
+ $contribution['is_recur'] = TRUE;
+
+ // This IPN only arrives at the time of the first payment,
+ // and it's only at the time of the first payment that we
+ // want to record a premium for this subscr.
+ $contribution['record_premium'] = TRUE;
+
+ break;
+ case 'subscr_payment':
+ // If N==1 and trxn_id is NULL then the subscr_signup IPN
+ // came in before this IPN and this must be the
+ // subscr_payment IPN for the very first payment, so we'll
+ // update the existing record instead of creating a new
+ // one. Similarly, if the txn_id of the IPN is equal to
+ // the trxn_id of the existing contribution, then this must
+ // be an update to an existing record, and we don't want to
+ // create a new record. This latter case can happen when
+ // someone pays with an eCheck, where we get two
+ // subscr_payment IPN, one to notify that it's pending and
+ // the next to notify that it's completed.
+ if ( ($existing_contr->N == 1 && empty($existing_contr->trxn_id)) ||
+ ($existing_contr->trxn_id == $ipn_data['txn_id'])
+ ) {
+ $contribution['id'] = $existing_contr->id;
+ $contribution['contact_id'] = $existing_contr->contact_id;
+ $contribution['contribution_recur_id'] = $existing_contr->contribution_recur_id;
+ } elseif ( $existing_contr->N >= 1 ) {
+ // This must simply be a subsequent subscr payment, in
+ // which case we want to create a new contribution
+ // record, but reuse the recur_id and contact_id
+ $contribution['contact_id'] = $existing_contr->contact_id;
+ $contribution['contribution_recur_id'] = $existing_contr->contribution_recur_id;
+ }
+
+ $contribution['is_recur'] = TRUE;
+ $recur['modified_date'] = $this->oc_iso8601_to_mysql($ipn_data['payment_date']);
+ $recur['trxn_id'] = $ipn_data['txn_id'];
+ $recur['invoice_id'] = $ipn_data['txn_id'];
+ $recur['processor_id'] = $ipn_data['subscr_id'];
+ // NOTE we do not break here, but proceed to web_accept
+ case 'web_accept':
+ $contribution['trxn_id'] = $ipn_data['txn_id'];
+ $contribution['invoice_id'] = $ipn_data['txn_id'];
+ $contribution['total_amount'] = $ipn_data['mc_gross'];
+ $contribution['fee_amount'] = $ipn_data['mc_fee'];
+ $contribution['net_amount'] = $ipn_data['mc_gross'] - $ipn_data['mc_fee'];
+ $contribution['currency'] = $ipn_data['mc_currency'];
+ $contribution['receive_date'] = $this->oc_iso8601_to_mysql($ipn_data['payment_date']);
+ if ( $ipn_data['payment_status'] == 'Completed' ) {
+ $contribution['contribution_status_id'] = '1';
+ } else {
+ $contribution['contribution_status_id'] = '2'; // Pending
+ }
+
+ // web_accept IPN are for single time donations, and all
+ // single donations need to have any premium recorded, but
+ // since the subscr_payment IPN passes into this block, we
+ // need to check for the IPN type again
+ if ( $ipn_data['txn_type'] == 'web_accept' ) {
+ $contribution['record_premium'] = TRUE;
+ }
+
+ }
+
+ // Extract the contents of any custom data
+ if ( $ipn_data['custom'] ) {
+ $contribution['custom_data'] = unserialize($ipn_data['custom']);
+ }
+
+ } elseif ( $payment_processor == 'google' ) {
+
+
+ $gc_contact = $ipn_data['buyer-shipping-address'];
+ $gc_order = $ipn_data['shopping-cart']['items']['item'];
+
+ // Extract the contents of any custom data. Google doesn't send merchant-private-data
+ // with subsequent payments in a subscription, so we have to stick it in our own field.
+ if ( isset($gc_order['merchant-private-item-data']['recurrent-custom-data']) ) {
+ $contribution['custom_data'] = unserialize($gc_order['merchant-private-item-data']['recurrent-custom-data']['VALUE']);
+ } elseif ( isset($ipn_data['shopping-cart']['merchant-private-data']) ) {
+ $contribution['custom_data'] = unserialize($ipn_data['shopping-cart']['merchant-private-data']['VALUE']);
+ }
+
+ // If the contribution already exists in the database then we can
+ // assume that we've already recorded all the relevant information
+ // for the contact and can just pass on the existing contact_ id,
+ // and we can also pass along the contribution id so that we update
+ // the existing record rather than creating a new one.
+ if ( $existing_contr ) {
+ $contribution['id'] = $existing_contr->id;
+ $contribution['contact_id'] = $existing_contr->contact_id;
+ } else {
+ // Contact name
+ if ( isset($gc_contact['structured-name']) ) {
+ $contact['first_name'] = $gc_contact['structured-name']['first-name']['VALUE'];
+ $contact['last_name'] = $gc_contact['structured-name']['last-name']['VALUE'];
+ } else {
+ $contact['full_name'] = $gc_contact['contact-name']['VALUE'];
+ }
+
+ // Email address
+ $email['email'] = $gc_contact['email']['VALUE'];
+
+ // Address information
+ $address['address_name'] = $gc_contact['contact-name']['VALUE'];
+ $address['street_address'] = $gc_contact['address1']['VALUE'];
+ if ( isset($gc_contact['address2']) ) {
+ $address['supplemental_address_1'] = $gc_contact['address2']['VALUE'];
+ }
+ $address['city'] = $gc_contact['city']['VALUE'];
+ $address['state_province'] = $gc_contact['region']['VALUE'];
+ $address['postal_code'] = $gc_contact['postal-code']['VALUE'];
+ $address['country'] = $gc_contact['country-code']['VALUE'];
+
+ }
+
+ // Contribution information
+ $contribution['source'] = $gc_order['item-name']['VALUE'];
+ $contribution['trxn_id'] = $ipn_data['google-order-number']['VALUE'];
+ $contribution['invoice_id'] = $contribution['trxn_id'];
+ $contribution['total_amount'] = $ipn_data['order-total']['VALUE'];
+ $contribution['currency'] = $ipn_data['order-total']['currency'];
+ $contribution['receive_date'] = $this->oc_iso8601_to_mysql($ipn_data['timestamp']['VALUE']);
+ // This function is only called by new-order-notifications and we
+ // have several more notifications before this order is done, so
+ // set this to Pending.
+ $contribution['contribution_status_id'] = '2';
+
+ // This is a subscription (recurring) contribution.
+ if ( isset($gc_order['subscription']) ) {
+ $recur['amount'] = $contribution['total_amount'];
+ $recur['start_date'] = $this->oc_iso8601_to_mysql($contribution['receive_date']);
+ $recur['modified_date'] = $this->oc_iso8601_to_mysql($contribution['receive_date']);
+ $contribution['is_recur'] = 1;
+ $freqUnits = array (
+ 'DAILY' => 'day',
+ 'WEEKLY' => 'week',
+ 'MONTHLY' => 'month',
+ 'YEARLY' => 'year'
+ );
+ $recur['frequency_unit'] = $freqUnits[$gc_order['subscription']['period']];
+ $recur['frequency_interval'] = 1;
+ $recur['installments'] = $gc_order['subscription']['payments']['subscription-payment']['times'];
+ // This is a number we generated and stuck in merchant-private-data.
+ $recur['processor_id'] = $contribution['custom_data']['subscription_id'];
+ $recur['trxn_id'] = $contribution['trxn_id'];
+ $recur['invoice_id'] = $contribution['invoice_id'];
+
+ // The <subscription> element is only present in the first
+ // new-order-notification of a subscription and it's only at
+ // the time of the first payment that we want to record a
+ // premium for this subscr.
+ $contribution['record_premium'] = TRUE;
+
+ } elseif ( isset($gc_order['merchant-private-item-data']['recurrent-custom-data']) ) {
+
+ // We will enter into this block if this is any other than the first
+ // payment in a subscription i.e. subsequent payments.
+ $recur['trxn_id'] = $contribution['trxn_id'];
+ $recur['invoice_id'] = $contribution['invoice_id'];
+ $recur['modified_date'] = $this->oc_iso8601_to_mysql($contribution['receive_date']);
+ $contribution['is_recur'] = 1;
+
+ // We have to use our generated subscription_id to find out which record in the
+ // contribution_recur table this belongs to.
+ if ( isset($contribution['custom_data']) ) {
+ require_once 'CRM/Contribute/BAO/ContributionRecur.php';
+ $existing_recur_obj = new CRM_Contribute_BAO_ContributionRecur;
+ $existing_recur_obj->processor_id = $contribution['custom_data']['subscription_id'];
+ if ( $existing_recur_obj->find(true) ) {
+ $recur['id'] = $existing_recur_obj->id;
+ }
+ }
+ }
+
+ }
+
+ // If this is not a recurring contribution, for which only the first
+ // payment should have a premium recorded, then we record a premium in
+ // every case
+ if ( ! $contribution['is_recur'] ) {
+ $contribution['record_premium'] = TRUE;
+ }
+
+ $contact['email'][1] = $email;
+ $contact['transaction'] = $contribution;
+ if ( ! empty($address) ) {
+ $contact['address'][1] = $address;
+ }
+
+ if ( ! empty($recur) ) {
+ $contact['recur'] = $recur;
+ }
+
+ return $contact;
+
+ }
+
+
+ /**
+ * Records a contribution based on IPN data. This should be payment processor
+ * agnostic.
+ */
+ function oc_record_contribution($params) {
+
+ if ( self::OC_DEBUG ) {
+ CRM_Core_Error::debug_log_message("OneClick: Entering oc_record_contribution()");
+ }
+
+ $transaction = $params['transaction'];
+ if ( $params['recur'] ) {
+ $recur = $params['recur'];
+ }
+
+ // If contact_id is already set then we can just fetch all the related
+ // data for the contact, else we'll need to record it all new.
+ require_once 'CRM/Contact/BAO/Contact.php';
+ if ( $transaction['contact_id'] ) {
+ $find_params = array('contact_id' => $transaction['contact_id']);
+ $contact = CRM_Contact_BAO_Contact::retrieve($find_params,
+ CRM_Core_DAO::$_nullArray,
+ CRM_Core_DAO::$_nullArray);
+ } else {
+
+ // Get the address and email fields configured. The following is
+ // largely borrowed from: CRM/Contribute/BAO/Contribution/Utils.php
+
+ $billing_loc_type_id = CRM_Core_DAO::getFieldValue( 'CRM_Core_DAO_LocationType', 'Billing', 'id', 'name' );
+ if ( ! $billing_loc_type_id ) {
+ $billing_loc_type_id = 1;
+ }
+ if ( ! CRM_Utils_System::isNull($params['address']) ) {
+ $params['address'][1]['is_primary'] = 1;
+ $params['address'][1]['location_type_id'] = $billing_loc_type_id;
+ }
+ if ( ! CRM_Utils_System::isNull($params['email']) ) {
+ $params['email'][1]['location_type_id'] = $billing_loc_type_id;
+ }
+
+ // Just assume that all donations via a payment processor are of
+ // type Individual.
+ $params['contact_type'] = 'Individual';
+
+ // Add contact using dedupe rule
+ require_once 'CRM/Dedupe/Finder.php';
+ $dedupe_params = CRM_Dedupe_Finder::formatParams ($params, 'Individual');
+ $dupe_ids = CRM_Dedupe_Finder::dupesByParams($dedupe_params, 'Individual');
+ // if we find more than one contact, use the first one
+ if ( CRM_Utils_Array::value( 0, $dupe_ids ) ) {
+ $params['contact_id'] = $dupe_ids[0];
+ }
+ $contact = CRM_Contact_BAO_Contact::create($params);
+ if ( $contact->id ) {
+ $transaction['contact_id'] = $contact->id;
+ } else {
+ return FALSE;
+ }
+
+ }
+
+ // Set to default contribution Type (Donation) unless it was already passed.
+ if ( ! isset($transaction['contribution_type_id']) ) {
+ require_once 'CRM/Contribute/BAO/ContributionType.php';
+ $params = array('name' => 'Donation');
+ $contribution_type = CRM_Contribute_BAO_ContributionType::retrieve($params, CRM_Core_DAO::$_nullArray);
+ if ( $contribution_type ) {
+ $transaction['contribution_type_id'] = $contribution_type->id;
+ } else {
+ // This is what CiviCRM does as a complete fallback method.
+ // CRM/Contribute/BAO/Contribution/Utils.php:410
+ require_once 'CRM/Contribute/PseudoConstant.php';
+ $contributionTypes = array_keys( CRM_Contribute_PseudoConstant::contributionType( ) );
+ $transaction['contribution_type_id'] = $contributionTypes[0];
+ }
+ }
+
+ // Handle a subscription payment before the contribution
+ if ( $transaction['is_recur'] && $recur ) {
+ require_once 'CRM/Contribute/BAO/ContributionRecur.php';
+ $recur_obj = new CRM_Contribute_BAO_ContributionRecur;
+ if ( $transaction['contribution_recur_id'] ) {
+ $recur_obj->id = $transaction['contribution_recur_id'];
+ }
+ $recur['contact_id'] = $contact->id;
+ $recur_obj->copyValues($recur);
+ $recur_obj->save();
+ if ( self::OC_DEBUG ) {
+ $debug_output = "RECURRING OBJECT:\n" . print_r($recur_obj, TRUE);
+ CRM_Core_Error::debug_log_message($debug_output);
+ }
+
+ $transaction['contribution_recur_id'] = $recur_obj->id;
+ }
+
+ $ids = array();
+ $ids['contribution'] = $transaction['id'] ? $transaction['id'] : '';
+ require_once 'CRM/Contribute/BAO/Contribution.php';
+ $contribution = CRM_Contribute_BAO_Contribution::create($transaction, $ids);
+
+ if ( is_a( $contribution, 'CRM_Core_Error') ) {
+ CRM_Core_Error::debug_log_message($contribution->_errors[0]['message']);
+ return FALSE;
+ }
+
+ if ( $contribution ) {
+
+ // When a contribution is added/update with create() (above), the
+ // returned contribution object may only contain the fields that were
+ // input to create(), but more often than not we're going to want every
+ // data point in the contribution, so we just look it up and re-capture
+ // it here.
+ $find_params = array('id' => $contribution->id);
+ $contribution = CRM_Contribute_BAO_Contribution::retrieve( $find_params,
+ CRM_Core_DAO::$_nullArray,
+ CRM_Core_DAO::$_nullArray);
+
+ // If we end up pinging the notification callback URLs, it's nice
+ // for us to include some contact info in the data we send. These
+ // fields are also used by some of the oc_ functions.
+ $contribution->first_name = $contact->first_name;
+ $contribution->last_name = $contact->last_name;
+ $contribution->full_name = $contact->display_name;
+ $email = array_shift($contact->email);
+ $contribution->email = $email->email;
+
+ // Here we record which payment processor was used.
+ $this->oc_set_payment_processor($transaction['payment_processor'], $contribution->id);
+
+ // The OneClick process packs a number of data elements into an array
+ // called custom_data. We store that data in a CiviCRM custom data
+ // field with the contribution for future reference. We may use it for
+ // anything, but in specific the motivating factor to store it is
+ // Google Checkout, since Google doesn't send custom data
+ // (merchant-private-data) with charge-amount-notifications and it is
+ // for this notification that we need the email donation receipt ID
+ // stored in the custom data. First Check to see if any custom data has
+ // already been stored.
+ $has_custom_data = $this->oc_store_fetch_custom_data('getValues', $contribution->id);
+ if ( isset($transaction['custom_data']) ) {
+ $custom_data = $transaction['custom_data'];
+ if ( ! $has_custom_data ) {
+ $this->oc_store_fetch_custom_data('setValues', $contribution->id, $custom_data);
+ }
+ } else {
+ $custom_data = $has_custom_data;
+ }
+
+ if ( $custom_data ) {
+ // Add a premium to the contribution
+ if ( $custom_data['premium'] && $transaction['record_premium'] ) {
+ $this->oc_set_premium( $contribution, $custom_data );
+ }
+
+ // This is a contribution for a PCP.
+ if ( array_key_exists("pcpid", $custom_data) ) {
+ $this->oc_add_soft_contribution( $contribution, $custom_data );
+ }
+
+ // The contributor wants to join some mail lists (groups)
+ if ( array_key_exists("groups", $custom_data) && $custom_data['groups'] ) {
+ $this->oc_set_groups( $contribution, $custom_data );
+ }
+
+ // Add user to public supporter lists unless they opted out.
+ if ( ! isset($custom_data['sloptout']) ) {
+ $this->oc_set_supporter_list( $contribution );
+ }
+
+ // Store the contributor's donation source
+ // This allows us to more easily track total contributions from
+ // mailings, special events, the widget, or other external source
+ if ( array_key_exists("contrib_source", $custom_data) ) {
+ $this->oc_set_contributor_source( $contribution, $custom_data );
+ }
+ } else {
+ // If no custom data exists, then the default is to add someone
+ // to supporter lists.
+ $this->oc_set_supporter_list( $contribution );
+ }
+
+ // Send the contributor a receipt, but only if the transaction is complete
+ // and we haven't already sent the user a receipt.
+ if ( $contribution->contribution_status_id == '1' && ! $contribution->receipt_date ) {
+ if ( $custom_data && $custom_data['receipt'] ) {
+ $receipt_id = $custom_data['receipt'];
+ // If this is a recurring contribution, then we should check to see
+ // if this is the last payment in the subscription. If it is, then
+ // we may send a special receipt that prompts the user to renew.
+ if ( $contribution->contribution_recur_id ) {
+ $query = "
+ SELECT installments, count(contr.id) AS payments
+ FROM civicrm_contribution_recur AS recur JOIN civicrm_contribution AS contr
+ ON recur.id = contr.contribution_recur_id
+ WHERE recur.id = '$contribution->contribution_recur_id'
+ AND recur.installments IS NOT NULL
+ AND recur.installments <> ''
+ GROUP BY recur.id;
+ ";
+ $recur_dao = CRM_Core_DAO::executeQuery( $query );
+ $recur_dao->fetch();
+
+ if ( isset($recur_dao->installments) && isset($recur_dao->payments) ) {
+ if ( $recur_dao->installments == $recur_dao->payments ) {
+ if ( isset($custom_data['final_receipt'] ) ) {
+ $receipt_id = $custom_data['final_receipt'];
+ }
+ }
+ }
+ }
+ $this->oc_send_receipt($contribution, $receipt_id);
+ }
+ }
+
+ }
+
+ return $contribution;
+
+ }
+
+}
+
+?>
diff --git a/OneClick.php b/OneClick.php
new file mode 100644
index 0000000..515d2de
--- /dev/null
+++ b/OneClick.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * This is just a wrapper/router for OneClick.class.php
+ */
+
+// Fire up CiviCRM
+require_once '../civicrm.config.php';
+require_once 'CRM/Core/Config.php';
+$config =& CRM_Core_Config::singleton();
+
+// Only allow this to run one at a time to avoid overlap of database operations
+// if IPN come in at nearly the same time. This is mostly here because of
+// PayPal recurring contributions and the fact that subscr_payment and
+// subscr_signup IPN come in at virtually the same time.
+require_once 'CRM/Core/Lock.php';
+while ( ! $lock = new CRM_Core_Lock('OneClick') ) {
+ // Sleep for 1 second, then try again.
+ sleep(1);
+}
+
+require 'OneClick.class.php';
+$oc = new OneClick($_REQUEST);
+
+// What are we doing?
+switch ( $_REQUEST['oc_action'] ) {
+ case 'donate':
+ if ( isset($_REQUEST['premium']) && ! isset($_REQUEST['size']) ) {
+ // Javascript should prevent this, but as a failsafe send them back
+ // if for some reason there is a premium set yet no shirt size.
+ $err_msg = "You must select a shirt size.";
+ $err_msg = urlencode($err_msg);
+ header("Location: {$oc->_cancel_return}?{$_SERVER['QUERY_STRING']}&error=$err_msg");
+ } else {
+ $oc->oc_donate();
+ }
+ break;
+ case 'paypalipn':
+ $oc->oc_ipn('paypal');
+ break;
+ case 'googleipn':
+ $oc->oc_ipn('google');
+ break;
+ default:
+ $err_msg = "Unknown oc_action.";
+ $err_msg = urlencode($err_msg);
+ header("Location: {$oc->_cancel_return}?{$_SERVER['QUERY_STRING']}&error=$err_msg");
+}
+
+$lock->release();
+
+?>