From d8d2f03a50b8f25533655dd3e0e0c3efe168d759 Mon Sep 17 00:00:00 2001 From: Boro Sitnikovski Date: Thu, 3 May 2018 09:46:07 +0200 Subject: [PATCH 1/3] Introduce GDPR export/erase for customer tokens --- includes/class-wc-privacy-erasers.php | 52 +++++++++++++++++++ includes/class-wc-privacy-exporters.php | 45 ++++++++++++++++ includes/class-wc-privacy.php | 2 + .../class-wc-payment-token-data-store.php | 8 ++- 4 files changed, 106 insertions(+), 1 deletion(-) diff --git a/includes/class-wc-privacy-erasers.php b/includes/class-wc-privacy-erasers.php index eb5a55614f6..b535152231a 100644 --- a/includes/class-wc-privacy-erasers.php +++ b/includes/class-wc-privacy-erasers.php @@ -310,4 +310,56 @@ class WC_Privacy_Erasers { */ do_action( 'woocommerce_privacy_remove_order_personal_data', $order ); } + + /** + * Finds and erases customer tokens by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs + */ + public static function customer_tokens_eraser( $email_address, $page ) { + $response = array( + 'items_removed' => false, + 'items_retained' => false, + 'messages' => array(), + 'done' => true, + ); + + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + + if ( ! $user instanceof WP_User ) { + return $response; + } + + $tokens = WC_Payment_Tokens::get_tokens( array( + 'user_id' => $user->ID, + ) ); + + if ( empty( $tokens ) ) { + return $response; + } + + foreach ( $tokens as $token ) { + WC_Payment_Tokens::delete( $token->get_id() ); + + $erased = apply_filters( 'woocommerce_privacy_erase_customer_token', true, $token ); + + if ( $erased ) { + /* Translators: %s Prop name. */ + $response['messages'][] = sprintf( __( 'Removed token "%d"', 'woocommerce' ), $token->get_id() ); + $response['items_removed'] = true; + } + } + + /** + * Allow extensions to remove data for tokens and adjust the response. + * + * @since 3.4.0 + * @param array $response Array resonse data. Must include messages, num_items_removed, num_items_retained, done. + * @param array $tokens Array of tokens. + */ + return apply_filters( 'woocommerce_privacy_erase_personal_data_tokens', $response, $tokens ); + } } diff --git a/includes/class-wc-privacy-exporters.php b/includes/class-wc-privacy-exporters.php index 19dae3a7a38..7da7788a96e 100644 --- a/includes/class-wc-privacy-exporters.php +++ b/includes/class-wc-privacy-exporters.php @@ -348,4 +348,49 @@ class WC_Privacy_Exporters { return $personal_data; } + + /** + * Finds and exports customer tokens by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs + */ + public static function customer_tokens_exporter( $email_address, $page ) { + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + $data_to_export = array(); + + if ( ! $user instanceof WP_User ) { + return array( + 'data' => $data_to_export, + 'done' => true, + ); + } + + $tokens = WC_Payment_Tokens::get_tokens( array( + 'user_id' => $user->ID, + 'limit' => 10, + 'page' => $page, + ) ); + + if ( 0 < count( $tokens ) ) { + foreach ( $tokens as $token ) { + $data_to_export[] = array( + 'group_id' => 'woocommerce_tokens', + 'group_label' => __( 'Tokens', 'woocommerce' ), + 'item_id' => 'token-' . $token->get_id(), + 'data' => json_encode( $token->get_data() ), + ); + } + $done = 10 > count( $tokens ); + } else { + $done = true; + } + + return array( + 'data' => $data_to_export, + 'done' => true, + ); + } } diff --git a/includes/class-wc-privacy.php b/includes/class-wc-privacy.php index a1173494d09..e77943f226b 100644 --- a/includes/class-wc-privacy.php +++ b/includes/class-wc-privacy.php @@ -38,11 +38,13 @@ class WC_Privacy extends WC_Abstract_Privacy { $this->add_exporter( __( 'Customer Data', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_data_exporter' ) ); $this->add_exporter( __( 'Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'order_data_exporter' ) ); $this->add_exporter( __( 'Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'download_data_exporter' ) ); + $this->add_exporter( __( 'Customer Tokens', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_tokens_exporter' ) ); // This hook registers WooCommerce data erasers. $this->add_eraser( __( 'Customer Data', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_data_eraser' ) ); $this->add_eraser( __( 'Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'order_data_eraser' ) ); $this->add_eraser( __( 'Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'download_data_eraser' ) ); + $this->add_eraser( __( 'Customer Tokens', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_tokens_eraser' ) ); // Cleanup orders daily - this is a callback on a daily cron event. add_action( 'woocommerce_cleanup_personal_data', array( $this, 'queue_cleanup_personal_data' ) ); diff --git a/includes/data-stores/class-wc-payment-token-data-store.php b/includes/data-stores/class-wc-payment-token-data-store.php index eaf8b23338b..e6aee033c00 100644 --- a/includes/data-stores/class-wc-payment-token-data-store.php +++ b/includes/data-stores/class-wc-payment-token-data-store.php @@ -262,6 +262,12 @@ class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Payment $gateway_ids = $gateways->get_payment_gateway_ids(); } + $page = isset( $args['page'] ) ? absint( $args['page'] ) : 1; + $posts_per_page = isset( $args['limit'] ) ? absint( $args['limit'] ) : get_option( 'posts_per_page' ); + + $pgstrt = absint( ( $page - 1 ) * $posts_per_page ) . ', '; + $limits = 'LIMIT ' . $pgstrt . $posts_per_page; + $gateway_ids[] = ''; $where[] = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')"; @@ -270,7 +276,7 @@ class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Payment } // phpcs:ignore WordPress.WP.PreparedSQL.NotPrepared - $token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) ); + $token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) . ' ' . $limits ); return $token_results; } From 8782be0b026110709f54f19cc3576184c61e6ee4 Mon Sep 17 00:00:00 2001 From: Boro Sitnikovski Date: Thu, 3 May 2018 11:06:11 +0200 Subject: [PATCH 2/3] Use payment tokens instead of tokens --- includes/class-wc-privacy-erasers.php | 10 +++------- includes/class-wc-privacy-exporters.php | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/includes/class-wc-privacy-erasers.php b/includes/class-wc-privacy-erasers.php index b535152231a..6d4a17751d8 100644 --- a/includes/class-wc-privacy-erasers.php +++ b/includes/class-wc-privacy-erasers.php @@ -344,13 +344,9 @@ class WC_Privacy_Erasers { foreach ( $tokens as $token ) { WC_Payment_Tokens::delete( $token->get_id() ); - $erased = apply_filters( 'woocommerce_privacy_erase_customer_token', true, $token ); - - if ( $erased ) { - /* Translators: %s Prop name. */ - $response['messages'][] = sprintf( __( 'Removed token "%d"', 'woocommerce' ), $token->get_id() ); - $response['items_removed'] = true; - } + /* Translators: %s Prop name. */ + $response['messages'][] = sprintf( __( 'Removed payment token "%d"', 'woocommerce' ), $token->get_id() ); + $response['items_removed'] = true; } /** diff --git a/includes/class-wc-privacy-exporters.php b/includes/class-wc-privacy-exporters.php index 7da7788a96e..f80c080b297 100644 --- a/includes/class-wc-privacy-exporters.php +++ b/includes/class-wc-privacy-exporters.php @@ -378,7 +378,7 @@ class WC_Privacy_Exporters { foreach ( $tokens as $token ) { $data_to_export[] = array( 'group_id' => 'woocommerce_tokens', - 'group_label' => __( 'Tokens', 'woocommerce' ), + 'group_label' => __( 'Payment Tokens', 'woocommerce' ), 'item_id' => 'token-' . $token->get_id(), 'data' => json_encode( $token->get_data() ), ); From 868034886ab0cfbbf54a16fb14670630f68fcc49 Mon Sep 17 00:00:00 2001 From: Boro Sitnikovski Date: Thu, 3 May 2018 12:00:07 +0200 Subject: [PATCH 3/3] Use display name for tokens --- includes/class-wc-privacy-exporters.php | 2 +- includes/payment-tokens/class-wc-payment-token-echeck.php | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/includes/class-wc-privacy-exporters.php b/includes/class-wc-privacy-exporters.php index f80c080b297..2fde99f0fe5 100644 --- a/includes/class-wc-privacy-exporters.php +++ b/includes/class-wc-privacy-exporters.php @@ -380,7 +380,7 @@ class WC_Privacy_Exporters { 'group_id' => 'woocommerce_tokens', 'group_label' => __( 'Payment Tokens', 'woocommerce' ), 'item_id' => 'token-' . $token->get_id(), - 'data' => json_encode( $token->get_data() ), + 'data' => $token->get_display_name(), ); } $done = 10 > count( $tokens ); diff --git a/includes/payment-tokens/class-wc-payment-token-echeck.php b/includes/payment-tokens/class-wc-payment-token-echeck.php index c04201ef908..878710cdd92 100644 --- a/includes/payment-tokens/class-wc-payment-token-echeck.php +++ b/includes/payment-tokens/class-wc-payment-token-echeck.php @@ -45,7 +45,12 @@ class WC_Payment_Token_ECheck extends WC_Payment_Token { * @return string */ public function get_display_name( $deprecated = '' ) { - return __( 'eCheck', 'woocommerce' ); + $display = sprintf( + /* translators: 1: credit card type 2: last 4 digits 3: expiry month 4: expiry year */ + __( 'eCheck ending in %1$s', 'woocommerce' ), + $this->get_last4() + ); + return $display; } /**