$atts Block attributes (`loginPageId`) or * shortcode attributes (`login_page_id`). */ public function render( array $atts ): string { if ( ! is_user_logged_in() ) { $loginPageId = Val::int( $atts['loginPageId'] ?? $atts['login_page_id'] ?? 0 ); return sprintf( '
%s %s.
', esc_html__( 'Please', 'unsupervised-schedular' ), esc_url( $this->loginUrl( $loginPageId ) ), esc_html__( 'log in to manage your profile', 'unsupervised-schedular' ) ); } wp_enqueue_style( 'us-scheduler' ); $userId = get_current_user_id(); $children = $this->guardians->children( $userId ); $questions = $this->questions->findByScope( Question::SCOPE_ACCOUNT, activeOnly: true ); $error = $this->submitError; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display flag; the submit that set it was nonce-checked. $result = sanitize_key( Val::string( wp_unslash( $_GET['us_family'] ?? '' ) ) ); $notice = $this->noticeFor( $result ); // Which child the "edit" link opened, if any — the row is swapped for an // editable form rather than every row carrying one. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing; the edit submit is nonce-checked. $editingId = absint( Val::int( $_GET['us_edit_child'] ?? 0 ) ); ob_start(); include USC_PLUGIN_DIR . 'templates/frontend/family-page.php'; return (string) ob_get_clean(); } /** * Process an add/edit/remove submission on `template_redirect`, before any * page output, then post/redirect/get back to the page. An error is stashed * for {@see render()} to show inline with the form. */ public function maybeHandleSubmit(): void { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- routing only; the action is nonce-checked immediately below. $action = sanitize_key( Val::string( wp_unslash( $_POST['us_family_action'] ?? '' ) ) ); if ( '' === $action || ! is_user_logged_in() ) { return; } if ( ! check_admin_referer( 'us_family' ) ) { return; } $userId = get_current_user_id(); $result = match ( $action ) { 'add' => $this->handleAdd( $userId ), 'edit' => $this->handleEdit( $userId ), 'remove' => $this->handleRemove( $userId ), default => new \WP_Error( 'unknown_action', __( 'Unrecognised request.', 'unsupervised-schedular' ) ), }; if ( $result instanceof \WP_Error ) { $this->submitError = $result->get_error_message(); return; } $this->redirect( add_query_arg( 'us_family', $result, $this->currentUrl() ) ); } /** * Add a child, then record their answers to the account-signup questions — * asked per child, since they describe the student rather than the account. * * Required answers are validated *before* the child is created, so a missing * one never leaves a nameless half-added child behind. */ private function handleAdd( int $guardianId ): string|\WP_Error { $name = $this->postString( 'child_name' ); $dateOfBirth = $this->postString( 'child_dob' ); $relationship = $this->postString( 'child_relationship' ); $questions = $this->questions->findByScope( Question::SCOPE_ACCOUNT, activeOnly: true ); $answers = $this->submittedAnswers(); $missing = $this->firstMissingAnswer( $questions, $answers ); if ( null !== $missing ) { return $missing; } $childId = $this->guardians->createChild( $guardianId, $name, $dateOfBirth, $relationship ); if ( $childId instanceof \WP_Error ) { return $childId; } $this->recordAnswers( $questions, $answers, $childId ); return self::RESULT_ADDED; } private function handleEdit( int $guardianId ): string|\WP_Error { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller. $childId = absint( Val::int( $_POST['child_id'] ?? 0 ) ); $error = $this->guardians->updateChild( $guardianId, $childId, $this->postString( 'child_name' ), $this->postString( 'child_dob' ) ); return $error ?? self::RESULT_UPDATED; } private function handleRemove( int $guardianId ): string|\WP_Error { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked by the caller. $childId = absint( Val::int( $_POST['child_id'] ?? 0 ) ); $error = $this->guardians->removeChild( $guardianId, $childId ); return $error ?? self::RESULT_REMOVED; } /** * The first required question left unanswered, as the error to show — or null * when every required question has a value. * * @param list