validate_ipn_invoice()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 2
nc 2
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Authorize.net legacy payment gateway
4
 *
5
 */
6
7
defined( 'ABSPATH' ) || exit;
8
9
/**
10
 * Authorize.net Legacy Payment Gateway class.
11
 *
12
 * As from version 1.0.19, subscriptions are now managed by WPI instead of Authorize.NET.
13
 * This class adds support for legacy subscriptions.
14
 */
15
abstract class GetPaid_Authorize_Net_Legacy_Gateway extends GetPaid_Payment_Gateway {
16
17
    /**
18
	 * Class constructor.
19
	 */
20
	public function __construct() {
21
        parent::__construct();
22
    }
23
24
    /**
25
	 * Returns the API URL.
26
	 *
27
	 *
28
	 * @param WPInv_Invoice $invoice Invoice.
29
	 * @return string
30
	 */
31
	public function get_api_url( $invoice ) {
32
        return $this->is_sandbox( $invoice ) ? 'https://apitest.authorize.net/xml/v1/request.api' : 'https://api.authorize.net/xml/v1/request.api';
33
    }
34
35
    /**
36
	 * Communicates with authorize.net
37
	 *
38
	 *
39
	 * @param array $post Data to post.
40
     * @param WPInv_Invoice $invoice Invoice.
41
	 * @return stdClass|WP_Error
42
	 */
43
    public function post( $post, $invoice ) {
44
45
        $url      = $this->get_api_url( $invoice );
46
        $response = wp_remote_post(
47
            $url,
48
            array(
49
                'headers' => array(
50
                    'Content-Type' => 'application/json; charset=utf-8',
51
                ),
52
                'body'    => json_encode( $post ),
53
                'method'  => 'POST',
54
            )
55
        );
56
57
        if ( is_wp_error( $response ) ) {
58
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response also could return the type array which is incompatible with the documented return type WP_Error|stdClass.
Loading history...
59
        }
60
61
        $response = wp_unslash( wp_remote_retrieve_body( $response ) );
62
        $response = preg_replace( '/\xEF\xBB\xBF/', '', $response ); // https://community.developer.authorize.net/t5/Integration-and-Testing/JSON-issues/td-p/48851
63
        $response = json_decode( $response );
64
65
        if ( empty( $response ) ) {
66
            return new WP_Error( 'invalid_reponse', __( 'Invalid gateway response', 'invoicing' ) );
67
        }
68
69
        if ( $response->messages->resultCode == 'Error' ) {
70
71
            if ( $this->is_sandbox( $invoice ) ) {
72
                wpinv_error_log( $response );
73
            }
74
75
            if ( $response->messages->message[0]->code == 'E00039' && ! empty( $response->customerProfileId ) && ! empty( $response->customerPaymentProfileId ) ) {
76
                return new WP_Error( 'dup_payment_profile', $response->customerProfileId . '.' . $response->customerPaymentProfileId );
77
            }
78
79
            if ( ! empty( $response->transactionResponse ) && ! empty( $response->transactionResponse->errors ) ) {
80
                $error = $response->transactionResponse->errors[0];
81
                return new WP_Error( $error->errorCode, $error->errorText );
82
            }
83
84
            return new WP_Error( $response->messages->message[0]->code, $response->messages->message[0]->text );
85
        }
86
87
        return $response;
88
89
    }
90
91
    /**
92
	 * Returns the API authentication params.
93
	 *
94
	 *
95
	 * @return array
96
	 */
97
	public function get_auth_params() {
98
99
        return array(
100
            'name'           => $this->get_option( 'login_id' ),
101
            'transactionKey' => $this->get_option( 'transaction_key' ),
102
        );
103
104
    }
105
106
    /**
107
	 * Cancels a subscription remotely
108
	 *
109
	 *
110
	 * @param WPInv_Subscription $subscription Subscription.
111
     * @param WPInv_Invoice $invoice Invoice.
112
	 */
113
	public function cancel_subscription( $subscription, $invoice ) {
114
115
        // Backwards compatibility. New version do not use authorize.net subscriptions.
116
        $this->post(
117
            array(
118
                'ARBCancelSubscriptionRequest' => array(
119
                    'merchantAuthentication' => $this->get_auth_params(),
120
                    'subscriptionId'         => $subscription->profile_id,
0 ignored issues
show
Bug Best Practice introduced by
The property profile_id does not exist on WPInv_Subscription. Since you implemented __get, consider adding a @property annotation.
Loading history...
121
                ),
122
            ),
123
            $invoice
124
        );
125
126
    }
127
128
    /**
129
	 * Processes ipns.
130
	 *
131
	 * @return void
132
	 */
133
	public function verify_ipn() {
134
135
        $this->maybe_process_old_ipn();
136
137
        // Validate the IPN.
138
        if ( empty( $_POST ) || ! $this->validate_ipn() ) {
139
		    wp_die( 'Authorize.NET IPN Request Failure', 'Authorize.NET IPN', array( 'response' => 200 ) );
140
        }
141
142
        // Event type.
143
        $posted = json_decode( file_get_contents( 'php://input' ) );
144
        if ( empty( $posted ) ) {
145
            wp_die( 'Invalid JSON', 'Authorize.NET IPN', array( 'response' => 200 ) );
146
        }
147
148
        // Process the IPN.
149
        $posted = (object) wp_unslash( $posted );
150
151
        // Process refunds.
152
        if ( 'net.authorize.payment.refund.created' == $posted->eventType ) {
153
            $invoice = new WPInv_Invoice( $posted->payload->merchantReferenceId );
154
            $this->validate_ipn_invoice( $invoice, $posted->payload );
155
            $invoice->refund();
156
        }
157
158
        // Held funds approved.
159
        if ( 'net.authorize.payment.fraud.approved' == $posted->eventType ) {
160
            $invoice = new WPInv_Invoice( $posted->payload->id );
161
            $this->validate_ipn_invoice( $invoice, $posted->payload );
162
            $invoice->mark_paid( false, __( 'Payment released', 'invoicing' ) );
163
        }
164
165
        // Held funds declined.
166
        if ( 'net.authorize.payment.fraud.declined' == $posted->eventType ) {
167
            $invoice = new WPInv_Invoice( $posted->payload->id );
168
            $this->validate_ipn_invoice( $invoice, $posted->payload );
169
            $invoice->set_status( 'wpi-failed', __( 'Payment declined', 'invoicing' ) );
170
            $invoice->save();
171
        }
172
173
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
174
175
    }
176
177
    /**
178
	 * Validates IPN invoices.
179
	 *
180
     * @param WPInv_Invoice $invoice
181
     * @param object $payload
182
	 * @return void
183
	 */
184
	public function validate_ipn_invoice( $invoice, $payload ) {
185
        if ( ! $invoice->exists() || $payload->id != $invoice->get_transaction_id() ) {
186
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
187
        }
188
    }
189
190
    /**
191
	 * Process subscriptio IPNS.
192
	 *
193
	 * @return void
194
	 */
195
	public function maybe_process_old_ipn() {
196
197
        $data = wp_kses_post_deep( wp_unslash( $_POST ) );
198
199
        // Only process subscriptions subscriptions.
200
        if ( empty( $data['x_subscription_id'] ) ) {
201
            return;
202
        }
203
204
        // Check validity.
205
        $this->validate_old_ipn_signature( $data );
206
207
        // Fetch the associated subscription.
208
        $subscription_id = WPInv_Subscription::get_subscription_id_by_field( $data['x_subscription_id'] );
209
        $subscription    = new WPInv_Subscription( $subscription_id );
210
211
        // Abort if it is missing or completed.
212
        if ( ! $subscription->get_id() || $subscription->has_status( 'completed' ) ) {
213
            return;
214
        }
215
216
        // Payment status.
217
        if ( 1 == $data['x_response_code'] ) {
218
219
            // Renew the subscription.
220
            $subscription->add_payment(
221
                array(
222
                    'transaction_id' => sanitize_text_field( $data['x_trans_id'] ),
223
                    'gateway'        => $this->id,
224
                )
225
            );
226
            $subscription->renew();
227
228
        } else {
229
            $subscription->failing();
230
        }
231
232
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
233
234
    }
235
236
    /**
237
	 * Validates the old IPN signature.
238
     *
239
     * @param array $posted
240
	 */
241
	public function validate_old_ipn_signature( $posted ) {
242
243
        $signature = $this->get_option( 'signature_key' );
244
        if ( ! empty( $signature ) ) {
245
            $login_id  = $this->get_option( 'login_id' );
246
            $trans_id  = wpinv_clean( $_POST['x_trans_id'] );
247
            $amount    = wpinv_clean( $_POST['x_amount'] );
248
            $hash      = hash_hmac( 'sha512', "^$login_id^$trans_id^$amount^", hex2bin( $signature ) );
249
250
            if ( ! hash_equals( $hash, $posted['x_SHA2_Hash'] ) ) {
251
                wpinv_error_log( $posted['x_SHA2_Hash'], "Invalid signature. Expected $hash" );
252
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
253
            }
254
}
255
256
    }
257
258
    /**
259
	 * Check Authorize.NET IPN validity.
260
	 */
261
	public function validate_ipn() {
262
263
        wpinv_error_log( 'Validating Authorize.NET IPN response' );
264
265
        if ( empty( $_SERVER['HTTP_X_ANET_SIGNATURE'] ) ) {
266
            return false;
267
        }
268
269
        $signature = $this->get_option( 'signature_key' );
270
271
        if ( empty( $signature ) ) {
272
            wpinv_error_log( 'Error: You have not set a signature key' );
273
            return false;
274
        }
275
276
        $hash  = hash_hmac( 'sha512', file_get_contents( 'php://input' ), hex2bin( $signature ) );
277
278
        if ( hash_equals( $hash, $_SERVER['HTTP_X_ANET_SIGNATURE'] ) ) {
279
            wpinv_error_log( 'Successfully validated the IPN' );
280
            return true;
281
        }
282
283
        wpinv_error_log( 'IPN hash is not valid' );
284
        wpinv_error_log( $_SERVER['HTTP_X_ANET_SIGNATURE'] );
285
        return false;
286
287
    }
288
289
}
290