__( 'Rooster LMS: invitations', 'rooster-lms' ), 'callback' => array( self::class, 'export' ), ); } return $exporters; } public static function erasers( $erasers ) { if ( is_array( $erasers ) ) { $erasers[ self::ID ] = array( 'eraser_friendly_name' => __( 'Rooster LMS: invitations', 'rooster-lms' ), 'callback' => array( self::class, 'erase' ), ); } return $erasers; } public static function registry( $tables ) { if ( ! is_array( $tables ) ) { return $tables; } $entry = new UserTable( 'invitees', 'user_id', 'invitations', __( 'Rooster LMS: invitations', 'rooster-lms' ), array( 'invitation_id' => __( 'Invitation', 'rooster-lms' ), 'email' => __( 'E-mail', 'rooster-lms' ), 'first_name' => __( 'First name', 'rooster-lms' ), 'last_name' => __( 'Last name', 'rooster-lms' ), 'source' => __( 'Added from (1 earlier learners, 2 CSV, 3 organisation, 4 newsletter list, 5 external list, 6 by hand, 7 asked to be notified)', 'rooster-lms' ), 'sent_at' => __( 'Sent', 'rooster-lms' ), 'answer' => __( 'Answer (0 none, 1 yes, 2 no, 3 maybe)', 'rooster-lms' ), 'answered_at' => __( 'Answered', 'rooster-lms' ), 'outcome' => __( 'Outcome (0 none yet, 1 attended, 2 enrolled, 3 joined the cohort)', 'rooster-lms' ), 'outcome_at' => __( 'Outcome recorded', 'rooster-lms' ), ), 'id' ); $last = end( $tables ); if ( $last instanceof UserTable && 'people' === $last->table ) { array_splice( $tables, count( $tables ) - 1, 0, array( $entry ) ); return $tables; } $tables[] = $entry; return $tables; } public static function export( $email, $page = 1 ): array { $email = strtolower( trim( (string) $email ) ); if ( '' === $email || (int) $page > 1 ) { return array( 'data' => array(), 'done' => true, ); } $rows = Db::rows( 'SELECT /* outside-tx */ i.id, i.email, i.first_name, i.last_name, i.lang, i.source, i.sent_at, i.answer, i.answered_at, i.outcome, i.outcome_at, i.created_at, v.course_id, v.unit_id, v.kind, c.name AS cohort_name FROM %i i JOIN %i v ON v.id = i.invitation_id LEFT JOIN %i c ON c.id = v.target_cohort_id WHERE i.email_hash = %s AND i.user_id = 0 ORDER BY i.id LIMIT 500', Db::table( 'invitees' ), Db::table( 'invitations' ), Db::table( 'cohorts' ), Tokens::email_hash( $email ) ); $items = array(); $checks = \RoosterLMS\Attendance\GuestCheckins::of_invitees( array_map( static fn( array $r ): int => (int) $r['id'], $rows ) ); foreach ( $rows as $row ) { $course = (int) $row['course_id']; $unit = (int) $row['unit_id']; $title = $unit > 0 ? (string) ( Services::posts()->titles( ObjectType::UNIT, array( $unit ) )[ $unit ]['title'] ?? '' ) : (string) ( Services::posts()->titles( ObjectType::COURSE, array( $course ) )[ $course ]['title'] ?? '' ); $title = '' === $title && $course <= 0 ? (string) ( $row['cohort_name'] ?? '' ) : $title; $answer = RsvpAnswer::tryFrom( (int) $row['answer'] ) ?? RsvpAnswer::NONE; $source = InviteeSource::tryFrom( (int) $row['source'] ); if ( InvitationKind::INTEREST->value === (int) $row['kind'] ) { $items[] = array( 'group_id' => self::ID, 'group_label' => __( 'Rooster LMS: invitations', 'rooster-lms' ), 'item_id' => self::ID . '-' . (int) $row['id'], 'data' => array( array( 'name' => __( 'Asked to be notified about', 'rooster-lms' ), 'value' => $title, ), array( 'name' => __( 'E-mail', 'rooster-lms' ), 'value' => (string) $row['email'], ), array( 'name' => __( 'Confirmed', 'rooster-lms' ), 'value' => (string) $row['created_at'], ), array( 'name' => __( 'Language', 'rooster-lms' ), 'value' => (string) $row['lang'], ), ), ); continue; } $items[] = array( 'group_id' => self::ID, 'group_label' => __( 'Rooster LMS: invitations', 'rooster-lms' ), 'item_id' => self::ID . '-' . (int) $row['id'], 'data' => array( array( 'name' => __( 'Invitation to', 'rooster-lms' ), 'value' => $title, ), array( 'name' => __( 'E-mail', 'rooster-lms' ), 'value' => (string) $row['email'], ), array( 'name' => __( 'First name', 'rooster-lms' ), 'value' => (string) $row['first_name'], ), array( 'name' => __( 'Last name', 'rooster-lms' ), 'value' => (string) $row['last_name'], ), array( 'name' => __( 'Added from', 'rooster-lms' ), 'value' => null === $source ? '' : $source->label(), ), array( 'name' => __( 'Sent', 'rooster-lms' ), 'value' => (string) ( $row['sent_at'] ?? '' ), ), array( 'name' => __( 'Answer', 'rooster-lms' ), 'value' => $answer->label(), ), array( 'name' => __( 'Answered', 'rooster-lms' ), 'value' => (string) ( $row['answered_at'] ?? '' ), ), array( 'name' => __( 'Outcome', 'rooster-lms' ), 'value' => ( InviteeOutcome::tryFrom( (int) ( $row['outcome'] ?? 0 ) ) ?? InviteeOutcome::NONE )->label(), ), array( 'name' => __( 'Outcome recorded', 'rooster-lms' ), 'value' => (string) ( $row['outcome_at'] ?? '' ), ), ), ); foreach ( $checks[ (int) $row['id'] ] ?? array() as $check ) { $items[ count( $items ) - 1 ]['data'][] = array( 'name' => __( 'Checked in at the door', 'rooster-lms' ), /* translators: 1: the check-in time, 2: the start of the date (both UTC). */ 'value' => sprintf( __( '%1$s (the date starting %2$s)', 'rooster-lms' ), $check['checked_in_at'], $check['starts_at'] ), ); } } return array( 'data' => $items, 'done' => true, ); } public static function erase( $email, $page = 1 ): array { unset( $page ); $email = strtolower( trim( (string) $email ) ); $user = '' === $email ? false : get_user_by( 'email', $email ); if ( '' !== $email ) { self::keep_stop( Tokens::email_hash( $email ) ); } $gone = '' === $email ? 0 : Interest::erase( Tokens::email_hash( $email ), $user instanceof \WP_User ? (int) $user->ID : 0 ); $n = '' === $email ? 0 : self::anonymise( Tokens::email_hash( $email ), $user instanceof \WP_User ? (int) $user->ID : 0 ); if ( $gone > 0 && 0 === $n ) { return array( 'items_removed' => true, 'items_retained' => false, 'messages' => array(), 'done' => true, ); } return array( 'items_removed' => $n > 0, 'items_retained' => $n > 0, 'messages' => $n > 0 ? array( __( 'Invitations: the address, names and links were removed; the answers stay in the counts without anyone\'s name.', 'rooster-lms' ) ) : array(), 'done' => true, ); } private static function keep_stop( string $email_hash ): void { if ( null !== Db::value( 'SELECT /* outside-tx */ 1 FROM %i WHERE email_hash = %s AND user_id = 0 AND state = %d LIMIT 1', Db::table( 'invitees' ), $email_hash, InviteeState::SUPPRESSED->value ) ) { Suppressions::add_hash( $email_hash, SuppressionSource::GUEST_STOP ); } } public static function on_user_gone( $user_id ): void { if ( (int) $user_id > 0 ) { Interest::erase( '', (int) $user_id ); self::anonymise( '', (int) $user_id ); } } public static function anonymise( string $email_hash, int $user_id ): int { if ( '' === $email_hash && $user_id <= 0 ) { return 0; } global $wpdb; $where = array(); if ( '' !== $email_hash ) { $where[] = SqlPart::of( 'email_hash = %s', $email_hash ); } if ( $user_id > 0 ) { $where[] = SqlPart::of( 'user_id = %d', $user_id ); } \RoosterLMS\Attendance\GuestCheckins::delete_where( SqlPart::of( '( i.' )->add( SqlPart::join( ' OR i.', $where ) )->add( ' )' ) ); $sql = SqlPart::of( "UPDATE %i SET user_id = 0, email = '', first_name = '', last_name = '', lang = '', token_hash = NULL, token_seed = NULL, token_expires_at = NULL, email_hash = SHA2( CONCAT( %s, id ), 256 ) WHERE ", Db::table( 'invitees' ), 'erased:' )->add( '( ' )->add( SqlPart::join( ' OR ', $where ) )->add( ' )' ); return max( 0, Db::query( $sql ) ); } }