handleFormAction(); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only instructor selector. $instructorId = absint( Val::int( $_GET['instructor_id'] ?? 0 ) ); $instructor = $instructorId > 0 ? get_userdata( $instructorId ) : false; if ( $instructor && in_array( RoleManager::INSTRUCTOR, (array) $instructor->roles, true ) ) { $grantable = $this->grantableCaps(); $capabilities = []; foreach ( InstructorCapabilities::MANAGED as $cap ) { $capabilities[ $cap ] = [ 'granted' => user_can( $instructor, $cap ), 'grantable' => in_array( $cap, $grantable, true ), ]; } $backUrl = admin_url( 'admin.php?page=' . self::PAGE_SLUG ); include USC_PLUGIN_DIR . 'templates/admin/instructor-detail.php'; return; } $instructors = array_map( static fn( \WP_User $user ): array => [ 'id' => (int) $user->ID, 'name' => $user->display_name, 'email' => $user->user_email, 'registered' => $user->user_registered, ], array_filter( get_users( [ 'role' => RoleManager::INSTRUCTOR, 'orderby' => 'display_name', 'order' => 'ASC', ] ), static fn( mixed $user ): bool => $user instanceof \WP_User ) ); $pageSlug = self::PAGE_SLUG; include USC_PLUGIN_DIR . 'templates/admin/instructors.php'; } private function handleFormAction(): string { // 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'] ?? '' ) ) ); // phpcs:enable WordPress.Security.NonceVerification.Missing if ( 'create' === $action ) { return $this->createInstructor(); } if ( 'update_caps' === $action ) { return $this->updateCaps(); } return ''; } private function createInstructor(): string { // phpcs:disable WordPress.Security.NonceVerification.Missing $email = sanitize_email( Val::string( wp_unslash( $_POST['email'] ?? '' ) ) ); $name = sanitize_text_field( Val::string( wp_unslash( $_POST['display_name'] ?? '' ) ) ); // phpcs:enable WordPress.Security.NonceVerification.Missing if ( ! is_email( $email ) ) { return __( 'Enter a valid email address.', 'unsupervised-schedular' ); } if ( false !== email_exists( $email ) ) { return __( 'A user with that email already exists.', 'unsupervised-schedular' ); } $userId = wp_insert_user( [ 'user_login' => $email, 'user_email' => $email, 'display_name' => '' !== $name ? $name : $email, 'user_pass' => wp_generate_password( 24 ), 'role' => RoleManager::INSTRUCTOR, ] ); if ( is_wp_error( $userId ) ) { return __( 'Could not create the instructor account.', 'unsupervised-schedular' ); } // Never let a new instructor exceed the creating admin's own capabilities: // the role grants every managed capability, so deny any the admin lacks. $grantable = $this->grantableCaps(); $user = new \WP_User( $userId ); foreach ( InstructorCapabilities::MANAGED as $cap ) { if ( ! in_array( $cap, $grantable, true ) ) { $user->add_cap( $cap, false ); } } wp_new_user_notification( $userId, null, 'user' ); return __( 'Instructor created and emailed a link to set their password.', 'unsupervised-schedular' ); } private function updateCaps(): string { // phpcs:disable WordPress.Security.NonceVerification.Missing $instructorId = absint( Val::int( $_POST['instructor_id'] ?? 0 ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- each capability key is sanitized with sanitize_key() in the array_map callback. $submitted = array_values( array_map( static fn( mixed $cap ): string => sanitize_key( Val::string( $cap ) ), (array) wp_unslash( $_POST['capabilities'] ?? [] ) ) ); // phpcs:enable WordPress.Security.NonceVerification.Missing $instructor = $instructorId > 0 ? get_userdata( $instructorId ) : false; if ( ! $instructor || ! in_array( RoleManager::INSTRUCTOR, (array) $instructor->roles, true ) ) { return __( 'Instructor not found.', 'unsupervised-schedular' ); } foreach ( InstructorCapabilities::resolve( $submitted, $this->grantableCaps() ) as $cap => $granted ) { $instructor->add_cap( $cap, $granted ); } return __( 'Capabilities updated.', 'unsupervised-schedular' ); } /** * Managed capabilities the acting user may assign. Uses `current_user_can()` * so capabilities the administrator holds only via the dynamic studio grant * still count. * * @return list */ private function grantableCaps(): array { $caps = []; foreach ( InstructorCapabilities::MANAGED as $cap ) { $caps[ $cap ] = current_user_can( $cap ); } return InstructorCapabilities::grantable( $caps ); } }