true, 'error' => '', 'wrote' => false]; } if (!self::got_rewrite_support()) { return ['ok' => false, 'error' => self::ERROR_UNAVAILABLE, 'wrote' => false]; } self::maybe_load_wp_admin_apis(); $file = self::validated_htaccess_path(); if ($file === null) { return ['ok' => false, 'error' => self::ERROR_UNAVAILABLE, 'wrote' => false]; } $rules = self::marker_rule_lines(); if ($rules === []) { return ['ok' => false, 'error' => self::ERROR_FAILED, 'wrote' => false]; } $written = insert_with_markers($file, self::MARKER, $rules); if ($written !== true) { return ['ok' => false, 'error' => self::ERROR_FAILED, 'wrote' => false]; } if (!self::htaccess_contains_exact_marker($file, $rules)) { return ['ok' => false, 'error' => self::ERROR_FAILED, 'wrote' => false]; } return ['ok' => true, 'error' => '', 'wrote' => true]; } /** Remove only the plugin marker block. Preserves unrelated .htaccess content. */ public static function remove(): bool { self::maybe_load_wp_admin_apis(); $home = self::home_path(); if ($home === '') { return true; } $file = trailingslashit($home) . '.htaccess'; if (!is_string($file) || $file === '' || !file_exists($file)) { return true; } if (!function_exists('insert_with_markers')) { return false; } return insert_with_markers($file, self::MARKER, []) === true; } /** * Deterministic, secret-free, subdirectory-aware rewrite lines (no BEGIN/END markers). * * @return list */ public static function marker_rule_lines(): array { $base = TheDotDevAgenticBridge_Artifacts::home_base_path(); $rewrite_base = $base === '' ? '/' : trailingslashit($base); $query_var = TheDotDevAgenticBridge_Artifacts::ARTIFACT_QUERY_VAR; // Exact file names only — pass artifact query var so PHP resolve matches pretty-permalink rewrites. // Optional trailing slash: core redirect_canonical may 301 /llms.txt → /llms.txt/ if we miss priority 0. return [ '', 'RewriteEngine On', 'RewriteBase ' . $rewrite_base, 'RewriteRule ^llms\.txt/?$ index.php?' . $query_var . '=llms [L]', 'RewriteRule ^agent-map\.json/?$ index.php?' . $query_var . '=agent-map [L]', '', ]; } public static function using_pretty_permalinks(): bool { $structure = get_option('permalink_structure', ''); return is_string($structure) && $structure !== ''; } public static function environment_is_apache(): bool { global $is_apache; if (!empty($is_apache)) { return true; } $software = self::sanitized_server_software(); if ($software !== '' && stripos($software, 'Apache') !== false) { return true; } return (bool) apply_filters('thedotdev_agentic_bridge_environment_is_apache', false); } /** * Bounded scalar from $_SERVER['SERVER_SOFTWARE'] for Apache detection only. */ private static function sanitized_server_software(): string { if (!isset($_SERVER['SERVER_SOFTWARE']) || !is_string($_SERVER['SERVER_SOFTWARE'])) { return ''; } $software = sanitize_text_field(wp_unslash($_SERVER['SERVER_SOFTWARE'])); if ($software === '') { return ''; } // Bound length: only used for stripos('Apache'); reject oversized junk. if (strlen($software) > 255) { $software = substr($software, 0, 255); } return $software; } private static function got_rewrite_support(): bool { if (function_exists('got_mod_rewrite')) { return (bool) got_mod_rewrite(); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core filter name (got_mod_rewrite fallback); must not be renamed. return (bool) apply_filters('got_rewrite', true); } private static function home_path(): string { if (!function_exists('get_home_path')) { return ''; } $home = get_home_path(); if (!is_string($home) || $home === '') { return ''; } $real = realpath($home); if ($real === false || !is_dir($real)) { // Allow test stubs / non-realpath environments when is_dir passes. if (!is_dir($home)) { return ''; } return $home; } return trailingslashit($real); } private static function validated_htaccess_path(): ?string { $home = self::home_path(); if ($home === '') { return null; } $file = trailingslashit($home) . '.htaccess'; $dir = dirname($file); if (!is_dir($dir)) { return null; } // Prefer wp_is_writable after admin file APIs are loaded (see maybe_load_wp_admin_apis). if (!function_exists('wp_is_writable')) { return null; } if (file_exists($file)) { if (!wp_is_writable($file)) { return null; } } elseif (!wp_is_writable($dir)) { return null; } // Refuse path traversal / unexpected names. if (str_contains($file, "\0") || !str_ends_with($file, '.htaccess')) { return null; } return $file; } /** @param list $rules */ private static function htaccess_contains_exact_marker(string $file, array $rules): bool { if (!is_readable($file)) { return false; } $contents = file_get_contents($file); if (!is_string($contents) || $contents === '') { return false; } $start = '# BEGIN ' . self::MARKER; $end = '# END ' . self::MARKER; if (!str_contains($contents, $start) || !str_contains($contents, $end)) { return false; } foreach ($rules as $line) { if (!str_contains($contents, $line)) { return false; } } $block = self::extract_marker_block($contents); if ($block === '') { return false; } $query_var = preg_quote(TheDotDevAgenticBridge_Artifacts::ARTIFACT_QUERY_VAR, '/'); $has_current = (bool) preg_match('/RewriteRule \^llms\\\\\.txt\/\?\$ index\.php\?' . $query_var . '=llms \[L\]/', $block) && (bool) preg_match('/RewriteRule \^agent-map\\\\\.json\/\?\$ index\.php\?' . $query_var . '=agent-map \[L\]/', $block); return $has_current && !preg_match('/RewriteRule\s+\.\s+/', $block); } private static function extract_marker_block(string $contents): string { $start = '# BEGIN ' . self::MARKER; $end = '# END ' . self::MARKER; $from = strpos($contents, $start); $to = strpos($contents, $end); if ($from === false || $to === false || $to < $from) { return ''; } return substr($contents, $from, $to - $from + strlen($end)); } private static function maybe_load_wp_admin_apis(): void { if (!defined('ABSPATH')) { return; } if (!function_exists('get_home_path')) { $file = ABSPATH . 'wp-admin/includes/file.php'; if (is_readable($file)) { require_once $file; } } if (!function_exists('insert_with_markers')) { $misc = ABSPATH . 'wp-admin/includes/misc.php'; if (is_readable($misc)) { require_once $misc; } } } }