Passed
Push — master ( 60cf75...7ee9b2 )
by Brian
128:01 queued 122:23
created

validate_ipn()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
c 1
b 0
f 0
dl 0
loc 25
rs 9.7998
cc 4
nc 4
nop 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 ( ! empty( $response->transactionResponse ) && ! empty( $response->transactionResponse->errors ) ) {
72
                $error = $response->transactionResponse->errors[0];
73
                return new WP_Error( $error->errorCode, $error->errorText );
74
            }
75
76
            return new WP_Error( $response->messages->message[0]->code, $response->messages->message[0]->text );
77
        }
78
79
        return $response;
80
81
    }
82
83
    /**
84
	 * Returns the API authentication params.
85
	 *
86
	 *
87
	 * @return array
88
	 */
89
	public function get_auth_params() {
90
91
        return array(
92
            'name'           => $this->get_option( 'login_id' ),
93
            'transactionKey' => $this->get_option( 'transaction_key' ),
94
        );
95
96
    }
97
98
    /**
99
	 * Cancels a subscription remotely
100
	 *
101
	 *
102
	 * @param WPInv_Subscription $subscription Subscription.
103
     * @param WPInv_Invoice $invoice Invoice.
104
	 */
105
	public function cancel_subscription( $subscription, $invoice ) {
106
107
        // Backwards compatibility. New version do not use authorize.net subscriptions.
108
        $this->post(
109
            array(
110
                'ARBCancelSubscriptionRequest' => array(
111
                    'merchantAuthentication'   => $this->get_auth_params(),
112
                    '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...
113
                )
114
            ),
115
            $invoice
116
        );
117
118
    }
119
120
    /**
121
	 * Processes ipns.
122
	 *
123
	 * @return void
124
	 */
125
	public function verify_ipn() {
126
127
        $this->maybe_process_old_ipn();
128
129
        // Validate the IPN.
130
        if ( empty( $_POST ) || ! $this->validate_ipn() ) {
131
		    wp_die( 'Authorize.NET IPN Request Failure', 'Authorize.NET IPN', array( 'response' => 500 ) );
132
        }
133
134
        // Event type.
135
        $posted = json_decode( file_get_contents( 'php://input' ) );
136
        if ( empty( $posted ) ) {
137
            wp_die( 'Invalid JSON', 'Authorize.NET IPN', array( 'response' => 500 ) );
138
        }
139
140
        // Process the IPN.
141
        $posted = (object) wp_unslash( $posted );
142
143
        // Process refunds.
144
        if ( 'net.authorize.payment.refund.created' == $posted->eventType ) {
145
            $invoice = new WPInv_Invoice( $posted->payload->merchantReferenceId );
146
            $this->validate_ipn_invoice( $invoice, $posted->payload );
147
            $invoice->refund();
148
        }
149
150
        // Held funds approved.
151
        if ( 'net.authorize.payment.fraud.approved' == $posted->eventType ) {
152
            $invoice = new WPInv_Invoice( $posted->payload->id );
153
            $this->validate_ipn_invoice( $invoice, $posted->payload );
154
            $invoice->mark_paid( false, __( 'Payment released', 'invoicing' ) );
155
        }
156
157
        // Held funds declined.
158
        if ( 'net.authorize.payment.fraud.declined' == $posted->eventType ) {
159
            $invoice = new WPInv_Invoice( $posted->payload->id );
160
            $this->validate_ipn_invoice( $invoice, $posted->payload );
161
            $invoice->set_status( 'wpi-failed', __( 'Payment desclined', 'invoicing' ) );
162
            $invoice->save();
163
        }
164
165
        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...
166
167
    }
168
169
    /**
170
	 * Validates IPN invoices.
171
	 *
172
     * @param WPInv_Invoice $invoice
173
     * @param object $payload
174
	 * @return void
175
	 */
176
	public function validate_ipn_invoice( $invoice, $payload ) {
177
        if ( ! $invoice->exists() || $payload->id != $invoice->get_transaction_id() ) {
178
            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...
179
        }
180
    }
181
182
    /**
183
	 * Process subscriptio IPNS.
184
	 *
185
	 * @return void
186
	 */
187
	public function maybe_process_old_ipn() {
188
189
        $data = wp_unslash( $_POST );
190
191
        // Only process subscriptions subscriptions.
192
        if ( empty( $_POST['x_subscription_id'] ) ) {
193
            return;
194
        }
195
196
        // Check validity.
197
        $this->validate_old_ipn_signature( $data );
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type string; however, parameter $posted of GetPaid_Authorize_Net_Le...ate_old_ipn_signature() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

197
        $this->validate_old_ipn_signature( /** @scrutinizer ignore-type */ $data );
Loading history...
198
199
        // Fetch the associated subscription.
200
        $subscription_id = WPInv_Subscription::get_subscription_id_by_field( $_POST['x_subscription_id'] );
201
        $subscription    = new WPInv_Subscription( $subscription_id );
202
203
        // Abort if it is missing or completed.
204
        if ( ! $subscription->get_id() || $subscription->has_status( 'completed' ) ) {
205
            return;
206
        }
207
208
        // Payment status.
209
        if ( 1 == $_POST['x_response_code'] ) {
210
211
            // Renew the subscription.
212
            $subscription->add_payment(
213
                array(
214
                    'transaction_id' => sanitize_text_field( $data['x_trans_id'] ),
215
                    'gateway'        => $this->id
216
                )
217
            );
218
            $subscription->renew();
219
220
        } else {
221
            $subscription->failing();
222
        }
223
224
        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...
225
226
    }
227
228
    /**
229
	 * Validates the old IPN signature.
230
     *
231
     * @param array $posted
232
	 */
233
	public function validate_old_ipn_signature( $posted ) {
234
235
        $signature = $this->get_option( 'signature_key' );
236
        if ( ! empty( $signature ) ) {
237
            $login_id  = $this->get_option( 'login_id' );
238
            $trans_id  = $_POST['x_trans_id'];
239
            $amount    = $_POST['x_amount'];
240
            $hash      = hash_hmac ( 'sha512', "^$login_id^$trans_id^$amount^", hex2bin( $signature ) );
241
242
            if ( ! hash_equals( $hash, $posted['x_SHA2_Hash'] ) ) {
243
                wpinv_error_log( $posted['x_SHA2_Hash'], "Invalid signature. Expected $hash" );
244
                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...
245
            }
246
247
        }
248
249
    }
250
251
    /**
252
	 * Check Authorize.NET IPN validity.
253
	 */
254
	public function validate_ipn() {
255
256
        wpinv_error_log( 'Validating Authorize.NET IPN response' );
257
258
        if ( empty( $_SERVER['HTTP_X_ANET_SIGNATURE'] ) ) {
259
            return false;
260
        }
261
262
        $signature = $this->get_option( 'signature_key' );
263
264
        if ( empty( $signature ) ) {
265
            wpinv_error_log( 'Error: You have not set a signature key' );
266
            return false;
267
        }
268
269
        $hash  = hash_hmac ( 'sha512', file_get_contents( 'php://input' ), hex2bin( $signature ) );
270
271
        if ( hash_equals( $hash, $_SERVER['HTTP_X_ANET_SIGNATURE'] ) ) {
272
            wpinv_error_log( 'Successfully validated the IPN' );
273
            return true;
274
        }
275
276
        wpinv_error_log( 'IPN hash is not valid' );
277
        wpinv_error_log(  $_SERVER['HTTP_X_ANET_SIGNATURE']  );
278
        return false;
279
280
    }
281
282
}
283