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
← All changes | includes/Classes/Import.php +99 -10 1.0.3 → 2.4.1 View file →
@@ -33,8 +33,64 @@
33 33 public function __construct() {
34 34
35 35 }
36 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 +
37 93 public function import_transactions() {
38 94 $message = __('Invalid File!', 'better-payment');
39 95
40 96 $invalid_nonce = ! wp_verify_nonce( $_REQUEST['nonce'], 'better_payment_transaction_import_nonce' );
@@ -54,9 +110,9 @@
54 110 $csv->setHeaderOffset(0);
55 111
56 112 // Header validation
57 113 $csvHeader = $csv->getHeader();
58 - if ( isset( $csvHeader[0] ) && str_contains( $csvHeader[0], ';' ) ) {
114 + if ( isset( $csvHeader[0] ) && false !== strpos( $csvHeader[0], ';' ) ) {
59 115 $csv->setDelimiter(';');
60 116 $csvHeader = $csv->getHeader();
61 117 }
62 118
@@ -79,14 +135,9 @@
79 135
80 136 $data = array();
81 137
82 138 foreach ( $records as $record ) {
83 - $better_form_fields = [
84 - 'primary_first_name' => sanitize_text_field( $record['name'] ),
85 - 'email' => sanitize_email( $record['email'] ),
86 - 'source' => sanitize_text_field( $record['source'] ),
87 - 'is_imported' => intval(1),
88 - ];
139 + $better_form_fields = $this->build_form_fields( $record );
89 140
90 141 $amount_currency_array = explode(' ', $record['amount']);
91 142 $currency = is_array($amount_currency_array) && count($amount_currency_array) > 1 ? $amount_currency_array[0] : esc_html('USD');
92 143 $amount = is_array($amount_currency_array) && count($amount_currency_array) > 1 ? $amount_currency_array[1] : $record['amount'];
@@ -94,20 +145,27 @@
94 145 $data[] = array(
95 146 'email' => sanitize_email( $record['email'] ),
96 147 'currency' => sanitize_text_field( $currency ),
97 148 'amount' => floatval( $amount ),
149 + // 'payment_type' => sanitize_text_field( $record['payment_type'] ),
98 150 'transaction_id' => sanitize_text_field( $record['transaction_id'] ),
99 151 'source' => sanitize_text_field( $record['source'] ),
100 152 'status' => sanitize_text_field( $record['status'] ),
101 - 'payment_date' => date( 'Y-m-d H:i:s', strtotime( sanitize_text_field( $record['payment_date'] ) ) ),
153 + 'payment_date' => self::payment_date_from_csv( sanitize_text_field( $record['payment_date'] ) ),
102 154 'form_fields_info' => maybe_serialize( $better_form_fields ),
103 155 );
104 156 }
105 157
158 + if ( empty( $data ) ) {
159 + throw new Exception( __('No valid records found in the CSV file!', 'better-payment') );
160 + }
161 +
106 162 $table = DB::get_table_name();
107 163 $rowsInserted = $this->bulk_insert_transactions( $table, $data );
108 - if ( $rowsInserted) {
164 + if ( $rowsInserted !== false && $rowsInserted > 0 ) {
109 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') );
110 168 }
111 169 } catch (\Exception $exception) {
112 170 set_transient('better_payment_import_transactions_error', esc_html( $exception->getMessage() ), 30);
113 171 wp_safe_redirect( $_SERVER['HTTP_REFERER'] );
@@ -117,8 +175,39 @@
117 175 wp_safe_redirect( $_SERVER['HTTP_REFERER'] );
118 176 exit;
119 177 }
120 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 +
121 210 public function validate_csv_file( $file ){
122 211 $message = __('Invalid File!', 'better-payment');
123 212
124 213 if ( empty( $file ) ){
@@ -148,9 +237,9 @@
148 237
149 238 foreach ($rows as $row) {
150 239 ksort($row);
151 240 $rowPlaceholders = array();
152 - foreach ($row as $key => $value) {
241 + foreach ($row as $value) {
153 242 $data[] = $value;
154 243 $rowPlaceholders[] = is_numeric($value) ? '%d' : '%s';
155 244 }
156 245 $placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')';