. 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 .= '

'; 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); }