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'; } private function handleFormAction( int $instructorId ): void { // 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 ) { $this->addSlot( $instructorId ); } if ( 'delete' === $action ) { $this->deleteOwnSlot( absint( Val::int( $_POST['slot_id'] ?? 0 ) ), $instructorId ); } 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'] ?? []; foreach ( is_array( $rawIds ) ? $rawIds : [] as $rawId ) { $this->deleteOwnSlot( absint( Val::int( $rawId ) ), $instructorId ); } } // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Delete a slot only when it exists and belongs to the given instructor. * The repository additionally refuses to delete booked slots. */ private function deleteOwnSlot( int $slotId, int $instructorId ): void { if ( $slotId <= 0 ) { return; } $slot = $this->repository->findById( $slotId ); if ( $slot && $slot->instructorId === $instructorId ) { $this->repository->delete( $slotId ); } } private function addSlot( int $instructorId ): void { // phpcs:disable WordPress.Security.NonceVerification.Missing $startDt = AvailabilitySlot::normalizeDateTime( sanitize_text_field( Val::string( wp_unslash( $_POST['start_dt'] ?? '' ) ) ) ); $endDt = AvailabilitySlot::normalizeDateTime( sanitize_text_field( Val::string( wp_unslash( $_POST['end_dt'] ?? '' ) ) ) ); // A window must start and end on the same day (weekly repeat covers longer // ranges) and fit at least one lesson; it is stored as lesson-length slots. if ( null === $startDt || null === $endDt || $endDt <= $startDt || substr( $startDt, 0, 10 ) !== substr( $endDt, 0, 10 ) ) { return; } $offeringId = absint( Val::int( $_POST['offering_id'] ?? 0 ) ); $duration = absint( Val::int( $_POST['duration_minutes'] ?? 0 ) ); $window = new AvailabilitySlot( instructorId: $instructorId, startDt: $startDt, endDt: $endDt, durationMinutes: $duration > 0 ? $duration : 60, offeringId: $offeringId > 0 ? $offeringId : null, ); $recurrence = sanitize_key( Val::string( wp_unslash( $_POST['recurrence'] ?? 'single' ) ) ); $weeks = absint( Val::int( $_POST['weeks'] ?? 1 ) ); $this->repository->createFromWindow( $window, 'weekly' === $recurrence, $weeks ); // phpcs:enable WordPress.Security.NonceVerification.Missing } }