JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
first stab at paypal_ipn framework
authorJason Woofenden <jason@jasonwoof.com>
Mon, 28 Dec 2015 20:54:59 +0000 (15:54 -0500)
committerJason Woofenden <jason@jasonwoof.com>
Mon, 28 Dec 2015 20:54:59 +0000 (15:54 -0500)
.htaccess
config.php
inc/misc.php
inc/wfpl
paypal_ipn.php [new file with mode: 0644]
paypal_ipn.sql [new file with mode: 0644]

index e2884e3..03351d2 100644 (file)
--- a/.htaccess
+++ b/.htaccess
@@ -23,12 +23,16 @@ php_flag engine off
 RemoveHandler .cgi .php .php3 .php4 .php5 .phtml .pl .py .pyc .pyo
 # code execution exception: allow only /wfpl_main.php
 # <Files> matches regardless of directory/path, so rewrite php in subdirs
-RewriteRule ^(wfpl_main\.php|cms_images_autoresize\.php)$  - [L]
+RewriteRule ^(wfpl_main\.php|paypal_ipn\.php|cms_images_autoresize\.php)$  - [L]
 RewriteRule .*\.php$ - [L,R=404]
 <Files "wfpl_main.php">
        php_flag engine on
        SetHandler application/x-httpd-php
 </Files>
+<Files "paypal_ipn.php">
+       php_flag engine on
+       SetHandler application/x-httpd-php
+</Files>
 <Files "cms_images_autoresize.php">
        php_flag engine on
        SetHandler application/x-httpd-php
index da021ef..3e0f8fd 100644 (file)
@@ -20,12 +20,12 @@ $GLOBALS['wfpl_image_widths'] = array(
 );
 
 # Enable features, auto-includes
-require_once(DOCROOT . 'inc/wfpl/format.php');
-require_once(DOCROOT . 'inc/wfpl/db.php');
-require_once(DOCROOT . 'inc/wfpl/session_messages.php');
-require_once(DOCROOT . 'inc/session_auth.php');
-require_once(DOCROOT . 'inc/cms.php');
-require_once(DOCROOT . 'inc/misc.php');
+require_once(__DIR__.'/'.'inc/wfpl/format.php');
+require_once(__DIR__.'/'.'inc/wfpl/db.php');
+require_once(__DIR__.'/'.'inc/wfpl/session_messages.php');
+require_once(__DIR__.'/'.'inc/session_auth.php');
+require_once(__DIR__.'/'.'inc/cms.php');
+require_once(__DIR__.'/'.'inc/misc.php');
 
 # Connect to the database
 db_connect(WFPL_DB, WFPL_DB_USER, WFPL_DB_PASS);
@@ -36,15 +36,23 @@ if (isset($_SERVER['HTTP_X_UPGRADE_DB_NOW'])) {
        db_upgrade();
 }
 
+# paypal_ipn.php calls these when it receives a valid payment
+$GLOBALS['payment_handlers'] = [
+       # the key (below) must be the first word in the paypal variable "custom"
+       # the file will be run with wfpl's file_run()
+       # example:
+       #'membership' => DOCROOT . 'inc/payment_membership.php'
+];
+
 $GLOBALS['email_templates'] = [
        'backend_bug' => [
                'title' => "Notification for site programmer(s)",
                'description' => "This email template is used if/when the back-end code of this site encounters an unusual/suspicious situation that it's not sure how to cope with.",
                'variables' => [
-                       ['message', "details about the unusual/suspicious situation"]
+                       ['details', "details about the unusual/suspicious situation"]
                ],
                'subject' => "backend alert",
-               'content' => "Hi developer,\n\nPlease investigate the following debugging message from the site:\n\n~message~"
+               'content' => "Hi developer,\n\nPlease investigate the following debugging message from the site:\n\n~details~",
                'from_addr' => 'noreply@example.com',
                'to_addr' => 'fixme@example.com' # not all templates need this field
        ]
index aed971d..0f3d23d 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-require_once(DOCROOT . 'inc/wfpl/email.php');
+require_once(__DIR__.'/'.'wfpl/email.php');
 
 # call this when you have class="unix_time" or class="unix_date"
 function render_timestamps() {
index f58acc2..33b391f 160000 (submodule)
--- a/inc/wfpl
+++ b/inc/wfpl
@@ -1 +1 @@
-Subproject commit f58acc2fd5e5fbfb4a01d6b66711b2f62bf77c4a
+Subproject commit 33b391fa326b3a118fcfb004807ab02432a68992
diff --git a/paypal_ipn.php b/paypal_ipn.php
new file mode 100644 (file)
index 0000000..168d46d
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+
+# this script does not use wfpl_main, so:
+require_once(__DIR__.'/'.'config.php');
+
+require_once(__DIR__.'/'.'inc/wfpl/template.php');
+require_once(__DIR__.'/'.'inc/wfpl/format.php');
+
+function paypal_ipn_main() {
+       // read the post from PayPal system and add 'cmd'
+       $req = 'cmd=_notify-validate';
+       $log = 'Received IPN:';
+
+       foreach ($_POST as $key => $value) {
+               $log .= "\n$key: $value";
+               $value = urlencode($value);
+               $req .= "&$key=$value";
+       }
+
+       // assign posted variables to local variables
+       $item_name      = isset($_POST['item_name'])      ? $_POST['item_name']      : '';
+       $item_number    = isset($_POST['item_number'])    ? $_POST['item_number']    : '';
+       $payment_status = isset($_POST['payment_status']) ? $_POST['payment_status'] : '';
+       $mc_gross       = isset($_POST['mc_gross'])       ? $_POST['mc_gross']       : '';
+       $mc_currency    = isset($_POST['mc_currency'])    ? $_POST['mc_currency']    : '';
+       $txn_id         = isset($_POST['txn_id'])         ? $_POST['txn_id']         : '';
+       $receiver_email = isset($_POST['receiver_email']) ? $_POST['receiver_email'] : '';
+       $payer_email    = isset($_POST['payer_email'])    ? $_POST['payer_email']    : '';
+       $custom         = isset($_POST['custom'])         ? $_POST['custom']         : '';
+       $txn_type       = isset($_POST['txn_type'])       ? $_POST['txn_type']       : '';
+       $subscr_id      = isset($_POST['subscr_id'])      ? $_POST['subscr_id']      : '';
+       $needs_review = 1;
+
+       $status = 'unknown';
+
+       $ch = curl_init($GLOBALS['paypal_site'] . '/cgi-bin/webscr');
+       if ($ch == false) {
+               $status = 'curl_init failed';
+       } else {
+               curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+               curl_setopt($ch, CURLOPT_POST, 1);
+               curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
+               curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
+               curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
+               curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
+               curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
+               curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
+               curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
+               $res = curl_exec($ch);
+               $curl_errno = curl_errno($ch);
+               curl_close($ch);
+               if ($curl_errno != 0) {
+                       $status = 'curl fail: ' . $curl_errno;
+               } else {
+                       // Split response headers and payload, a better way for strcmp
+                       $tokens = explode("\r\n\r\n", trim($res));
+                       $res = trim(end($tokens));
+                       $res_word = trim($tokens[count($tokens) - 1]);
+                       if ($res_word === 'VERIFIED') {
+                               $status = 'verified';
+                       } elseif ($res_word === 'INVALID') {
+                               $status = 'invalid';
+                       } else {
+                               $log .= "\n\nCan't figure out PayPal verify reply:\n" . $res;
+                       }
+               }
+       }
+
+       $row = [
+               'txn_id' =>            $txn_id,
+               'status' =>            $status,
+               'custom' =>            $custom,
+               'item_name' =>         $item_name,
+               'item_number' =>       $item_number,
+               'needs_review' =>      $needs_review,
+               'payment_status' =>    $payment_status,
+               'mc_gross' =>          $mc_gross,
+               'mc_currency' =>       $mc_currency,
+               'receiver_email' =>    $receiver_email,
+               'payer_email' =>       $payer_email,
+               'log' =>               $log,
+               'txn_type' =>          $txn_type,
+               'subscr_id' =>         $subscr_id,
+               'user_id' =>           $user_id,
+               'ipn_at' =>            time()
+       ];
+
+       db_insert_assoc('paypal_ipn', $row);
+       $row['id'] = $ipn_id = db_auto_id();
+
+       if($status !== 'verified') {    # it's really from PayPal
+               paypal_ipn_main_debug("status is not \"verified\" but is \"$status\"");
+       } elseif ($txn_type !== 'subscr_payment' && $txn_type !== 'web_accept') {
+               if ($txn_type !== 'subscr_signup' && $txn_type !== 'subscr_cancel' && $txn_type !== 'subscr_eot') {
+                       # subscr_cancel is sent when they cancel. After that:
+                       # subscr_eot is sent when their next payment would have been
+                       paypal_ipn_main_debug("txn_type is not \"subscr_payment\", \"subscr_signup\", \"subscr_cancel\", \"subscr_eot\" or \"web_accept\" but is \"$txn_type\"");
+               }
+       } elseif ($payment_status !== 'Completed') { # payment has completed
+               if ($payment_status !== 'Pending') {
+                       paypal_ipn_main_debug("payment_status is not \"Completed\" or \"Pending\", but is \"$payment_status\"");
+               }
+       } elseif ($receiver_email !== $GLOBALS['paypal_email']) {
+               paypal_ipn_main_debug("payment isn't to us ($GLOBALS[paypal_email]) but to \"$receiver_email\"");
+       } elseif ($mc_currency !== 'USD') {
+               paypal_ipn_main_debug("Currency isn't \"USD\" but is \"$mc_currency\"");
+       } else {
+               $custom_words = explode(' ', $custom);
+               if (!isset($GLOBALS['payment_handlers'][$custom_words[0]])) {
+                       paypal_ipn_main_debug("\$custom's first word isn't in GLOBALS[payment_handlers]. \$custom: \"$custom\"");
+               } else {
+                       # FIXME add parameter with everything from paypal
+                       $ret = file_run($GLOBALS['payment_handlers'][$custom_words[0]], $custom_words, $mc_gross, $row);
+                       if ($ret and is_array($ret) and isset($ret['success']) and $ret['success']) {
+                               $update = ['processed' => '1'];
+                               if (isset($ret['for_table_id']) and isset($ret['for_row_id'])) {
+                                       $tid = format_int_0((string)$ret['for_table_id']);
+                                       $rid = format_int_0((string)$ret['for_row_id']);
+                                       if ((int)$tid > 0 and (int)$rid > 0) {
+                                               $update['for_table_id'] = $tid;
+                                               $update['for_row_id'] = $rid;
+                                       }
+                               }
+                               db_update_assoc('paypal_ipn', $update);
+                       } else {
+                               paypal_ipn_main_debug($user, $old_date, $was_expired);
+                       }
+               }
+       }
+}
+
+function paypal_ipn_main_debug($message) {
+       $message = this_host() . ' paypal payment failure ' . $_POST['ipn_track_id'] . "\n\n" . $message;
+       $message .= "\n\nDump of all info received:\n";
+       foreach ($_POST as $key => $value) {
+               $message .= "\t$key: $value\n";
+       }
+       $template_vars = ['details' => $message];
+       email_with_template(null, 'backend_debug', $template_vars);
+}
+
+# this file is accessed directly from the paypal IPN system
+ini_set('display_errors', '0');
+ini_set('log_errors', '1');
+paypal_ipn_main();
diff --git a/paypal_ipn.sql b/paypal_ipn.sql
new file mode 100644 (file)
index 0000000..4806c35
--- /dev/null
@@ -0,0 +1,21 @@
+drop table if exists paypal_ipn;
+create table paypal_ipn (
+       id int unique auto_increment,
+       txn_id varchar(250) not null default "",
+       status varchar(250) not null default "",
+       ipn_at int(11) not null default 0
+       txn_type varchar(100) not null default "",
+       subscr_id varchar(100) not null default "",
+       custom varchar(250) not null default "",
+       for_table_id int not null default 0,
+       for_row_id int not null default 0,
+       processed int(1) not null default 0,
+       item_name varchar(250) not null default "",
+       item_number varchar(250) not null default "",
+       needs_review int(1) not null default 0,
+       payment_status varchar(250) not null default "",
+       mc_gross varchar(250) not null default "",
+       mc_currency varchar(250) not null default "",
+       receiver_email varchar(250) not null default "",
+       payer_email varchar(250) not null default "",
+       log text not null default "");