/[drupal]/contributions/modules/lm_paypal/lm_paypal.module
ViewVC logotype

Contents of /contributions/modules/lm_paypal/lm_paypal.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.27 - (show annotations) (download) (as text)
Sat Feb 24 15:34:52 2007 UTC (2 years, 9 months ago) by leemcl
Branch: MAIN
CVS Tags: DRUPAL-5--1-0, DRUPAL-4-7--1-0, HEAD
Branch point for: DRUPAL-5, DRUPAL-4-7, DRUPAL-6--1
Changes since 1.26: +8 -3 lines
File MIME type: text/x-php
Dropped the link back to my site. Stopped using the Euro symbol as it does not work everywhere
1 <?php
2
3 // $Id: lm_paypal.module,v 1.26 2006/12/20 09:05:35 leemcl Exp $
4
5 /**
6 * @file
7 *
8 * PayPal interface.
9 *
10 * Lee McLoughlin <lee@lmmrtech.com>. July 2006
11 * This is a Drupal 4.7 module to processes incoming PayPal IPN messages.
12 *
13 * This module is licensed under Gnu General Public License Version 2
14 * see the LICENSE.txt file for more details.
15 */
16
17 //TODO:
18 // change all the output blocks so they can be themed more easily
19 // Summarise money in/out - or should I just point people at PayPals pages?
20 //
21
22 define(LM_PAYPAL, 'LM_PayPal');
23
24 // Don't change these here! Use the admin interface at admin/lm_paypal
25 define(LM_PAYPAL_HOST_DEFAULT, 'www.paypal.com');
26 define(LM_PAYPAL_OBEY_TEST_IPNS_DEFAULT, 0);
27 define(LM_PAYPAL_IPNS_MAX_AGE_DEFAULT, 5 * 24); // Max hours to keep IPNS
28 define(LM_PAYPAL_JS_HIDE_EMAIL,1); // Use Javascript to hide the email in forms
29
30 // Never change these unless you really know what you are doing?
31 define(LM_PAYPAL_DEBUG_DEFAULT, FALSE);
32 define(LM_PAYPAL_VALIDATE_TIMEOUT, 30);
33
34 /**
35 * Initialize global variables
36 * Note: Originally this was a hook_init() function but a user was getting
37 * hit by this function being called before common.inc was loaded.
38 */
39 function _lm_paypal_ini() {
40 global $_lm_paypal_debug; // Is debugging enabled
41 global $_lm_paypal_welcome; // Welcome message
42 global $_lm_paypal_host; // Where to send paypal requests/verifies to
43 global $_lm_paypal_business; // what is my business email
44 global $_lm_paypal_obey_test_ipns; // Treat test ipn messages as real
45 global $_lm_paypal_ipns_max_age; // How many hours to keep old ipns for
46 global $_lm_paypal_currency_option;
47 global $_lm_paypal_period_units_option;
48 global $_lm_paypal_currency_syms;
49 global $_lm_paypal_drupal_major;
50 global $_lm_paypal_drupal_minor;
51 global $_lm_paypal_js_hide_email;
52
53 static $inited = 0;
54
55 if ($inited) {
56 return;
57 }
58 $inited = 1;
59
60 // These are used to allow the same code to work on multiple Drupal versions
61 $_lm_paypal_drupal_major = substr(VERSION,0,1);
62 $_lm_paypal_drupal_minor = substr(VERSION,2,1);
63
64 $_lm_paypal_debug = variable_get('lm_paypal_debug', LM_PAYPAL_DEBUG_DEFAULT);
65 $_lm_paypal_host = variable_get('lm_paypal_host', LM_PAYPAL_HOST_DEFAULT);
66 $_lm_paypal_business = variable_get('lm_paypal_business', '');
67 $_lm_paypal_obey_test_ipns = variable_get('lm_paypal_obey_test_ipns', LM_PAYPAL_OBEY_TESTIPNS_DEFAULT);
68 $_lm_paypal_ipns_max_age = variable_get('lm_paypal_ipns_max_age', LM_PAYPAL_IPNS_MAX_AGE_DEFAULT);
69 $_lm_paypal_js_hide_email = variable_get('lm_paypal_js_hide_email', LM_PAYPAL_JS_HIDE_EMAIL);
70
71 // Drupal 5 - t() has changed and to use a link in it use '!'
72 $c = ($_lm_paypal_drupal_major > 4 ? '!' : '%');
73
74 // If, eventually, a lot of companies help support this module the welcome
75 // message could get pretty long :)
76 // $lm = l('LMMR Tech', 'http://lmmrtech.com');
77 $lm = 'LM';
78 $_lm_paypal_welcome = '<p>'. t("Welcome to the ${c}lm PayPal modules for Drupal.", array("${c}lm" => $lm)) .'</p>';
79
80 //$_lm_paypal_welcome .= '<p><b>'. t('These modules are still undergoing development so it is strongly advised that you to test them out against the PayPal Sandbox first.') .'</b></p>';
81
82 $_lm_paypal_currency_option = array(
83 // '' => t('default currency'), Force a currency to be specified
84 'USD' => t('U.S. Dollar'),
85 'GBP' => t('Pound Sterling'),
86 'EUR' => t('Euro'),
87 'AUD' => t('Australian Dollar'),
88 'CAD' => t('Canadian Dollar'),
89 'JPY' => t('Japanese Yen'),
90 'CHF' => t('Swiss Franc'),
91 'CZK' => t('Czech Koruna'),
92 'DKK' => t('Danish Krone'),
93 'HKD' => t('Hong Kong Dollar'),
94 'HUF' => t('Hungarian Forint'),
95 'NOK' => t('Norwegian Krone'),
96 'NZD' => t('New Zealand Dollar'),
97 'PLN' => t('Polish Zloty'),
98 'SEK' => t('Swedish Krona'),
99 'SGD' => t('Singapore Dollar'),
100 );
101
102 $_lm_paypal_currency_syms = array(
103 '' => '?',
104 'AUD' => 'A$',
105 'CAD' => 'Can$',
106 //'EUR' => '&euro;', This causes problems depending on the fonts
107 // available and the version of the browser used
108 'EUR' => 'Euro',
109 'GBP' => '&#163;',
110 'JPY' => '&#165;',
111 'USD' => '$',
112 'CHF' => 'CHF', // Yes - it really is writen as CHF
113 'CZK' => 'Kc',
114 'DKK' => 'kr',
115 'HKD' => 'HK$',
116 'HUF' => 'Ft',
117 'NOK' => 'kr',
118 'NZD' => 'NZ$',
119 'PLN' => 'zl', // I cannot find the HTML character for a crossed l
120 'SEK' => 'kr',
121 'SGD' => 'S$',
122 );
123
124 $_lm_paypal_period_units_option = array(
125 //'' => t('default'),
126 'D' => t('Days'),
127 'W' => t('Weeks'),
128 'M' => t('Months'),
129 'Y' => t('Years'),
130 );
131
132 // Call all the _ini functions of all the lm_paypal modules.
133 // This is mostly to ensure that all the web_accept_register's are called.
134 foreach (module_list() as $module) {
135 if (strncmp($module, "lm_paypal", 9) == 0) {
136 $f = '_' . $module . '_ini';
137 if (function_exists($f)) {
138 $f();
139 }
140 }
141 }
142 }
143
144 /**
145 * Implementation of hook_help().
146 */
147 function lm_paypal_help($section) {
148 _lm_paypal_ini();
149 global $_lm_paypal_welcome; // Welcome message
150 global $_lm_paypal_drupal_major;
151 global $_lm_paypal_drupal_minor;
152
153 if ($_lm_paypal_drupal_major > 4) {
154 // Drupal 5
155 $c = '!'; // t() has changed and to use a link in it use '!'
156 $admin = l('LM PayPal Admin', 'admin/lm_paypal/settings');
157 $access = l('access control', 'admin/user/access');
158 }
159 else {
160 // Drupal 4
161 $c = '%';
162 $admin = l('LM PayPal Admin', 'admin/settings/lm_paypal');
163 $access = l('access control', 'admin/access');
164 }
165 $help = l('LM PayPal Help', 'admin/help/lm_paypal');
166 $ipn = url('lm_paypal/ipn', null, null, TRUE);
167
168 switch ($section) {
169 case 'admin/help#lm_paypal':
170 $output = $_lm_paypal_welcome;
171
172 $output .= '<p>'. t('If you are not already familar with PayPal please go to their <a href="http://www.paypal.com">website</a> and read up.') .'</p>';
173 $output .= '<p>' . t('If you are new to this module you need to:');
174 $output .= '<ul>';
175 $output .= '<li>' . t("Update the site specific settings via ${c}admin. Normally you only need to provide your PayPal Business/Premier Email.", array("${c}admin" => $admin)) . '</li>';
176 $output .= '<li>' . t("On PayPal login to your Business/Premier account. Under <b>Profile</b> go to <b>Instant Payment Notification Preferences</b> and enable IPN.") . '</li>';
177 $output .= '<li>' . 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: <pre>${c}ipn</pre><br>However it could be set to another url perhaps for ecommerce", array("${c}ipn" => $ipn)) . '</li>';
178 $output .= '<li>' . t('While on PayPal if you plan to handle multiple currencies then go to <b>Payment Receiving Preferences</b>. For the entry <b>Block payments sent to me in a currency I do not hold:</b> I suggest setting it either <b>Yes</b> (to block them) or <b>No, accept them and convert them to ...</b>. If set on <b>Ask Me</b> then each payment will have to be manually confirmed!') .'</li>';
179 $output .= '<li>' . t('Next configure one of the LM PayPal services such as subscriptions, donations or paid adverts') .'</li>';
180 $output .= '</ul>';
181 return $output;
182
183 // This is the brief description of the module displayed on the modules page
184 case 'admin/modules#description':
185 // New to Drupal 5 (because the page has moved)
186 case 'admin/settings/modules#description':
187 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));
188
189 // Help at the start of admin/lm_paypal
190 case 'admin/lm_paypal':
191 // Drupal 5 - admin now has its own page
192 case 'admin/lm_paypal/settings':
193 $output = $_lm_paypal_welcome;
194
195 $output .= '<p>'. t("If you are looking to configure LM PayPal please follow the instructions ${c}help.", array("${c}help" => $help)) . '</p>';
196 return $output;
197
198 //case 'admin/help#settings/lm_paypal': // causes a [more help] to appear
199 //case 'admin/help/settings/lm_paypal': // clicking [more help] gets this
200 // This appears at the start of the module settings page before the options
201 case 'admin/settings/lm_paypal':
202 $output = $_lm_paypal_welcome;
203
204 $output .= '<p>'. 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)) . '</p>';
205 return $output;
206
207 // This appears at the start of the ipns viewing page before the options
208 case 'admin/lm_paypal/ipns':
209 $output = $_lm_paypal_welcome;
210
211 $output .= '<p>'. t('These are the IPN messages received from PayPal.') . '</p>';
212 return $output;
213 }
214 }
215
216 /**
217 * Implementation of hook_perm().
218 * Return a list of the access control permissions that this module defines
219 */
220 function lm_paypal_perm() {
221 return array('administer lm_paypal');
222 }
223
224 /**
225 * Implementation of hook_menu().
226 */
227 function lm_paypal_menu($may_cache) {
228 _lm_paypal_ini();
229 global $_lm_paypal_drupal_major;
230 global $_lm_paypal_drupal_minor;
231
232 $items = array();
233
234 if ($may_cache) {
235 if ($_lm_paypal_drupal_major > 4) {
236 // New to Drupal 5 - hook_settings gone so settings is a normal page
237 $items[] = array(
238 'path' => 'admin/lm_paypal',
239 'title' => t('LM PayPal'),
240 'callback' => 'lm_paypal_overview',
241 //'callback' => 'system_settings_overview',
242 'access' => user_access('administer site configuration'),
243 'type' => MENU_NORMAL_ITEM,
244 // New to Drupal 5 - every path has a description
245 '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)'),
246 );
247
248 $items[] = array(
249 'path' => 'admin/lm_paypal/settings',
250 'title' => t('LM PayPal IPN Settings'),
251 'callback' => 'drupal_get_form',
252 'callback arguments' => array('lm_paypal_admin_settings'),
253 'access' => user_access('administer site configuration'),
254 'type' => MENU_NORMAL_ITEM,
255 'weight' => 0,
256 // New to Drupal 5 - every path has a description
257 'description' => t('PayPal IPN interface configuration.'),
258 );
259 }
260 else {
261 // Top level of LM PayPal Admin
262 $items[] = array(
263 'path' => 'admin/lm_paypal',
264 'title' => t('LM PayPal'),
265 'callback' => 'lm_paypal_ipns',
266 'access' => user_access('administer lm_paypal'),
267 );
268 }
269
270 // Display all the saved ipns
271 $items[] = array(
272 'path' => 'admin/lm_paypal/ipns',
273 'title' => t('LM PayPal Saved IPNs'),
274 'callback' => 'lm_paypal_ipns',
275 'access' => user_access('administer lm_paypal'),
276 'weight' => -1,
277 // New to Drupal 5 - every path has a description
278 'description' => t('Show details of all saved PayPal IPN\'s'),
279 );
280
281 // Display the details of a saved ipn - internal
282 $items[] = array(
283 'path' => 'admin/lm_paypal/id',
284 'title' => t('Show ID Details'),
285 'type' => MENU_CALLBACK,
286 'callback' => 'lm_paypal_id',
287 'access' => user_access('administer lm_paypal'),
288 // New to Drupal 5 - every path has a description
289 'description' => t('Show details of a single saved IPN'),
290 );
291
292 // PayPal will send IPN pages at this. PayPal will not login first so
293 // make this open access
294 $items[] = array(
295 'path' => 'lm_paypal/ipn',
296 'title' => t('LM PayPal Incoming IPN'),
297 'type' => MENU_CALLBACK,
298 'callback' => 'lm_paypal_ipn_in',
299 'access' => TRUE,
300 );
301 }
302
303 return $items;
304 }
305
306 /**
307 * Display a saved ipn.
308 *
309 * @param $id
310 * Required. The id of the ipn to display.
311 * @return
312 * The string containing the details of the ipn.
313 */
314 function lm_paypal_id($id = '') {
315 _lm_paypal_ini();
316 global $_lm_paypal_debug;
317
318 $id = check_plain($id);
319
320 if ($id == '' || !is_numeric($id) || intval($id) != $id) {
321 watchdog(LM_PAYPAL, t('Bad id passed: %id', array('%id' => $id)), WATCHDOG_WARNING);
322 return t('Huh?');
323 }
324
325 // Output the transaction as a table of fields/values (skip the empty ones)
326 $output = '<h2>' . t('Transaction %id', array('%id' => $id)) . '</h2>';
327 $header = array(t('field'), t('value'));
328 $sql = "SELECT * FROM {lm_paypal_ipns} WHERE id = %d";
329 $ipns = db_query($sql, $id);
330 $ipn = db_fetch_array($ipns);
331 foreach ($ipn as $key => $value) {
332 if ($value == '') {
333 continue;
334 }
335 if ($key == 'timestamp') {
336 $value = format_date($value);
337 }
338 else {
339 $value = check_plain($value);
340 }
341 $rows[] = array('data' => array($key,$value));
342 }
343 $output .= theme('table', $header, $rows);
344 return $output;
345 }
346
347 /**
348 * Provide the admin settings page.
349 * Note: New to Drupal 5
350 */
351 function lm_paypal_admin_settings() {
352 $form = lm_paypal_settings_form();
353 return system_settings_form($form);
354 }
355
356 /**
357 * Overview of the LM PayPal available items.
358 */
359 function lm_paypal_overview() {
360 // Return the menu from this page downwards as a block
361 return system_admin_menu_block_page();
362 }
363
364 // Ugly magic to hide lm_paypal_settings from Drupal 5.0 as it spots its
365 // existance and refuses to access the real page.
366 if (strncmp(VERSION, '4', 1) == 0) {
367 /**
368 * Implementation of hook_settings()
369 * Note: hook_settings not used in Drupal 5.
370 */
371 function lm_paypal_settings() {
372 return lm_paypal_settings_form();
373 }
374 }
375
376 /**
377 * Return the main LM PayPal settings form.
378 */
379 function lm_paypal_settings_form() {
380 _lm_paypal_ini();
381 global $_lm_paypal_debug;
382 global $_lm_paypal_host;
383 global $_lm_paypal_business;
384 global $_lm_paypal_ipns_max_age;
385 global $_lm_paypal_obey_test_ipns;
386 global $_lm_paypal_ipns_max_age;
387 global $_lm_paypal_js_hide_email;
388
389 if (!user_access('administer lm_paypal')) {
390 drupal_access_denied();
391 return;
392 }
393
394 // $site_name = variable_get('site_name', 'drupal');
395 $site_name = url('', null, null, TRUE);
396
397 // Show these in order of most likely to be changed
398 $form ['lm_paypal_business'] = array(
399 '#type' => 'textfield',
400 '#title' => t('LM PayPal Business/Premier Email'),
401 '#default_value' => $_lm_paypal_business,
402 '#maxlength' => 100,
403 '#required' => TRUE,
404 '#validate' => array('lm_paypal_is_email_shaped' => array(0)),
405 '#description' => t('The PayPal Business/Premier Email for the current website: %site_name', array('%site_name' => $site_name)),
406 );
407
408 $form ['lm_paypal_host'] = array(
409 '#type' => 'textfield',
410 '#title' => t('LM PayPal Host'),
411 '#default_value' => $_lm_paypal_host,
412 '#maxlength' => 100,
413 '#required' => TRUE,
414 '#description' => t('The host to send PayPal requests to usually www.paypal.com (when testing use www.sandbox.paypal.com)'),
415 );
416
417 $form ['lm_paypal_ipns_max_age'] = array(
418 '#type' => 'textfield',
419 '#title' => t('LM PayPal Max Age IPNS'),
420 '#default_value' => $_lm_paypal_ipns_max_age,
421 '#maxlength' => 10,
422 '#required' => TRUE,
423 '#validate' => array('lm_paypal_is_integer_between' => array(1)),
424 '#description' => t('Maximum age of an old IPN record in hours before it is deleted. Minimum is 1.'),
425 );
426
427 $form ['lm_paypal_js_hide_email'] = array(
428 '#type' => 'checkbox',
429 '#title' => t('LM PayPal Javascript Hide Email'),
430 '#default_value' => $_lm_paypal_js_hide_email,
431 '#description' => t('Use some Javascript to obscure the email address in forms.'),
432 );
433
434 $form ['lm_paypal_obey_test_ipns'] = array(
435 '#type' => 'checkbox',
436 '#title' => t('LM PayPal Obey Test IPNS'),
437 '#default_value' => $_lm_paypal_obey_test_ipns,
438 '#description' => t('Obey test IPNS, from PayPal Sandbox, as if real'),
439 );
440
441 $form ['lm_paypal_debug'] = array(
442 '#type' => 'checkbox',
443 '#title' => t('LM PayPal Debug'),
444 '#default_value' => $_lm_paypal_debug,
445 '#description' => t('Enabled verbose debugging output of LM PayPal'),
446 );
447
448 $form ['submit'] = array(
449 '#type' => 'submit',
450 '#value' => t('Update settings'),
451 );
452
453 return $form;
454 }
455
456 /**
457 * Validates a formelement to ensure it is shaped like an email
458 *
459 * @param $formelement
460 * The form element to be checked.
461 *
462 * If the element fails any of the tests form_set_error() is called.
463 */
464 function lm_paypal_is_email_shaped($formelement) {
465 $biz = $formelement['#value'];
466 $fieldname = $formelement['#name'];
467 if (strpos($biz, '@') === false) {
468 form_set_error($fieldname, t('Email address required.'));
469 }
470 }
471
472 /**
473 * Validates a formelement to ensure it is a number inside a given range.
474 *
475 * @param $formelement
476 * The form element to be checked.
477 * @param $min
478 * If present the minimum value the element is allowed to have
479 * @param $max
480 * If present the maximum value the element is allowed to have
481 *
482 * If the element fails any of the tests form_set_error() is called.
483 * Based on code by Coyote see http://drupal.org/node/36899
484 */
485 function lm_paypal_is_integer_between($formelement, $min=NULL, $max=NULL) {
486 $thevalue = $formelement['#value'];
487 $fieldname = $formelement['#name'];
488 if (is_numeric($thevalue)) {
489 $thevalue = $thevalue + 0;
490 }
491 else {
492 form_set_error($fieldname, t('Item entered must be an integer.'));
493 }
494 if (!is_int($thevalue)) {
495 form_set_error($fieldname, t('Item entered must be an integer.'));
496 }
497 else {
498 if (isset($min) && ($thevalue < $min)) {
499 form_set_error($fieldname, t('Item entered must be no smaller than:%min', array('%min' => $min)));
500 }
501 else if (isset($max) && ($thevalue > $max)) {
502 form_set_error($fieldname, t('Item entered must be no greater than:%max', array('%max' => $max)));
503 }
504 }
505 }
506
507 /**
508 * Handle an incoming IPN
509 *
510 * PayPal sends an IPN here for each transaction that takes place. The IPN
511 * is present as a form submission which this routine unravels and saves for
512 * processing.
513 */
514 function lm_paypal_ipn_in() {
515 _lm_paypal_ini();
516 global $_lm_paypal_debug;
517 global $_lm_paypal_host;
518
519 // Don't bother with these fields - but don't flag them as errors
520 $ignore_fields = array(
521 'notify_version',
522 'receipt_id',
523 'charset',
524 );
525 // These fields, if present, should be saved in the ipn log
526 $ipn_fields = array(
527 'txn_id',
528 'test_ipn',
529 'verify_sign',
530 'address_city',
531 'address_country',
532 'address_country_code',
533 'address_name',
534 'address_state',
535 'address_status',
536 'address_street',
537 'address_zip',
538 'first_name',
539 'last_name',
540 'payer_business_name',
541 'payer_email',
542 'payer_id',
543 'payer_status',
544 'residence_country',
545 'business',
546 'item_name',
547 'item_number',
548 'quantity',
549 'shipping',
550 'receiver_email',
551 'receiver_id',
552 'custom',
553 'invoice',
554 'memo',
555 'option_name1',
556 'option_name2',
557 'option_selection1',
558 'option_selection2',
559 'tax',
560 'parent_txn_id',
561 'payment_date',
562 'payment_status',
563 'payment_type',
564 'pending_reason',
565 'reason_code',
566 'mc_currency',
567 'payment_fee',
568 'payment_gross',
569 'mc_fee',
570 'mc_gross',
571 'settle_amount',
572 'settle_currency',
573 'exchange_rate',
574 'txn_type',
575 'subscr_date',
576 'subscr_effective',
577 'period1',
578 'period2',
579 'period3',
580 'amount1',
581 'amount2',
582 'amount3',
583 'mc_amount1',
584 'mc_amount2',
585 'mc_amount3',
586 'recurring',
587 'reattempt',
588 'retry_at',
589 'recur_times',
590 'subscr_id',
591 );
592
593 // Get ready to send the incoming query back to paypal to be verified
594 $req = 'cmd=_notify-validate';
595
596 // Also prepare to save this transaction
597 $sql = 'INSERT INTO {lm_paypal_ipns} SET timestamp = ' . time();
598
599 // Process the incoming form results
600 $fields = 0;
601 foreach ($_POST as $key => $value) {
602 $req .= "&$key=" . urlencode(stripslashes($value));
603 if ($value == '' || in_array($key, $ignore_fields)) {
604 continue;
605 }
606 if (in_array($key, $ipn_fields)) {
607 $fields++;
608 $sql .= ", $key = ";
609 $sql .= "'" . db_escape_string($value) . "'";
610 }
611 else {
612 watchdog(LM_PAYPAL, t('IPN unknown field ignored: %key => %value', array('%key' => check_plain($key), '%value' => check_plain($value))));
613 }
614 }
615
616 if ($fields == 0) {
617 watchdog(LM_PAYPAL, t('IPN but no fields, ignored'), WATCHDOG_WARNING);
618 return '';
619 }
620
621 // Validate this incoming IPN by sending it to PayPal to be checked
622 $ph = "POST /cgi-bin/webscr HTTP/1.0\r\n";
623 $ph .= "Content-Type: application/x-www-form-urlencoded\r\n";
624 $ph .= "Content-Length: " . strlen($req) . "\r\n\r\n";
625
626 $fp = fsockopen($_lm_paypal_host, 80, $errno, $errstr, LM_PAYPAL_VALIDATE_TIMEOUT);
627 if (!$fp) {
628 watchdog(LM_PAYPAL, t('Cannot validate with host: %host', array('%host' => check_plain($_lm_paypal_host))), WATCHDOG_ERROR);
629 // Return an HTTP error and hopefully PayPal will resend the ipn to me
630 // later on and then I can try validating again! Maybe PayPal is very busy
631 // or there is a network problem at the moment
632 drupal_set_header('HTTP/1.0 404 Not Found');
633 return '';
634 }
635 stream_set_timeout($fp, LM_PAYPAL_VALIDATE_TIMEOUT);
636
637 // Put the headers and request body
638 fputs($fp, $ph . $req);
639 // Read the response line at a time. The last line is the response.
640 while (!feof($fp)) {
641 $ret = fgets($fp, 1024);
642 }
643 fclose($fp);
644
645 $verified = (strcmp($ret, 'VERIFIED') == 0);
646
647 $insert = db_query($sql);
648 if (!$insert) {
649 watchdog(LM_PAYPAL, t('IPN in failed to run sql: %sql', array('%sql' => check_plain($sql))), WATCHDOG_ERROR);
650 // Return an HTTP error and hopefully PayPal will resend the ipn to me
651 // later on and then I can try again! Maybe PayPal is very busy
652 // or there is a network problem at the moment
653 drupal_set_header('HTTP/1.0 404 Not Found');
654 }
655 else {
656 $last = mysql_insert_id();
657 $link = l(t('view'), "admin/lm_paypal/id/$last");
658
659 if ($verified) {
660 watchdog(LM_PAYPAL, t('IPN incoming %type', array('%type' => check_plain($_POST['txn_type']))), WATCHDOG_NOTICE, $link);
661 lm_paypal_process_in($last);
662 }
663 else {
664 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);
665 }
666 }
667
668 return 'IPN: Only PayPal will ever see this page - humans go away!';
669 }
670
671 /**
672 * Process a newly arrived ipn message that has been verified and saved.
673 *
674 * @param $id
675 * The id of the saved ipn to be processed.
676 */
677 function lm_paypal_process_in($id) {
678 _lm_paypal_ini();
679 global $_lm_paypal_debug;
680 global $_lm_paypal_host;
681 global $_lm_paypal_business;
682 global $_lm_paypal_obey_test_ipns;
683
684 $sql = "SELECT * FROM {lm_paypal_ipns} WHERE id = %d";
685 $r = db_query($sql, $id);
686 $ipn = db_fetch_object($r);
687 if (! $ipn) {
688 watchdog(LM_PAYPAL, t('process_in cannot find ipn: %id', array('%id' => $id)), WATCHDOG_ERROR);
689 return;
690 }
691
692 $link = l(t('view'), "admin/lm_paypal/id/$ipn->id");
693
694 if ($ipn->test_ipn != '' && !$_lm_paypal_obey_test_ipns) {
695 watchdog(LM_PAYPAL, t('test_ipn received - ignoring'), WATCHDOG_WARNING, $link);
696 return;
697 }
698
699 if (strcasecmp(trim($ipn->receiver_email), trim($_lm_paypal_business)) != 0) {
700 watchdog(
701 LM_PAYPAL,
702 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))),
703 WATCHDOG_ERROR,
704 $link);
705 return;
706 }
707
708 // Don't check for processed txn_id's here as txn_id's are not provided
709 // for all subscr messages. Check then in the message type specific processors
710
711 // Find a processer.
712 // Its ether lm_paypal_process_in_<type> (e.g.: ..._in_subscr_payment)
713 // or if not then strip any trailing _XXX and try the remaining
714 // (e.g.: ..._in_subscr)
715 $in = 'lm_paypal_process_in_';
716 $f = $in . $ipn->txn_type;
717 if (function_exists($f)) {
718 return $f($ipn);
719 }
720 $u = strpos($ipn->txn_type, '_');
721 if ($u > 0) {
722 $f = $in . substr($ipn->txn_type, 0, $u);
723 if (function_exists($f)) {
724 return $f($ipn);
725 }
726 }
727
728 watchdog(LM_PAYPAL, t('No processor for this IPN, ignoring: %type', array('%type' => check_plain($ipn->txn_type))), WATCHDOG_WARNING, $link);
729 }
730
731 /**
732 * Process a newly arrived send_money ipn message
733 *
734 * @param $ipn
735 */
736 function lm_paypal_process_in_send_money($ipn) {
737 _lm_paypal_ini();
738 global $_lm_paypal_debug;
739
740 if ($_lm_paypal_debug) {
741 watchdog(LM_PAYPAL, 'in_send_money (passing to web_accept)');
742 }
743
744 return lm_paypal_process_in_web_accept($ipn);
745 }
746
747 /**
748 * Process a newly arrived web_accept ipn message
749 *
750 * @param $ipn
751 */
752 function lm_paypal_process_in_web_accept($ipn) {
753 _lm_paypal_ini();
754 global $_lm_paypal_debug;
755
756 if ($_lm_paypal_debug) {
757 watchdog(LM_PAYPAL, 'in_web_accept');
758 }
759
760 $link = l(t('view'), "admin/lm_paypal/id/$ipn->id");
761
762 if (lm_paypal_already_processed($ipn->txn_id)) {
763 watchdog(
764 LM_PAYPAL,
765 t('This transaction has already been processed, ignored: %id', array('%id' => check_plain($ipn->txn_id))),
766 WATCHDOG_WARNING,
767 $link);
768 return;
769 }
770
771 lm_paypal_mark_processed($ipn);
772
773 if ($ipn->payment_status == 'Pending') {
774 watchdog(
775 LM_PAYPAL,
776 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))),
777 WATCHDOG_ERROR,
778 $link);
779 return;
780 }
781
782 // The uid is in the bottom of 'custom'
783 $uid = $ipn->custom & 0xFFFF;
784
785 // Some other value may in the top
786 $other = ($ipn->custom >> 16) & 0xFFFF;
787
788 if ($uid == '') {
789 $uid = 0;
790 if ($_lm_paypal_debug) {
791 watchdog(
792 LM_PAYPAL,
793 t('No uid, try to lookup payer_email'),
794 WATCHDOG_WARNING,
795 $link);
796 }
797 $users = db_query("SELECT uid FROM {users} WHERE LOWER(mail) = LOWER('%s')", $ipn->payer_email);
798 if (db_num_rows($users) <= 0) {
799 watchdog(
800 LM_PAYPAL,
801 t('IPN web_accept no uid presuming uid 0, cannot find payer_email: %email', array('%email' => check_plain($ipn->payer_email))),
802 WATCHDOG_WARNING,
803 $link);
804 $uid = 0;
805 }
806 else {
807 $user = db_fetch_object($users);
808 $uid = $user->uid;
809 watchdog(
810 LM_PAYPAL,
811 t('IPN web_accept no uid, found payer_email %email for uid %uid', array('%email' => check_plain($ipn->payer_email), '%uid' => $uid)),
812 WATCHDOG_WARNING,
813 $link);
814 }
815 }
816 else if (!is_numeric($uid) || intval($uid) != $uid || $uid < 0) {
817 watchdog(
818 LM_PAYPAL,
819 t('Invalid uid, ignoring IPN: %uid', array('%uid' => $uid)),
820 WATCHDOG_WARNING,
821 $link);
822 return;
823 }
824
825 // If I receive a web_accept without a uid then presume it came from anon
826 if ($uid != '') {
827 // Is this uid valid?
828 $users = db_query("SELECT * FROM {users} WHERE uid = %d", $uid);
829 if (db_num_rows($users) <= 0) {
830 watchdog(
831 LM_PAYPAL,
832 t('IPN web_accept unknown uid, presuming uid 0: %uid', array('%uid' => check_plain($uid))),
833 WATCHDOG_ERROR,
834 $link);
835 $uid = 0;
836 }
837 }
838
839 // Use the item_number to select the kind of payment coming in
840 $item_number = $ipn->item_number;
841
842 // If you use the Send Money menu item on PayPal I treat this pretty the
843 // same as a donation (item_number = 0)
844 if ($ipn->txn_type == 'send_money') {
845 if ($_lm_paypal_debug) {
846 watchdog(LM_PAYPAL, "send_money - being converted to web_accept");
847 }
848 $item_number = 0;
849 }
850 else if ($item_number == '') {
851 if ($_lm_paypal_debug) {
852 watchdog(LM_PAYPAL, "empty item_number - being converted to web_accept");
853 }
854 $item_number = 0;
855 }
856 else if (!is_numeric($item_number) || intval($item_number) != $item_number || $item_number < 0) {
857 watchdog(
858 LM_PAYPAL,
859 t('Invalid item_number, ignoring IPN: %item_number', array('%item_number' => check_plain($item_number))),
860 WATCHDOG_WARNING,
861 $link);
862 return;
863 }
864
865 return lm_paypal_web_accept_invoke($ipn, $link, $uid, $other, $item_number);
866 }
867
868 function lm_paypal_web_accept_invoke($ipn, $link, $uid, $other, $item_number)
869 {
870 // Find the correct web_accept processor
871 $ranges = lm_paypal_web_accept_register();
872 foreach ($ranges as $r) {
873 $f = $r['fun'];
874 $min = $r['min'];
875 $max = $r['max'];
876 //watchdog(LM_PAYPAL,"found $f $min $max");
877 if ($min <= $item_number && $item_number <= $max) {
878 return $f($ipn, $link, $uid, $other, $item_number);
879 }
880 }
881
882 watchdog(
883 LM_PAYPAL,
884 t('No web_accept processor registered for this item_number: %item_number', array('%item_number' => check_plain($item_number))),
885 WATCHDOG_WARNING,
886 $link);
887 }
888
889 /**
890 * Register the handler function for a range of item_numbers
891 *
892 * @param $function_name
893 * The function to call when an item number in the given range arrives
894 * @param $min
895 * The minimum item_number in the range
896 * @param $max
897 * The maximum item_number in the range
898 * @return
899 * If $function_name is set then nothing is returned. If null then
900 * the entire registered array of ($fun, $min, $max) is returned.
901 */
902 function lm_paypal_web_accept_register($function_name = null, $min = null, $max = null)
903 {
904 static $ranges = null;
905
906 if (is_null($function_name)) {
907 return $ranges;
908 }
909
910 if (is_null($ranges)) {
911 $ranges = array();
912 }
913 $ranges[] = array('fun' => $function_name, 'min' => $min, 'max' => $max);
914 }
915
916 /**
917 * Mark a saved ipn as processed.
918 *
919 * @param $ipn
920 * The ipn to be marked.
921 */
922 function lm_paypal_mark_processed($ipn) {
923 $sql = "UPDATE {lm_paypal_ipns} SET processed = 1 WHERE id = %d";
924 $update = db_query($sql, $ipn->id);
925 // TODO: Check for error
926 }
927
928 function lm_paypal_already_processed($txn_id) {
929 // Has this transaction already been processed?
930 // Changed to allow for echecks which can be payment_status = 'Pending' for
931 // quite a while
932 // OLD:
933 //$sql = "SELECT * FROM {lm_paypal_ipns} WHERE txn_id = '%s' and processed = '1'";
934 $sql = "SELECT * FROM {lm_paypal_ipns} WHERE txn_id = '%s' and processed = '1' and payment_status = 'Completed'";
935 $r = db_query($sql, $txn_id);
936 return (db_num_rows($r) > 0);
937 }
938
939 /**
940 * Finds the option value corresponding to a period unit
941 *
942 * @param $unit
943 * A period unit such 'D' or 'W'
944 * @return
945 * The string representation of the unit such as 'Day' or 'Week'
946 */
947 function lm_paypal_unit2str($unit) {
948 _lm_paypal_ini();
949 global $_lm_paypal_period_units_option;
950
951 return $_lm_paypal_period_units_option [$unit];
952 }
953
954 /**
955 * Finds the currency symbol corresponding to a three letter code
956 *
957 * @param $ccc
958 * A three letter currency code such as USD
959 * @return
960 * A currency symbol such as $
961 */
962 function lm_paypal_ccc2symbol($ccc) {
963 _lm_paypal_ini();
964 global $_lm_paypal_currency_syms;
965
966 return $_lm_paypal_currency_syms [$ccc];
967 }
968
969 /**
970 * Returns the number of days given a period and unit
971 *
972 * @param $period
973 * An integer period
974 * @param $unit
975 * A time unit such as 'D', 'W' ...
976 * @return
977 * The equivalent number of days
978 */
979 function lm_paypal_period_unit2days($period, $unit) {
980 $multiply = 1;
981 switch ($unit) {
982 case 'D':
983 $multiply = 1;
984 break;
985 case 'W':
986 $multiply = 7;
987 break;
988 case 'M':
989 $multiply = 31;
990 break;
991 case 'Y':
992 $multiply = 365;
993 break;
994 }
995
996 return $period * $multiply;
997 }
998
999 /**
1000 * Finds the option value corresponding to a three letter currency
1001 *
1002 * @param $ccc
1003 * A PayPal three letter currency code (eg: USD)
1004 * @return
1005 * The string representation the currency (eg: U.S. Dollar)
1006 */
1007 function lm_paypal_ccc2currency($ccc) {
1008 _lm_paypal_ini();
1009 global $_lm_paypal_currency_option;
1010
1011 return $_lm_paypal_currency_option [$ccc];
1012 }
1013
1014 /**
1015 * Generates a human readable string from a number and a 3 letter currency code
1016 *
1017 * @param $n
1018 * A numeric amount
1019 * @param $ccc
1020 * A PayPal three letter currency code (eg: USD)
1021 * @return
1022 * The string representation the amount in that currency (eg: $5)
1023 */
1024 function lm_paypal_nccc2str($n, $ccc) {
1025 $sym = lm_paypal_ccc2symbol($ccc);
1026 if ($sym != '' && $sym != '?') {
1027 $str = $sym . $n;
1028 }
1029 else {
1030 $cur = lm_paypal_ccc2currency($ccc);
1031 $str = $n . ' ' . $cur;
1032 }
1033 return $str;
1034 }
1035
1036 /**
1037 * Validation callback; implements dependency checking
1038 *
1039 * See http://drupal.org/node/54463 for more info.
1040 */
1041 function lm_paypal_system_module_validate(&$form,$module,$dependencies) {
1042 foreach ($dependencies as $dependency) {
1043 if (!in_array($dependency, $form['status']['#default_value'])) {
1044 $missing_dependency = TRUE;
1045 $missing_dependency_list[] = $dependency;
1046 }
1047 }
1048
1049 if (in_array($module, $form['status']['#default_value']) && isset($missing_dependency)) {
1050 db_query("UPDATE {system} SET status = 0 WHERE type = 'module' AND name = '%s'", $module);
1051 $key = array_search($module, $form['status']['#default_value']);
1052 unset($form['status']['#default_value'][$key]);
1053 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');
1054 }
1055 }
1056
1057 function lm_paypal_ipns_filter ()
1058 {
1059 $names = array (
1060 'all' => t('all messages'),
1061 'web_accept' => t('donation/sent money (web_accept)'),
1062 'subscr_%' => t('all subscription IPNs'),
1063 'subscr_signup' => t('subscription signup'),
1064 'subscr_payment' => t('subscription payment'),
1065 'subscr_cancel' => t('subscription cancel'),
1066 'subscr_eot' => t('subscription eot'),
1067 );
1068
1069 if (empty($_SESSION['lm_paypal_ipns_filter'])) {
1070 $_SESSION['lm_paypal_ipns_filter'] = 'all';
1071 }
1072
1073 // Under Drupal 5 this form has the following base name and all the
1074 // processing is done by calling #base_validate, ...
1075 $form['#base'] = 'lm_paypal_ipns';
1076
1077 $form['filter'] = array(
1078 '#type' => 'select',
1079 '#title' => t('Filter IPN type'),
1080 '#options' => $names,
1081 '#default_value' => $_SESSION['lm_paypal_ipns_filter'],
1082 );
1083 $form['#action'] = url('admin/lm_paypal/ipns');
1084
1085 $form['submit'] = array('#type' => 'submit', '#value' =>t('Filter'));
1086
1087 return $form;
1088 }
1089
1090 /**
1091 * View all saved ipns
1092 *
1093 * Mostly borrowed from watchdog.module.
1094 */
1095 function lm_paypal_ipns() {
1096 _lm_paypal_ini();
1097 global $_lm_paypal_debug;
1098 global $_lm_paypal_drupal_major;
1099
1100 $ipns_per_page = 50;
1101
1102 if ($_lm_paypal_drupal_major > 4) {
1103 // New to Drupal 5 - pass the form creator function as the first param
1104 $output = drupal_get_form('lm_paypal_ipns_filter');
1105 }
1106 else {
1107 $form = lm_paypal_ipns_filter();
1108 $output = drupal_get_form('lm_paypal_ipns', $form);
1109 }
1110
1111 $header = array(
1112 array('data' => t('Id'), 'field' => 'id'),
1113 array('data' => t('Date'), 'field' => 'timestamp', 'sort' => 'desc'),
1114 array('data' => t('Txn Type'), 'field' => 'txn_type'),
1115 array('data' => t('User'), 'field' => 'custom'),
1116 );
1117
1118 $sql = "SELECT id, timestamp, txn_type, custom FROM {lm_paypal_ipns}";
1119 $tablesort = tablesort_sql($header);
1120
1121 // If not sorting by timestamp then make that the 2nd field to sort on
1122 if (strpos($tablesort,'timestamp') === FALSE) {
1123 $tablesort .= ', timestamp DESC';
1124 }
1125
1126 $type = $_SESSION['lm_paypal_ipns_filter'];
1127 if ($type != 'all') {
1128 if (strpos($type,'%') === FALSE) {
1129 $result = pager_query($sql ." WHERE txn_type = '%s'". $tablesort, $ipns_per_page, 0, NULL, $type);
1130 }
1131 else {
1132 // If type contains a '%' use like to match it
1133 $result = pager_query($sql ." WHERE txn_type like '%s'". $tablesort, $ipns_per_page, 0, NULL, $type);
1134 }
1135 }
1136 else {
1137 $result = pager_query($sql . $tablesort, $ipns_per_page);
1138 }
1139
1140 while ($ipn = db_fetch_object($result)) {
1141 $uid = $ipn->custom & 0xFFFF;
1142 $other = ($ipn->custom >> 16) & 0xFFFF;
1143 $rows[] = array('data' =>
1144 array(
1145 l($ipn->id, "admin/lm_paypal/id/$ipn->id"),
1146 format_date($ipn->timestamp, 'small'),
1147 check_plain($ipn->txn_type),
1148 $uid . ($other == '' ? '' : " ($other)"),
1149 ),
1150 );
1151 }
1152
1153 if (!$rows) {
1154 $rows[] = array(array('data' => t('No ipns found.'), 'colspan' => 3));
1155 }
1156
1157 $output .= theme('table', $header, $rows);
1158 $output .= theme('pager', NULL, $ipns_per_page, 0);
1159
1160 return $output;
1161 }
1162
1163 /**
1164 * Process the form submission for lm_paypal_ipns
1165 */
1166 function lm_paypal_ipns_submit($form_id, $form) {
1167 global $form_values;
1168 $_SESSION['lm_paypal_ipns_filter'] = $form_values['filter'];
1169 }
1170
1171 /**
1172 * Email a user
1173 *
1174 * @param $to_uid
1175 * The uid of user to send this email to
1176 * @param $about_uid
1177 * The uid of the user this email is about
1178 * @param $subject
1179 * The subject line of the email (note it will be run thru strtr() and t())
1180 * @param $message
1181 * The body of the email (note it will be run thru strtr() and t())
1182 * @param $var
1183 * An array of name,value pairs that will be added to the builtin arrary
1184 * before being expanded using strtr()
1185 *
1186 * Will email the $to_uid user an email. The subject and message will first
1187 * be expanded with all the variables being replaced by values.
1188 * In addition to any vars passed in the following are also present
1189 * %Username = about_uid's username
1190 * %Login = about_uid's login
1191 * %Site' = the local site name
1192 * %Uri' = the local url
1193 * %Uri_brief' = the local url without leading http://
1194 * %Mailto = to_uid's email address
1195 * %Date' = the date-time
1196 * (In case you are wondering why they all begin with a capital letter this
1197 * is to avoid them clashing with db_query's % handling. There is probably
1198 * a better way around this but there was nothing mentioned in the
1199 * documentation.)
1200 */
1201 function lm_paypal_mail_user($to_uid, $about_uid, $subject, $message, $vars)
1202 {
1203 _lm_paypal_ini();
1204 global $_lm_paypal_debug;
1205 global $base_url;
1206 global $_lm_paypal_drupal_major;
1207 global $_lm_paypal_drupal_minor;
1208
1209 if ($_lm_paypal_debug) {
1210 watchdog(LM_PAYPAL, "lm_paypal_mail_user($to_uid, $about_uid, $subject, $message, $vars)");
1211 }
1212
1213 $to_account = user_load(array('uid' => $to_uid, 'status' => 1));
1214 $to = $to_account->mail;
1215
1216 $about_user = user_load(array('uid' => $about_uid, 'status' => 1));
1217
1218 //TODO: Maybe use the subscription adminstrators email instead?
1219 $from = variable_get('site_mail', ini_get('sendmail_from'));
1220
1221 $variables = array(
1222 '%Username' => $about_user->name,
1223 '%Login' => $about_user->login,
1224 '%Site' => variable_get('site_name', 'drupal'),
1225 '%Uri' => $base_url,
1226 '%Uri_brief' => substr($base_url, strlen('http://')),
1227 '%Mailto' => $to,
1228 '%Date' => format_date(time()));
1229 $variables = $variables + $vars;
1230
1231 $body = strtr(t($message), $variables);
1232 $subject = strtr(t($subject), $variables);
1233
1234 if ($_lm_paypal_drupal_major > 4) {
1235 // New to Drupal 5 - drupal_mail replaces user_mail
1236 drupal_mail('lm_paypal', $to, $subject, $body, $from);
1237 </