logger = $logger; } /** * Hooks. * * @return void */ public function init() { add_action( self::CLEANUP_HOOK, array( $this, 'cleanup' ) ); add_action( 'init', array( $this, 'sync_schedule' ), 20 ); add_action( 'deleted_user', array( $this, 'on_deleted_user' ), 10, 1 ); add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporter' ) ); add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_eraser' ) ); add_action( 'admin_init', array( $this, 'add_policy_text' ) ); } /** * Schedule the daily clean-up only while a retention period is set. * * @return void */ public function sync_schedule() { $days = (int) EVLFL_Settings::get( 'logging', 'retention_days' ); $scheduled = (bool) wp_next_scheduled( self::CLEANUP_HOOK ); if ( $days > 0 && ! $scheduled ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CLEANUP_HOOK ); } elseif ( $days <= 0 && $scheduled ) { wp_clear_scheduled_hook( self::CLEANUP_HOOK ); } } /** * Remove scheduled events (deactivation/uninstall). * * @return void */ public static function clear_schedules() { wp_clear_scheduled_hook( self::CLEANUP_HOOK ); } /** * Cron: delete events older than the retention period, in bounded batches. * * @return int Rows deleted. */ public function cleanup() { $days = (int) EVLFL_Settings::get( 'logging', 'retention_days' ); if ( $days < 1 ) { return 0; } $cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) ); $deleted = 0; for ( $i = 0; $i < self::MAX_BATCHES; $i++ ) { $n = $this->logger->delete_older_than( $cutoff, self::BATCH ); $deleted += $n; if ( $n < self::BATCH ) { break; } } return $deleted; } /** * A user account was deleted: when opted in, erase their events and forget them as actor. * * @param int $user_id Deleted user ID. * @return void */ public function on_deleted_user( $user_id = 0 ) { $user_id = absint( $user_id ); if ( $user_id < 1 || empty( EVLFL_Settings::get( 'privacy', 'erase_on_user_delete' ) ) ) { return; } $this->erase_user( $user_id ); } /** * Delete a user's events (bounded batches) and clear them as the actor of others' events. * * @param int $user_id User ID. * @return int Rows deleted. */ private function erase_user( $user_id ) { $deleted = 0; for ( $i = 0; $i < self::MAX_BATCHES; $i++ ) { $n = $this->logger->delete_for_user( $user_id, self::BATCH ); $deleted += $n; if ( $n < self::BATCH ) { break; } } $this->logger->clear_actor( $user_id ); return $deleted; } /** * Register the personal data exporter. * * @param array $exporters Exporters. * @return array */ public function register_exporter( $exporters ) { $exporters['accesstrace-for-learndash'] = array( 'exporter_friendly_name' => __( 'AccessTrace enrollment logs', 'accesstrace-for-learndash' ), 'callback' => array( $this, 'export' ), ); return $exporters; } /** * Register the personal data eraser. * * @param array $erasers Erasers. * @return array */ public function register_eraser( $erasers ) { $erasers['accesstrace-for-learndash'] = array( 'eraser_friendly_name' => __( 'AccessTrace enrollment logs', 'accesstrace-for-learndash' ), 'callback' => array( $this, 'erase' ), ); return $erasers; } /** * Exporter callback. * * @param string $email Email address. * @param int $page Page (1-based). * @return array */ public function export( $email, $page = 1 ) { $user = get_user_by( 'email', (string) $email ); if ( ! $user ) { return array( 'data' => array(), 'done' => true, ); } $page = max( 1, absint( $page ) ); $result = $this->logger->query( array( 'user_id' => (int) $user->ID, 'orderby' => 'id', 'order' => 'ASC', 'per_page' => self::EXPORT_PAGE, 'paged' => $page, ) ); $data = array(); foreach ( $result['items'] as $row ) { $course = get_post( $row['course_id'] ); $data[] = array( 'group_id' => 'evlfl-enrollment-logs', 'group_label' => __( 'Enrollment log', 'accesstrace-for-learndash' ), 'item_id' => 'evlfl-event-' . $row['id'], 'data' => array( array( 'name' => __( 'Date (UTC)', 'accesstrace-for-learndash' ), 'value' => $row['event_time'], ), array( 'name' => __( 'Course', 'accesstrace-for-learndash' ), 'value' => $course ? $course->post_title : '#' . $row['course_id'], ), array( 'name' => __( 'Action', 'accesstrace-for-learndash' ), 'value' => EVLFL_Event::label( 'action', $row['action'] ), ), array( 'name' => __( 'Source', 'accesstrace-for-learndash' ), 'value' => EVLFL_Event::label( 'source', $row['source'] ), ), array( 'name' => __( 'Status', 'accesstrace-for-learndash' ), 'value' => EVLFL_Event::label( 'status', $row['status'] ), ), ), ); } return array( 'data' => $data, 'done' => ( $page * self::EXPORT_PAGE ) >= $result['total'], ); } /** * Eraser callback. * * @param string $email Email address. * @param int $page Page (unused; each call removes up to MAX_BATCHES batches). * @return array */ public function erase( $email, $page = 1 ) { unset( $page ); $user = get_user_by( 'email', (string) $email ); $removed = $user ? $this->erase_user( (int) $user->ID ) : 0; return array( 'items_removed' => $removed > 0, 'items_retained' => false, 'messages' => array(), 'done' => true, ); } /** * Suggested privacy policy text. * * @return void */ public function add_policy_text() { if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { return; } wp_add_privacy_policy_content( 'AccessTrace for LearnDash', wp_kses_post( wpautop( __( 'When a student is enrolled in or removed from a LearnDash course, this site records the event: the student ID, course ID, the action, its cause and status, the ID of the user who made the change, and the time. Passwords, IP addresses and email addresses are not stored. If administrator emails are enabled, the student name (and email address, if you turned that on) is included in emails sent to the site administrators. Events can be exported or erased with the WordPress privacy tools and are deleted automatically if a retention period is set.', 'accesstrace-for-learndash' ) ) ) ); } }