| @@ -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) . ')'; |