post_status || '' !== (string) $post->post_password ) { return false; } $public = true; if ( function_exists( 'is_post_type_viewable' ) ) { $public = is_post_type_viewable( $post->post_type ); } /** * Whether a published post may be used to answer questions. * * Return false for posts that are published but meant for a restricted audience as a * whole, for example a members-only post type a membership plugin guards per request. * * @param bool $public Whether the post is public. * @param WP_Post $post Post. */ return (bool) apply_filters( 'ai_chat_bedrock_is_public_post', $public, $post ); } /** * Whether a public post may be used to answer questions, by the chat and by agents. * * A page the site keeps out of search engines is usually kept out for a reason: a thank-you * page with the download a form gives away, a campaign landing page, a page for one * customer. The chat is a search of the site, so it leaves those out too unless the site * says otherwise. Reading a page aloud is not affected: the visitor is already on it. * * @param WP_Post|null $post Post. * @param array|null $options Settings; the saved ones when omitted. * @return bool */ public static function is_answerable( $post, $options = null ) { if ( ! self::is_public( $post ) ) { return false; } if ( ! is_array( $options ) ) { $options = function_exists( 'get_option' ) ? get_option( 'ai_chat_bedrock_settings', array() ) : array(); $options = is_array( $options ) ? $options : array(); } $answerable = ! empty( $options['include_noindex'] ) || ! self::is_noindex( $post ); /** * Whether a public post may be used to answer questions and by agents. * * @param bool $answerable Whether it may, after the search engine settings. * @param WP_Post $post Post. */ return (bool) apply_filters( 'ai_chat_bedrock_is_answerable_post', $answerable, $post ); } /** * The version stored indexes are built for: the extraction, and whether pages hidden * from search engines are used. A change to either has every post processed again, so * a page left out before is indexed once the site includes them. * * @param array|null $options Settings; the saved ones when omitted. * @return string */ public static function index_version( $options = null ) { if ( ! is_array( $options ) ) { $options = function_exists( 'get_option' ) ? get_option( 'ai_chat_bedrock_settings', array() ) : array(); $options = is_array( $options ) ? $options : array(); } return self::VERSION . ( ! empty( $options['include_noindex'] ) ? '+noindex' : '' ); } /** * Whether the site's SEO plugin tells search engines not to index a post. * * Read from the settings each plugin keeps, since its own logic only runs for the page * being viewed: Yoast SEO, Rank Math and SEOPress for the post and its post type's * default, All in One SEO for a post set to noindex on its own. WooCommerce's cart, * checkout and account pages count too: WooCommerce tells search engines not to index * them, since each visitor sees their own. Discouraging search engines in Settings > * Reading does not count: that is how staging sites are hidden. * * @param WP_Post $post Post. * @return bool */ public static function is_noindex( $post ) { $noindex = false; if ( $post instanceof WP_Post && function_exists( 'get_post_meta' ) ) { $type = (string) $post->post_type; if ( defined( 'WPSEO_VERSION' ) ) { // 1 is noindex and 2 is index; anything else follows the post type. $value = (string) get_post_meta( $post->ID, '_yoast_wpseo_meta-robots-noindex', true ); if ( '1' === $value ) { $noindex = true; } elseif ( '2' !== $value ) { $titles = get_option( 'wpseo_titles', array() ); $noindex = is_array( $titles ) && ! empty( $titles[ 'noindex-' . $type ] ); } } if ( ! $noindex && defined( 'RANK_MATH_VERSION' ) ) { $robots = get_post_meta( $post->ID, 'rank_math_robots', true ); if ( empty( $robots ) || ! is_array( $robots ) ) { $titles = get_option( 'rank-math-options-titles', array() ); $titles = is_array( $titles ) ? $titles : array(); $custom = isset( $titles[ 'pt_' . $type . '_custom_robots' ] ) && 'on' === $titles[ 'pt_' . $type . '_custom_robots' ]; $robots = $custom && isset( $titles[ 'pt_' . $type . '_robots' ] ) ? $titles[ 'pt_' . $type . '_robots' ] : ( isset( $titles['robots_global'] ) ? $titles['robots_global'] : array() ); } $noindex = is_array( $robots ) && in_array( 'noindex', $robots, true ); } if ( ! $noindex && defined( 'SEOPRESS_VERSION' ) ) { $titles = get_option( 'seopress_titles_option_name', array() ); $titles = is_array( $titles ) ? $titles : array(); $noindex = 'yes' === get_post_meta( $post->ID, '_seopress_robots_index', true ) || ! empty( $titles['seopress_titles_noindex'] ) || ! empty( $titles['seopress_titles_single_titles'][ $type ]['noindex'] ); } if ( ! $noindex && defined( 'AIOSEO_FILE' ) && class_exists( '\AIOSEO\Plugin\Common\Models\Post' ) ) { try { $meta = \AIOSEO\Plugin\Common\Models\Post::getPost( $post->ID ); $noindex = is_object( $meta ) && empty( $meta->robots_default ) && ! empty( $meta->robots_noindex ); } catch ( Throwable $error ) { $noindex = false; } } } if ( ! $noindex && $post instanceof WP_Post && 'page' === $post->post_type && function_exists( 'wc_get_page_id' ) ) { foreach ( array( 'cart', 'checkout', 'myaccount' ) as $woocommerce_page ) { if ( (int) wc_get_page_id( $woocommerce_page ) === (int) $post->ID ) { $noindex = true; break; } } } /** * Whether search engines are told not to index a post, for an SEO plugin not read here. * * @param bool $noindex Whether the post is noindex. * @param WP_Post $post Post. */ return (bool) apply_filters( 'ai_chat_bedrock_post_is_noindex', $noindex, $post ); } /** * Plain text of a post as a guest sees it, title first. Paragraph breaks are kept so the * text can be split into passages. * * @param WP_Post $post Post. * @return string Empty for a post that is not public. */ public static function public_text( $post ) { if ( ! self::is_public( $post ) ) { return ''; } $key = $post->ID . ':' . md5( (string) $post->post_modified_gmt . '|' . self::VERSION ); $cached = function_exists( 'wp_cache_get' ) ? wp_cache_get( $key, self::CACHE_GROUP ) : false; if ( is_string( $cached ) ) { return $cached; } $body = self::to_text( self::render_as_guest( $post ) ); $text = trim( self::title( $post ) . "\n\n" . $body ); /** * The text used for a post in semantic search, keyword passages and site abilities. * * It is already what a guest sees. Filter it to remove more, never to add text a guest * cannot read. * * @param string $text Plain text. * @param WP_Post $post Post. */ $text = trim( (string) apply_filters( 'ai_chat_bedrock_indexable_text', $text, $post ) ); if ( function_exists( 'wp_cache_set' ) ) { wp_cache_set( $key, $text, self::CACHE_GROUP, HOUR_IN_SECONDS ); } return $text; } /** * The rendered HTML of a post with nobody signed in. * * @param WP_Post $post Post. * @return string */ public static function render_as_guest( $post ) { if ( self::$rendering || ! function_exists( 'apply_filters' ) ) { return ''; } $user_id = function_exists( 'get_current_user_id' ) ? get_current_user_id() : 0; $previous = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null; self::$rendering = true; ob_start(); try { if ( $user_id && function_exists( 'wp_set_current_user' ) ) { wp_set_current_user( 0 ); } $GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- blocks and shortcodes read the global post while rendering; it is restored below. if ( function_exists( 'setup_postdata' ) ) { setup_postdata( $post ); } $html = (string) apply_filters( 'the_content', self::without_restricted_blocks( (string) $post->post_content ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applying a core filter, not declaring a hook. } finally { // A filter that echoes instead of returning must not reach the response. ob_end_clean(); if ( $user_id && function_exists( 'wp_set_current_user' ) ) { wp_set_current_user( $user_id ); } $GLOBALS['post'] = $previous; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- restoring the value saved above. if ( $previous instanceof WP_Post && function_exists( 'setup_postdata' ) ) { setup_postdata( $previous ); } self::$rendering = false; } return $html; } /** * Post content without the blocks that are shown to a restricted audience. * * Rendering as a guest is not enough on its own. Block Visibility, for one, only loads its * filter on front-end requests, so in admin-ajax, where the settings screen indexes and the * chat answers, a members-only block rendered for everyone and was indexed. A block that * carries visibility rules is therefore left out whatever the request, including one shown * only to guests: losing a sign-in prompt costs nothing, while a guess about which rules * would hide a block could leak it. * * @param string $content Post content. * @return string */ public static function without_restricted_blocks( $content ) { $content = (string) $content; if ( false === strpos( $content, '