| @@ -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' ); |
| @@ -79,29 +135,10 @@ | ||
| 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 | - if ( ! empty( $record['payment_type'] ) && strtolower($record['payment_type']) === 'subscription' ) { | |
| 91 | - $better_form_fields_selected = isset( $record['form_fields_info'] ) ? maybe_unserialize( $record['form_fields_info'] ) : []; | |
| 92 | - | |
| 93 | - $better_form_fields['subscription_id'] = isset( $better_form_fields_selected['subscription_id'] ) ? sanitize_text_field( $better_form_fields_selected['subscription_id'] ) : ''; | |
| 94 | - $better_form_fields['subscription_customer_id'] = isset( $better_form_fields_selected['subscription_customer_id'] ) ? sanitize_text_field( $better_form_fields_selected['subscription_customer_id'] ) : ''; | |
| 95 | - $better_form_fields['subscription_plan_id'] = isset( $better_form_fields_selected['subscription_plan_id'] ) ? sanitize_text_field( $better_form_fields_selected['subscription_plan_id'] ) : ''; | |
| 96 | - $better_form_fields['subscription_interval'] = isset( $better_form_fields_selected['subscription_interval'] ) ? sanitize_text_field( $better_form_fields_selected['subscription_interval'] ) : ''; | |
| 97 | - $better_form_fields['subscription_current_period_start'] = isset( $better_form_fields_selected['subscription_current_period_start'] ) ? sanitize_text_field( $better_form_fields_selected['subscription_current_period_start'] ) : ''; | |
| 98 | - $better_form_fields['subscription_current_period_end'] = isset( $better_form_fields_selected['subscription_current_period_end'] ) ? sanitize_text_field( $better_form_fields_selected['subscription_current_period_end'] ) : ''; | |
| 99 | - $better_form_fields['subscription_status'] = isset( $better_form_fields_selected['subscription_status'] ) ? sanitize_text_field( $better_form_fields_selected['subscription_status'] ) : ''; | |
| 100 | - $better_form_fields['subscription_created_date'] = isset( $better_form_fields_selected['subscription_created_date'] ) ? sanitize_text_field( $better_form_fields_selected['subscription_created_date'] ) : ''; | |
| 101 | - $better_form_fields['is_payment_split_payment'] = isset( $better_form_fields_selected['is_payment_split_payment'] ) ? sanitize_text_field( $better_form_fields_selected['is_payment_split_payment'] ) : ''; | |
| 102 | - } | |
| 103 | - | |
| 104 | 141 | $amount_currency_array = explode(' ', $record['amount']); |
| 105 | 142 | $currency = is_array($amount_currency_array) && count($amount_currency_array) > 1 ? $amount_currency_array[0] : esc_html('USD'); |
| 106 | 143 | $amount = is_array($amount_currency_array) && count($amount_currency_array) > 1 ? $amount_currency_array[1] : $record['amount']; |
| 107 | 144 | |
| @@ -112,9 +149,9 @@ | ||
| 112 | 149 | // 'payment_type' => sanitize_text_field( $record['payment_type'] ), |
| 113 | 150 | 'transaction_id' => sanitize_text_field( $record['transaction_id'] ), |
| 114 | 151 | 'source' => sanitize_text_field( $record['source'] ), |
| 115 | 152 | 'status' => sanitize_text_field( $record['status'] ), |
| 116 | - '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'] ) ), | |
| 117 | 154 | 'form_fields_info' => maybe_serialize( $better_form_fields ), |
| 118 | 155 | ); |
| 119 | 156 | } |
| 120 | 157 | |
| @@ -136,8 +173,39 @@ | ||
| 136 | 173 | } |
| 137 | 174 | |
| 138 | 175 | wp_safe_redirect( $_SERVER['HTTP_REFERER'] ); |
| 139 | 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; | |
| 140 | 208 | } |
| 141 | 209 | |
| 142 | 210 | public function validate_csv_file( $file ){ |
| 143 | 211 | $message = __('Invalid File!', 'better-payment'); |