course_id, $lang ); $index = null; foreach ( $listed['items'] as $i => $entry ) { if ( $entry->id === $item->id ) { $index = $i; break; } } if ( null === $index ) { return array( 'previous' => null, 'next' => null, ); } return array( 'previous' => self::previous( $user_id, $item, $listed, $index, $lang ), 'next' => self::next( $user_id, $item, $listed, $index, $lang, $completable, $done ), ); } private static function next( int $user_id, Item $item, array $listed, int $index, string $lang, bool $completable, bool $done ): ?array { $gate = Services::gate(); $count = count( $listed['items'] ); for ( $i = $index + 1; $i < $count; $i++ ) { $entry = $listed['items'][ $i ]; $decision = $gate->can_open_item( $user_id, $entry->id ); if ( $decision->allowed ) { return self::step( $entry, $listed['posts'], $lang, true ); } if ( AccessReason::LOCKED_SEQUENCE !== $decision->reason ) { continue; } $waits = ! $done && $gate->previous_item( $user_id, $entry ) === $item->id; if ( $waits && $completable ) { return array( 'needs' => true ) + self::step( $entry, $listed['posts'], $lang, true ); } $reason = Locks::item_reason( $decision->reason, $entry, $user_id ); return array( 'waits' => $waits, 'reason' => '' !== $reason ? $reason : __( 'Not available yet.', 'rooster-lms' ), ) + self::step( $entry, $listed['posts'], $lang, false ); } $other = self::other_course( $user_id, $item->course_id, true, $lang ); if ( null !== $other ) { return $other; } $page = self::course_page( $item->course_id, $lang ); return $page['open'] ? $page : null; } private static function previous( int $user_id, Item $item, array $listed, int $index, string $lang ): ?array { for ( $i = $index - 1; $i >= 0; $i-- ) { $entry = $listed['items'][ $i ]; if ( Services::gate()->can_open_item( $user_id, $entry->id )->allowed ) { return self::step( $entry, $listed['posts'], $lang, true ); } } return self::other_course( $user_id, $item->course_id, false, $lang ); } private static function other_course( int $user_id, int $course_id, bool $forward, string $lang ): ?array { $gate = Services::gate(); $takes = array(); $candidate = array(); foreach ( $forward ? Services::course_links()->links( $course_id ) : Services::course_links()->linked_from( $course_id ) as $link ) { if ( LinkKind::CONTINUES === $link['kind'] ) { $candidate[] = (int) ( $forward ? $link['linked_id'] : $link['course_id'] ); } } $trail = Services::category_resolver()->trail_for_object( ObjectType::COURSE, $course_id, $lang ); $node = array() === $trail ? null : $trail[ count( $trail ) - 1 ]; if ( null !== $node ) { $rows = Services::course_catalogue()->page( $user_id, new CatalogueQuery( 1, self::COURSES_MAX, 'category', false, $lang, $node->category->id, true, false ) )->rows; $ids = array(); foreach ( $rows as $row ) { $ids[] = $row->course_id; if ( EnrolmentStatus::ACTIVE === $row->enrolment_status ) { $takes[ $row->course_id ] = true; } } $at = array_search( $course_id, $ids, true ); if ( false !== $at ) { $candidate = array_merge( $candidate, $forward ? array_slice( $ids, $at + 1 ) : array_reverse( array_slice( $ids, 0, $at ) ) ); } } foreach ( array_values( array_unique( $candidate ) ) as $other ) { if ( $other === $course_id || $other <= 0 ) { continue; } if ( ! isset( $takes[ $other ] ) && ! Learner::is_enrolled( $user_id, $other ) && null === $gate->staff_access( $user_id, $other ) ) { continue; } $listed = self::listed( $other, $lang ); $items = $forward ? $listed['items'] : array_reverse( $listed['items'] ); foreach ( $items as $entry ) { $decision = $gate->can_open_item( $user_id, $entry->id ); if ( $decision->allowed && AccessReason::PREVIEW !== $decision->reason ) { return array( 'kind' => 'course', 'course' => self::course_title( $other, $lang ), ) + self::step( $entry, $listed['posts'], $lang, true ); } } } return null; } private static function course_page( int $course_id, string $lang ): array { $post = (int) ( Services::posts()->for_objects( ObjectType::COURSE, array( $course_id ), $lang )[ $course_id ] ?? 0 ); $url = $post > 0 ? (string) get_permalink( $post ) : ''; return array( 'kind' => 'course_page', 'item_id' => 0, 'course_id' => $course_id, 'url' => $url, 'title' => self::course_title( $course_id, $lang ), 'course' => '', 'open' => '' !== $url, 'reason' => '', 'needs' => false, 'waits' => false, ); } private static function step( Item $entry, array $posts, string $lang, bool $open ): array { $title = wp_strip_all_tags( Services::posts()->title( (int) ( $posts[ $entry->unit_id ] ?? 0 ) ) ); return array( 'kind' => 'item', 'item_id' => $entry->id, 'course_id' => $entry->course_id, 'url' => $open ? Urls::item_url( $entry->course_id, $entry->id, $lang ) : '', 'title' => '' !== trim( $title ) ? $title : __( '(no title)', 'rooster-lms' ), 'course' => '', 'open' => $open, 'reason' => '', 'needs' => false, 'waits' => false, ); } private static function listed( int $course_id, string $lang ): array { $outline = Learner::outline( $course_id ); $unit_ids = array_values( array_unique( array_map( static fn( Item $entry ): int => $entry->unit_id, $outline->items ) ) ); $posts = array() === $unit_ids ? array() : array_map( 'intval', Services::posts()->for_objects( ObjectType::UNIT, $unit_ids, $lang ) ); return array( 'items' => array_values( array_filter( $outline->items, static fn( Item $entry ): bool => ( $posts[ $entry->unit_id ] ?? 0 ) > 0 ) ), 'posts' => $posts, ); } private static function course_title( int $course_id, string $lang ): string { $post = (int) ( Services::posts()->for_objects( ObjectType::COURSE, array( $course_id ), $lang )[ $course_id ] ?? 0 ); $title = $post > 0 ? wp_strip_all_tags( get_the_title( $post ) ) : ''; return '' !== trim( $title ) ? $title : __( '(no title)', 'rooster-lms' ); } }