';
wp_nonce_field( 'abientot_privacy_tools' );
}
private static function types(): array {
return array(
'customer' => __( 'Customer', 'abientot' ),
'staff' => __( 'Staff', 'abientot' ),
'attendee' => __( 'Attendee', 'abientot' ),
'dependant' => __( 'Dependant', 'abientot' ),
'calendar' => __( 'External calendar connection', 'abientot' )
);
}
public function render(): void {
if ( ! Retention::permitted() ) {
return;
}
$this->render_result();
try {
$policy = Retention::policy();
} catch ( \Throwable $e ) {
echo '' . esc_html__( 'Privacy settings need review before cleanup.', 'abientot' ) . '
';
return;
}
echo '' . esc_html__( 'Verified personal-data requests', 'abientot' ) . ' ' . esc_html__( 'Use this route for phone-only customers, historical identity changes, staff, attendees, dependants or staff-managed calendars. Independently verify the requester and selected record. Knowing an email or phone is not proof of ownership.', 'abientot' ) . '
';
$this->start( 'manual' );
echo '' . esc_html__( 'Record type', 'abientot' ) . ' ';
foreach ( self::types()as$key => $label ) {
echo '' . esc_html( $label ) . ' ';
}
echo '
' . esc_html__( 'Record ID', 'abientot' ) . '
' . esc_html__( 'Export page', 'abientot' ) . '
' . esc_html__( 'I verified the requester’s identity, record scope and any retention obligations.', 'abientot' ) . '
' . esc_html__( 'I approve clearing this record’s personal details if I choose erasure.', 'abientot' ) . '
' . esc_html__( 'Download personal-data JSON', 'abientot' ) . ' ' . esc_html__( 'Clear personal details', 'abientot' ) . '
' . esc_html__( 'Each export page contains up to 100 records per category. Check has_more in the downloaded file and request the next page when needed. Erasure preserves operational and financial links; review shared media, emails, provider copies and backups separately.', 'abientot' ) . '
';
echo '' . esc_html__( 'Data retention', 'abientot' ) . ' ' . esc_html__( 'Choose periods from your documented legal policy. Zero disables cleanup for that category. Cleanup requires a fresh preview and confirmation; nothing runs automatically.', 'abientot' ) . '
';
$this->start( 'save' );
foreach ( array(
'customer_days' => __( 'Inactive customer details (days)', 'abientot' ),
'sms_days' => __( 'Finished SMS activity (days)', 'abientot' ),
'email_days' => __( 'Email activity (days)', 'abientot' )
) as$key => $label ) {
echo '' . esc_html( $label ) . '
';
}
echo '' . esc_html__( 'Enabled periods must be at least 30 days. Future or recent bookings are protected. Uncertain or pending SMS attempts are preserved for review.', 'abientot' ) . '
' . esc_html__( 'Preserve customer details linked to financial records until I review their legal retention.', 'abientot' ) . '
' . esc_html__( 'Customer IDs on legal hold (comma-separated)', 'abientot' ) . '
';
submit_button( __( 'Save retention policy', 'abientot' ) );
echo '';
$this->start( 'preview' );
submit_button( __( 'Preview privacy cleanup', 'abientot' ), 'secondary' );
echo ' ';
}
private function review( array $preview ): void {
echo '' . esc_html__( 'Review privacy cleanup', 'abientot' ) . ' ' . esc_html__( 'Only the listed inactive contact details and finished activity entries will be cleared. Booking IDs, business relationships and financial history remain.', 'abientot' ) . '
';
foreach ( $preview['rows'] as$kind => $ids ) {
$labels = array(
'customers' => __( 'Customers', 'abientot' ),
'sms' => __( 'SMS activity', 'abientot' ),
'sms_state' => __( 'SMS booking state', 'abientot' ),
'email' => __( 'Email activity', 'abientot' )
);
if ( ! isset( $labels[ $kind ] ) ) {
continue;
}
echo '' . esc_html( $labels[ $kind ] . ': ' . count( $ids ) . ' — ' . implode( ', ', $ids ) ) . ' ';
}
echo ' ' . esc_html__( 'Customer cleanup also clears eligible linked private content and activity. Disabled or unresolved activity and active entitlements prevent customer cleanup.', 'abientot' ) . '
';
foreach ( $preview['related'] as$key => $count ) {
$labels = array(
'sms' => __( 'SMS activity', 'abientot' ),
'sms_state' => __( 'SMS booking state', 'abientot' ),
'email' => __( 'Email activity', 'abientot' )
);
echo '' . esc_html( $labels[ $key ] . ': ' . $count ) . ' ';
}
echo ' ';
$this->start( 'execute' );
echo ' ' . esc_html__( 'I reviewed this cleanup and the applicable retention obligations.', 'abientot' ) . '
';
submit_button( __( 'Confirm privacy cleanup', 'abientot' ), 'delete' );
echo ' ';
}
private static function result_key( string $token ): string {
return 'abientot_privacy_tools_result_' . get_current_user_id() . '_' . $token;
}
/** Shows the outcome stored by handle() once, inside the Privacy & Data tab. */
private function render_result(): void {
$token = \Abientot\Admin\RedirectNotice::get( 'abientot_privacy_result', 'text' );
if ( ! preg_match( '/^[a-f0-9]{32}$/D', $token ) ) {
return;
}
$result = get_transient( self::result_key( $token ) );
if ( ! is_array( $result ) ) {
return;
}
delete_transient( self::result_key( $token ) );
if ( isset( $result['message'] ) && is_string( $result['message'] ) ) {
echo '' . esc_html( $result['message'] ) . '
';
}
if ( isset( $result['preview'] ) && is_array( $result['preview'] ) ) {
$this->review( $result['preview'] );
}
}
/** Stores the outcome for the current user and returns to the Privacy & Data tab. */
private function finish( array $result ): void {
$token = bin2hex( random_bytes( 16 ) );
set_transient( self::result_key( $token ), $result, 10 * MINUTE_IN_SECONDS );
wp_safe_redirect( \Abientot\Admin\RedirectNotice::url( admin_url( 'admin.php?page=abientot-settings&tab=privacy-data' ), array( 'abientot_privacy_result' => $token ) ) );
exit;
}
public function handle(): void {
check_admin_referer( 'abientot_privacy_tools' );
if ( ! Retention::permitted() ) {
wp_die( esc_html__( 'You do not have permission to process this request.', 'abientot' ), '', array( 'response' => 403 ) );
}
if ( ! \Abientot\Support\Request::is_post() ) {
wp_die( esc_html__( 'You do not have permission to process this request.', 'abientot' ), '', array( 'response' => 405 ) );
}
$input = \Abientot\Support\Request::post_fields(
'abientot_privacy_tools',
array(
'operation',
'verified',
'subject_type',
'subject_id',
'page',
'manual_action',
'erase_confirm',
'confirmed',
'token',
'customer_days',
'sms_days',
'email_days',
'financial_hold',
'held_customers' => 'textarea'
)
);
$operation = $input['operation'] ?? '';
$out = '';
try {
if ( $operation === 'manual' ) {
if ( ( $input['verified'] ?? '' ) !== '1' || ! is_string( $input['subject_type'] ?? null ) || ! isset( self::types()[ $input['subject_type'] ] ) || ! is_string( $input['subject_id'] ?? null ) || ! preg_match( '/^[1-9][0-9]{0,9}$/D', $input['subject_id'] ) || ! is_string( $input['page'] ?? null ) || ! preg_match( '/^[1-9][0-9]{0,5}$/D', $input['page'] ) || (int) $input['page'] > 100000 ) {
throw new \DomainException( 'privacy_input' );
}
$rights = new ManualRights();
if ( ( $input['manual_action'] ?? '' ) === 'export' ) {
$data = $rights->export( $input['subject_type'], (int) $input['subject_id'], (int) $input['page'] );
nocache_headers();
header( 'Content-Type: application/json; charset=UTF-8' );
header( 'Content-Disposition: attachment; filename="abientot-personal-data.json"' );
header( 'X-Content-Type-Options: nosniff' );
echo wp_json_encode( $data );
exit;
}
if ( ( $input['manual_action'] ?? '' ) !== 'erase' || ( $input['erase_confirm'] ?? '' ) !== '1' ) {
throw new \DomainException( 'privacy_input' );
}
$result = $rights->erase( $input['subject_type'], (int) $input['subject_id'] );
$out = __( 'Personal details processed. Review retained records and external copies.', 'abientot' ) . ' ' . implode( ' ', $result['messages'] );
} elseif ( $operation === 'save' ) {
( new Retention() )->save( $input );
$out = __( 'Retention policy saved. No cleanup has run.', 'abientot' );
} elseif ( $operation === 'preview' ) {
$preview = ( new Retention() )->preview();
$rows = array();
foreach ( $preview['rows'] as$kind => $items ) {
if ( $kind !== 'related_activity' ) {
$rows[ $kind ] = array_map( 'intval', array_column( $items, 'id' ) );
}
}
$this->finish(
array(
'preview' => array(
'token' => (string) $preview['token'],
'rows' => $rows,
'related' => array_map( 'intval', (array) $preview['rows']['related_activity'] )
)
)
);
} elseif ( $operation === 'execute' && ( $input['confirmed'] ?? '' ) === '1' && is_string( $input['token'] ?? null ) ) {
( new Retention() )->execute( $input['token'] );
$out = __( 'Reviewed cleanup finished. Preview again to check for another bounded batch.', 'abientot' );
} else {
throw new \DomainException( 'privacy_input' );
}
} catch ( \Throwable $e ) {
$out = ( $operation === 'preview' || $operation === 'save' ) ? __( 'Privacy settings need review before cleanup.', 'abientot' ) : __( 'The operation stopped or needs a fresh review. Some completed records may already be cleared. Review the data and preview again before continuing.', 'abientot' );
}
$this->finish( array( 'message' => $out ) );
}
}