*/ public static function defaults() { return array( 'mode' => 'manual', // 'manual' | 'connected'. 'manual_content' => '', 'synced_content' => '', 'connected' => false, 'last_synced' => 0, 'connection_token' => '', // Server-side — not shown in UI. 'bound_site_url' => '', // Canonical WordPress home this link is tied to. 'bound_property_id' => '', // GA4 property id in AI View when connect completed. 'bound_property_label' => '', // Human-readable label from AI View (hostname / display) for admin UI. 'connection_site_id' => '', // Deprecated: migrated to bound_property_id in get_all(). 'exclude_path_patterns' => '', // Newline-separated path substrings to exclude (e.g. /thank-you/). 'exclude_post_types' => array(), // Post type slugs to exclude from WP URL discovery. 'last_sync_ga4_count' => 0, // Pages counted from AI View body (last sync). 'last_sync_wp_count' => 0, // Additional pages appended from WordPress (last sync). 'write_root_file' => false, // Opt-in: also write llms.txt to the site root (ABSPATH). Needed on hosts (e.g. WP Engine) whose web server answers .txt URLs statically and 404s before WordPress can serve the virtual /llms.txt. 'root_file_managed' => false, // Internal: true once this plugin created the root llms.txt, so it never overwrites/deletes a file it does not own. ); } /** * @return array */ public static function get_all() { $stored = get_option( self::OPTION_KEY, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } $out = array_merge( self::defaults(), $stored ); if ( ! empty( $out['connection_site_id'] ) && '' === (string) $out['bound_property_id'] ) { $out['bound_property_id'] = (string) $out['connection_site_id']; } return $out; } /** * @param array $data Partial settings. */ public static function update( array $data ) { $merged = array_merge( self::get_all(), $data ); update_option( self::OPTION_KEY, self::sanitize_all( $merged ), false ); } /** * @param array $data Raw input. * @return array */ public static function sanitize_all( array $data ) { $defaults = self::defaults(); $out = array(); $mode = isset( $data['mode'] ) ? sanitize_key( (string) $data['mode'] ) : $defaults['mode']; $out['mode'] = in_array( $mode, array( 'manual', 'connected' ), true ) ? $mode : 'manual'; $out['manual_content'] = isset( $data['manual_content'] ) ? self::sanitize_llms_body( (string) $data['manual_content'] ) : ''; $out['synced_content'] = isset( $data['synced_content'] ) ? self::sanitize_llms_body( (string) $data['synced_content'] ) : ''; $out['connected'] = ! empty( $data['connected'] ); $last = isset( $data['last_synced'] ) ? absint( $data['last_synced'] ) : 0; $out['last_synced'] = $last; $out['connection_token'] = isset( $data['connection_token'] ) ? sanitize_text_field( (string) $data['connection_token'] ) : ''; $out['connection_site_id'] = isset( $data['connection_site_id'] ) ? sanitize_text_field( (string) $data['connection_site_id'] ) : ''; $out['bound_site_url'] = isset( $data['bound_site_url'] ) ? esc_url_raw( (string) $data['bound_site_url'] ) : ''; $out['bound_property_id'] = isset( $data['bound_property_id'] ) ? sanitize_text_field( (string) $data['bound_property_id'] ) : ''; $out['bound_property_label'] = isset( $data['bound_property_label'] ) ? sanitize_text_field( (string) $data['bound_property_label'] ) : ''; $out['exclude_path_patterns'] = isset( $data['exclude_path_patterns'] ) ? self::sanitize_llms_body( (string) $data['exclude_path_patterns'] ) : ''; $ex_pt = array(); if ( isset( $data['exclude_post_types'] ) && is_array( $data['exclude_post_types'] ) ) { foreach ( $data['exclude_post_types'] as $t ) { $t = sanitize_key( (string) $t ); if ( '' !== $t ) { $ex_pt[] = $t; } } } $out['exclude_post_types'] = array_values( array_unique( $ex_pt ) ); $out['last_sync_ga4_count'] = isset( $data['last_sync_ga4_count'] ) ? absint( $data['last_sync_ga4_count'] ) : 0; $out['last_sync_wp_count'] = isset( $data['last_sync_wp_count'] ) ? absint( $data['last_sync_wp_count'] ) : 0; $out['write_root_file'] = ! empty( $data['write_root_file'] ); $out['root_file_managed'] = ! empty( $data['root_file_managed'] ); return $out; } /** * Plain-text llms body: preserve newlines, strip null bytes. * * @param string $text Raw. * @return string */ public static function sanitize_llms_body( $text ) { $text = str_replace( "\0", '', $text ); return $text; } /** * Single source of truth for public output: follows active mode. * * @return string */ public static function get_active_content() { $s = self::get_all(); if ( 'connected' === $s['mode'] && ! empty( $s['connected'] ) ) { return (string) $s['synced_content']; } return (string) $s['manual_content']; } /** * Public URL for /llms.txt (no trailing slash on path). * * @return string */ public static function get_public_llms_url() { return home_url( '/llms.txt' ); } /** * Normalize site URL for comparison (scheme, host without leading www, path trimmed). * * @param string $url Full URL. * @return string */ public static function normalize_site_url( $url ) { $url = trim( (string) $url ); if ( '' === $url ) { return ''; } $p = wp_parse_url( $url ); if ( ! is_array( $p ) || empty( $p['host'] ) ) { return ''; } $scheme = isset( $p['scheme'] ) && in_array( $p['scheme'], array( 'http', 'https' ), true ) ? $p['scheme'] : 'https'; $host = strtolower( $p['host'] ); $host = preg_replace( '/^www\./', '', $host ); $path = isset( $p['path'] ) ? '/' . trim( $p['path'], '/' ) : ''; if ( '/' === $path ) { $path = ''; } return $scheme . '://' . $host . $path; } /** * Whether this install still matches the site URL stored at connect time. * * @return bool */ public static function is_bound_site_current() { $s = self::get_all(); if ( '' === (string) $s['bound_site_url'] ) { return true; } $here = self::normalize_site_url( home_url( '/' ) ); $bind = self::normalize_site_url( $s['bound_site_url'] ); return $here === $bind; } /** * Connected to AI View but no GA4 property chosen for this site yet. * * @return bool */ public static function needs_property_selection() { $s = self::get_all(); return ! empty( $s['connected'] ) && '' === trim( (string) ( $s['bound_property_id'] ?? '' ) ); } /** * @return string URL or empty. */ public static function get_generator_url() { if ( ! defined( 'AIVISY_APP_URL' ) || ! is_string( AIVISY_APP_URL ) || '' === trim( AIVISY_APP_URL ) ) { return ''; } return rtrim( trim( AIVISY_APP_URL ), '/' ) . '/generator_llm_txt.php'; } /** * wp-admin URL for this plugin screen (for return links from AI View). * * @return string */ public static function get_plugin_settings_url() { return admin_url( 'admin.php?page=aivisy-settings' ); } /** * Generator URL with params so AI View can show "Return to WordPress" after generation. * * @return string */ public static function get_generator_url_with_wp_return() { $base = self::get_generator_url(); if ( '' === $base ) { return ''; } $return = self::get_plugin_settings_url(); return add_query_arg( array( 'wp_llms_return' => '1', 'wp_llms_return_url' => $return, ), $base ); } /** * AI View sign-in / app entry (login.php). Sync/properties APIs still use AIVISY_APP_URL + specific PHP endpoints. * * @return string URL or empty. */ public static function get_dashboard_url() { if ( ! defined( 'AIVISY_APP_URL' ) || ! is_string( AIVISY_APP_URL ) || '' === trim( AIVISY_APP_URL ) ) { return ''; } return rtrim( trim( AIVISY_APP_URL ), '/' ) . '/login.php'; } /** * Whether synced content is still the initial stub (before a real sync). * * @return bool */ public static function synced_is_placeholder_stub() { $s = self::get_all(); $c = (string) $s['synced_content']; return false !== strpos( $c, 'placeholder until API' ) || false !== strpos( $c, 'placeholder — use Connect' ); } }