__( 'OrderArc Archived Orders', 'orderarc' ), 'callback' => array( __CLASS__, 'exporter' ), ); return $exporters; } public static function register_eraser( $erasers ) { $erasers['orderarc-archived-orders'] = array( 'eraser_friendly_name' => __( 'OrderArc Archived Orders', 'orderarc' ), 'callback' => array( __CLASS__, 'eraser' ), ); return $erasers; } private static function anon( $type ) { return function_exists( 'wp_privacy_anonymize_data' ) ? wp_privacy_anonymize_data( $type ) : ( 'email' === $type ? 'deleted@site.invalid' : '' ); } /** Se puede leer y cambiar el archivo (existe y no es el de otra copia del sitio). */ private static function usable() { return OrderArc_Engine::archive_ready() && '' === OrderArc_Engine::archive_blocked(); } /* ---------------------------------------------------------------- Marcas */ /** Tabla de marcas (vive con el archivo: si el archivo está en otra base, se mueve con él). */ public static function marks() { return OrderArc_Engine::arch( 'anon_marks' ); } public static function ensure_marks() { global $wpdb; $t = self::marks(); if ( ! OrderArc_Engine::table_exists( $t ) ) { $wpdb->query( $wpdb->prepare( 'CREATE TABLE IF NOT EXISTS %i (order_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, done_gmt DATETIME NOT NULL) ENGINE=InnoDB', $t ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange -- tabla propia. } return null !== $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $t ) ); } /** * Pedidos que vuelven a la tienda: si estaban anonimizados, reciben en la tabla viva la meta `_anonymized` * (con un id nuevo de esa tabla) y la nota de WooCommerce, y salen de la tabla de marcas. */ public static function on_restored( $ids ) { global $wpdb; $ids = array_values( array_filter( array_map( 'intval', (array) $ids ) ) ); $t = self::marks(); if ( ! $ids || ! OrderArc_Engine::table_exists( $t ) ) { return; } $id_placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); $done = array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT order_id FROM %i WHERE order_id IN ($id_placeholders)", array_merge( array( $t ), $ids ) ) ) ); if ( ! $done ) { return; } $done_placeholders = implode( ',', array_fill( 0, count( $done ), '%d' ) ); $lo = OrderArc_Engine::live( 'wc_orders' ); $lm = OrderArc_Engine::live( 'wc_orders_meta' ); if ( OrderArc_Engine::table_exists( $lo ) && OrderArc_Engine::table_exists( $lm ) ) { $wpdb->query( $wpdb->prepare( "INSERT INTO %i (order_id, meta_key, meta_value) SELECT o.id, '_anonymized', 'yes' FROM %i o WHERE o.id IN ($done_placeholders) AND NOT EXISTS (SELECT 1 FROM %i m WHERE m.order_id=o.id AND m.meta_key='_anonymized')", array_merge( array( $lm, $lo ), $done, array( $lm ) ) ) ); } $lp = OrderArc_Engine::live( 'posts' ); $lpm = OrderArc_Engine::live( 'postmeta' ); $wpdb->query( $wpdb->prepare( "INSERT INTO %i (post_id, meta_key, meta_value) SELECT p.ID, '_anonymized', 'yes' FROM %i p WHERE p.ID IN ($done_placeholders) AND p.post_type IN ('shop_order','shop_order_refund') AND NOT EXISTS (SELECT 1 FROM %i m WHERE m.post_id=p.ID AND m.meta_key='_anonymized')", array_merge( array( $lpm, $lp ), $done, array( $lpm ) ) ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE order_id IN ($done_placeholders)", array_merge( array( $t ), $done ) ) ); OrderArc_Engine::flush_caches( $done ); foreach ( $done as $id ) { $o = wc_get_order( $id ); if ( $o && 'shop_order_refund' !== $o->get_type() ) { $o->add_order_note( __( 'Personal data removed.', 'woocommerce' ) ); // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- texto de WooCommerce, ya traducido. } } } /* ---------------------------------------------------------------- Anonimizar */ /** * Anonimiza pedidos y reembolsos del archivo, en una sola transacción. Devuelve cuántos quedaron marcados * (0 si algo falló: entonces no cambia nada). * * $keep_tax_data = true (retención de la versión Pro): conserva país, provincia y el ID de transacción del * pago, que pide la contabilidad. false (solicitud de borrado): los borra también, como WooCommerce. * Siempre conserva importes, impuestos, fechas y productos. Borra las notas internas del pedido. */ public static function anonymize( $ids, $keep_tax_data ) { global $wpdb; $ids = array_values( array_unique( array_filter( array_map( 'intval', (array) $ids ) ) ) ); if ( ! $ids || ! self::ensure_marks() ) { return 0; } $id_placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); $text = self::anon( 'text' ); $email = self::anon( 'email' ); $wpdb->query( 'START TRANSACTION' ); $ok = true; // HPOS. $hp = OrderArc_Engine::arch( 'wc_orders' ); if ( OrderArc_Engine::table_exists( $hp ) ) { $ha = OrderArc_Engine::arch( 'wc_order_addresses' ); $hm = OrderArc_Engine::arch( 'wc_orders_meta' ); if ( $keep_tax_data ) { $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "UPDATE %i SET billing_email=IF(billing_email IS NULL OR billing_email='', billing_email, %s), ip_address=IF(ip_address IS NULL OR ip_address='', ip_address, '0.0.0.0'), user_agent='', customer_note='', customer_id=0 WHERE id IN ($id_placeholders)", array_merge( array( $hp, $email ), $ids ) ) ); } else { $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "UPDATE %i SET billing_email=IF(billing_email IS NULL OR billing_email='', billing_email, %s), ip_address=IF(ip_address IS NULL OR ip_address='', ip_address, '0.0.0.0'), user_agent='', customer_note='', customer_id=0, transaction_id='' WHERE id IN ($id_placeholders)", array_merge( array( $hp, $email ), $ids ) ) ); } if ( OrderArc_Engine::table_exists( $ha ) ) { $vals = array( $ha, $text, $text, $text, $text, $text, $text, $text, $email, $text ); if ( $keep_tax_data ) { $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "UPDATE %i SET first_name=IF(first_name IS NULL OR first_name='', first_name, %s), last_name=IF(last_name IS NULL OR last_name='', last_name, %s), company=IF(company IS NULL OR company='', company, %s), address_1=IF(address_1 IS NULL OR address_1='', address_1, %s), address_2=IF(address_2 IS NULL OR address_2='', address_2, %s), city=IF(city IS NULL OR city='', city, %s), postcode=IF(postcode IS NULL OR postcode='', postcode, %s), email=IF(email IS NULL OR email='', email, %s), phone=IF(phone IS NULL OR phone='', phone, %s) WHERE order_id IN ($id_placeholders)", array_merge( $vals, $ids ) ) ); } else { $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "UPDATE %i SET first_name=IF(first_name IS NULL OR first_name='', first_name, %s), last_name=IF(last_name IS NULL OR last_name='', last_name, %s), company=IF(company IS NULL OR company='', company, %s), address_1=IF(address_1 IS NULL OR address_1='', address_1, %s), address_2=IF(address_2 IS NULL OR address_2='', address_2, %s), city=IF(city IS NULL OR city='', city, %s), postcode=IF(postcode IS NULL OR postcode='', postcode, %s), email=IF(email IS NULL OR email='', email, %s), phone=IF(phone IS NULL OR phone='', phone, %s), state=IF(state IS NULL OR state='', state, ''), country=IF(country IS NULL OR country='', country, '') WHERE order_id IN ($id_placeholders)", array_merge( $vals, $ids ) ) ); } } if ( OrderArc_Engine::table_exists( $hm ) ) { $mk = array( '_billing_address_index', '_shipping_address_index' ); if ( ! $keep_tax_data ) { $mk = array_merge( $mk, array( 'Payer first name', 'Payer last name', 'Payer PayPal address', 'Transaction ID' ) ); } $key_placeholders = implode( ',', array_fill( 0, count( $mk ), '%s' ) ); $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "UPDATE %i SET meta_value=%s WHERE meta_key IN ($key_placeholders) AND meta_value<>'' AND order_id IN ($id_placeholders)", array_merge( array( $hm, $text ), $mk, $ids ) ) ); } } // wp_posts / wp_postmeta (legacy, o la copia sincronizada). $pp = OrderArc_Engine::arch( 'posts' ); $pm = OrderArc_Engine::arch( 'postmeta' ); if ( OrderArc_Engine::table_exists( $pp ) && OrderArc_Engine::table_exists( $pm ) ) { $keys = array( '_customer_ip_address', '_customer_user_agent', '_customer_user', '_billing_address_index', '_shipping_address_index' ); if ( ! $keep_tax_data ) { $keys = array_merge( $keys, array( '_transaction_id', 'Payer first name', 'Payer last name', 'Payer PayPal address', 'Transaction ID' ) ); } $addr = $keep_tax_data ? self::ADDRESS : array_merge( self::ADDRESS, array( 'state', 'country' ) ); foreach ( array( 'billing', 'shipping' ) as $t ) { foreach ( $addr as $c ) { $keys[] = '_' . $t . '_' . $c; } } $key_placeholders = implode( ',', array_fill( 0, count( $keys ), '%s' ) ); $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "UPDATE %i SET meta_value=CASE WHEN meta_value IS NULL OR meta_value='' THEN meta_value WHEN meta_key LIKE %s THEN %s WHEN meta_key='_customer_ip_address' THEN '0.0.0.0' WHEN meta_key IN ('_customer_user_agent','_transaction_id') THEN '' WHEN meta_key='_customer_user' THEN '0' WHEN meta_key LIKE %s OR meta_key LIKE %s THEN '' ELSE %s END WHERE meta_key IN ($key_placeholders) AND post_id IN ($id_placeholders)", array_merge( array( $pm, '%\\_email', $email, '%\\_state', '%\\_country', $text ), $keys, $ids ) ) ); $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "UPDATE %i SET post_excerpt='' WHERE post_type IN ('shop_order','shop_order_refund') AND ID IN ($id_placeholders)", array_merge( array( $pp ), $ids ) ) ); } // Notas internas del pedido (y su meta): se borran, como en el borrado de datos de WooCommerce. $ac = OrderArc_Engine::arch( 'comments' ); $acm = OrderArc_Engine::arch( 'commentmeta' ); if ( OrderArc_Engine::table_exists( $ac ) ) { if ( OrderArc_Engine::table_exists( $acm ) ) { $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "DELETE m FROM %i m JOIN %i c ON c.comment_ID=m.comment_id WHERE c.comment_type='order_note' AND c.comment_post_ID IN ($id_placeholders)", array_merge( array( $acm, $ac ), $ids ) ) ); } $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE comment_type='order_note' AND comment_post_ID IN ($id_placeholders)", array_merge( array( $ac ), $ids ) ) ); } // Marca de cada pedido (INSERT IGNORE por si una pasada anterior quedó a medias). $row_placeholders = implode( ',', array_fill( 0, count( $ids ), '(%d, UTC_TIMESTAMP())' ) ); $ok = $ok && false !== $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO %i (order_id, done_gmt) VALUES $row_placeholders", array_merge( array( self::marks() ), $ids ) ) ); if ( ! $ok ) { $wpdb->query( 'ROLLBACK' ); return 0; } $wpdb->query( 'COMMIT' ); delete_transient( 'orderarc_arch_sources' ); return count( $ids ); } /* ---------------------------------------------------------------- Buscar por persona */ /** * Pedidos archivados (no reembolsos) de un email o de la cuenta con ese email, del más nuevo al más viejo. * $exclude_marked: sin los que ya están anonimizados (para el borrado, que los va sacando de la lista). */ private static function orders_for( $email, $offset, $limit, $exclude_marked = false ) { global $wpdb; $user = get_user_by( 'email', $email ); $uid = $user ? (int) $user->ID : -1; if ( $exclude_marked && ! self::ensure_marks() ) { return array(); } $mk = self::marks(); $ids = array(); $hp = OrderArc_Engine::arch( 'wc_orders' ); $ha = OrderArc_Engine::arch( 'wc_order_addresses' ); if ( OrderArc_Engine::table_exists( $hp ) && OrderArc_Engine::table_exists( $ha ) ) { if ( $exclude_marked ) { $ids = $wpdb->get_col( $wpdb->prepare( "SELECT o.id FROM %i o WHERE o.type='shop_order' AND ( o.billing_email=%s OR o.customer_id=%d OR EXISTS (SELECT 1 FROM %i a WHERE a.order_id=o.id AND a.email=%s) ) AND NOT EXISTS (SELECT 1 FROM %i k WHERE k.order_id=o.id)", $hp, $email, $uid, $ha, $email, $mk ) ); } else { $ids = $wpdb->get_col( $wpdb->prepare( "SELECT o.id FROM %i o WHERE o.type='shop_order' AND ( o.billing_email=%s OR o.customer_id=%d OR EXISTS (SELECT 1 FROM %i a WHERE a.order_id=o.id AND a.email=%s) )", $hp, $email, $uid, $ha, $email ) ); } } $pp = OrderArc_Engine::arch( 'posts' ); $pm = OrderArc_Engine::arch( 'postmeta' ); if ( OrderArc_Engine::table_exists( $pp ) && OrderArc_Engine::table_exists( $pm ) ) { if ( $exclude_marked ) { $more = $wpdb->get_col( $wpdb->prepare( "SELECT p.ID FROM %i p WHERE p.post_type='shop_order' AND EXISTS (SELECT 1 FROM %i m WHERE m.post_id=p.ID AND ( (m.meta_key='_billing_email' AND m.meta_value=%s) OR (m.meta_key='_customer_user' AND m.meta_value=%s) )) AND NOT EXISTS (SELECT 1 FROM %i k WHERE k.order_id=p.ID)", $pp, $pm, $email, (string) $uid, $mk ) ); } else { $more = $wpdb->get_col( $wpdb->prepare( "SELECT p.ID FROM %i p WHERE p.post_type='shop_order' AND EXISTS (SELECT 1 FROM %i m WHERE m.post_id=p.ID AND ( (m.meta_key='_billing_email' AND m.meta_value=%s) OR (m.meta_key='_customer_user' AND m.meta_value=%s) ))", $pp, $pm, $email, (string) $uid ) ); } $ids = array_merge( $ids, $more ); } $ids = array_values( array_unique( array_map( 'intval', $ids ) ) ); rsort( $ids ); return array_slice( $ids, $offset, $limit ); } /** Reembolsos archivados de esos pedidos (también se anonimizan). */ private static function refunds_of( $ids ) { global $wpdb; if ( ! $ids ) { return array(); } $out = array(); $id_placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); $hp = OrderArc_Engine::arch( 'wc_orders' ); if ( OrderArc_Engine::table_exists( $hp ) ) { $out = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM %i WHERE type='shop_order_refund' AND parent_order_id IN ($id_placeholders)", array_merge( array( $hp ), $ids ) ) ); } $pp = OrderArc_Engine::arch( 'posts' ); if ( OrderArc_Engine::table_exists( $pp ) ) { $out = array_merge( $out, $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM %i WHERE post_type='shop_order_refund' AND post_parent IN ($id_placeholders)", array_merge( array( $pp ), $ids ) ) ) ); } return array_values( array_unique( array_map( 'intval', $out ) ) ); } /* ---------------------------------------------------------------- Exportar */ public static function exporter( $email, $page = 1 ) { $page = max( 1, (int) $page ); $out = array(); if ( ! self::usable() || ! is_email( $email ) ) { return array( 'data' => $out, 'done' => true ); } $ids = self::orders_for( $email, ( $page - 1 ) * self::PER_PAGE, self::PER_PAGE ); foreach ( $ids as $id ) { $o = OrderArc_Archived::get( $id, true ); if ( ! $o ) { continue; } $items = array(); foreach ( (array) ( $o['items'] ?? array() ) as $it ) { if ( 'line_item' === $it['type'] ) { $items[] = $it['name'] . ' × ' . ( '' !== (string) $it['qty'] ? (float) $it['qty'] : 1 ); } } $data = array( array( 'name' => __( 'Order Number', 'orderarc' ), 'value' => $id ), array( 'name' => __( 'Order Date', 'orderarc' ), 'value' => $o['date'] ), array( 'name' => __( 'Billing Address', 'orderarc' ), 'value' => wp_strip_all_tags( str_replace( '
', ', ', (string) $o['billing'] ) ) ), array( 'name' => __( 'Shipping Address', 'orderarc' ), 'value' => wp_strip_all_tags( str_replace( '
', ', ', (string) $o['shipping'] ) ) ), array( 'name' => __( 'Phone Number', 'orderarc' ), 'value' => $o['phone'] ), array( 'name' => __( 'Email Address', 'orderarc' ), 'value' => $o['email'] ), array( 'name' => __( 'Items Purchased', 'orderarc' ), 'value' => implode( ', ', $items ) ), ); $net = self::network_data( $id ); if ( '' !== $net['ip'] ) { $data[] = array( 'name' => __( 'IP Address', 'orderarc' ), 'value' => $net['ip'] ); } if ( '' !== $net['ua'] ) { $data[] = array( 'name' => __( 'Browser User Agent', 'orderarc' ), 'value' => $net['ua'] ); } $out[] = array( 'group_id' => 'orderarc_archived_orders', 'group_label' => __( 'Archived orders', 'orderarc' ), 'group_description' => __( 'Orders moved to the OrderArc archive.', 'orderarc' ), 'item_id' => 'orderarc-order-' . $id, 'data' => array_values( array_filter( $data, function ( $d ) { return '' !== (string) $d['value']; } ) ), ); } return array( 'data' => $out, 'done' => count( $ids ) < self::PER_PAGE ); } /** IP y navegador guardados en el pedido archivado. */ private static function network_data( $id ) { global $wpdb; $hp = OrderArc_Engine::arch( 'wc_orders' ); if ( OrderArc_Engine::table_exists( $hp ) ) { $r = $wpdb->get_row( $wpdb->prepare( 'SELECT ip_address ip, user_agent ua FROM %i WHERE id=%d', $hp, $id ), ARRAY_A ); if ( $r ) { return array( 'ip' => (string) $r['ip'], 'ua' => (string) $r['ua'] ); } } $pm = OrderArc_Engine::arch( 'postmeta' ); if ( OrderArc_Engine::table_exists( $pm ) ) { $ip = (string) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM %i WHERE post_id=%d AND meta_key='_customer_ip_address' LIMIT 1", $pm, $id ) ); $ua = (string) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM %i WHERE post_id=%d AND meta_key='_customer_user_agent' LIMIT 1", $pm, $id ) ); return array( 'ip' => $ip, 'ua' => $ua ); } return array( 'ip' => '', 'ua' => '' ); } /* ---------------------------------------------------------------- Borrar */ public static function eraser( $email, $page = 1 ) { $page = max( 1, (int) $page ); $res = array( 'items_removed' => false, 'items_retained' => false, 'messages' => array(), 'done' => true ); if ( ! self::usable() || ! is_email( $email ) ) { return $res; } $erase = function_exists( 'wc_string_to_bool' ) && wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_order_data', 'no' ) ); if ( ! $erase ) { // Se conservan: se informan de a 10 (la lista no cambia entre páginas). $ids = self::orders_for( $email, ( $page - 1 ) * self::PER_PAGE, self::PER_PAGE ); foreach ( $ids as $id ) { /* translators: %s: order number */ $res['messages'][] = sprintf( __( 'Personal data within archived order %s has been retained.', 'orderarc' ), $id ); $res['items_retained'] = true; } $res['done'] = count( $ids ) < self::PER_PAGE; return $res; } if ( ! OrderArc_Runner::lock() ) { $res['messages'][] = __( 'Archived orders were not changed because an archive or restore job is running. Run the erasure again in a few minutes.', 'orderarc' ); $res['items_retained'] = true; return $res; } // Los anonimizados salen de la lista, así que siempre se toma la primera página de los que faltan. $ids = self::orders_for( $email, 0, self::PER_PAGE, true ); if ( $ids && self::anonymize( array_merge( $ids, self::refunds_of( $ids ) ), false ) ) { OrderArc_Engine::flush_caches( $ids ); foreach ( $ids as $id ) { /* translators: %s: order number */ $res['messages'][] = sprintf( __( 'Removed personal data from archived order %s.', 'orderarc' ), $id ); $res['items_removed'] = true; } $res['done'] = count( $ids ) < self::PER_PAGE; } elseif ( $ids ) { $res['messages'][] = __( 'The database did not accept the change, so archived orders were left as they were.', 'orderarc' ); $res['items_retained'] = true; } OrderArc_Runner::unlock(); return $res; } }