'rooster_courses', PostTypes::LESSON => 'rooster_lessons', PostTypes::QUIZ => 'rooster_quizzes', ); public const CREATE_COURSES = 'create_rooster_courses'; public const MANAGE_STAFF = 'rooster_lms_manage_course_staff'; private const POST_META = array( 'edit_post', 'read_post', 'delete_post', 'publish_post' ); public const LIST_LIMIT = 5000; private static bool $registered = false; private static bool $bypass = false; private static int $rest_post = 0; private static array $courses_of_post = array(); private static array $list_posts = array(); public function register(): void { if ( self::$registered ) { return; } self::$registered = true; add_filter( 'map_meta_cap', array( self::class, 'map' ), 20, 4 ); add_filter( 'rest_request_before_callbacks', array( self::class, 'rest_context' ), 10, 3 ); add_filter( 'rest_request_after_callbacks', array( self::class, 'rest_context_end' ), 10, 1 ); add_filter( 'wp_insert_post_data', array( self::class, 'status_guard' ), 20, 2 ); add_action( 'pre_get_posts', array( self::class, 'scope_admin_list' ), 20, 1 ); add_action( 'rooster_lms_course_staff_changed', array( self::class, 'forget' ), 10, 0 ); } public static function forget(): void { self::$courses_of_post = array(); self::$rest_post = 0; self::$list_posts = array(); } public static function map( $caps, $cap, $user_id, $args ): array { $caps = is_array( $caps ) ? $caps : (array) $caps; if ( self::$bypass || ! is_string( $cap ) ) { return $caps; } $user_id = is_numeric( $user_id ) ? (int) $user_id : 0; $args = is_array( $args ) ? array_values( $args ) : array(); if ( self::MANAGE_STAFF === $cap ) { return self::manage_staff( $user_id, is_numeric( $args[0] ?? null ) ? (int) $args[0] : 0 ) ? array( 'exist' ) : array( 'do_not_allow' ); } if ( self::CREATE_COURSES === $cap ) { return array( 'edit_rooster_courses' ); } if ( $user_id <= 0 || ! isset( Services::course_staff()::staff_users()[ $user_id ] ) ) { return $caps; } if ( in_array( $cap, self::POST_META, true ) ) { return self::map_post( $caps, $cap, $user_id, is_numeric( $args[0] ?? null ) ? (int) $args[0] : 0 ); } if ( ! str_starts_with( $cap, 'edit_rooster_' ) && ! str_starts_with( $cap, 'publish_rooster_' ) ) { return $caps; } $type = array_search( substr( $cap, strpos( $cap, '_rooster_' ) + 1 ), self::TYPES, true ); if ( false === $type || $user_id <= 0 || self::has_all( $user_id, $caps ) ) { return $caps; } if ( str_starts_with( $cap, 'edit_' ) ) { return array() !== Services::course_staff()->instructed_course_ids( $user_id ) ? array( 'exist' ) : $caps; } $post = get_post( self::context_post() ); if ( $post instanceof \WP_Post && $post->post_type === $type && self::is_principal_of_post( $user_id, $post ) ) { return array( 'exist' ); } return $caps; } private static function map_post( array $caps, string $cap, int $user_id, int $post_id ): array { if ( $user_id <= 0 || $post_id <= 0 || in_array( 'do_not_allow', $caps, true ) ) { return $caps; } $post = get_post( $post_id ); if ( ! $post instanceof \WP_Post || ! isset( self::TYPES[ $post->post_type ] ) || ( 'auto-draft' === $post->post_status && 'edit_post' !== $cap ) ) { return $caps; } if ( self::has_all( $user_id, $caps ) ) { return $caps; } if ( 'read_post' === $cap && 'publish' === $post->post_status ) { return $caps; } if ( in_array( $cap, array( 'edit_post', 'read_post' ), true ) ) { if ( (int) $post->post_author === $user_id && PostTypes::COURSE !== $post->post_type && array() !== Services::course_staff()->instructed_course_ids( $user_id ) ) { return array( 'exist' ); } $mine = array_intersect( self::courses_of( $post ), Services::course_staff()->instructed_course_ids( $user_id ) ); return array() !== $mine ? array( 'exist' ) : $caps; } return self::is_principal_of_post( $user_id, $post ) ? array( 'exist' ) : $caps; } private static function is_principal_of_post( int $user_id, \WP_Post $post ): bool { $courses = self::courses_of( $post ); if ( array() === $courses ) { return false; } $staff = Services::course_staff(); foreach ( $courses as $course_id ) { if ( StaffRole::PRINCIPAL !== $staff->role_of( $user_id, $course_id ) ) { return false; } } return true; } public static function courses_of( \WP_Post $post ): array { $post_id = (int) $post->ID; if ( ! isset( self::$courses_of_post[ $post_id ] ) ) { if ( PostTypes::COURSE === $post->post_type ) { $course = Services::course_repo()->id_for_post( $post_id ); $out = $course > 0 ? array( $course ) : array(); } else { $unit = Services::unit_repo()->id_for_post( $post_id ); $out = $unit > 0 ? Services::curriculum_repo()->course_ids_for_units( array( $unit ) ) : array(); } self::$courses_of_post[ $post_id ] = $out; } return self::$courses_of_post[ $post_id ]; } private static function manage_staff( int $user_id, int $course_id ): bool { if ( $user_id <= 0 || $course_id <= 0 ) { return false; } $course = Services::course_repo()->get( $course_id ); if ( null === $course ) { return false; } if ( StaffRole::PRINCIPAL === Services::course_staff()->role_of( $user_id, $course_id ) ) { return true; } return $course->home_post_id > 0 && self::can_without_staff( $user_id, 'edit_post', $course->home_post_id ); } public static function can_without_staff( int $user_id, string $cap, ...$args ): bool { $was = self::$bypass; self::$bypass = true; try { return \RoosterLMS\Domain\Users::can( $user_id, $cap, ...$args ); } finally { self::$bypass = $was; } } private static function has_all( int $user_id, array $caps ): bool { $user = \RoosterLMS\Domain\Users::get( $user_id ); if ( null === $user ) { return false; } foreach ( $caps as $primitive ) { if ( 'exist' !== $primitive && empty( $user->allcaps[ $primitive ] ) ) { return false; } } return true; } public static function context_post(): int { if ( self::$rest_post > 0 ) { return self::$rest_post; } if ( ! is_admin() || 'post.php' !== ( $GLOBALS['pagenow'] ?? '' ) ) { return 0; } $id = isset( $GLOBALS['post_ID'] ) && is_numeric( $GLOBALS['post_ID'] ) ? absint( $GLOBALS['post_ID'] ) : 0; if ( $id <= 0 ) { return 0; } $method = isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET'; return 'POST' !== $method || \RoosterLMS\Admin\Request::posted_nonce_valid( '_wpnonce', 'update-post_' . $id ) ? $id : 0; } public static function rest_context( $response, $handler = null, $request = null ) { unset( $handler ); self::$rest_post = 0; if ( $request instanceof \WP_REST_Request && 1 === preg_match( '#^/wp/v2/([a-z0-9_-]+)/(\d+)(?:/autosaves)?/?$#', $request->get_route(), $m ) ) { $id = (int) $m[2]; $type = get_post_type( $id ); if ( is_string( $type ) && isset( self::TYPES[ $type ] ) ) { self::$rest_post = $id; } } return $response; } public static function rest_context_end( $response ) { self::$rest_post = 0; return $response; } public static function status_guard( $data, $postarr = array() ) { if ( ! is_array( $data ) || ! is_array( $postarr ) || empty( $postarr['ID'] ) || ! isset( self::TYPES[ (string) ( $data['post_type'] ?? '' ) ] ) ) { return $data; } $user_id = get_current_user_id(); $post = get_post( (int) $postarr['ID'] ); if ( $user_id <= 0 || ! $post instanceof \WP_Post || (int) $post->post_author === $user_id ) { return $data; } if ( ! isset( Services::course_staff()::staff_users()[ $user_id ] ) ) { return $data; } $old = (string) $post->post_status; $new = (string) ( $data['post_status'] ?? $old ); $live = array( 'publish', 'future', 'private' ); if ( $old === $new || ( ! in_array( $old, $live, true ) && ! in_array( $new, $live, true ) ) || 'trash' === $new ) { return $data; } if ( current_user_can( 'publish_post', (int) $post->ID ) || self::can_without_staff( $user_id, 'edit_others_' . self::TYPES[ $post->post_type ] ) ) { return $data; } $data['post_status'] = $old; return $data; } public static function scope_admin_list( $query ): void { if ( ! $query instanceof \WP_Query || ! is_admin() || ! $query->is_main_query() || 'edit.php' !== ( $GLOBALS['pagenow'] ?? '' ) ) { return; } $type = $query->get( 'post_type' ); if ( ! is_string( $type ) || ! isset( self::TYPES[ $type ] ) ) { return; } $ids = self::admin_list_scope( $type, get_current_user_id() ); if ( null === $ids ) { return; } $query->set( 'post__in', array() === $ids ? array( 0 ) : $ids ); } public static function admin_list_scope( string $type, int $user_id ): ?array { if ( $user_id <= 0 || ! isset( self::TYPES[ $type ] ) || self::can_without_staff( $user_id, 'edit_others_' . self::TYPES[ $type ] ) ) { return null; } $courses = Services::course_staff()->instructed_course_ids( $user_id ); if ( array() === $courses ) { return null; } $key = $type . '|' . $user_id . '|' . implode( ',', $courses ); if ( ! isset( self::$list_posts[ $key ] ) ) { self::$list_posts[ $key ] = self::list_post_ids( $type, $user_id, $courses ); } return self::$list_posts[ $key ]; } public static function list_post_ids( string $type, int $user_id, array $courses ): array { global $wpdb; $own = \RoosterLMS\Data\Db::col( SqlPart::of( 'SELECT ID FROM %i WHERE post_type = %s AND post_author = %d LIMIT %d', $wpdb->posts, $type, $user_id, self::LIST_LIMIT ) ); $tbl = \RoosterLMS\Data\Db::table( 'object_posts' ); $sql = PostTypes::COURSE === $type ? SqlPart::of( 'SELECT post_id FROM %i WHERE object_type = %d AND object_id IN (' . implode( ', ', array_fill( 0, count( $courses ), '%d' ) ) . ') LIMIT %d', array_merge( array( $tbl, \RoosterLMS\Content\ObjectType::COURSE->value ), $courses, array( self::LIST_LIMIT ) ) ) : SqlPart::of( 'SELECT DISTINCT op.post_id FROM %i i JOIN %i op ON op.object_type = %d AND op.object_id = i.unit_id JOIN %i p ON p.ID = op.post_id AND p.post_type = %s WHERE i.course_id IN (' . implode( ', ', array_fill( 0, count( $courses ), '%d' ) ) . ') LIMIT %d', array_merge( array( \RoosterLMS\Data\Db::table( 'items' ), $tbl, \RoosterLMS\Content\ObjectType::UNIT->value, $wpdb->posts, $type ), $courses, array( self::LIST_LIMIT ) ) ); $ids = array_merge( $own, \RoosterLMS\Data\Db::col( $sql ) ); return array_values( array_unique( array_map( 'intval', $ids ) ) ); } }