handleFormAction( $instructorId ); } $slots = $this->repository->findByInstructor( $instructorId ); $offeringChoices = $this->offerings->findAll( $instructorId, Offering::KIND_PRIVATE_LESSON, true ); // View-state query params only (which view, which week) — nothing is // mutated from them, so no nonce applies. // phpcs:disable WordPress.Security.NonceVerification.Recommended $view = 'list' === sanitize_key( Val::string( wp_unslash( $_GET['usc_view'] ?? '' ) ) ) ? 'list' : 'week'; $requestedWeek = sanitize_text_field( Val::string( wp_unslash( $_GET['usc_week'] ?? '' ) ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended $weekStart = WeekCalendar::weekStart( $requestedWeek, Val::int( get_option( 'start_of_week', 1 ) ), current_time( 'Y-m-d' ) ); $weekDays = WeekCalendar::days( $weekStart, $slots ); $prevWeek = ( new \DateTimeImmutable( $weekStart ) )->modify( '-7 days' )->format( 'Y-m-d' ); $nextWeek = ( new \DateTimeImmutable( $weekStart ) )->modify( '+7 days' )->format( 'Y-m-d' ); include USC_PLUGIN_DIR . 'templates/admin/availability.php'; } /** * Run the submitted action and report what happened. Every branch returns a * message: a form that silently reloads leaves the instructor unable to tell * "saved 41 slots" from "saved nothing". * * @return array{string, string} Success notice and error message; each is * empty when it does not apply. */ private function handleFormAction( int $instructorId ): 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 ( 'add' === $action ) { return $this->addSlot( $instructorId ); } if ( 'delete' === $action ) { return $this->deleteOwnSlot( absint( Val::int( $_POST['slot_id'] ?? 0 ) ), $instructorId ) ? [ __( 'Availability slot deleted.', 'unsupervised-schedular' ), '' ] : [ '', __( 'That slot could not be deleted. It may already be booked, or belong to someone else.', 'unsupervised-schedular' ) ]; } if ( 'bulk_delete' === $action ) { // The array itself carries no data; each element is coerced and // absint-sanitized individually below. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $rawIds = $_POST['slot_ids'] ?? []; $deleted = 0; $failed = 0; foreach ( is_array( $rawIds ) ? $rawIds : [] as $rawId ) { if ( $this->deleteOwnSlot( absint( Val::int( $rawId ) ), $instructorId ) ) { ++$deleted; continue; } ++$failed; } return $this->bulkDeleteResult( $deleted, $failed ); } // phpcs:enable WordPress.Security.NonceVerification.Missing return [ '', '' ]; } /** * Wording for a bulk delete, which can partly succeed. * * @return array{string, string} */ private function bulkDeleteResult( int $deleted, int $failed ): array { $notice = $deleted > 0 ? sprintf( /* translators: %d: number of availability slots deleted. */ _n( '%d slot deleted.', '%d slots deleted.', $deleted, 'unsupervised-schedular' ), $deleted ) : ''; $error = $failed > 0 ? sprintf( /* translators: %d: number of slots that could not be deleted. */ _n( '%d slot could not be deleted — it may already be booked.', '%d slots could not be deleted — they may already be booked.', $failed, 'unsupervised-schedular' ), $failed ) : ''; if ( 0 === $deleted && 0 === $failed ) { $error = __( 'No slots were selected.', 'unsupervised-schedular' ); } return [ $notice, $error ]; } /** * Delete a slot only when it exists and belongs to the given instructor. * The repository additionally refuses to delete booked slots. Returns whether * the row actually went away. */ private function deleteOwnSlot( int $slotId, int $instructorId ): bool { if ( $slotId <= 0 ) { return false; } $slot = $this->repository->findById( $slotId ); if ( null === $slot || $slot->instructorId !== $instructorId ) { return false; } return $this->repository->delete( $slotId ); } /** * Validate and persist a submitted window. * * @return array{string, string} */ private function addSlot( int $instructorId ): array { // phpcs:disable WordPress.Security.NonceVerification.Missing $window = $this->validator->validate( $instructorId, sanitize_text_field( Val::string( wp_unslash( $_POST['start_dt'] ?? '' ) ) ), sanitize_text_field( Val::string( wp_unslash( $_POST['end_dt'] ?? '' ) ) ), absint( Val::int( $_POST['duration_minutes'] ?? 0 ) ), absint( Val::int( $_POST['offering_id'] ?? 0 ) ), ); if ( $window instanceof \WP_Error ) { return [ '', $window->get_error_message() ]; } $recurrence = sanitize_key( Val::string( wp_unslash( $_POST['recurrence'] ?? 'single' ) ) ); $weeks = absint( Val::int( $_POST['weeks'] ?? 1 ) ); // phpcs:enable WordPress.Security.NonceVerification.Missing $ids = $this->repository->createFromWindow( $window, 'weekly' === $recurrence, $weeks ); // The window was valid, so it split into at least one slot — an empty // result means every insert failed. if ( [] === $ids ) { return [ '', __( 'The availability could not be saved. Please try again.', 'unsupervised-schedular' ) ]; } return [ sprintf( /* translators: %d: number of bookable slots created. */ _n( 'Added %d bookable slot.', 'Added %d bookable slots.', count( $ids ), 'unsupervised-schedular' ), count( $ids ) ), '', ]; } }