%d', "ph.post_status NOT IN ('trash', 'auto-draft')", '(u.unit_type <> %d OR u.quiz_mode <> %d)' ); $args = array( ObjectState::TRASHED->value, UnitType::QUIZ->value, QuizMode::BANK->value ); $type = self::kind_where( $kind ); if ( null === $type ) { return array( 'items' => array(), 'page' => $page, 'more' => false, ); } $where[] = $type[0]; $args = array_merge( $args, $type[1] ); if ( '' !== $q ) { $like = '%' . $wpdb->esc_like( $q ) . '%'; $where[] = '(ph.post_title LIKE %s OR EXISTS (SELECT 1 FROM %i o2 JOIN %i p2 ON p2.ID = o2.post_id WHERE o2.object_type = %d AND o2.object_id = u.id AND p2.post_title LIKE %s))'; $args = array_merge( $args, array( $like, Db::table( 'object_posts' ), $wpdb->posts, ObjectType::UNIT->value, $like ) ); } if ( $not_in_course > 0 ) { $where[] = 'NOT EXISTS (SELECT 1 FROM %i ni WHERE ni.course_id = %d AND ni.unit_id = u.id AND ni.state = %d)'; $args = array_merge( $args, array( Db::table( 'items' ), $not_in_course, ItemState::ACTIVE->value ) ); } $having = ''; if ( 'stream' === $kind ) { $having = ' HAVING provider = ' . (int) ProviderKey::STREAM->value; } elseif ( 'live' === $kind ) { $having = ' HAVING provider IS NULL OR provider <> ' . (int) ProviderKey::STREAM->value; } $sql = 'SELECT u.id, u.unit_type, u.lesson_kind, u.home_post_id, ph.post_status, ph.post_title AS home_title, MAX(pc.post_title) AS shown_title, MAX(ls.provider) AS provider,' . ' (SELECT COUNT(DISTINCT ic.course_id) FROM %i ic WHERE ic.unit_id = u.id AND ic.state = %d) AS courses' . ' FROM %i u JOIN %i ph ON ph.ID = u.home_post_id' . ' LEFT JOIN %i oc ON oc.object_type = %d AND oc.object_id = u.id AND oc.lang = %s' . ' LEFT JOIN %i pc ON pc.ID = oc.post_id AND pc.post_status NOT IN (\'trash\', \'auto-draft\')' . ' LEFT JOIN %i ls ON ls.unit_id = u.id AND ls.placement = %d AND ls.state <> %d' . ' WHERE ' . implode( ' AND ', $where ) . ' GROUP BY u.id' . $having . ( '' !== $q ? ' ORDER BY COALESCE(MAX(pc.post_title), ph.post_title), u.id' : ' ORDER BY ph.post_modified_gmt DESC, u.id DESC' ) . ' LIMIT %d OFFSET %d'; $head = array( Db::table( 'items' ), ItemState::ACTIVE->value, Db::table( 'units' ), $wpdb->posts, Db::table( 'object_posts' ), ObjectType::UNIT->value, $lang, $wpdb->posts, Db::table( 'live_sessions' ), Placement::LESSON->value, SessionState::TRASHED->value, ); $rows = Db::rows( $sql, array_merge( $head, $args, array( self::PER_PAGE + 1, ( $page - 1 ) * self::PER_PAGE ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- the query is built above from constant SQL and placeholders (the HAVING numbers are enum constants); every value is a replacement. $more = count( $rows ) > self::PER_PAGE; $rows = array_slice( $rows, 0, self::PER_PAGE ); $all = self::edits_all( $user_id ); $ids = array_values( array_filter( array_map( static fn( array $r ): int => (int) $r['home_post_id'], $rows ) ) ); if ( ! $all && array() !== $ids ) { _prime_post_caches( $ids, false, false ); } $out = array(); foreach ( $rows as $r ) { $post_id = (int) $r['home_post_id']; if ( $post_id <= 0 || ( ! $all && ! self::can( $user_id, 'edit_post', $post_id ) ) ) { continue; } $title = trim( html_entity_decode( wp_strip_all_tags( (string) ( null !== $r['shown_title'] && '' !== trim( (string) $r['shown_title'] ) ? $r['shown_title'] : $r['home_title'] ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ); $out[] = array( 'unit_id' => (int) $r['id'], 'post_id' => $post_id, 'title' => '' !== $title ? $title : __( '(no title)', 'rooster-lms' ), 'kind' => self::kind_of_row( $r ), 'status' => (string) $r['post_status'], 'courses' => (int) $r['courses'], ); } return array( 'items' => $out, 'page' => $page, 'more' => $more, ); } private static function can( int $user_id, string $cap, ...$args ): bool { return get_current_user_id() === $user_id ? current_user_can( $cap, ...$args ) : user_can( $user_id, $cap, ...$args ); // phpcs:ignore WordPress.WP.Capabilities.Undetermined -- edit_post or a post type's edit_others capability. } private static function edits_all( int $user_id ): bool { foreach ( array( \RoosterLMS\Content\PostTypes::LESSON, \RoosterLMS\Content\PostTypes::QUIZ ) as $type ) { $object = get_post_type_object( $type ); if ( null === $object || ! self::can( $user_id, (string) $object->cap->edit_others_posts ) ) { return false; } } return true; } private static function kind_where( string $kind ): ?array { if ( '' === $kind ) { return array( '1 = 1', array() ); } $lesson = LessonKind::from_slug( $kind ); if ( null !== $lesson ) { return array( 'u.unit_type = %d AND u.lesson_kind = %d', array( UnitType::LESSON->value, $lesson->value ) ); } $type = match ( $kind ) { 'quiz' => UnitType::QUIZ, 'assignment' => UnitType::ASSIGNMENT, 'live', 'stream' => UnitType::LIVE, 'scorm' => UnitType::PACKAGE, 'external' => UnitType::EXTERNAL, default => null, }; return null === $type ? null : array( 'u.unit_type = %d', array( $type->value ) ); } private static function kind_of_row( array $r ): string { return Kinds::of_row( (int) $r['unit_type'], null === $r['lesson_kind'] ? null : (int) $r['lesson_kind'], null === $r['provider'] ? null : (int) $r['provider'] ); } }