PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.4.1
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.4.1
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
better-payment / includes / Classes / Import.php

Import.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.4.1, at includes/Classes/Import.php

253 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\Classes;
4
5 use Better_Payment\Lite\Admin\DB;
6 use Better_Payment\Lite\Controller;
7 use Better_Payment\Lite\Models\TransactionModel;
8 use Better_Payment\Lite\Traits\Helper;
9 use Exception;
10 use League\Csv\Reader;
11 use League\Csv\Statement;
12
13 /**
14 * Exit if accessed directly
15 */
16 if (!defined('ABSPATH')) {
17 exit;
18 }
19
20 /**
21 * Export handler class
22 *
23 * @since 0.0.4
24 */
25 class Import extends Controller{
26
27 use Helper;
28 /**
29 * Class constructor
30 *
31 * @since 0.0.4
32 */
33 public function __construct() {
34
35 }
36
37 /**
38 * A CSV `payment_date` cell as the UTC `Y-m-d H:i:s` the table stores.
39 *
40 * The export writes the date in site time, in the site's date and time
41 * format (`Export`: `wp_date()`), so a cell without a timezone is read as
42 * site time and converted. It used to be stored as if it were UTC, so an
43 * export → import round trip moved every row by the site's UTC offset. A
44 * cell that names its own timezone (`Z`, `+06:00`, `UTC`, `GMT`) is honoured
45 * as given. An unparseable cell keeps the old result (the epoch).
46 *
47 * @since 2.4.1
48 *
49 * @param string $value Cell value.
50 * @return string
51 */
52 public static function payment_date_from_csv( $value ) {
53 $value = trim( (string) $value );
54 $timestamp = strtotime( $value );
55
56 if ( false === $timestamp ) {
57 return gmdate( 'Y-m-d H:i:s', 0 );
58 }
59
60 if ( preg_match( '/(?:Z|[+-]\d{2}:?\d{2}|\bUTC|\bGMT)$/i', $value ) ) {
61 return gmdate( 'Y-m-d H:i:s', $timestamp );
62 }
63
64 // strtotime() read the wall-clock time as UTC (WordPress runs PHP in UTC);
65 // it is site time, so convert it.
66 return get_gmt_from_date( gmdate( 'Y-m-d H:i:s', $timestamp ), 'Y-m-d H:i:s' );
67 }
68
69 /**
70 * Decode a CSV `form_fields_info` cell into an array.
71 *
72 * The cell comes from an uploaded file, so it is never handed to maybe_unserialize():
73 * that instantiates any class a serialized payload names, which turns an import into an
74 * object-injection sink for whatever gadget chain the site's plugins happen to provide.
75 * Objects are refused outright; only a plain array survives.
76 *
77 * @since 2.3.4
78 *
79 * @param mixed $value Cell value.
80 * @return array
81 */
82 public static function decode_form_fields_info( $value ) {
83 if ( ! is_string( $value ) || ! is_serialized( $value ) ) {
84 return [];
85 }
86
87 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, WordPress.PHP.NoSilencedErrors.Discouraged -- classes are disallowed; a malformed cell is expected input.
88 $decoded = @unserialize( trim( $value ), [ 'allowed_classes' => false ] );
89
90 return is_array( $decoded ) ? $decoded : [];
91 }
92
93 public function import_transactions() {
94 $message = __('Invalid File!', 'better-payment');
95
96 $invalid_nonce = ! wp_verify_nonce( $_REQUEST['nonce'], 'better_payment_transaction_import_nonce' );
97 $invalid_access = ! current_user_can('manage_options') || ! isset( $_FILES['better-payment-transaction-import-input'] );
98
99 if ( $invalid_nonce || $invalid_access ) {
100 set_transient('better_payment_import_transactions_error', esc_html( $message ), 30);
101 wp_safe_redirect( $_SERVER['HTTP_REFERER'] );
102 exit;
103 }
104
105 $file = $_FILES['better-payment-transaction-import-input'];
106
107 try {
108 $this->validate_csv_file($file);
109 $csv = Reader::createFromPath($file['tmp_name'], 'r');
110 $csv->setHeaderOffset(0);
111
112 // Header validation
113 $csvHeader = $csv->getHeader();
114 if ( isset( $csvHeader[0] ) && false !== strpos( $csvHeader[0], ';' ) ) {
115 $csv->setDelimiter(';');
116 $csvHeader = $csv->getHeader();
117 }
118
119 if ( ! is_array( $csvHeader ) || ! count( $csvHeader) ){
120 throw new Exception( $message );
121 }
122
123 $csvHeader = array_map('sanitize_text_field', $csvHeader);
124
125 $exportObj = new Export();
126 $allowedHeaders = $exportObj->export_transactions_heading();
127
128 if ( count( $allowedHeaders ) !== count( $csvHeader ) ) {
129 throw new Exception( $message );
130 }
131
132 // Fetch rows
133 $stmt = Statement::create()->offset(0)->limit(3000);
134 $records = $stmt->process($csv);
135
136 $data = array();
137
138 foreach ( $records as $record ) {
139 $better_form_fields = $this->build_form_fields( $record );
140
141 $amount_currency_array = explode(' ', $record['amount']);
142 $currency = is_array($amount_currency_array) && count($amount_currency_array) > 1 ? $amount_currency_array[0] : esc_html('USD');
143 $amount = is_array($amount_currency_array) && count($amount_currency_array) > 1 ? $amount_currency_array[1] : $record['amount'];
144
145 $data[] = array(
146 'email' => sanitize_email( $record['email'] ),
147 'currency' => sanitize_text_field( $currency ),
148 'amount' => floatval( $amount ),
149 // 'payment_type' => sanitize_text_field( $record['payment_type'] ),
150 'transaction_id' => sanitize_text_field( $record['transaction_id'] ),
151 'source' => sanitize_text_field( $record['source'] ),
152 'status' => sanitize_text_field( $record['status'] ),
153 'payment_date' => self::payment_date_from_csv( sanitize_text_field( $record['payment_date'] ) ),
154 'form_fields_info' => maybe_serialize( $better_form_fields ),
155 );
156 }
157
158 if ( empty( $data ) ) {
159 throw new Exception( __('No valid records found in the CSV file!', 'better-payment') );
160 }
161
162 $table = DB::get_table_name();
163 $rowsInserted = $this->bulk_insert_transactions( $table, $data );
164 if ( $rowsInserted !== false && $rowsInserted > 0 ) {
165 set_transient('better_payment_import_transactions_success', esc_html__( $rowsInserted . ' rows inserted successfully!', 'better-payment' ), 30);
166 } else {
167 throw new Exception( __('Failed to insert records into database!', 'better-payment') );
168 }
169 } catch (\Exception $exception) {
170 set_transient('better_payment_import_transactions_error', esc_html( $exception->getMessage() ), 30);
171 wp_safe_redirect( $_SERVER['HTTP_REFERER'] );
172 exit;
173 }
174
175 wp_safe_redirect( $_SERVER['HTTP_REFERER'] );
176 exit;
177 }
178
179 /**
180 * The `form_fields_info` an imported CSV row is stored with.
181 *
182 * Lite keeps the name, email and source and marks the row imported.
183 * `better_payment/import/form_fields_info` receives that, the CSV row and the
184 * row's decoded `form_fields_info` cell: Pro adds the subscription keys of a
185 * "Subscription" row there.
186 *
187 * @since 2.4.0
188 *
189 * @param mixed $record CSV row keyed by header; anything else is treated as empty.
190 * @return array
191 */
192 public function build_form_fields( $record ) {
193 $record = is_array( $record ) ? $record : [];
194
195 $better_form_fields = [
196 'primary_first_name' => sanitize_text_field( $record['name'] ?? '' ),
197 'email' => sanitize_email( $record['email'] ?? '' ),
198 'source' => sanitize_text_field( $record['source'] ?? '' ),
199 'is_imported' => intval(1),
200 ];
201
202 // Decoded without instantiating objects; see decode_form_fields_info().
203 $decoded = isset( $record['form_fields_info'] ) ? self::decode_form_fields_info( $record['form_fields_info'] ) : [];
204
205 $filtered = apply_filters( 'better_payment/import/form_fields_info', $better_form_fields, $record, $decoded );
206
207 return is_array( $filtered ) ? $filtered : $better_form_fields;
208 }
209
210 public function validate_csv_file( $file ){
211 $message = __('Invalid File!', 'better-payment');
212
213 if ( empty( $file ) ){
214 throw new Exception( $message );
215 }
216
217 $extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
218 $allowed_extensions = ['csv'];
219
220 if ( empty( $extension ) || ! in_array( $extension, $allowed_extensions ) ) {
221 throw new Exception( $message );
222 }
223
224 return true;
225 }
226
227 public function bulk_insert_transactions($table, $rows) {
228 global $wpdb;
229
230 $headers = array_keys($rows[0]);
231 asort($headers);
232 $columnList = '`' . implode('`, `', $headers) . '`';
233
234 $sql = "INSERT INTO `$table` ($columnList) VALUES\n";
235 $placeholders = array();
236 $data = array();
237
238 foreach ($rows as $row) {
239 ksort($row);
240 $rowPlaceholders = array();
241 foreach ($row as $value) {
242 $data[] = $value;
243 $rowPlaceholders[] = is_numeric($value) ? '%d' : '%s';
244 }
245 $placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')';
246 }
247
248 $sql .= implode(",\n", $placeholders);
249
250 $rowsInserted = $wpdb->query($wpdb->prepare($sql, $data));
251 return $rowsInserted;
252 }
253 }