Export/Erase Personal Data and * Settings -> Privacy): * * - suggested text for the privacy policy, * - exporter for care log entries and user meta, * - eraser that anonymizes care log entries (user ID -> 0) and * deletes user meta. * * @since 0.1.2 */ class Privacy { /** * Entries per export/erase pass. * * @since 0.1.2 * @var int */ const BATCH_SIZE = 50; /** * User meta keys the plugin stores per user. * * @since 0.1.2 * @var string[] */ const USER_META_KEYS = array( 'quietadmin_last_login', 'quietadmin_ghost_mode' ); /** * Keys in the event meta that may contain a user ID. * * @since 0.1.2 * @var string[] */ const META_USER_KEYS = array( 'user_id', 'client_id', 'target_id', 'origin_id' ); /** * Register hooks. * * @since 0.1.2 * * @return void */ public function register() { add_action( 'admin_init', array( $this, 'add_policy_content' ) ); add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporter' ) ); add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_eraser' ) ); } /** * Adds the suggested text to the privacy policy. * * @since 0.1.2 * * @return void */ public function add_policy_content() { if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { return; } $days = max( 7, (int) apply_filters( 'quietadmin_log_retention_days', 180 ) ); $content = '
' . esc_html__( 'This is a suggestion for your privacy policy. Please review it and adapt it to your situation.', 'oms-quietadmin' ) . '
'; $content .= '' . esc_html__( 'Suggested text:', 'oms-quietadmin' ) . ' '; $content .= esc_html( sprintf( /* translators: %d: number of days the care log is kept. */ __( 'The maintenance service for this website keeps a care log in the website database. It records maintenance events (for example installed updates and backups), the number of failed login attempts and spam comments, support requests (sender and subject). The log contains no IP addresses and no user names of failed login attempts. To slow repeated login attempts, short-lived keyed hashes of the submitted account name and IP address are stored for up to 15 minutes; the raw values cannot be recovered from these hashes. Log entries are deleted automatically after %d days.', 'oms-quietadmin' ), $days ) ); $content .= '
'; $content .= esc_html__( 'If you send a message through the help form in the dashboard, your name, email address, the optional phone number and your message are sent by email to the support address of your maintenance provider. The message itself is not stored on this website.', 'oms-quietadmin' ); $content .= '
'; $content .= esc_html__( 'We store the time of your last login so that your maintenance provider can see whether the account is still in use.', 'oms-quietadmin' ); $content .= '
'; wp_add_privacy_policy_content( 'OMS QuietAdmin', wp_kses_post( $content ) ); } /** * Registers the exporter. * * @since 0.1.2 * * @param array $exporters Existing exporters. * @return array */ public function register_exporter( $exporters ) { $exporters['quietadmin'] = array( 'exporter_friendly_name' => __( 'OMS QuietAdmin care log', 'oms-quietadmin' ), 'callback' => array( $this, 'export_data' ), ); return $exporters; } /** * Registers the eraser. * * @since 0.1.2 * * @param array $erasers Existing erasers. * @return array */ public function register_eraser( $erasers ) { $erasers['quietadmin'] = array( 'eraser_friendly_name' => __( 'OMS QuietAdmin care log', 'oms-quietadmin' ), 'callback' => array( $this, 'erase_data' ), ); return $erasers; } /** * Exports the data for an email address. * * @since 0.1.2 * * @param string $email_address Email address. * @param int $page Page (starting at 1). * @return array{data:array,done:bool} */ public function export_data( $email_address, $page = 1 ) { $user = get_user_by( 'email', (string) $email_address ); $page = max( 1, (int) $page ); $data = array(); if ( ! $user instanceof \WP_User ) { return array( 'data' => array(), 'done' => true, ); } if ( 1 === $page ) { $last_login = (int) get_user_meta( $user->ID, 'quietadmin_last_login', true ); if ( $last_login ) { $data[] = array( 'group_id' => 'quietadmin-user', 'group_label' => __( 'OMS QuietAdmin', 'oms-quietadmin' ), 'group_description' => __( 'Data stored by the OMS QuietAdmin plugin for this user.', 'oms-quietadmin' ), 'item_id' => 'quietadmin-user-' . $user->ID, 'data' => array( array( 'name' => __( 'Last login', 'oms-quietadmin' ), 'value' => wp_date( 'Y-m-d H:i:s', $last_login ), ), ), ); } } $rows = Event_Log::rows_for_user( $user->ID, self::BATCH_SIZE, ( $page - 1 ) * self::BATCH_SIZE ); foreach ( $rows as $row ) { $data[] = array( 'group_id' => 'quietadmin-events', 'group_label' => __( 'OMS QuietAdmin care log', 'oms-quietadmin' ), 'group_description' => __( 'Care log entries that refer to this user.', 'oms-quietadmin' ), 'item_id' => 'quietadmin-event-' . (int) $row->id, 'data' => array( array( 'name' => __( 'Date (UTC)', 'oms-quietadmin' ), 'value' => (string) $row->created_at, ), array( 'name' => __( 'Type', 'oms-quietadmin' ), 'value' => (string) $row->event_type, ), array( 'name' => __( 'Event', 'oms-quietadmin' ), 'value' => (string) $row->title, ), ), ); } return array( 'data' => $data, 'done' => count( $rows ) < self::BATCH_SIZE, ); } /** * Anonymizes care log entries and deletes user meta. * * The entries themselves are kept as statistics but no longer * refer to the user. * * @since 0.1.2 * * @param string $email_address Email address. * @param int $page Page (starting at 1, not needed). * @return array{items_removed:bool,items_retained:bool,messages:string[],done:bool} */ public function erase_data( $email_address, $page = 1 ) { unset( $page ); $result = array( 'items_removed' => false, 'items_retained' => false, 'messages' => array(), 'done' => true, ); $user = get_user_by( 'email', (string) $email_address ); if ( ! $user instanceof \WP_User ) { return $result; } foreach ( self::USER_META_KEYS as $meta_key ) { if ( metadata_exists( 'user', $user->ID, $meta_key ) ) { delete_user_meta( $user->ID, $meta_key ); $result['items_removed'] = true; } } // One round per call: processed rows no longer refer to // the user and therefore drop out of the next query. $rows = Event_Log::rows_for_user( $user->ID, self::BATCH_SIZE, 0 ); $processed = Event_Log::anonymize_rows( $rows, $user->ID ); if ( $processed > 0 ) { $result['items_removed'] = true; } if ( count( $rows ) >= self::BATCH_SIZE && $processed > 0 ) { $result['done'] = false; } return $result; } }