'; 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__( '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 '

'; } 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' ) . '

'; 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' ) . '

' . esc_html__( 'Customer cleanup also clears eligible linked private content and activity. Disabled or unresolved activity and active entitlements prevent customer cleanup.', 'abientot' ) . '

'; $this->start( 'execute' ); echo '

'; 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 ) ); } }