. July 2006
* This is a Drupal 4.7 module to processes incoming PayPal IPN messages.
*
* This module is licensed under Gnu General Public License Version 2
* see the LICENSE.txt file for more details.
*/
//TODO:
// change all the output blocks so they can be themed more easily
// Summarise money in/out - or should I just point people at PayPals pages?
//
define(LM_PAYPAL, 'LM_PayPal');
// Don't change these here! Use the admin interface at admin/lm_paypal
define(LM_PAYPAL_HOST_DEFAULT, 'www.paypal.com');
define(LM_PAYPAL_OBEY_TEST_IPNS_DEFAULT, 0);
define(LM_PAYPAL_IPNS_MAX_AGE_DEFAULT, 5 * 24); // Max hours to keep IPNS
define(LM_PAYPAL_JS_HIDE_EMAIL,1); // Use Javascript to hide the email in forms
// Never change these unless you really know what you are doing?
define(LM_PAYPAL_DEBUG_DEFAULT, FALSE);
define(LM_PAYPAL_VALIDATE_TIMEOUT, 30);
/**
* Initialize global variables
* Note: Originally this was a hook_init() function but a user was getting
* hit by this function being called before common.inc was loaded.
*/
function _lm_paypal_ini() {
global $_lm_paypal_debug; // Is debugging enabled
global $_lm_paypal_welcome; // Welcome message
global $_lm_paypal_host; // Where to send paypal requests/verifies to
global $_lm_paypal_business; // what is my business email
global $_lm_paypal_obey_test_ipns; // Treat test ipn messages as real
global $_lm_paypal_ipns_max_age; // How many hours to keep old ipns for
global $_lm_paypal_currency_option;
global $_lm_paypal_period_units_option;
global $_lm_paypal_currency_syms;
global $_lm_paypal_drupal_major;
global $_lm_paypal_drupal_minor;
global $_lm_paypal_js_hide_email;
static $inited = 0;
if ($inited) {
return;
}
$inited = 1;
// These are used to allow the same code to work on multiple Drupal versions
$_lm_paypal_drupal_major = substr(VERSION,0,1);
$_lm_paypal_drupal_minor = substr(VERSION,2,1);
$_lm_paypal_debug = variable_get('lm_paypal_debug', LM_PAYPAL_DEBUG_DEFAULT);
$_lm_paypal_host = variable_get('lm_paypal_host', LM_PAYPAL_HOST_DEFAULT);
$_lm_paypal_business = variable_get('lm_paypal_business', '');
$_lm_paypal_obey_test_ipns = variable_get('lm_paypal_obey_test_ipns', LM_PAYPAL_OBEY_TESTIPNS_DEFAULT);
$_lm_paypal_ipns_max_age = variable_get('lm_paypal_ipns_max_age', LM_PAYPAL_IPNS_MAX_AGE_DEFAULT);
$_lm_paypal_js_hide_email = variable_get('lm_paypal_js_hide_email', LM_PAYPAL_JS_HIDE_EMAIL);
// Drupal 5 - t() has changed and to use a link in it use '!'
$c = ($_lm_paypal_drupal_major > 4 ? '!' : '%');
// If, eventually, a lot of companies help support this module the welcome
// message could get pretty long :)
// $lm = l('LMMR Tech', 'http://lmmrtech.com');
$lm = 'LM';
$_lm_paypal_welcome = '
'. t("Welcome to the ${c}lm PayPal modules for Drupal.", array("${c}lm" => $lm)) .'
';
//$_lm_paypal_welcome .= ''. t('These modules are still undergoing development so it is strongly advised that you to test them out against the PayPal Sandbox first.') .'
';
$_lm_paypal_currency_option = array(
// '' => t('default currency'), Force a currency to be specified
'USD' => t('U.S. Dollar'),
'GBP' => t('Pound Sterling'),
'EUR' => t('Euro'),
'AUD' => t('Australian Dollar'),
'CAD' => t('Canadian Dollar'),
'JPY' => t('Japanese Yen'),
'CHF' => t('Swiss Franc'),
'CZK' => t('Czech Koruna'),
'DKK' => t('Danish Krone'),
'HKD' => t('Hong Kong Dollar'),
'HUF' => t('Hungarian Forint'),
'NOK' => t('Norwegian Krone'),
'NZD' => t('New Zealand Dollar'),
'PLN' => t('Polish Zloty'),
'SEK' => t('Swedish Krona'),
'SGD' => t('Singapore Dollar'),
);
$_lm_paypal_currency_syms = array(
'' => '?',
'AUD' => 'A$',
'CAD' => 'Can$',
//'EUR' => '€', This causes problems depending on the fonts
// available and the version of the browser used
'EUR' => 'Euro',
'GBP' => '£',
'JPY' => '¥',
'USD' => '$',
'CHF' => 'CHF', // Yes - it really is writen as CHF
'CZK' => 'Kc',
'DKK' => 'kr',
'HKD' => 'HK$',
'HUF' => 'Ft',
'NOK' => 'kr',
'NZD' => 'NZ$',
'PLN' => 'zl', // I cannot find the HTML character for a crossed l
'SEK' => 'kr',
'SGD' => 'S$',
);
$_lm_paypal_period_units_option = array(
//'' => t('default'),
'D' => t('Days'),
'W' => t('Weeks'),
'M' => t('Months'),
'Y' => t('Years'),
);
// Call all the _ini functions of all the lm_paypal modules.
// This is mostly to ensure that all the web_accept_register's are called.
foreach (module_list() as $module) {
if (strncmp($module, "lm_paypal", 9) == 0) {
$f = '_' . $module . '_ini';
if (function_exists($f)) {
$f();
}
}
}
}
/**
* Implementation of hook_help().
*/
function lm_paypal_help($section) {
_lm_paypal_ini();
global $_lm_paypal_welcome; // Welcome message
global $_lm_paypal_drupal_major;
global $_lm_paypal_drupal_minor;
if ($_lm_paypal_drupal_major > 4) {
// Drupal 5
$c = '!'; // t() has changed and to use a link in it use '!'
$admin = l('LM PayPal Admin', 'admin/lm_paypal/settings');
$access = l('access control', 'admin/user/access');
}
else {
// Drupal 4
$c = '%';
$admin = l('LM PayPal Admin', 'admin/settings/lm_paypal');
$access = l('access control', 'admin/access');
}
$help = l('LM PayPal Help', 'admin/help/lm_paypal');
$ipn = url('lm_paypal/ipn', null, null, TRUE);
switch ($section) {
case 'admin/help#lm_paypal':
$output = $_lm_paypal_welcome;
$output .= ''. t('If you are not already familar with PayPal please go to their website and read up.') .'
';
$output .= '' . t('If you are new to this module you need to:');
$output .= '
';
$output .= '- ' . t("Update the site specific settings via ${c}admin. Normally you only need to provide your PayPal Business/Premier Email.", array("${c}admin" => $admin)) . '
';
$output .= '- ' . t("On PayPal login to your Business/Premier account. Under Profile go to Instant Payment Notification Preferences and enable IPN.") . '
';
$output .= '- ' . t("To have lm_paypal handle IPN messages that it did not generate, such as a Send Money originated from PayPal.com, also set the IPN URL to:
${c}ipn
However it could be set to another url perhaps for ecommerce", array("${c}ipn" => $ipn)) . ' ';
$output .= '- ' . t('While on PayPal if you plan to handle multiple currencies then go to Payment Receiving Preferences. For the entry Block payments sent to me in a currency I do not hold: I suggest setting it either Yes (to block them) or No, accept them and convert them to .... If set on Ask Me then each payment will have to be manually confirmed!') .'
';
$output .= '- ' . t('Next configure one of the LM PayPal services such as subscriptions, donations or paid adverts') .'
';
$output .= '
';
return $output;
// This is the brief description of the module displayed on the modules page
case 'admin/modules#description':
// New to Drupal 5 (because the page has moved)
case 'admin/settings/modules#description':
return t("Lowest level PayPal interface required by other LM PayPal modules. Once enabled go to ${c}admin and configure the site specific settings.", array("${c}admin" => $admin));
// Help at the start of admin/lm_paypal
case 'admin/lm_paypal':
// Drupal 5 - admin now has its own page
case 'admin/lm_paypal/settings':
$output = $_lm_paypal_welcome;
$output .= ''. t("If you are looking to configure LM PayPal please follow the instructions ${c}help.", array("${c}help" => $help)) . '
';
return $output;
//case 'admin/help#settings/lm_paypal': // causes a [more help] to appear
//case 'admin/help/settings/lm_paypal': // clicking [more help] gets this
// This appears at the start of the module settings page before the options
case 'admin/settings/lm_paypal':
$output = $_lm_paypal_welcome;
$output .= ''. t("If you have not done so already you will need to configure the LM PayPal modules and your PayPal business account. Please follow the instructions ${c}help.", array("${c}help" => $help)) . '
';
return $output;
// This appears at the start of the ipns viewing page before the options
case 'admin/lm_paypal/ipns':
$output = $_lm_paypal_welcome;
$output .= ''. t('These are the IPN messages received from PayPal.') . '
';
return $output;
}
}
/**
* Implementation of hook_perm().
* Return a list of the access control permissions that this module defines
*/
function lm_paypal_perm() {
return array('administer lm_paypal');
}
/**
* Implementation of hook_menu().
*/
function lm_paypal_menu($may_cache) {
_lm_paypal_ini();
global $_lm_paypal_drupal_major;
global $_lm_paypal_drupal_minor;
$items = array();
if ($may_cache) {
if ($_lm_paypal_drupal_major > 4) {
// New to Drupal 5 - hook_settings gone so settings is a normal page
$items[] = array(
'path' => 'admin/lm_paypal',
'title' => t('LM PayPal'),
'callback' => 'lm_paypal_overview',
//'callback' => 'system_settings_overview',
'access' => user_access('administer site configuration'),
'type' => MENU_NORMAL_ITEM,
// New to Drupal 5 - every path has a description
'description' => t('LM PayPal is a set of modules that interface to PayPal.com and provide subscriptions (paid role memberships), donations (tip jar), and paid node publishing (classified adverts)'),
);
$items[] = array(
'path' => 'admin/lm_paypal/settings',
'title' => t('LM PayPal IPN Settings'),
'callback' => 'drupal_get_form',
'callback arguments' => array('lm_paypal_admin_settings'),
'access' => user_access('administer site configuration'),
'type' => MENU_NORMAL_ITEM,
'weight' => 0,
// New to Drupal 5 - every path has a description
'description' => t('PayPal IPN interface configuration.'),
);
}
else {
// Top level of LM PayPal Admin
$items[] = array(
'path' => 'admin/lm_paypal',
'title' => t('LM PayPal'),
'callback' => 'lm_paypal_ipns',
'access' => user_access('administer lm_paypal'),
);
}
// Display all the saved ipns
$items[] = array(
'path' => 'admin/lm_paypal/ipns',
'title' => t('LM PayPal Saved IPNs'),
'callback' => 'lm_paypal_ipns',
'access' => user_access('administer lm_paypal'),
'weight' => -1,
// New to Drupal 5 - every path has a description
'description' => t('Show details of all saved PayPal IPN\'s'),
);
// Display the details of a saved ipn - internal
$items[] = array(
'path' => 'admin/lm_paypal/id',
'title' => t('Show ID Details'),
'type' => MENU_CALLBACK,
'callback' => 'lm_paypal_id',
'access' => user_access('administer lm_paypal'),
// New to Drupal 5 - every path has a description
'description' => t('Show details of a single saved IPN'),
);
// PayPal will send IPN pages at this. PayPal will not login first so
// make this open access
$items[] = array(
'path' => 'lm_paypal/ipn',
'title' => t('LM PayPal Incoming IPN'),
'type' => MENU_CALLBACK,
'callback' => 'lm_paypal_ipn_in',
'access' => TRUE,
);
}
return $items;
}
/**
* Display a saved ipn.
*
* @param $id
* Required. The id of the ipn to display.
* @return
* The string containing the details of the ipn.
*/
function lm_paypal_id($id = '') {
_lm_paypal_ini();
global $_lm_paypal_debug;
$id = check_plain($id);
if ($id == '' || !is_numeric($id) || intval($id) != $id) {
watchdog(LM_PAYPAL, t('Bad id passed: %id', array('%id' => $id)), WATCHDOG_WARNING);
return t('Huh?');
}
// Output the transaction as a table of fields/values (skip the empty ones)
$output = '' . t('Transaction %id', array('%id' => $id)) . '
';
$header = array(t('field'), t('value'));
$sql = "SELECT * FROM {lm_paypal_ipns} WHERE id = %d";
$ipns = db_query($sql, $id);
$ipn = db_fetch_array($ipns);
foreach ($ipn as $key => $value) {
if ($value == '') {
continue;
}
if ($key == 'timestamp') {
$value = format_date($value);
}
else {
$value = check_plain($value);
}
$rows[] = array('data' => array($key,$value));
}
$output .= theme('table', $header, $rows);
return $output;
}
/**
* Provide the admin settings page.
* Note: New to Drupal 5
*/
function lm_paypal_admin_settings() {
$form = lm_paypal_settings_form();
return system_settings_form($form);
}
/**
* Overview of the LM PayPal available items.
*/
function lm_paypal_overview() {
// Return the menu from this page downwards as a block
return system_admin_menu_block_page();
}
// Ugly magic to hide lm_paypal_settings from Drupal 5.0 as it spots its
// existance and refuses to access the real page.
if (strncmp(VERSION, '4', 1) == 0) {
/**
* Implementation of hook_settings()
* Note: hook_settings not used in Drupal 5.
*/
function lm_paypal_settings() {
return lm_paypal_settings_form();
}
}
/**
* Return the main LM PayPal settings form.
*/
function lm_paypal_settings_form() {
_lm_paypal_ini();
global $_lm_paypal_debug;
global $_lm_paypal_host;
global $_lm_paypal_business;
global $_lm_paypal_ipns_max_age;
global $_lm_paypal_obey_test_ipns;
global $_lm_paypal_ipns_max_age;
global $_lm_paypal_js_hide_email;
if (!user_access('administer lm_paypal')) {
drupal_access_denied();
return;
}
// $site_name = variable_get('site_name', 'drupal');
$site_name = url('', null, null, TRUE);
// Show these in order of most likely to be changed
$form ['lm_paypal_business'] = array(
'#type' => 'textfield',
'#title' => t('LM PayPal Business/Premier Email'),
'#default_value' => $_lm_paypal_business,
'#maxlength' => 100,
'#required' => TRUE,
'#validate' => array('lm_paypal_is_email_shaped' => array(0)),
'#description' => t('The PayPal Business/Premier Email for the current website: %site_name', array('%site_name' => $site_name)),
);
$form ['lm_paypal_host'] = array(
'#type' => 'textfield',
'#title' => t('LM PayPal Host'),
'#default_value' => $_lm_paypal_host,
'#maxlength' => 100,
'#required' => TRUE,
'#description' => t('The host to send PayPal requests to usually www.paypal.com (when testing use www.sandbox.paypal.com)'),
);
$form ['lm_paypal_ipns_max_age'] = array(
'#type' => 'textfield',
'#title' => t('LM PayPal Max Age IPNS'),
'#default_value' => $_lm_paypal_ipns_max_age,
'#maxlength' => 10,
'#required' => TRUE,
'#validate' => array('lm_paypal_is_integer_between' => array(1)),
'#description' => t('Maximum age of an old IPN record in hours before it is deleted. Minimum is 1.'),
);
$form ['lm_paypal_js_hide_email'] = array(
'#type' => 'checkbox',
'#title' => t('LM PayPal Javascript Hide Email'),
'#default_value' => $_lm_paypal_js_hide_email,
'#description' => t('Use some Javascript to obscure the email address in forms.'),
);
$form ['lm_paypal_obey_test_ipns'] = array(
'#type' => 'checkbox',
'#title' => t('LM PayPal Obey Test IPNS'),
'#default_value' => $_lm_paypal_obey_test_ipns,
'#description' => t('Obey test IPNS, from PayPal Sandbox, as if real'),
);
$form ['lm_paypal_debug'] = array(
'#type' => 'checkbox',
'#title' => t('LM PayPal Debug'),
'#default_value' => $_lm_paypal_debug,
'#description' => t('Enabled verbose debugging output of LM PayPal'),
);
$form ['submit'] = array(
'#type' => 'submit',
'#value' => t('Update settings'),
);
return $form;
}
/**
* Validates a formelement to ensure it is shaped like an email
*
* @param $formelement
* The form element to be checked.
*
* If the element fails any of the tests form_set_error() is called.
*/
function lm_paypal_is_email_shaped($formelement) {
$biz = $formelement['#value'];
$fieldname = $formelement['#name'];
if (strpos($biz, '@') === false) {
form_set_error($fieldname, t('Email address required.'));
}
}
/**
* Validates a formelement to ensure it is a number inside a given range.
*
* @param $formelement
* The form element to be checked.
* @param $min
* If present the minimum value the element is allowed to have
* @param $max
* If present the maximum value the element is allowed to have
*
* If the element fails any of the tests form_set_error() is called.
* Based on code by Coyote see http://drupal.org/node/36899
*/
function lm_paypal_is_integer_between($formelement, $min=NULL, $max=NULL) {
$thevalue = $formelement['#value'];
$fieldname = $formelement['#name'];
if (is_numeric($thevalue)) {
$thevalue = $thevalue + 0;
}
else {
form_set_error($fieldname, t('Item entered must be an integer.'));
}
if (!is_int($thevalue)) {
form_set_error($fieldname, t('Item entered must be an integer.'));
}
else {
if (isset($min) && ($thevalue < $min)) {
form_set_error($fieldname, t('Item entered must be no smaller than:%min', array('%min' => $min)));
}
else if (isset($max) && ($thevalue > $max)) {
form_set_error($fieldname, t('Item entered must be no greater than:%max', array('%max' => $max)));
}
}
}
/**
* Handle an incoming IPN
*
* PayPal sends an IPN here for each transaction that takes place. The IPN
* is present as a form submission which this routine unravels and saves for
* processing.
*/
function lm_paypal_ipn_in() {
_lm_paypal_ini();
global $_lm_paypal_debug;
global $_lm_paypal_host;
// Don't bother with these fields - but don't flag them as errors
$ignore_fields = array(
'notify_version',
'receipt_id',
'charset',
);
// These fields, if present, should be saved in the ipn log
$ipn_fields = array(
'txn_id',
'test_ipn',
'verify_sign',
'address_city',
'address_country',
'address_country_code',
'address_name',
'address_state',
'address_status',
'address_street',
'address_zip',
'first_name',
'last_name',
'payer_business_name',
'payer_email',
'payer_id',
'payer_status',
'residence_country',
'business',
'item_name',
'item_number',
'quantity',
'shipping',
'receiver_email',
'receiver_id',
'custom',
'invoice',
'memo',
'option_name1',
'option_name2',
'option_selection1',
'option_selection2',
'tax',
'parent_txn_id',
'payment_date',
'payment_status',
'payment_type',
'pending_reason',
'reason_code',
'mc_currency',
'payment_fee',
'payment_gross',
'mc_fee',
'mc_gross',
'settle_amount',
'settle_currency',
'exchange_rate',
'txn_type',
'subscr_date',
'subscr_effective',
'period1',
'period2',
'period3',
'amount1',
'amount2',
'amount3',
'mc_amount1',
'mc_amount2',
'mc_amount3',
'recurring',
'reattempt',
'retry_at',
'recur_times',
'subscr_id',
);
// Get ready to send the incoming query back to paypal to be verified
$req = 'cmd=_notify-validate';
// Also prepare to save this transaction
$sql = 'INSERT INTO {lm_paypal_ipns} SET timestamp = ' . time();
// Process the incoming form results
$fields = 0;
foreach ($_POST as $key => $value) {
$req .= "&$key=" . urlencode(stripslashes($value));
if ($value == '' || in_array($key, $ignore_fields)) {
continue;
}
if (in_array($key, $ipn_fields)) {
$fields++;
$sql .= ", $key = ";
$sql .= "'" . db_escape_string($value) . "'";
}
else {
watchdog(LM_PAYPAL, t('IPN unknown field ignored: %key => %value', array('%key' => check_plain($key), '%value' => check_plain($value))));
}
}
if ($fields == 0) {
watchdog(LM_PAYPAL, t('IPN but no fields, ignored'), WATCHDOG_WARNING);
return '';
}
// Validate this incoming IPN by sending it to PayPal to be checked
$ph = "POST /cgi-bin/webscr HTTP/1.0\r\n";
$ph .= "Content-Type: application/x-www-form-urlencoded\r\n";
$ph .= "Content-Length: " . strlen($req) . "\r\n\r\n";
$fp = fsockopen($_lm_paypal_host, 80, $errno, $errstr, LM_PAYPAL_VALIDATE_TIMEOUT);
if (!$fp) {
watchdog(LM_PAYPAL, t('Cannot validate with host: %host', array('%host' => check_plain($_lm_paypal_host))), WATCHDOG_ERROR);
// Return an HTTP error and hopefully PayPal will resend the ipn to me
// later on and then I can try validating again! Maybe PayPal is very busy
// or there is a network problem at the moment
drupal_set_header('HTTP/1.0 404 Not Found');
return '';
}
stream_set_timeout($fp, LM_PAYPAL_VALIDATE_TIMEOUT);
// Put the headers and request body
fputs($fp, $ph . $req);
// Read the response line at a time. The last line is the response.
while (!feof($fp)) {
$ret = fgets($fp, 1024);
}
fclose($fp);
$verified = (strcmp($ret, 'VERIFIED') == 0);
$insert = db_query($sql);
if (!$insert) {
watchdog(LM_PAYPAL, t('IPN in failed to run sql: %sql', array('%sql' => check_plain($sql))), WATCHDOG_ERROR);
// Return an HTTP error and hopefully PayPal will resend the ipn to me
// later on and then I can try again! Maybe PayPal is very busy
// or there is a network problem at the moment
drupal_set_header('HTTP/1.0 404 Not Found');
}
else {
$last = mysql_insert_id();
$link = l(t('view'), "admin/lm_paypal/id/$last");
if ($verified) {
watchdog(LM_PAYPAL, t('IPN incoming %type', array('%type' => check_plain($_POST['txn_type']))), WATCHDOG_NOTICE, $link);
lm_paypal_process_in($last);
}
else {
watchdog(LM_PAYPAL, t('IPN incoming NOT VERIFIED %type got %ret', array('%type' => check_plain($_POST['txn_type']), '%ret' => check_plain($ret))), WATCHDOG_ERROR, $link);
}
}
return 'IPN: Only PayPal will ever see this page - humans go away!';
}
/**
* Process a newly arrived ipn message that has been verified and saved.
*
* @param $id
* The id of the saved ipn to be processed.
*/
function lm_paypal_process_in($id) {
_lm_paypal_ini();
global $_lm_paypal_debug;
global $_lm_paypal_host;
global $_lm_paypal_business;
global $_lm_paypal_obey_test_ipns;
$sql = "SELECT * FROM {lm_paypal_ipns} WHERE id = %d";
$r = db_query($sql, $id);
$ipn = db_fetch_object($r);
if (! $ipn) {
watchdog(LM_PAYPAL, t('process_in cannot find ipn: %id', array('%id' => $id)), WATCHDOG_ERROR);
return;
}
$link = l(t('view'), "admin/lm_paypal/id/$ipn->id");
if ($ipn->test_ipn != '' && !$_lm_paypal_obey_test_ipns) {
watchdog(LM_PAYPAL, t('test_ipn received - ignoring'), WATCHDOG_WARNING, $link);
return;
}
if (strcasecmp(trim($ipn->receiver_email), trim($_lm_paypal_business)) != 0) {
watchdog(
LM_PAYPAL,
t('Incoming IPN received email does not match business email (received %received, business %business)', array('%received' => check_plain($ipn->receiver_email), '%business' => check_plain($_lm_paypal_business))),
WATCHDOG_ERROR,
$link);
return;
}
// Don't check for processed txn_id's here as txn_id's are not provided
// for all subscr messages. Check then in the message type specific processors
// Find a processer.
// Its ether lm_paypal_process_in_ (e.g.: ..._in_subscr_payment)
// or if not then strip any trailing _XXX and try the remaining
// (e.g.: ..._in_subscr)
$in = 'lm_paypal_process_in_';
$f = $in . $ipn->txn_type;
if (function_exists($f)) {
return $f($ipn);
}
$u = strpos($ipn->txn_type, '_');
if ($u > 0) {
$f = $in . substr($ipn->txn_type, 0, $u);
if (function_exists($f)) {
return $f($ipn);
}
}
watchdog(LM_PAYPAL, t('No processor for this IPN, ignoring: %type', array('%type' => check_plain($ipn->txn_type))), WATCHDOG_WARNING, $link);
}
/**
* Process a newly arrived send_money ipn message
*
* @param $ipn
*/
function lm_paypal_process_in_send_money($ipn) {
_lm_paypal_ini();
global $_lm_paypal_debug;
if ($_lm_paypal_debug) {
watchdog(LM_PAYPAL, 'in_send_money (passing to web_accept)');
}
return lm_paypal_process_in_web_accept($ipn);
}
/**
* Process a newly arrived web_accept ipn message
*
* @param $ipn
*/
function lm_paypal_process_in_web_accept($ipn) {
_lm_paypal_ini();
global $_lm_paypal_debug;
if ($_lm_paypal_debug) {
watchdog(LM_PAYPAL, 'in_web_accept');
}
$link = l(t('view'), "admin/lm_paypal/id/$ipn->id");
if (lm_paypal_already_processed($ipn->txn_id)) {
watchdog(
LM_PAYPAL,
t('This transaction has already been processed, ignored: %id', array('%id' => check_plain($ipn->txn_id))),
WATCHDOG_WARNING,
$link);
return;
}
lm_paypal_mark_processed($ipn);
if ($ipn->payment_status == 'Pending') {
watchdog(
LM_PAYPAL,
t('Ignoring IPN with status: Pending. Check your PayPal account to see why it is pending. Note: pending_reason: %reason', array('%reason' => check_plain($ipn->pending_reason))),
WATCHDOG_ERROR,
$link);
return;
}
// The uid is in the bottom of 'custom'
$uid = $ipn->custom & 0xFFFF;
// Some other value may in the top
$other = ($ipn->custom >> 16) & 0xFFFF;
if ($uid == '') {
$uid = 0;
if ($_lm_paypal_debug) {
watchdog(
LM_PAYPAL,
t('No uid, try to lookup payer_email'),
WATCHDOG_WARNING,
$link);
}
$users = db_query("SELECT uid FROM {users} WHERE LOWER(mail) = LOWER('%s')", $ipn->payer_email);
if (db_num_rows($users) <= 0) {
watchdog(
LM_PAYPAL,
t('IPN web_accept no uid presuming uid 0, cannot find payer_email: %email', array('%email' => check_plain($ipn->payer_email))),
WATCHDOG_WARNING,
$link);
$uid = 0;
}
else {
$user = db_fetch_object($users);
$uid = $user->uid;
watchdog(
LM_PAYPAL,
t('IPN web_accept no uid, found payer_email %email for uid %uid', array('%email' => check_plain($ipn->payer_email), '%uid' => $uid)),
WATCHDOG_WARNING,
$link);
}
}
else if (!is_numeric($uid) || intval($uid) != $uid || $uid < 0) {
watchdog(
LM_PAYPAL,
t('Invalid uid, ignoring IPN: %uid', array('%uid' => $uid)),
WATCHDOG_WARNING,
$link);
return;
}
// If I receive a web_accept without a uid then presume it came from anon
if ($uid != '') {
// Is this uid valid?
$users = db_query("SELECT * FROM {users} WHERE uid = %d", $uid);
if (db_num_rows($users) <= 0) {
watchdog(
LM_PAYPAL,
t('IPN web_accept unknown uid, presuming uid 0: %uid', array('%uid' => check_plain($uid))),
WATCHDOG_ERROR,
$link);
$uid = 0;
}
}
// Use the item_number to select the kind of payment coming in
$item_number = $ipn->item_number;
// If you use the Send Money menu item on PayPal I treat this pretty the
// same as a donation (item_number = 0)
if ($ipn->txn_type == 'send_money') {
if ($_lm_paypal_debug) {
watchdog(LM_PAYPAL, "send_money - being converted to web_accept");
}
$item_number = 0;
}
else if ($item_number == '') {
if ($_lm_paypal_debug) {
watchdog(LM_PAYPAL, "empty item_number - being converted to web_accept");
}
$item_number = 0;
}
else if (!is_numeric($item_number) || intval($item_number) != $item_number || $item_number < 0) {
watchdog(
LM_PAYPAL,
t('Invalid item_number, ignoring IPN: %item_number', array('%item_number' => check_plain($item_number))),
WATCHDOG_WARNING,
$link);
return;
}
return lm_paypal_web_accept_invoke($ipn, $link, $uid, $other, $item_number);
}
function lm_paypal_web_accept_invoke($ipn, $link, $uid, $other, $item_number)
{
// Find the correct web_accept processor
$ranges = lm_paypal_web_accept_register();
foreach ($ranges as $r) {
$f = $r['fun'];
$min = $r['min'];
$max = $r['max'];
//watchdog(LM_PAYPAL,"found $f $min $max");
if ($min <= $item_number && $item_number <= $max) {
return $f($ipn, $link, $uid, $other, $item_number);
}
}
watchdog(
LM_PAYPAL,
t('No web_accept processor registered for this item_number: %item_number', array('%item_number' => check_plain($item_number))),
WATCHDOG_WARNING,
$link);
}
/**
* Register the handler function for a range of item_numbers
*
* @param $function_name
* The function to call when an item number in the given range arrives
* @param $min
* The minimum item_number in the range
* @param $max
* The maximum item_number in the range
* @return
* If $function_name is set then nothing is returned. If null then
* the entire registered array of ($fun, $min, $max) is returned.
*/
function lm_paypal_web_accept_register($function_name = null, $min = null, $max = null)
{
static $ranges = null;
if (is_null($function_name)) {
return $ranges;
}
if (is_null($ranges)) {
$ranges = array();
}
$ranges[] = array('fun' => $function_name, 'min' => $min, 'max' => $max);
}
/**
* Mark a saved ipn as processed.
*
* @param $ipn
* The ipn to be marked.
*/
function lm_paypal_mark_processed($ipn) {
$sql = "UPDATE {lm_paypal_ipns} SET processed = 1 WHERE id = %d";
$update = db_query($sql, $ipn->id);
// TODO: Check for error
}
function lm_paypal_already_processed($txn_id) {
// Has this transaction already been processed?
// Changed to allow for echecks which can be payment_status = 'Pending' for
// quite a while
// OLD:
//$sql = "SELECT * FROM {lm_paypal_ipns} WHERE txn_id = '%s' and processed = '1'";
$sql = "SELECT * FROM {lm_paypal_ipns} WHERE txn_id = '%s' and processed = '1' and payment_status = 'Completed'";
$r = db_query($sql, $txn_id);
return (db_num_rows($r) > 0);
}
/**
* Finds the option value corresponding to a period unit
*
* @param $unit
* A period unit such 'D' or 'W'
* @return
* The string representation of the unit such as 'Day' or 'Week'
*/
function lm_paypal_unit2str($unit) {
_lm_paypal_ini();
global $_lm_paypal_period_units_option;
return $_lm_paypal_period_units_option [$unit];
}
/**
* Finds the currency symbol corresponding to a three letter code
*
* @param $ccc
* A three letter currency code such as USD
* @return
* A currency symbol such as $
*/
function lm_paypal_ccc2symbol($ccc) {
_lm_paypal_ini();
global $_lm_paypal_currency_syms;
return $_lm_paypal_currency_syms [$ccc];
}
/**
* Returns the number of days given a period and unit
*
* @param $period
* An integer period
* @param $unit
* A time unit such as 'D', 'W' ...
* @return
* The equivalent number of days
*/
function lm_paypal_period_unit2days($period, $unit) {
$multiply = 1;
switch ($unit) {
case 'D':
$multiply = 1;
break;
case 'W':
$multiply = 7;
break;
case 'M':
$multiply = 31;
break;
case 'Y':
$multiply = 365;
break;
}
return $period * $multiply;
}
/**
* Finds the option value corresponding to a three letter currency
*
* @param $ccc
* A PayPal three letter currency code (eg: USD)
* @return
* The string representation the currency (eg: U.S. Dollar)
*/
function lm_paypal_ccc2currency($ccc) {
_lm_paypal_ini();
global $_lm_paypal_currency_option;
return $_lm_paypal_currency_option [$ccc];
}
/**
* Generates a human readable string from a number and a 3 letter currency code
*
* @param $n
* A numeric amount
* @param $ccc
* A PayPal three letter currency code (eg: USD)
* @return
* The string representation the amount in that currency (eg: $5)
*/
function lm_paypal_nccc2str($n, $ccc) {
$sym = lm_paypal_ccc2symbol($ccc);
if ($sym != '' && $sym != '?') {
$str = $sym . $n;
}
else {
$cur = lm_paypal_ccc2currency($ccc);
$str = $n . ' ' . $cur;
}
return $str;
}
/**
* Validation callback; implements dependency checking
*
* See http://drupal.org/node/54463 for more info.
*/
function lm_paypal_system_module_validate(&$form,$module,$dependencies) {
foreach ($dependencies as $dependency) {
if (!in_array($dependency, $form['status']['#default_value'])) {
$missing_dependency = TRUE;
$missing_dependency_list[] = $dependency;
}
}
if (in_array($module, $form['status']['#default_value']) && isset($missing_dependency)) {
db_query("UPDATE {system} SET status = 0 WHERE type = 'module' AND name = '%s'", $module);
$key = array_search($module, $form['status']['#default_value']);
unset($form['status']['#default_value'][$key]);
drupal_set_message(t('The module %module was deactivated--it requires the following disabled/non-existant modules to function properly: %dependencies', array('%module' => $module, '%dependencies' => implode(', ', $missing_dependency_list))), 'error');
}
}
function lm_paypal_ipns_filter ()
{
$names = array (
'all' => t('all messages'),
'web_accept' => t('donation/sent money (web_accept)'),
'subscr_%' => t('all subscription IPNs'),
'subscr_signup' => t('subscription signup'),
'subscr_payment' => t('subscription payment'),
'subscr_cancel' => t('subscription cancel'),
'subscr_eot' => t('subscription eot'),
);
if (empty($_SESSION['lm_paypal_ipns_filter'])) {
$_SESSION['lm_paypal_ipns_filter'] = 'all';
}
// Under Drupal 5 this form has the following base name and all the
// processing is done by calling #base_validate, ...
$form['#base'] = 'lm_paypal_ipns';
$form['filter'] = array(
'#type' => 'select',
'#title' => t('Filter IPN type'),
'#options' => $names,
'#default_value' => $_SESSION['lm_paypal_ipns_filter'],
);
$form['#action'] = url('admin/lm_paypal/ipns');
$form['submit'] = array('#type' => 'submit', '#value' =>t('Filter'));
return $form;
}
/**
* View all saved ipns
*
* Mostly borrowed from watchdog.module.
*/
function lm_paypal_ipns() {
_lm_paypal_ini();
global $_lm_paypal_debug;
global $_lm_paypal_drupal_major;
$ipns_per_page = 50;
if ($_lm_paypal_drupal_major > 4) {
// New to Drupal 5 - pass the form creator function as the first param
$output = drupal_get_form('lm_paypal_ipns_filter');
}
else {
$form = lm_paypal_ipns_filter();
$output = drupal_get_form('lm_paypal_ipns', $form);
}
$header = array(
array('data' => t('Id'), 'field' => 'id'),
array('data' => t('Date'), 'field' => 'timestamp', 'sort' => 'desc'),
array('data' => t('Txn Type'), 'field' => 'txn_type'),
array('data' => t('User'), 'field' => 'custom'),
);
$sql = "SELECT id, timestamp, txn_type, custom FROM {lm_paypal_ipns}";
$tablesort = tablesort_sql($header);
// If not sorting by timestamp then make that the 2nd field to sort on
if (strpos($tablesort,'timestamp') === FALSE) {
$tablesort .= ', timestamp DESC';
}
$type = $_SESSION['lm_paypal_ipns_filter'];
if ($type != 'all') {
if (strpos($type,'%') === FALSE) {
$result = pager_query($sql ." WHERE txn_type = '%s'". $tablesort, $ipns_per_page, 0, NULL, $type);
}
else {
// If type contains a '%' use like to match it
$result = pager_query($sql ." WHERE txn_type like '%s'". $tablesort, $ipns_per_page, 0, NULL, $type);
}
}
else {
$result = pager_query($sql . $tablesort, $ipns_per_page);
}
while ($ipn = db_fetch_object($result)) {
$uid = $ipn->custom & 0xFFFF;
$other = ($ipn->custom >> 16) & 0xFFFF;
$rows[] = array('data' =>
array(
l($ipn->id, "admin/lm_paypal/id/$ipn->id"),
format_date($ipn->timestamp, 'small'),
check_plain($ipn->txn_type),
$uid . ($other == '' ? '' : " ($other)"),
),
);
}
if (!$rows) {
$rows[] = array(array('data' => t('No ipns found.'), 'colspan' => 3));
}
$output .= theme('table', $header, $rows);
$output .= theme('pager', NULL, $ipns_per_page, 0);
return $output;
}
/**
* Process the form submission for lm_paypal_ipns
*/
function lm_paypal_ipns_submit($form_id, $form) {
global $form_values;
$_SESSION['lm_paypal_ipns_filter'] = $form_values['filter'];
}
/**
* Email a user
*
* @param $to_uid
* The uid of user to send this email to
* @param $about_uid
* The uid of the user this email is about
* @param $subject
* The subject line of the email (note it will be run thru strtr() and t())
* @param $message
* The body of the email (note it will be run thru strtr() and t())
* @param $var
* An array of name,value pairs that will be added to the builtin arrary
* before being expanded using strtr()
*
* Will email the $to_uid user an email. The subject and message will first
* be expanded with all the variables being replaced by values.
* In addition to any vars passed in the following are also present
* %Username = about_uid's username
* %Login = about_uid's login
* %Site' = the local site name
* %Uri' = the local url
* %Uri_brief' = the local url without leading http://
* %Mailto = to_uid's email address
* %Date' = the date-time
* (In case you are wondering why they all begin with a capital letter this
* is to avoid them clashing with db_query's % handling. There is probably
* a better way around this but there was nothing mentioned in the
* documentation.)
*/
function lm_paypal_mail_user($to_uid, $about_uid, $subject, $message, $vars)
{
_lm_paypal_ini();
global $_lm_paypal_debug;
global $base_url;
global $_lm_paypal_drupal_major;
global $_lm_paypal_drupal_minor;
if ($_lm_paypal_debug) {
watchdog(LM_PAYPAL, "lm_paypal_mail_user($to_uid, $about_uid, $subject, $message, $vars)");
}
$to_account = user_load(array('uid' => $to_uid, 'status' => 1));
$to = $to_account->mail;
$about_user = user_load(array('uid' => $about_uid, 'status' => 1));
//TODO: Maybe use the subscription adminstrators email instead?
$from = variable_get('site_mail', ini_get('sendmail_from'));
$variables = array(
'%Username' => $about_user->name,
'%Login' => $about_user->login,
'%Site' => variable_get('site_name', 'drupal'),
'%Uri' => $base_url,
'%Uri_brief' => substr($base_url, strlen('http://')),
'%Mailto' => $to,
'%Date' => format_date(time()));
$variables = $variables + $vars;
$body = strtr(t($message), $variables);
$subject = strtr(t($subject), $variables);
if ($_lm_paypal_drupal_major > 4) {
// New to Drupal 5 - drupal_mail replaces user_mail
drupal_mail('lm_paypal', $to, $subject, $body, $from);
}
else {
$headers = "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from";
user_mail($to, $subject, $body, $headers);
}
}
function lm_paypal_add_js() {
static $js_add = true;
if ($js_add) {
drupal_add_js(drupal_get_path('module', 'lm_paypal') . '/lm_paypal.js');
$js_add = false;
}
}
/**
* Implementation of hook_cron().
*/
function lm_paypal_cron() {
_lm_paypal_ini();
global $_lm_paypal_debug;
global $_lm_paypal_ipns_max_age;
if ($_lm_paypal_debug) {
watchdog(LM_PAYPAL, 'cron');
}
$max_age = time() - ($_lm_paypal_ipns_max_age * 3600);
$sql = "DELETE FROM {lm_paypal_ipns} WHERE timestamp < %d";
db_query($sql, $max_age);
}