handleFormAction(); } $pendingInvites = $this->invites->findPending(); $registrationPageId = Val::int( get_option( self::OPTION_PAGE, 0 ) ); $registrationPageUrl = $registrationPageId > 0 ? (string) get_permalink( $registrationPageId ) : ''; include USC_PLUGIN_DIR . 'templates/admin/invites.php'; } /** * Handle a posted admin action. Returns `[link, error]`: the registration * link for a freshly created invite — the only time it can be shown, since * just the token's hash is stored — or an error message when creation * failed; both empty for every other action. * * @return array{string, string} */ private function handleFormAction(): array { // Nonce is verified by the caller (renderPage) before this method runs. // phpcs:disable WordPress.Security.NonceVerification.Missing $action = sanitize_key( Val::string( wp_unslash( $_POST['usc_action'] ?? '' ) ) ); if ( 'set_page' === $action ) { update_option( self::OPTION_PAGE, absint( Val::int( $_POST['registration_page_id'] ?? 0 ) ) ); } if ( 'invite' === $action ) { $email = sanitize_email( Val::string( wp_unslash( $_POST['email'] ?? '' ) ) ); if ( ! is_email( $email ) || false !== email_exists( $email ) || null !== $this->invites->findPendingByEmail( $email ) ) { return [ '', esc_html__( 'Could not create the invite: enter a valid email address that has no account and no pending invite.', 'unsupervised-schedular' ) ]; } $rawToken = wp_generate_password( 32, false ); $id = $this->invites->insert( new Invite( email: $email, token: Invite::hashToken( $rawToken ), invitedBy: get_current_user_id(), ) ); return $this->linkOrError( $id, $rawToken ); } if ( 'group_invite' === $action ) { $expiresAt = $this->normalizeExpiry( sanitize_text_field( Val::string( wp_unslash( $_POST['expires_at'] ?? '' ) ) ) ); if ( null === $expiresAt ) { return [ '', esc_html__( 'Could not create the group link: choose an expiry date of today or later.', 'unsupervised-schedular' ) ]; } $rawToken = wp_generate_password( 32, false ); $id = $this->invites->insert( new Invite( email: '', token: Invite::hashToken( $rawToken ), invitedBy: get_current_user_id(), kind: Invite::KIND_GROUP, expiresAt: $expiresAt, ) ); return $this->linkOrError( $id, $rawToken ); } if ( 'revoke' === $action ) { $inviteId = absint( Val::int( $_POST['invite_id'] ?? 0 ) ); if ( $inviteId > 0 ) { $this->invites->revoke( $inviteId ); } } // phpcs:enable WordPress.Security.NonceVerification.Missing return [ '', '' ]; } /** * The registration link for a stored invite, or an error when the insert * failed — a link must never be shown for a token that was not persisted, * since it could only ever dead-end as "invalid or expired". * * @return array{string, string} */ private function linkOrError( int $insertedId, string $rawToken ): array { if ( $insertedId <= 0 ) { return [ '', esc_html__( 'Could not save the invite. Deactivate and reactivate the plugin to update the database, then try again.', 'unsupervised-schedular' ) ]; } return [ $this->registrationLink( $rawToken ), '' ]; } /** * Validate a submitted group-link expiry date (strict `Y-m-d`, today or * later) and expand it to the end of that day; null when invalid or past. */ private function normalizeExpiry( string $date ): ?string { $day = \DateTimeImmutable::createFromFormat( '!Y-m-d', $date ); if ( false === $day || $day->format( 'Y-m-d' ) !== $date ) { return null; } if ( $date < Val::string( current_time( 'Y-m-d' ) ) ) { return null; } return $date . ' 23:59:59'; } /** * Build the registration URL for a raw invite token. */ private function registrationLink( string $rawToken ): string { $pageId = Val::int( get_option( self::OPTION_PAGE, 0 ) ); $linkBase = $pageId > 0 ? (string) get_permalink( $pageId ) : ''; return add_query_arg( 'us_invite', rawurlencode( $rawToken ), '' !== $linkBase ? $linkBase : home_url( '/' ) ); } }