#!/usr/bin/perl -T
$versions{'pplistnr.cgi'} = '06.6.00.000';
#######################################################################
#                   PayPal IPN Listener for AgoraCart
#######################################################################
#
# AgoraCart and all associated files, except where noted, are
# Copyright 2001 to Present jointly by K-Factor Technologies, Inc.
# and by C E Mayo (aka Mister Ed) at AgoraCart.com & K-Factor.net
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This copyright notice may not be removed or altered in any way.
#
#######################################################################

BEGIN {
    $| = 1; # Flush
    #use strict;
    #use warnings;
    # Catch fatal errors and die with 200 OK plain/text header
    # Otherwise PP IPN will keep sending up to 16 requests till the right header is posted.
    $SIG{__DIE__} = \&print_header2;
    use lib 'system66/perl_modules';
    unshift( @INC, 'system66/perl_modules' );
}

# version 6 and upwards of the UserAgent module provides for tighter server certificate validation
use LWP::UserAgent 6;
use File::Copy;

require_supporting_libraries( __FILE__, __LINE__,
    './system66/library/common_subroutines.pl',
    './user_config66/cart/agora_user_lib.pl'
);

%form_data;
local ( %orderLoggingHash );
local ( $cart_invoiceNumber, $pp_cart_id, $pp_txn_id, $payment_date, $pp_child_txn_id, $pp_exchange_rate ) = q{};
local ( $pp_mc_gross, $pp_mc_fee, $pp_memo, $pp_payment_type, $pp_payment_status, $pp_pending_reason, $pp_reason_code ) = q{};
local ( $pp_echeck_time_processed, $pp_payer_status, $pp_case_id, $case_logfile ) = q{};
local ( $pp_orderline_found, $query, $pp_confirm, $pp_error, $pp_ipn_admin_email_body, $logfile ) = q{};
$lib_process_order_ok = q{};
$sc_global_bot_tracker = q{};
#$sc_set_paypal_notify_script_to_test = ''; # for dev convenience
my ( $pp_log_string, $ppfile, $pp_ipn_update_successful, $pp_ipn_log_found  ) = q{};
my $pp_lib = 'PayPalIPN-order_lib.pl';
my $PP_server_sandbox = 'ipnpb.sandbox.paypal.com';
my $PP_server = 'ipnpb.paypal.com'; # production
my $DATE_SEP = ' ';
my $TIME_SEP = ':';
my $sleep = q{};
my $pp_status_already_processed = q{};

# Check if enabled, if not print header & exit
if ( $mc_mgr_gateways_enabled !~ /PayPalExpress|PayPalIPN/i ) {
    &print_header2;
}

# Check if valid PayPal IP. If not print header & exit
if ( $ENV{'REMOTE_ADDR'} !~ /^64\.4\.24\d\.\d{1,3}$|^66\.211\.1\d{2}\.\d{1,3}$|^173\.0\.8\d\.\d{1,3}$|^173\.0\.9\d\.\d{1,3}$/ )  {
    &file_loaders();
    my $tempip = $ENV{'REMOTE_ADDR'};
    send_admin_ipn_email("IP observed - $tempip not recognized IP");
    &print_header2;
}

# Read post from PayPal system and add 'cmd'
read(STDIN, $query, $ENV{'CONTENT_LENGTH'});
$query .= '&cmd=_notify-validate';

# split posted variables into key/value pairs
@pairs = split(/&/, $query);
foreach $pair (@pairs) {
    ($key, $value) = split(/=/, $pair);
    $value =~ tr/+/ /;
    $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    $form_data{$key} = $value;
}

# switch to sandbox server
if ( $sc_set_paypal_notify_script_to_test eq 'yes' ) {
    $PP_server = $PP_server_sandbox;
}

# post back to PayPal system to validate
$ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => "0", }, protocols_allowed => ["https"] );
$req = HTTP::Request->new('POST', 'https://' . $PP_server . '/cgi-bin/webscr');
$req->content_type('application/x-www-form-urlencoded');
$req->header(Host => 'www.paypal.com');
$req->content($query);
$res = $ua->request($req);
$pp_error = $res->is_error;
$pp_confirm = $res->content;

&print_header;
&file_loaders();

if ( $pp_error ) {
    # HTTP error
    send_admin_ipn_email('PayPal IPN HTTP Error: ');
}
elsif ( $pp_confirm eq 'INVALID' ) {
    send_admin_ipn_email('Invalid PayPal IPN Data Received');
}
elsif ( $pp_confirm  =~ /VERIFIED/i) {

    # HH:MM:SS Mmm DD, YYYY PDT - Month: $4 Year: $5
    $form_data{'payment_date'} =~ /^(\d{1,2})$TIME_SEP(\d{1,2})$TIME_SEP(\d{1,2})$DATE_SEP([[:alpha:]]{3})$DATE_SEP\d{1,2},$DATE_SEP(\d{4})\s[[:alpha:]]{3}$/;
    $year = $5;
    $month = $4;

    $logfile = $sc_ppipn_log_path . "/" . $year . '_' . $month . '_ppipn.log';
    $case_logfile = $sc_ppipn_log_path . "/" . $year . '_ppipn_case.log';

    if ( $form_data{'case_id'} ) {
        send_admin_ipn_email('PayPal IPN Dispute Case/Update Received: not logged by cart');
        require_supporting_libraries( __FILE__, __LINE__, "$sc_lib_dir/paypal_dispute.pl" );
        &log_pp_dispute;
        &print_header2; # exit
    }

    if (-e $logfile ) {
        # check to see if previously processed.
        open(IPNLOGCHECK, "<", "$logfile") or file_open_error("$logfile", "Opening $logfile for reading", __FILE__, __LINE__);
        while (<IPNLOGCHECK>) {
            if ( $pp_orderline_found eq '' ) {
                (
                    $cart_invoiceNumber, # $form_data{'invoice'} - cart invoice
                    $pp_cart_id, # $form_data{'custom'} -  cart id
                    $pp_txn_id, # $form_data{'txn_id'}; or $form_data{'parent_txn_id'} if payment status = cancel, reversal or cancel reversal
                    $pp_child_txn_id, # $form_data{'txn_id'} if $form_data{'parent_txn_id'} - if payment status = cancel, reversal or cancel reversal
                    $payment_date, # HH:MM:SS Mmm DD, YYYY PDT
                    $pp_mc_gross, # $form_data{'mc_gross'} - total of order (or adjustment/refund)
                    $pp_mc_fee, # $form_data{'mc_fee'} - transaction fee (cost to merchant)
                    $pp_memo, # $form_data{'memo'} - memo from customer at checkout
                    $pp_payment_type, # $form_data{'payment_type'}
                    $pp_payment_status, # $form_data{'payment_status'}
                    $pp_pending_reason, # $form_data{'pending_reason'} - set only if payment_status is set to pending
                    $pp_reason_code, # $form_data{'reason_code'} - set if payment_status is Reversed, Refunded, Canceled_Reversal, or Denied
                    $pp_echeck_time_processed, # $form_data{'echeck_time_processed'} if echeck
                    $pp_exchange_rate, # $form_data{'exchange_rate'} if a currency exchange happened
                    $pp_payer_status, # $form_data{'payer_status'} - indicates if customer is paypal verified or not.
                    $pp_case_id # $form_data{'case_id'} - pp dispute case id
                ) = split( /\t/, $_ );

                if ( ( ( $pp_txn_id =~ /$form_data{'parent_txn_id'}|$form_data{'txn_id'}/ ) || ( $pp_child_txn_id =~ /$form_data{'parent_txn_id'}|$form_data{'txn_id'}/ ) )
                    && ( $cart_invoiceNumber =~ /$form_data{'invoice'}/ )
                    && ( $pp_cart_id =~ /$form_data{'custom'}/ )
                ) {
                    $pp_orderline_found = 1;
                }
                if ( $pp_orderline_found && $pp_payment_status eq $form_data{'payment_status'} && $pp_reason_code eq $form_data{'reason_code'} ) {
                    # we have already processed this notification, duplicated
                    #$pp_status_already_processed = 1;
                }
            } # End of if ( $pp_orderline_found eq '' )
        } # End of while $file
        close(IPNLOGCHECK);
    } # End of if ( -e $logfile )

    # For Dev Testing mostly
    # send_admin_ipn_email('PayPal IPN Data Submitted to Listener - Dev Testing');

    if ( $pp_status_already_processed ) {
        #send_mail($sc_admin_email, $sc_order_email, 'Duplicate IPN', '');
        exit(0);
    }

    require_supporting_libraries( __FILE__, __LINE__, "$sc_paygates_dir/$pp_lib" );
    require_supporting_libraries( __FILE__, __LINE__, "$sc_lib_dir/loader_routines.pl" );

    if ( $form_data{'payment_status'} =~ /Completed|Pending/ && $form_data{'payment_type'} =~ /instant|echeck/ && $pp_orderline_found eq '' ) {
        # New Order - Completed or Pending
        $lib_process_order_ok = 'yes';
        $pp_log_string = &new_pp_log_string;

        get_file_lock("$logfile.lockfile");
        # >:encoding(utf8)
        open(LOGIPN, ">>$logfile") or file_open_error("$logfile", "Opening $logfile for adding new entry", __FILE__, __LINE__);
        print LOGIPN $pp_log_string;
        close(LOGIPN);
        release_file_lock("$logfile.lockfile");

        &ready_to_process_PayPalIPN_order;

    }
    elsif ( $pp_orderline_found ) {

        &update_ipn_log_file;

        if ( $form_data{'payment_status'} =~ /Completed|Pending/ && $form_data{'payment_type'} =~ /echeck/ ) {
            $lib_process_order_ok = 'yes';
            &ready_to_process_PayPalIPN_order;
        }
        #Failed: This happens only if the payment was made from your customer's bank account.
        #Denied: This happens only if the payment was previously pending because of one of the reasons listed in the pending_reason variable or the Fraud_Management_Filters_x variable.
        elsif ( $form_data{'payment_status'} =~ /Declined|Denied|Failed/ && $form_data{'payment_type'} =~ /echeck|instant/ ) {
            &cancel_PayPalIPN_pending_order;
        }
        #Refunded: You refunded the payment.
        #Reversed: A payment was reversed due to a chargeback or other type of reversal. The funds have been removed from your account balance and returned to the buyer. The reason for the reversal is specified in the ReasonCode element.
        #Canceled_Reversal: A reversal has been canceled. For example, you won a dispute with the customer, and the funds for the transaction that was reversed have been returned to you.
        elsif ( $form_data{'payment_status'} =~ /Refunded|Reversed|Canceled_Reversal/ ) {
            &payPalIPN_payment_status_update;
        }

    }
    else {
        # not sure what to do; email IPN information to admin
        send_admin_ipn_email('Verified PayPal IPN Received, But not found in Logs and Unsure How to Process It');
    }

}

exit(0);

#######################################################################
sub file_loaders {
    require_supporting_libraries( __FILE__, __LINE__,
        './system66/library/dates.pl',
        './system66/library/infosec.pl',
        './system66/library/cookies_sessions_cart_ids.pl',
        './system66/library/misc_core.pl',
        './system66/library/log_and_error_subroutines.pl',
        './system66/library/tags_and_tokens.pl',
        './system66/cart/cart_formatters.pl'
    );
    require_supporting_libraries( __FILE__, __LINE__,'./system66/library/agora_setup.pl' );
    require_supporting_libraries( __FILE__, __LINE__,
        "$sc_lang_dir/$sc_lang_path/agora_messages.pl",
        "$sc_mail_lib_path"#,
    );
}

#######################################################################
#                       Update IPN Log File
#######################################################################
sub update_ipn_log_file {

    my ( $cart_invoiceNumber2, $pp_cart_id2, $pp_txn_id2, $payment_date2, $pp_child_txn_id2, $pp_exchange_rate2 ) = q{};
    my ( $pp_mc_gross2, $pp_mc_fee2, $pp_memo2, $pp_payment_type2, $pp_payment_status2, $pp_pending_reason2, $pp_reason_code2 ) = q{};
    my ( $pp_echeck_time_processed2, $pp_payer_status2, $pp_case_id2, $newFile ) = q{};

    get_file_lock("$logfile.lockfile");
    # <:encoding(utf8)
    open(IPNLOG, "<", "$logfile") or file_open_error("$logfile", "Opening $logfile for reading", __FILE__, __LINE__);
    while ( <IPNLOG> ) {
        (
            $cart_invoiceNumber2, # $form_data{'invoice'} - cart invoice
            $pp_cart_id2, # $form_data{'custom'} -  cart id
            $pp_txn_id2, # $form_data{'txn_id'}; or $form_data{'parent_txn_id'} if payment status = cancel, reversal or cancel reversal
            $pp_child_txn_id2, # $form_data{'txn_id'} if $form_data{'parent_txn_id'} - if payment status = cancel, reversal or cancel reversal
            $payment_date2, # HH:MM:SS Mmm DD, YYYY PDT
            $pp_mc_gross2, # $form_data{'mc_gross'} - total of order (or adjustment/refund)
            $pp_mc_fee2, # $form_data{'mc_fee'} - transaction fee (cost to merchant)
            $pp_memo2, # $form_data{'memo'} - memo from customer at checkout
            $pp_payment_type2, # $form_data{'payment_type'}
            $pp_payment_status2, # $form_data{'payment_status'}
            $pp_pending_reason2, # $form_data{'pending_reason'} - set only if payment_status is set to pending
            $pp_reason_code2, # $form_data{'reason_code'} - set if payment_status is Reversed, Refunded, Canceled_Reversal, or Denied
            $pp_echeck_time_processed2, # $form_data{'echeck_time_processed'} if echeck
            $pp_exchange_rate2, # $form_data{'exchange_rate'} if a currency exchange happened
            $pp_payer_status2, # $form_data{'payer_status'} - indicates if customer is paypal verified or not.
            $pp_case_id2 # $form_data{'case_id'} - pp dispute case id
        ) = split( /\t/, $_ );

        if (
            ( $pp_txn_id2 =~ /$form_data{'parent_txn_id'}|$form_data{'txn_id'}/ )
            && ( $cart_invoiceNumber2 eq $form_data{'invoice'} )
            && ( $pp_cart_id2 eq $form_data{'custom'} )
           ) {

            $pp_ipn_update_successful = 'yes';

            if ( $form_data{'parent_txn_id'} && $form_data{'txn_id'} ) {
                $pp_child_txn_id2 = $form_data{'txn_id'};
            }

            if ( $form_data{'mc_gross'} =~ /\-/ && $pp_mc_gross2 >= $form_data{'mc_gross'} ) {
                $pp_mc_gross2 -= $form_data{'mc_gross'};
            }
            elsif ( $pp_mc_gross2 < $form_data{'mc_gross'} && $form_data{'mc_gross'} !~ /\-/ ) {
                $pp_mc_gross2 = $form_data{'mc_gross'};
            }

            if ( $form_data{'echeck_time_processed'} ) {
                $pp_echeck_time_processed2 = $form_data{'echeck_time_processed'};
            }

            if ( $form_data{'mc_fee'} =~ /\-/ && $pp_mc_fee2 >= $form_data{'mc_fee'} ) {
                $pp_mc_fee2 -= $form_data{'mc_fee'};
            }
            elsif ( $pp_mc_fee2 < $form_data{'mc_fee'} && $form_data{'mc_fee'} !~ /\-/ ) {
                $pp_mc_fee2 = $form_data{'mc_fee'};
            }

            if ( $form_data{'exchange_rate'} ) {
                $pp_exchange_rate2 = $form_data{'exchange_rate'};
            }

            $newFile .= qq|$cart_invoiceNumber2\t$pp_cart_id2\t$pp_txn_id2\t$pp_child_txn_id2\t$payment_date2\t$pp_mc_gross2\t$pp_mc_fee2\t$pp_memo2\t$pp_payment_type2\t$form_data{'payment_status'}\t$form_data{'pending_reason'}\t$form_data{'reason_code'}\t$pp_echeck_time_processed2\t$pp_exchange_rate2\t$pp_payer_status2\t$pp_case_id2|;
        }
        else {
            $newFile .= $_;
        }
    }    # End of while $file
    close(IPNLOG);

    open(LOGIPN,  ">", "$logfile") or file_open_error("$logfile", "Opening $logfile for Update", __FILE__, __LINE__);
    print LOGIPN $newFile;
    close(LOGIPN);
    release_file_lock("$logfile.lockfile");
}


#######################################################################
#                       New PayPal IPN Log String
#######################################################################
sub new_pp_log_string {
    my $str = qq|$form_data{'invoice'}\t$form_data{'custom'}\t$form_data{'txn_id'}\t$form_data{'parent_txn_id'}\t$form_data{'payment_date'}\t$form_data{'mc_gross'}\t$form_data{'mc_fee'}\t$form_data{'memo'}\t$form_data{'payment_type'}\t$form_data{'payment_status'}\t$form_data{'pending_reason'}\t$form_data{'reason_code'}\t$form_data{'echeck_time_processed'}\t$form_data{'exchange_rate'}\t$form_data{'payer_status'}\t$form_data{'case_id'}\n|;
    return $str;
}
#######################################################################
#                       Send Admin IPN Email
#######################################################################
# Email message to admin that case was recieved or updated.
# Replace with email template later?

sub send_admin_ipn_email {
    my $email_subject = shift;
    $pp_ipn_admin_email_body = $email_subject . $pp_error . "\n\n";
    $pp_ipn_admin_email_body .= '#########################################################################' . "\n\n";
    $pp_ipn_admin_email_body .= 'PayPal HTTP_USER_AGENT: ' . "$ENV{'HTTP_USER_AGENT'}\n";
    $pp_ipn_admin_email_body .= 'PayPal IP (REMOTE_ADDR): ' . "$ENV{'REMOTE_ADDR'}\n";
    $pp_ipn_admin_email_body .= 'Host Name (REMOTE_HOST): ' . "$ENV{'REMOTE_HOST'}\n";
    $pp_ipn_admin_email_body .= "\$res->content = $pp_confirm\n";
    $pp_ipn_admin_email_body .= "\$res->is_error = $pp_error\n\n\n";
    $pp_ipn_admin_email_body .= "Complete Query String = $query\n\n";
    $pp_ipn_admin_email_body .= '#########################################################################' . "\n\n";
    $pp_ipn_admin_email_body .= 'The Query String key / value pairs from PayPal (IPN):' . "\n\n";

    $pp_ipn_admin_email_body .= "\$ENV{'CONTENT_LENGTH'} = $ENV{'CONTENT_LENGTH'} \n\n";
    $pp_ipn_admin_email_body .= "\$query = $query \n\n";

    while ( ( $key, $value ) = each %form_data ) {
        $pp_ipn_admin_email_body .= "$key = $value\n";
    }
    $pp_ipn_admin_email_body .= "\n\$pp_orderline_found = $pp_orderline_found\n\n";
    send_mail($sc_admin_email, $sc_admin_email, $email_subject, $pp_ipn_admin_email_body);
}

#######################################################################
#                       Require Supporting Libraries
#######################################################################
sub require_supporting_libraries {

    # The libraries are required by us,so exit if loading error
    my ( $file, $line, @require_files ) = @_;
    local ($require_file);
    request_supporting_libraries( 'exit', $file, $line, @require_files );
}
#######################################################################
sub request_supporting_libraries {

    local ( $what_to_do_on_error, $file, $line, @require_files ) = @_;
    local ($require_file);

    foreach $require_file (@require_files) {
        # Check if file exists or can be loaded, if not show blank screen
        if ( -e "$require_file" && -r "$require_file" )
        {    # file is there, now try to require it
            $result = eval('require "$require_file"'); # require in not-fatal way
            die( 'Required File Could Not Be Loaded' ) if ( $@ ne '' );
        }
        else {
            die( 'Required File Could Not Be Found' ) if ( $@ ne '' );
        }
    }
}
#######################################################################
#                       Print Header
#######################################################################
sub print_header {
    print "content-type: text/plain\n\n";
}
sub print_header2 {
    print "content-type: text/plain\n\n";
    exit(0);
}
#######################################################################
