$id, 'element_type' => 'post_' . get_post_type($id)]); return is_object($details) && isset($details->language_code) ? $details->language_code : (defined('ICL_SITEPRESS_VERSION') ? (string) apply_filters('wpml_default_language', get_locale()) : get_locale()); } public static function languages(): array { $result = []; if (function_exists('pll_languages_list')) { foreach (pll_languages_list(['fields' => 'slug']) as $code) { $url = pll_home_url($code); $result[] = ['code' => $code, 'origin' => Chat4U_Protocol::origin($url), 'url' => $url]; } } elseif (defined('ICL_SITEPRESS_VERSION')) { foreach ((array) apply_filters('wpml_active_languages', null, ['skip_missing' => 0]) as $language) { $result[] = ['code' => $language['language_code'], 'origin' => Chat4U_Protocol::origin($language['url']), 'url' => $language['url']]; } } if (!$result) { $result[] = ['code' => get_locale(), 'origin' => Chat4U_Protocol::origin(get_option('home')), 'url' => home_url('/')]; } $rest = rest_url(); foreach ($result as &$language) { $language['rest_url'] = $language['origin'] . substr($rest, strlen(Chat4U_Protocol::origin($rest))); } usort($result, static fn($a, $b) => strcmp($a['code'], $b['code'])); return $result; } public static function public_post($post): bool { if (function_exists('wc_get_page_id')) { if (get_option('woocommerce_coming_soon')==='yes' && get_option('woocommerce_store_pages_only')!=='yes') { return false; } if ($post instanceof WP_Post && in_array($post->ID,array_filter([wc_get_page_id('cart'),wc_get_page_id('checkout'),wc_get_page_id('myaccount')],static fn($id)=>$id>0),true)) { return false; } } return $post instanceof WP_Post && is_post_publicly_viewable($post) && !$post->post_password && (bool) apply_filters('chat4u_is_public_record', true, $post); } public static function types(): array { $excluded = ['attachment']; if (defined('WC_VERSION')) { $excluded = array_merge($excluded, ['product', 'product_variation', 'shop_order', 'shop_order_refund', 'shop_coupon']); } return array_values(array_filter(get_post_types(['public' => true]), static fn($type) => !in_array($type, $excluded, true))); } private static function language_clause(string $locale): string { global $wpdb; if (!function_exists('pll_get_post_language')) { return ''; } $term = get_term_by('slug', $locale, 'language'); if (!$term) { return ' AND 1=0'; } $matches = $wpdb->prepare("EXISTS (SELECT 1 FROM {$wpdb->term_relationships} chat4u_lang WHERE chat4u_lang.object_id = {$wpdb->posts}.ID AND chat4u_lang.term_taxonomy_id = %d)", $term->term_taxonomy_id); if ($locale === pll_default_language('slug')) { // Untranslated public custom types and unassigned posts belong to // the configured default language, once. Let SQL filter them rather // than scanning every locale's records in application code. $unassigned = "NOT EXISTS (SELECT 1 FROM {$wpdb->term_relationships} chat4u_any JOIN {$wpdb->term_taxonomy} chat4u_tax ON chat4u_tax.term_taxonomy_id = chat4u_any.term_taxonomy_id WHERE chat4u_any.object_id = {$wpdb->posts}.ID AND chat4u_tax.taxonomy = 'language')"; return " AND ({$matches} OR {$unassigned})"; } return " AND {$matches}"; } public static function front(string $locale): int { $id = (int) get_option('page_on_front'); if ($id && function_exists('pll_get_post')) { $id = (int) pll_get_post($id, $locale); } elseif ($id) { $id = (int) apply_filters('wpml_object_id', $id, 'page', false, $locale); } return $id; } public static function home(string $locale): array { $url = home_url('/'); foreach (self::languages() as $language) { if ($language['code'] === $locale) { $url = $language['url']; } } return ['id' => 0, 'type' => 'home', 'title' => get_bloginfo('name'), 'url' => $url, 'locale' => $locale, 'translation_group' => 'home', 'terms' => [], 'version' => hash('sha256', Chat4U_Dependencies::revision() . ':' . Chat4U_Changes::head())]; } public static function record(int $id, string $type, string $locale): array { if ($type === 'home' && $id === 0) { return self::home($locale); } $post = get_post($id); if (!in_array($type, self::types(), true) || !self::public_post($post) || $post->post_type !== $type || self::locale($id) !== $locale) { return ['removed' => true, 'id' => $id, 'type' => $type, 'locale' => $locale]; } $terms = []; $taxonomies = []; foreach (get_object_taxonomies($type, 'objects') as $taxonomy) { if (!$taxonomy->public) { continue; } $values = wp_get_post_terms($id, $taxonomy->name); if (is_wp_error($values)) { continue; } foreach ($values as $term) { $label = wp_html_excerpt($term->name, 200, ''); $terms[] = $label; $taxonomies[] = ['taxonomy' => $taxonomy->name, 'id' => (int) $term->term_id, 'label' => $label, 'slug' => wp_html_excerpt($term->slug, 200, '')]; } } $group = (string) $id; if (function_exists('pll_get_post_translations')) { $translations = pll_get_post_translations($id); $group = $translations ? (string) min($translations) : $group; } elseif (defined('ICL_SITEPRESS_VERSION')) { $group = (string) apply_filters('wpml_element_trid', null, $id, 'post_' . $type); } $url = get_permalink($post); $hash = hash_init('sha256'); hash_update($hash, Chat4U_Dependencies::revision()); hash_update($hash, wp_json_encode(Chat4U_Public_Fields::schema($type))); hash_update($hash, $post->post_content . $post->post_title . $post->post_modified_gmt . $url . wp_json_encode($taxonomies)); foreach (get_post_meta($id) as $key => $values) { hash_update($hash, $key); foreach ($values as $value) { hash_update($hash, (string) $value); } } return ['id' => $id, 'type' => $type, 'title' => wp_html_excerpt(wp_strip_all_tags($post->post_title), 500, ''), 'url' => $url, 'locale' => $locale, 'translation_group' => $group, 'published_at' => get_post_time('c', true, $post), 'modified_at' => get_post_modified_time('c', true, $post), 'taxonomies' => $taxonomies, 'terms' => array_map(static fn($term) => wp_html_excerpt($term, 200, ''), $terms), 'version' => hash_final($hash)] + Chat4U_Public_Fields::record($id, $type); } public static function page(WP_REST_Request $request): array { global $wpdb; $locale = (string) $request->get_param('locale'); if (!in_array($locale, array_column(self::languages(), 'code'), true)) { return ['items' => [], 'complete' => false, 'cursor' => '0']; } $after = (string) ($request->get_param('after') ?: '0'); $limit = min(100, max(1, (int) ($request->get_param('limit') ?: 25))); $water = (int) ($request->get_param('high_water') ?? self::high_water()); $front = self::front($locale); $items = []; if ($after === '0') { if ($front && $front <= $water && self::public_post(get_post($front))) { $items[] = Chat4U_Protocol::discovery_record(self::record($front, 'page', $locale)); } elseif (!$front) { $items[] = Chat4U_Protocol::discovery_record(self::home($locale)); } if (count($items) >= $limit) { return ['items' => $items, 'cursor' => 'home', 'complete' => false]; } } $last = ctype_digit($after) ? (int) $after : 0; $where = static function ($sql) use ($wpdb, $last, $water, $locale) { return $sql . $wpdb->prepare(" AND {$wpdb->posts}.ID > %d AND {$wpdb->posts}.ID <= %d", $last, $water) . self::language_clause($locale); }; $old_language = apply_filters('wpml_current_language', null); if (defined('ICL_SITEPRESS_VERSION')) { do_action('wpml_switch_language', $locale); } add_filter('posts_where', $where); try { $query = new WP_Query(['post_type' => self::types(), 'post_status' => 'publish', 'posts_per_page' => $limit + 1, 'orderby' => 'ID', 'order' => 'ASC', 'no_found_rows' => true, 'suppress_filters' => false, 'post__not_in' => $front ? [$front] : [], 'lang' => function_exists('pll_get_post_language') ? '' : $locale]); } finally { remove_filter('posts_where', $where); if (defined('ICL_SITEPRESS_VERSION')) { do_action('wpml_switch_language', $old_language); } } $processed = 0; $available = $limit - count($items); foreach ($query->posts as $post) { if ($processed >= $available) { break; } if (self::public_post($post) && self::locale($post->ID) === $locale) { $value = Chat4U_Protocol::discovery_record(self::record($post->ID, $post->post_type, $locale)); if ($items && !Chat4U_Protocol::fits(['items' => [...$items, $value], 'cursor' => (string) $post->ID, 'complete' => false])) { break; } $items[] = $value; } // A byte-budget stop must leave this identity for the next page. $last = $post->ID; $processed++; } return ['items' => $items, 'cursor' => (string) $last, 'complete' => $processed === count($query->posts)]; } public static function high_water(): int { global $wpdb; return (int) $wpdb->get_var("SELECT COALESCE(MAX(ID),0) FROM {$wpdb->posts}"); } }