'ids', 'number' => 0 ) ); foreach ( $sites as $site_id ) { switch_to_blog( (int) $site_id ); self::activate_site(); restore_current_blog(); } return; } self::activate_site(); } /** Activate for current site. */ private static function activate_site() { self::$settings_cache = null; if ( false === get_option( self::SETTINGS_OPTION, false ) ) { add_option( self::SETTINGS_OPTION, self::get_default_settings(), '', false ); } self::register_rewrites(); flush_rewrite_rules( false ); update_option( self::REWRITE_OPTION, self::REWRITE_VERSION, false ); } /** Deactivate plugin. */ public static function deactivate( $network_wide = false ) { if ( is_multisite() && $network_wide ) { $sites = get_sites( array( 'fields' => 'ids', 'number' => 0 ) ); foreach ( $sites as $site_id ) { switch_to_blog( (int) $site_id ); flush_rewrite_rules( false ); restore_current_blog(); } return; } flush_rewrite_rules( false ); } /** Register only the virtual routes currently enabled by the site. */ public static function register_rewrites() { $settings = self::get_settings(); if ( ! empty( $settings['manage_llms'] ) ) { add_rewrite_rule( '^llms\.txt/?$', 'index.php?' . self::QUERY_LLMS_VAR . '=1', 'top' ); } if ( ! empty( $settings['enabled'] ) ) { add_rewrite_rule( '^index\.md/?$', 'index.php?' . self::QUERY_VAR . '=__front__', 'top' ); add_rewrite_rule( '^(.+?)/index\.md/?$', 'index.php?' . self::QUERY_VAR . '=$matches[1]', 'top' ); } } /** Register custom query vars. */ public static function register_query_vars( $vars ) { $vars[] = self::QUERY_VAR; $vars[] = self::QUERY_ID_VAR; $vars[] = self::QUERY_LLMS_VAR; return $vars; } /** Flush rewrite rules once after rewrite changes. */ public static function maybe_flush_rewrites() { if ( ! current_user_can( 'manage_options' ) || self::REWRITE_VERSION === get_option( self::REWRITE_OPTION ) ) { return; } self::register_rewrites(); flush_rewrite_rules( false ); update_option( self::REWRITE_OPTION, self::REWRITE_VERSION, false ); } /** Refresh rewrites only when virtual endpoint availability changes. */ public static function maybe_flush_rewrites_after_settings_update( $old_value, $new_value ) { self::$settings_cache = null; $old_value = is_array( $old_value ) ? $old_value : array(); $new_value = is_array( $new_value ) ? $new_value : array(); $markdown_changed = ! empty( $old_value['enabled'] ) !== ! empty( $new_value['enabled'] ); $llms_changed = ! empty( $old_value['manage_llms'] ) !== ! empty( $new_value['manage_llms'] ); if ( ! $markdown_changed && ! $llms_changed ) { return; } self::register_rewrites(); flush_rewrite_rules( false ); } /** Default plugin settings. */ public static function get_default_settings() { return array( 'enabled' => 1, 'post_types' => array( 'post', 'page' ), 'title_mode' => 'auto', 'pre_h1_mode' => 'ignore', 'exclude_classes' => '', 'alternate_link' => 1, 'content_negotiation' => 0, 'front_matter' => 0, 'cache_enabled' => 1, 'cache_ttl' => 43200, 'http_cache_seconds' => 300, 'x_robots' => 'none', 'analytics' => 0, 'frontend_button' => 0, 'button_label' => '', 'button_copy' => 0, 'copy_label' => '', 'button_llms' => 0, 'llms_button_label' => '', 'button_position' => 'bottom-center', 'button_style' => 'dark', 'button_shape' => 'pill', 'describedby_link' => 0, 'llms_url' => '/llms.txt', 'manage_llms' => 0, 'llms_content' => '', 'llms_generator_summary' => '', 'llms_generator_ids' => array(), 'llms_generator_use_markdown'=> 1, ); } /** Get normalized settings once per request. */ public static function get_settings() { if ( null !== self::$settings_cache ) { return self::$settings_cache; } $settings = get_option( self::SETTINGS_OPTION, array() ); self::$settings_cache = wp_parse_args( is_array( $settings ) ? $settings : array(), self::get_default_settings() ); return self::$settings_cache; } /** Reset request-local settings after an in-request update. */ public static function reset_settings_cache() { self::$settings_cache = null; } /** Public post types available to the plugin. */ public static function get_available_post_types() { $objects = get_post_types( array( 'public' => true ), 'objects' ); unset( $objects['attachment'] ); foreach ( $objects as $name => $object ) { if ( ! is_post_type_viewable( $object ) ) { unset( $objects[ $name ] ); } } return $objects; } /** Return expected physical root llms.txt path. */ public static function get_physical_llms_path() { return wp_normalize_path( trailingslashit( ABSPATH ) . 'llms.txt' ); } /** Whether physical llms.txt exists. */ public static function physical_llms_exists() { return is_file( self::get_physical_llms_path() ); } /** Whether physical llms.txt is a symlink. */ public static function physical_llms_is_symlink() { return is_link( self::get_physical_llms_path() ); } /** * Whether WordPress can use direct filesystem access at the site root. * * Physical llms.txt editing is intentionally limited to direct access so the * plugin never requests or stores FTP/SSH credentials. The virtual manager * remains available when direct access is not supported. * * @return bool */ public static function physical_llms_direct_filesystem_available() { require_once ABSPATH . 'wp-admin/includes/file.php'; return 'direct' === get_filesystem_method( array(), ABSPATH ); } /** Local direct-filesystem wrapper used for safe root llms.txt access. */ private static function get_local_filesystem() { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; return new WP_Filesystem_Direct( null ); } /** Read physical llms.txt. */ public static function read_physical_llms_content() { if ( ! self::physical_llms_exists() || self::physical_llms_is_symlink() ) { return false; } try { $filesystem = self::get_local_filesystem(); $path = self::get_physical_llms_path(); return $filesystem->is_readable( $path ) ? $filesystem->get_contents( $path ) : false; } catch ( Throwable $error ) { return false; } } /** * Normalize user-authored Markdown without stripping Markdown syntax. * * The public representation is served as text/markdown with nosniff; this * sanitizer therefore preserves Markdown and code samples while removing * invalid UTF-8, NUL bytes, and unsafe ASCII control characters. * * @param mixed $value Raw value. * @param int $max_bytes Maximum accepted byte length. * @return string */ public static function sanitize_markdown_text( $value, $max_bytes = 2097152 ) { $value = is_string( $value ) ? $value : ''; $value = wp_check_invalid_utf8( $value ); $value = str_replace( array( "\r\n", "\r", "\0" ), array( "\n", "\n", '' ), $value ); $value = preg_replace( '/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value ); $value = is_string( $value ) ? $value : ''; $max_bytes = max( 1024, absint( $max_bytes ) ); if ( strlen( $value ) > $max_bytes ) { $value = substr( $value, 0, $max_bytes ); $value = wp_check_invalid_utf8( $value ); } return $value; } /** Return physical llms.txt modification time when available. */ public static function get_physical_llms_modified_timestamp() { if ( ! self::physical_llms_exists() || self::physical_llms_is_symlink() ) { return 0; } try { $filesystem = self::get_local_filesystem(); $modified = $filesystem->mtime( self::get_physical_llms_path() ); return $modified ? (int) $modified : 0; } catch ( Throwable $error ) { return 0; } } /** Physical llms.txt writable status. */ public static function physical_llms_is_writable() { if ( ! self::physical_llms_exists() || self::physical_llms_is_symlink() || ! self::physical_llms_direct_filesystem_available() ) { return false; } try { $filesystem = self::get_local_filesystem(); return $filesystem->is_writable( self::get_physical_llms_path() ); } catch ( Throwable $error ) { return false; } } /** * Safely replace a physical llms.txt using a same-directory temp file and * temporary backup. Symlinks are deliberately rejected. * * @param string $content Content to write. * @return true|WP_Error */ public static function write_physical_llms_content( $content ) { $path = self::get_physical_llms_path(); if ( ! self::physical_llms_exists() ) { return new WP_Error( 'agentic_mb_llms_missing', __( 'The physical llms.txt file no longer exists.', 'agentic-markdown-bridge' ) ); } if ( self::physical_llms_is_symlink() ) { return new WP_Error( 'agentic_mb_llms_symlink', __( 'For safety, Agentic Markdown Bridge will not edit a symbolic-link llms.txt file.', 'agentic-markdown-bridge' ) ); } if ( ! self::physical_llms_direct_filesystem_available() ) { return new WP_Error( 'agentic_mb_llms_filesystem_method', __( 'Physical llms.txt editing requires direct filesystem access. Use the WordPress-managed virtual llms.txt option on this host.', 'agentic-markdown-bridge' ) ); } if ( strlen( (string) $content ) > 2 * MB_IN_BYTES ) { return new WP_Error( 'agentic_mb_llms_too_large', __( 'llms.txt is larger than the 2 MB safety limit.', 'agentic-markdown-bridge' ) ); } $root = wp_normalize_path( trailingslashit( ABSPATH ) ); if ( 0 !== strpos( $path, $root ) || $path !== $root . 'llms.txt' ) { return new WP_Error( 'agentic_mb_llms_path', __( 'The llms.txt path failed the safety check.', 'agentic-markdown-bridge' ) ); } try { $filesystem = self::get_local_filesystem(); if ( ! $filesystem->is_writable( $path ) ) { return new WP_Error( 'agentic_mb_llms_not_writable', __( 'The physical llms.txt file is not writable by WordPress.', 'agentic-markdown-bridge' ) ); } $chmod = $filesystem->getchmod( $path ); $mode = $chmod ? octdec( $chmod ) : 0644; $token = strtolower( wp_generate_password( 10, false, false ) ); $tmp = $path . '.amb-' . $token . '.tmp'; $bak = $path . '.amb-' . $token . '.bak'; if ( ! $filesystem->put_contents( $tmp, (string) $content, $mode ) ) { return new WP_Error( 'agentic_mb_llms_temp_write', __( 'WordPress could not create the temporary llms.txt file.', 'agentic-markdown-bridge' ) ); } if ( ! $filesystem->copy( $path, $bak, true, $mode ) ) { $filesystem->delete( $tmp, false, 'f' ); return new WP_Error( 'agentic_mb_llms_backup', __( 'WordPress could not create a temporary backup before updating llms.txt.', 'agentic-markdown-bridge' ) ); } if ( ! $filesystem->move( $tmp, $path, true ) ) { $filesystem->move( $bak, $path, true ); $filesystem->delete( $tmp, false, 'f' ); return new WP_Error( 'agentic_mb_llms_replace', __( 'WordPress could not replace llms.txt. The original file was restored.', 'agentic-markdown-bridge' ) ); } $filesystem->chmod( $path, $mode ); $filesystem->delete( $bak, false, 'f' ); } catch ( Throwable $error ) { return new WP_Error( 'agentic_mb_llms_filesystem_error', __( 'WordPress could not update llms.txt because of a filesystem error.', 'agentic-markdown-bridge' ) ); } return true; } /** Default starter llms.txt. */ public static function get_default_llms_content() { $name = trim( wp_strip_all_tags( (string) get_bloginfo( 'name' ) ) ); $description = trim( wp_strip_all_tags( (string) get_bloginfo( 'description' ) ) ); $name = $name ? $name : 'Website'; $content = '# ' . $name . "\n"; if ( $description ) { $content .= "\n> " . $description . "\n"; } $content .= "\n## Important Pages\n\n- [Home](" . home_url( '/' ) . "): Main website.\n"; return $content; } /** Generate an editable llms.txt draft from selected posts. */ public static function generate_llms_content( $settings ) { $settings = wp_parse_args( is_array( $settings ) ? $settings : array(), self::get_settings() ); $name = trim( wp_strip_all_tags( (string) get_bloginfo( 'name' ) ) ); $name = $name ? $name : 'Website'; $summary = trim( (string) $settings['llms_generator_summary'] ); if ( ! $summary ) { $summary = trim( wp_strip_all_tags( (string) get_bloginfo( 'description' ) ) ); } $content = '# ' . $name . "\n"; if ( $summary ) { $content .= "\n> " . preg_replace( '/\s+/u', ' ', $summary ) . "\n"; } $ids = isset( $settings['llms_generator_ids'] ) && is_array( $settings['llms_generator_ids'] ) ? array_map( 'absint', $settings['llms_generator_ids'] ) : array(); $ids = array_values( array_filter( array_unique( $ids ) ) ); if ( ! $ids ) { $front = (int) get_option( 'page_on_front' ); if ( $front ) { $ids[] = $front; } $pages = get_posts( array( 'post_type' => 'page', 'post_status' => 'publish', 'numberposts' => 20, 'orderby' => array( 'menu_order' => 'ASC', 'date' => 'DESC' ), 'fields' => 'ids' ) ); $ids = array_values( array_unique( array_merge( $ids, $pages ) ) ); } $groups = array(); foreach ( $ids as $id ) { $post = get_post( $id ); if ( ! $post || 'publish' !== $post->post_status || ! is_post_type_viewable( $post->post_type ) ) { continue; } $object = get_post_type_object( $post->post_type ); $label = $object && ! empty( $object->labels->name ) ? $object->labels->name : ucfirst( $post->post_type ); $groups[ $label ][] = $post; } foreach ( $groups as $label => $posts ) { $content .= "\n## " . wp_strip_all_tags( $label ) . "\n\n"; foreach ( $posts as $post ) { $url = ! empty( $settings['llms_generator_use_markdown'] ) && self::is_post_enabled( $post ) ? self::get_markdown_url( $post ) : get_permalink( $post ); $description = trim( wp_strip_all_tags( get_the_excerpt( $post ) ) ); if ( ! $description ) { $description = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post->post_content ) ), 28, '…' ); } $title = wp_strip_all_tags( get_the_title( $post ) ); $content .= '- [' . str_replace( array( '[', ']' ), '', $title ) . '](' . esc_url_raw( $url ) . ')'; if ( $description ) { $content .= ': ' . preg_replace( '/\s+/u', ' ', $description ); } $content .= "\n"; } } return rtrim( $content ) . "\n"; } /** llms.txt discovery URL. */ public static function get_llms_txt_url() { $settings = self::get_settings(); if ( ! empty( $settings['manage_llms'] ) ) { return (string) apply_filters( 'agentic_markdown_bridge_llms_txt_url', home_url( '/llms.txt' ) ); } if ( empty( $settings['describedby_link'] ) ) { return ''; } $raw = trim( (string) $settings['llms_url'] ); if ( ! $raw ) { return ''; } $url = 0 === strpos( $raw, '/' ) ? home_url( $raw ) : esc_url_raw( $raw ); return (string) apply_filters( 'agentic_markdown_bridge_llms_txt_url', $url ); } /** Get per-post Markdown enable mode. */ public static function get_post_enabled_mode( $post_id ) { $mode = get_post_meta( (int) $post_id, self::ENABLED_MODE_META, true ); return in_array( $mode, array( 'enable', 'disable' ), true ) ? $mode : 'inherit'; } /** Determine whether a post exposes Markdown. */ public static function is_post_enabled( $post ) { $settings = self::get_settings(); if ( empty( $settings['enabled'] ) ) { return false; } $post = get_post( $post ); if ( ! $post instanceof WP_Post || 'publish' !== $post->post_status || ! empty( $post->post_password ) || 'attachment' === $post->post_type ) { return false; } $post_type = get_post_type_object( $post->post_type ); if ( ! $post_type || ! is_post_type_viewable( $post_type ) ) { return false; } $mode = self::get_post_enabled_mode( $post->ID ); if ( 'disable' === $mode ) { $enabled = false; } elseif ( 'enable' === $mode ) { $enabled = true; } else { $allowed = isset( $settings['post_types'] ) && is_array( $settings['post_types'] ) ? array_map( 'sanitize_key', $settings['post_types'] ) : array(); $allowed = (array) apply_filters( 'agentic_markdown_bridge_allowed_post_types', $allowed ); $enabled = in_array( $post->post_type, $allowed, true ); } return (bool) apply_filters( 'agentic_markdown_bridge_is_enabled_for_post', $enabled, $post ); } /** Get an inherit/enable/disable mode for a per-content front-end option. */ public static function get_post_toggle_mode( $post_id, $meta_key ) { $allowed_meta = array( self::FRONTEND_MODE_META, self::COPY_MODE_META, self::LLMS_ACTION_MODE_META ); if ( ! in_array( $meta_key, $allowed_meta, true ) ) { return 'inherit'; } $mode = get_post_meta( (int) $post_id, $meta_key, true ); return in_array( $mode, array( 'enable', 'disable' ), true ) ? $mode : 'inherit'; } /** Resolve a global front-end setting with an optional per-content override. */ private static function resolve_post_toggle( $post, $meta_key, $global_enabled ) { $post = get_post( $post ); if ( ! $post instanceof WP_Post ) { return (bool) $global_enabled; } $mode = self::get_post_toggle_mode( $post->ID, $meta_key ); if ( 'enable' === $mode ) { return true; } if ( 'disable' === $mode ) { return false; } return (bool) $global_enabled; } /** Whether the fixed front-end action group is enabled for a post. */ public static function is_frontend_actions_enabled( $post ) { $post = get_post( $post ); if ( ! $post instanceof WP_Post || ! self::is_post_enabled( $post ) ) { return false; } $enabled = self::resolve_post_toggle( $post, self::FRONTEND_MODE_META, ! empty( self::get_settings()['frontend_button'] ) ); return (bool) apply_filters( 'agentic_markdown_bridge_frontend_actions_enabled', $enabled, $post ); } /** Whether Copy Markdown is enabled for a post. */ public static function is_copy_action_enabled( $post ) { $post = get_post( $post ); if ( ! $post instanceof WP_Post ) { return false; } $enabled = self::resolve_post_toggle( $post, self::COPY_MODE_META, ! empty( self::get_settings()['button_copy'] ) ); return (bool) apply_filters( 'agentic_markdown_bridge_copy_action_enabled', $enabled, $post ); } /** Whether the llms.txt front-end action is enabled for a post. */ public static function is_llms_action_enabled( $post ) { $post = get_post( $post ); if ( ! $post instanceof WP_Post ) { return false; } $enabled = self::resolve_post_toggle( $post, self::LLMS_ACTION_MODE_META, ! empty( self::get_settings()['button_llms'] ) ); return (bool) apply_filters( 'agentic_markdown_bridge_llms_action_enabled', $enabled, $post ); } /** Markdown URL for a post. */ public static function get_markdown_url( $post ) { $post = get_post( $post ); if ( ! $post ) { return ''; } $permalink = get_permalink( $post ); if ( ! $permalink ) { return ''; } if ( ! get_option( 'permalink_structure' ) ) { $url = add_query_arg( self::QUERY_ID_VAR, (int) $post->ID, home_url( '/' ) ); } else { $home_path = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ); $post_path = (string) wp_parse_url( $permalink, PHP_URL_PATH ); $home_norm = trailingslashit( '/' . trim( $home_path, '/' ) ); $post_norm = trailingslashit( '/' . trim( $post_path, '/' ) ); $url = $home_norm === $post_norm ? home_url( '/index.md' ) : trailingslashit( $permalink ) . 'index.md'; } return (string) apply_filters( 'agentic_markdown_bridge_markdown_url', $url, $post ); } /** Root homepage Markdown URL. */ public static function get_front_markdown_url() { if ( ! get_option( 'permalink_structure' ) ) { return add_query_arg( self::QUERY_VAR, '__front__', home_url( '/' ) ); } return home_url( '/index.md' ); } /** Resolve path route to post. */ private static function resolve_requested_post( $requested_path ) { $requested_path = (string) $requested_path; if ( '__front__' === $requested_path ) { $front_id = (int) get_option( 'page_on_front' ); return $front_id ? get_post( $front_id ) : null; } $path = trim( rawurldecode( $requested_path ), '/' ); if ( '' === $path || false !== strpos( $path, '..' ) ) { return null; } foreach ( array_unique( array( home_url( '/' . $path . '/' ), home_url( '/' . $path ) ) ) as $candidate_url ) { $post_id = url_to_postid( $candidate_url ); if ( $post_id ) { return get_post( $post_id ); } } foreach ( get_post_types( array( 'public' => true ), 'names' ) as $post_type ) { if ( 'attachment' === $post_type ) { continue; } $post = get_page_by_path( $path, OBJECT, $post_type ); if ( $post ) { return $post; } } return null; } /** Determine effective post-title mode. */ private static function should_include_post_title( $post, $content_html, $converted_markdown = '' ) { $settings = self::get_settings(); $global = sanitize_key( (string) $settings['title_mode'] ); $override = get_post_meta( $post->ID, self::TITLE_MODE_META, true ); if ( 'include' === $override ) { $mode = 'always'; } elseif ( 'exclude' === $override ) { $mode = 'never'; } elseif ( 'auto' === $override ) { $mode = 'auto'; } else { $mode = in_array( $global, array( 'auto', 'always', 'never' ), true ) ? $global : 'auto'; } $has_h1 = '' !== $converted_markdown ? (bool) preg_match( '/^#\s+\S+/m', (string) $converted_markdown ) : (bool) preg_match( '/