Drupal 6: Webform + Paypal Donations Integration

Parent Feed: 

This module provides integration between the Drupal Webform module and Paypal donation buttons. This includes the facility to receive payment status responses from Paypal and record these in the webforms as a hidden field for audit / inspection later to ascertain whether payments were successful or refused. The module should be useful for charities and NGOs as a mechanism to collect donations from their followers.

The module was developed as a commission from a well-known NGO. The NGO was already utilising a SECPAY gateway on their 'Donate Now' webform, and required a radio button in the donation process for the user to select either the SECPAY credit card / debit card direct payment, or a Paypal payment. As a consequence, this module presupposes a SECPAY gateway is being used. It should be relatively trivial for even a low experience PHP / Drupal developer to remove this constraint if necessary from my code so the only option is Paypal, or I can be contacted directly and undertake the work at a small cost.

Usage

To ensure the module works correctly, the steps identified below should be followed closely, paying particular attention when requested to do so.

Paypal Donation Button Creation

You will need a donation button from the Paypal site. It is also recommended you create a Paypal sandbox account for test purposes before you 'go live'. By doing this you can satisfy yourself everything is working fine before you commence your campaign.

The navigation around the Paypal site is inordinately complex. To create a button in the UK (may vary depending upon your territory), do:
1. Log into your Paypal account;
2. Click on "Merchant Services";
3. Click on "Website Payments Standard";
4. Click on "Create Button Now";
This will take you to the button factory. Once you have changed the button type to 'Donations' your Step 1 screen should be filled in like below. Enter reasonable text for the Organisation name/service and for the Donation ID values. This will be very useful in the future when you have a whole bunch of buttons and you are having trouble distinguishing between them all.



Paypal1

Paypal Donation Button Step 1
Step 3 of the process will look like the screen below. This is where you need to add you own site address to the two URL fields I have partially pixelated out. The first URL is the address the user will be returned to once payment from Paypal has been collected. The second URL is the 'secret' URL that Paypal will use to send the payment status information to your site.



Paypal1

Paypal Donation Button Step 3

At the end of the process you will get a link to the button in the format:
https: //www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=xxxxxxxxxxxxxx
if you are using Paypal, and a URL prefixing the word 'sandbox' before 'paypal' in the address if you are using the test Sandbox system. You should copy these into your clipboard for later.

Installation of the paypal_webform Module

I'm assuming you know how to install a Drupal module. Smile

paypal_webform.info

; $Id: $
name = Paypal Webform
description = Adds the Paypal donation colleciton method to the donations page
core = 6.x
dependencies[] = webform
php = 5.2

paypal_webform.module

<?php
// Copyright  <a href="http://www.badzilla.co.uk
//" title="www.badzilla.co.uk
//">www.badzilla.co.uk
//</a> Author: <a href="http://www.badzilla.co.uk" title="www.badzilla.co.uk">www.badzilla.co.uk</a> @badzillacouk
// All Drupal work undertaken - competitive hourly and daily rates
// 13/11/2011
// paypal_webform - Adds Paypal functionality to the single donation webform
define('PAYPAL_PRODUCTION', 0);
define('PAYPAL_SANDBOX', 1);

global

$paypal_webform;$paypal_webform = array(array('url' => 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=xxxxxxxxxxxxx',
                                                   
'listener' => 'ssl://www.paypal.com'),
                        array(
'url' => 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=xxxxxxxxxxxxx',
                                                   
'listener' => 'ssl://www.sandbox.paypal.com'),
                        );
// Change this value dependent upon whether production, sandbox, or running in test mode on badzilla
define('PAYPAL_SYSTEM', PAYPAL_PRODUCTION);/**
* Implementation of hook_form_alter().
*/
function paypal_webform_form_alter($form, &$form_state, $form_id) { // locate the webform id and override
   
if (strpos($form_id, 'webform_client_form_') === FALSE)
        if (!
$form_state['webform']['component_tree']['children'])
            return;
$is_pay = FALSE;
    foreach(
$form_state['webform']['component_tree']['children'] as $child)
        if (
$child['type'] == 'pay') {
           
$is_pay = TRUE;
            break;
        }

    if (!

$is_pay)
        return;
// check whether there is a Paypal widget on this form
   
if (!preg_match("#webform_client_form_(\d+)$#", $form_id, $match))
        return;

    if (!

db_result(db_query("SELECT COUNT(*) FROM {webform_component} WHERE nid = %d AND form_key = 'payment_method'", $match[1])))
        return;
// add theme
   
$form['submitted']['payment_method']['#theme'] = 'paypal_webform_radio'; // Knock out the title and use our own
   
$form['submitted']['payment_method']['#title'] = ''; // after build so we can see which radio button was selected
   
$form['#after_build'][] = 'paypal_webform_form_after_build'; // add paypal submission
   
array_push($form['#submit'], 'paypal_webform_submit_redirect'); //dsm($form, __FUNCTION__);}

function

paypal_webform_form_after_build($form, &$form_state) { //dsm($_SESSION); if (isset($form['#post']['submitted']['payment_method']) and $form['#post']['submitted']['payment_method'] == 'paypal') {
       
// remove the secpay submit
       
if (($ind = array_search('webform_simple_payments_submit_redirect', $form['#submit'])) !== FALSE) {
            unset(
$form['#submit'][$ind]);
        }
    } elseif (isset(
$form['#post']['submitted']['payment_method']) and $form['#post']['submitted']['payment_method'] == 'cc_dd') {
        if ((
$ind = array_search('paypal_webform_submit_redirect', $form['#submit'])) !== FALSE) {
            unset(
$form['#submit'][$ind]);
        }
    }

    return

$form;
}
/**
* Implementation of hook_menu().
*/
function paypal_webform_menu() { $items['webform/submissions/paypal'] = array(
       
'title' => 'Pay',
       
'page callback' => 'drupal_get_form',
       
'page arguments' => array('paypal_webform_submit_redirect'),
       
'access callback' => TRUE,
       
'type' => MENU_LOCAL_TASK,
    );
$items['webform/submissions/paypal-response'] = array(
       
'title' => 'Response',
       
'page callback' => 'paypal_webform_response',
       
'access callback' => TRUE,
       
'type' => MENU_LOCAL_TASK,
    );
$items['webform/submissions/paypal-listener'] = array(
       
'title' => 'Listener',
       
'page callback' => 'paypal_webform_listener',
       
'access callback' => TRUE,
       
'type' => MENU_LOCAL_TASK,
    );

    return

$items;

}

function

paypal_webform_submit_redirect($form, &$form_state) {
    global
$paypal_webform;
  
   
$_SESSION['webform_paypal_payment'] = array(
       
'nid' => $form_state['values']['details']['nid'],
       
'sid' => $form_state['values']['details']['sid'],
    );
drupal_goto($paypal_webform[PAYPAL_SYSTEM]['url']);

}

function

paypal_webform_listener() {
    global
$paypal_webform; $header = "";
   
$emailtext = ""; // Read the post from PayPal and add 'cmd'
   
$req = 'cmd=_notify-validate';
    if(
function_exists('get_magic_quotes_gpc'))
       
$get_magic_quotes_exists = true;

    foreach (

$_POST as $key => $value) {
        if(
$get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1) {
           
$value = urlencode(stripslashes($value));
        } else {
           
$value = urlencode($value);
        }
       
$req .= "&$key=$value";
    }
// Post back to PayPal to validate
   
$header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
   
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
   
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
   
$fp = fsockopen ($paypal_webform[PAYPAL_SYSTEM]['listener'], 443, $errno, $errstr, 30);

    if (!

$fp) { // HTTP ERROR
   
} else {
       
// NO HTTP ERROR
       
fputs($fp, $header . $req);
       
//while (!feof($fp)) {
       
$res = fgets($fp);
       
fclose ($fp);
       
       
// create the output record
       
$out = sprintf("Amount %s %s %s", $_POST['mc_currency'], $_POST['mc_gross'], $_POST['payment_status']); // now try and find the correct record in the database by using the email address from Paypal
        // It would kill the server to try and do this in one sql statement so it has been chunked for performance reasons
       
$found = FALSE;
       
$res = db_query("SELECT sid FROM {webform_submitted_data}
                         INNER JOIN {webform_component} ON {webform_submitted_data}.nid = {webform_component}.nid
                         INNER JOIN {webform_submissions} USING (sid)
                         WHERE data = '%s' AND form_key = 'email_address'
                         ORDER BY submitted DESC"
, $_POST['payer_email']);

        while(

$ret = db_fetch_object($res)) {
           
$res2 = db_query("SELECT cid, nid FROM {webform_component} INNER JOIN {webform_submissions} USING (nid)
                              WHERE sid = %d AND form_key = 'payment_response'"
, $ret->sid);
            while(
$ret2 = db_fetch_object($res2)) {
               
$found = TRUE;
                break
2;
            }
        }
// HAve we found a record to write?
       
if ($found == TRUE and isset($ret2)) {
           
db_query("UPDATE {webform_submitted_data} SET `data` = '%s' WHERE nid = %d AND sid = %d and cid = %d",
                     
$out, $ret2->nid, $ret->sid, $ret2->cid);
        } else {
           
watchdog("paypal_webform",
                    
"Unreconciled Paypal payment from @email with data @data",
                     array(
'@email' => $_POST['payer_email'], '@data' => $out),
                    
WATCHDOG_NOTICE);
        }

    }
}

function

paypal_webform_response() {

    if (isset(

$_SESSION['webform_paypal_payment']['nid'])
        and
$_SESSION['webform_paypal_payment']['nid']
        and isset(
$_SESSION['webform_paypal_payment']['sid'])
        and
$_SESSION['webform_paypal_payment']['sid']) { // redirect to the thank you screen
       
drupal_goto('node/' . $_SESSION['webform_paypal_payment']['nid'] . '/done', 'sid=' . $_SESSION['webform_paypal_payment']['sid']);

    } else {
       

watchdog('paypal_webform', t('Cannot reconcile response from Paypal with database entries'), NULL, WATCHDOG_ERROR);
        die;
    }

}

/**
* Implementation of hook_theme().
*/
function paypal_webform_theme() {

    return array(

'paypal_webform_radio' => array('arguments' => array('element' => NULL)));
}

function

theme_paypal_webform_radio($element) { drupal_add_css(drupal_get_path('module', 'paypal_webform') . "/css/webform-cc-paypal.css");
   
   
$output = drupal_render($element); // get the images directory
   
$image_dir = drupal_get_path('module', 'paypal_webform') . "/images/";
   
$credit_img = "<img class=\"webform-credit-card-image\" src=\"/" . $image_dir . "credit-cards.gif\" />";
   
$paypal_img = "<img class=\"webform-paypal-image\" src=\"/" . $image_dir . "paypal.gif\" />"; $output = ''; $output .= "<table class=\"payment-method-paypal-widget\"><tbody>\n";
   
$output .= "<tr>\n"; $output .= "<td>\n";
   
$output .= "<label class=\"paypal-option\" for=\"edit-submitted-payment-method-1\">\n";
   
$output .= "<input type=\"radio\" id=\"edit-submitted-payment-method-1\" name=\"submitted[payment_method]\" value=\"cc_dd\"  checked=\"checked\"  class=\"form-radio\" /> Credit / Debit cards</label>\n";
   
$output .= "</td>\n";
   
$output .= "<td class=\"payment-method-paypal-widget-image\">\n";
   
$output .= $credit_img;
   
$output .= "</td>\n";
   
$output .= "</tr>\n";
   
$output .= "<tr>\n";
   
$output .= "<td>\n";
   
$output .= "<label class=\"paypal-option\" for=\"edit-submitted-payment-method-2\"><input type=\"radio\" id=\"edit-submitted-payment-method-2\" name=\"submitted[payment_method]\" value=\"paypal\"   class=\"form-radio\" /> Paypal</label>\n";
   
$output .= "</td>\n";
   
$output .= "<td class=\"payment-method-paypal-widget-image\">\n";
   
$output .= $paypal_img;
   
$output .= "</td>\n";
   
$output .= "</tr>\n";
   
$output .= "</tbody></table>\n";

    return

$output;
}
?>

css/webform-cc-paypal.css

#edit-submitted-payment-method-1-wrapper, #edit-submitted-payment-method-2-wrapper {
    position: relative;
    vertical-align: center;
    padding-bottom: 0px;
    padding-top: 14px;
}

.payment-method-paypal-widget tbody {
    border: none;
}

table.payment-method-paypal-widget {
    width: auto;
}

td.payment-method-paypal-widget-image {
    padding-left: 30px;
}

images/credit-cards.gif
Credit Cards

images/paypal.gif
Paypal

.module file Configuration

You must now copy the button URLs you created in Paypal (and perhaps Sandbox) at the top of the paypal_webform.module code. It should be obvious where they go - they are replacing the two URLS in the $paypal_webform array. In addition, you need to set the enumerator type to either use the Paypal or the Sandbox URL by changing the

define('PAYPAL_SYSTEM', PAYPAL_PRODUCTION);


accordingly.

Webform creation

You can now make your first webform with Paypal donations available. Below is a sample webform - note the "Select payment method" selection drop down, and the "Payment Response" hidden field.




Paypal3

Sample Webform

The "Select payment method" component should be filled in as below, paying particular attention to the ringed values.




Paypal4

Select payment method
The "Payment Response" component stores the returned values from Paypal and must be as ringed below.



Paypal5

Payment Response

The Finished Webform

If everything has gone well, you should see your completed webform looking something like the image below. If the user selects a Paypal payment type, they will be whisked off to Paypal to effect the payment Smile
Select payment method
The "Payment Response" component stores the returned values from Paypal and must be as ringed below.



Paypal6

Completed Webform

Next Steps

Due to severe budget constraints, there are a number of activities that weren't completed to my satisfaction, but really aught to be.

  • As mentioned previously, there is a small dependency on there being another gateway in use (in this case SECPAY) and a radio button is added to provide a choice. It would be good not to have that requirement
  • The configuration is messy. At the moment, Paypal button URLs need to be pasted into the PHP code
  • Linked to the above concern, there is no convenient way of switching between buttons (i.e. production and sandbox)

These problem areas, as mentioned, were forced due to the lack of complete sponsorship. If you or your organisation would like to pay to have this tidied up, drop me a line Smile


Attachment Size
paypal_webform.tar.gz 10.29 KB

Author: 
RSS Tags: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web