WP maple cart and page subtitle plugin upload
This commit is contained in:
parent
b3e87772ec
commit
c85895d306
18 changed files with 5741 additions and 0 deletions
|
|
@ -0,0 +1,502 @@
|
|||
<?php
|
||||
/**
|
||||
* GDPR Privacy Handler for Maple Carts.
|
||||
*
|
||||
* Integrates with WordPress and WooCommerce privacy tools.
|
||||
*
|
||||
* @package MapleCarts
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Privacy class.
|
||||
*/
|
||||
class Maple_Carts_Privacy {
|
||||
|
||||
/**
|
||||
* Single instance.
|
||||
*
|
||||
* @var Maple_Carts_Privacy
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance.
|
||||
*
|
||||
* @return Maple_Carts_Privacy
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( is_null( self::$instance ) ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
// WordPress Privacy hooks.
|
||||
add_action( 'admin_init', [ $this, 'add_privacy_policy_content' ] );
|
||||
add_filter( 'wp_privacy_personal_data_exporters', [ $this, 'register_data_exporter' ] );
|
||||
add_filter( 'wp_privacy_personal_data_erasers', [ $this, 'register_data_eraser' ] );
|
||||
|
||||
// Checkout consent checkbox.
|
||||
if ( 'yes' === Maple_Carts::get_option( 'require_consent', 'no' ) ) {
|
||||
add_action( 'woocommerce_review_order_before_submit', [ $this, 'add_consent_checkbox' ] );
|
||||
add_action( 'woocommerce_checkout_process', [ $this, 'validate_consent_checkbox' ] );
|
||||
}
|
||||
|
||||
// Data deletion request from unsubscribe.
|
||||
add_action( 'wp', [ $this, 'handle_data_deletion_request' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add privacy policy suggested content.
|
||||
*/
|
||||
public function add_privacy_policy_content() {
|
||||
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $this->get_privacy_policy_content();
|
||||
wp_add_privacy_policy_content( 'Maple Carts', $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get privacy policy suggested content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_privacy_policy_content() {
|
||||
$retention_days = Maple_Carts::get_option( 'delete_after_days', 90 );
|
||||
|
||||
return sprintf(
|
||||
'<h2>%s</h2>
|
||||
<p>%s</p>
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<ul>
|
||||
<li>%s</li>
|
||||
<li>%s</li>
|
||||
<li>%s</li>
|
||||
<li>%s</li>
|
||||
<li>%s</li>
|
||||
<li>%s</li>
|
||||
</ul>
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>
|
||||
<h3>%s</h3>
|
||||
<p>%s</p>',
|
||||
__( 'Abandoned Cart Recovery', 'maple-carts' ),
|
||||
__( 'We use abandoned cart recovery technology to help remind you about items left in your shopping cart. This section explains what data we collect and how we use it.', 'maple-carts' ),
|
||||
__( 'What data we collect', 'maple-carts' ),
|
||||
__( 'When you begin the checkout process, we may collect:', 'maple-carts' ),
|
||||
__( 'Email address', 'maple-carts' ),
|
||||
__( 'Name and phone number', 'maple-carts' ),
|
||||
__( 'Billing address', 'maple-carts' ),
|
||||
__( 'Cart contents and total', 'maple-carts' ),
|
||||
__( 'IP address', 'maple-carts' ),
|
||||
__( 'Browser information', 'maple-carts' ),
|
||||
__( 'Why we collect this data', 'maple-carts' ),
|
||||
__( 'We collect this data to send you reminder emails if you leave items in your cart without completing your purchase. These emails may include a discount code to encourage you to complete your order.', 'maple-carts' ),
|
||||
__( 'How long we retain your data', 'maple-carts' ),
|
||||
sprintf(
|
||||
/* translators: %d: number of days */
|
||||
__( 'Abandoned cart data is automatically deleted after %d days. You can request immediate deletion at any time.', 'maple-carts' ),
|
||||
$retention_days
|
||||
),
|
||||
__( 'Your rights', 'maple-carts' ),
|
||||
__( 'You can unsubscribe from abandoned cart emails at any time by clicking the unsubscribe link in any email. You can also request a copy of your data or request deletion through our privacy tools.', 'maple-carts' ),
|
||||
__( 'Third-party sharing', 'maple-carts' ),
|
||||
__( 'We do not share your abandoned cart data with any third parties. Emails are sent through our own email system or your configured SMTP provider.', 'maple-carts' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register data exporter.
|
||||
*
|
||||
* @param array $exporters Existing exporters.
|
||||
* @return array
|
||||
*/
|
||||
public function register_data_exporter( $exporters ) {
|
||||
$exporters['maple-carts'] = [
|
||||
'exporter_friendly_name' => __( 'Maple Carts Abandoned Cart Data', 'maple-carts' ),
|
||||
'callback' => [ $this, 'export_personal_data' ],
|
||||
];
|
||||
return $exporters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register data eraser.
|
||||
*
|
||||
* @param array $erasers Existing erasers.
|
||||
* @return array
|
||||
*/
|
||||
public function register_data_eraser( $erasers ) {
|
||||
$erasers['maple-carts'] = [
|
||||
'eraser_friendly_name' => __( 'Maple Carts Abandoned Cart Data', 'maple-carts' ),
|
||||
'callback' => [ $this, 'erase_personal_data' ],
|
||||
];
|
||||
return $erasers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export personal data for a user.
|
||||
*
|
||||
* @param string $email_address Email address.
|
||||
* @param int $page Page number.
|
||||
* @return array
|
||||
*/
|
||||
public function export_personal_data( $email_address, $page = 1 ) {
|
||||
global $wpdb;
|
||||
|
||||
$export_items = [];
|
||||
$table = $wpdb->prefix . MAPLE_CARTS_TABLE;
|
||||
$per_page = 10;
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
$carts = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE email = %s LIMIT %d OFFSET %d",
|
||||
$email_address,
|
||||
$per_page,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $carts as $cart ) {
|
||||
$cart_contents = maybe_unserialize( $cart->cart_contents );
|
||||
$product_names = [];
|
||||
|
||||
if ( is_array( $cart_contents ) ) {
|
||||
foreach ( $cart_contents as $item ) {
|
||||
$product_names[] = $item['name'] ?? 'Unknown Product';
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
[
|
||||
'name' => __( 'Email', 'maple-carts' ),
|
||||
'value' => $cart->email,
|
||||
],
|
||||
[
|
||||
'name' => __( 'Name', 'maple-carts' ),
|
||||
'value' => $cart->customer_name ?: __( 'Not provided', 'maple-carts' ),
|
||||
],
|
||||
[
|
||||
'name' => __( 'Phone', 'maple-carts' ),
|
||||
'value' => $cart->customer_phone ?: __( 'Not provided', 'maple-carts' ),
|
||||
],
|
||||
[
|
||||
'name' => __( 'Cart Contents', 'maple-carts' ),
|
||||
'value' => implode( ', ', $product_names ),
|
||||
],
|
||||
[
|
||||
'name' => __( 'Cart Total', 'maple-carts' ),
|
||||
'value' => wc_price( $cart->cart_total, [ 'currency' => $cart->currency ] ),
|
||||
],
|
||||
[
|
||||
'name' => __( 'Status', 'maple-carts' ),
|
||||
'value' => ucfirst( $cart->status ),
|
||||
],
|
||||
[
|
||||
'name' => __( 'IP Address', 'maple-carts' ),
|
||||
'value' => $cart->ip_address ?: __( 'Not recorded', 'maple-carts' ),
|
||||
],
|
||||
[
|
||||
'name' => __( 'Created', 'maple-carts' ),
|
||||
'value' => $cart->created_at,
|
||||
],
|
||||
[
|
||||
'name' => __( 'Unsubscribed', 'maple-carts' ),
|
||||
'value' => $cart->unsubscribed ? __( 'Yes', 'maple-carts' ) : __( 'No', 'maple-carts' ),
|
||||
],
|
||||
];
|
||||
|
||||
// Add billing data if present.
|
||||
$billing_data = maybe_unserialize( $cart->billing_data );
|
||||
if ( ! empty( $billing_data ) ) {
|
||||
$address_parts = array_filter( [
|
||||
$billing_data['address_1'] ?? '',
|
||||
$billing_data['address_2'] ?? '',
|
||||
$billing_data['city'] ?? '',
|
||||
$billing_data['state'] ?? '',
|
||||
$billing_data['postcode'] ?? '',
|
||||
$billing_data['country'] ?? '',
|
||||
] );
|
||||
|
||||
if ( ! empty( $address_parts ) ) {
|
||||
$data[] = [
|
||||
'name' => __( 'Billing Address', 'maple-carts' ),
|
||||
'value' => implode( ', ', $address_parts ),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$export_items[] = [
|
||||
'group_id' => 'maple-carts',
|
||||
'group_label' => __( 'Abandoned Cart Data', 'maple-carts' ),
|
||||
'group_description' => __( 'Cart data collected during checkout.', 'maple-carts' ),
|
||||
'item_id' => 'cart-' . $cart->id,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
// Check if there are more records.
|
||||
$total = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE email = %s",
|
||||
$email_address
|
||||
)
|
||||
);
|
||||
|
||||
$done = ( $page * $per_page ) >= $total;
|
||||
|
||||
return [
|
||||
'data' => $export_items,
|
||||
'done' => $done,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase personal data for a user.
|
||||
*
|
||||
* @param string $email_address Email address.
|
||||
* @param int $page Page number.
|
||||
* @return array
|
||||
*/
|
||||
public function erase_personal_data( $email_address, $page = 1 ) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . MAPLE_CARTS_TABLE;
|
||||
$log_table = $wpdb->prefix . MAPLE_CARTS_EMAIL_LOG_TABLE;
|
||||
$per_page = 50;
|
||||
$items_removed = 0;
|
||||
$items_retained = 0;
|
||||
|
||||
// Get carts to delete.
|
||||
$carts = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE email = %s LIMIT %d",
|
||||
$email_address,
|
||||
$per_page
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $carts as $cart ) {
|
||||
// Delete email logs first.
|
||||
$wpdb->delete( $log_table, [ 'cart_id' => $cart->id ] );
|
||||
|
||||
// Delete cart.
|
||||
$deleted = $wpdb->delete( $table, [ 'id' => $cart->id ] );
|
||||
|
||||
if ( $deleted ) {
|
||||
$items_removed++;
|
||||
} else {
|
||||
$items_retained++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are more records.
|
||||
$remaining = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE email = %s",
|
||||
$email_address
|
||||
)
|
||||
);
|
||||
|
||||
$done = ( $remaining == 0 );
|
||||
|
||||
return [
|
||||
'items_removed' => $items_removed,
|
||||
'items_retained' => $items_retained,
|
||||
'messages' => [],
|
||||
'done' => $done,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add consent checkbox to checkout.
|
||||
*/
|
||||
public function add_consent_checkbox() {
|
||||
$consent_text = Maple_Carts::get_option(
|
||||
'consent_text',
|
||||
__( 'I agree to receive cart reminder emails if I don\'t complete my purchase.', 'maple-carts' )
|
||||
);
|
||||
|
||||
woocommerce_form_field(
|
||||
'maple_carts_consent',
|
||||
[
|
||||
'type' => 'checkbox',
|
||||
'class' => [ 'form-row maple-carts-consent' ],
|
||||
'label' => esc_html( $consent_text ),
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate consent checkbox.
|
||||
*/
|
||||
public function validate_consent_checkbox() {
|
||||
// If consent is required and not given, we don't track the cart.
|
||||
// This is handled in the tracking class.
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has given consent.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_consent() {
|
||||
// If consent is not required, always return true.
|
||||
if ( 'yes' !== Maple_Carts::get_option( 'require_consent', 'no' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if consent checkbox was checked.
|
||||
if ( isset( $_POST['maple_carts_consent'] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check session for previous consent.
|
||||
if ( WC()->session && WC()->session->get( 'maple_carts_consent' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record consent.
|
||||
*/
|
||||
public static function record_consent() {
|
||||
if ( WC()->session ) {
|
||||
WC()->session->set( 'maple_carts_consent', true );
|
||||
WC()->session->set( 'maple_carts_consent_time', current_time( 'mysql' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data deletion request from unsubscribe page.
|
||||
*/
|
||||
public function handle_data_deletion_request() {
|
||||
if ( ! isset( $_GET['maple_delete_data'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$token = sanitize_text_field( wp_unslash( $_GET['maple_delete_data'] ) );
|
||||
|
||||
// Verify signed token (same as unsubscribe).
|
||||
$tracking = Maple_Carts_Tracking::instance();
|
||||
$data = $this->verify_deletion_token( $token );
|
||||
|
||||
if ( ! $data || empty( $data['sid'] ) ) {
|
||||
wc_add_notice( __( 'This data deletion link has expired or is invalid.', 'maple-carts' ), 'error' );
|
||||
wp_safe_redirect( wc_get_page_permalink( 'shop' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$session_id = sanitize_text_field( $data['sid'] );
|
||||
$cart = Maple_Carts_DB::get_cart_by_session( $session_id );
|
||||
|
||||
if ( $cart ) {
|
||||
// Delete all data for this cart.
|
||||
Maple_Carts_DB::delete_cart( $cart->id );
|
||||
|
||||
wc_add_notice( __( 'Your cart data has been permanently deleted.', 'maple-carts' ), 'success' );
|
||||
}
|
||||
|
||||
wp_safe_redirect( wc_get_page_permalink( 'shop' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify deletion token (reuses tracking token verification).
|
||||
*
|
||||
* @param string $token Token to verify.
|
||||
* @return array|false
|
||||
*/
|
||||
private function verify_deletion_token( $token ) {
|
||||
$decoded = base64_decode( $token, true );
|
||||
if ( ! $decoded || strpos( $decoded, '|' ) === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts = explode( '|', $decoded, 2 );
|
||||
if ( count( $parts ) !== 2 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list( $payload, $signature ) = $parts;
|
||||
|
||||
// Verify HMAC signature.
|
||||
$secret = wp_salt( 'auth' ) . 'maple_carts';
|
||||
$expected_signature = hash_hmac( 'sha256', $payload, $secret );
|
||||
|
||||
if ( ! hash_equals( $expected_signature, $signature ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = json_decode( $payload, true );
|
||||
if ( ! $data ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Token valid for 90 days.
|
||||
if ( isset( $data['t'] ) && ( time() - $data['t'] ) > ( 90 * DAY_IN_SECONDS ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate data deletion URL.
|
||||
*
|
||||
* @param string $session_id Session ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function generate_deletion_url( $session_id ) {
|
||||
$data = [
|
||||
'sid' => $session_id,
|
||||
't' => time(),
|
||||
];
|
||||
|
||||
$payload = wp_json_encode( $data );
|
||||
$secret = wp_salt( 'auth' ) . 'maple_carts';
|
||||
$signature = hash_hmac( 'sha256', $payload, $secret );
|
||||
$token = base64_encode( $payload . '|' . $signature );
|
||||
|
||||
return add_query_arg( 'maple_delete_data', $token, home_url() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get anonymized data (for when full deletion isn't required).
|
||||
*
|
||||
* @param int $cart_id Cart ID.
|
||||
*/
|
||||
public static function anonymize_cart( $cart_id ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . MAPLE_CARTS_TABLE;
|
||||
|
||||
$wpdb->update(
|
||||
$table,
|
||||
[
|
||||
'email' => 'deleted@anonymized.invalid',
|
||||
'customer_name' => 'Anonymized',
|
||||
'customer_phone' => null,
|
||||
'billing_data' => null,
|
||||
'ip_address' => '0.0.0.0',
|
||||
'user_agent' => null,
|
||||
],
|
||||
[ 'id' => $cart_id ]
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue