get_bloginfo( 'name' ), 'geistm_organization_tagline' => get_bloginfo( 'description' ), 'geistm_content_license' => 'cc-by-4.0', 'geistm_allow_quoting' => '1', 'geistm_allow_training' => '0', 'geistm_sector' => '', 'geistm_enable_seo_tags' => '1', 'geistm_enable_llms_txt' => '1', 'geistm_enable_llms_full' => '0', 'geistm_llms_posts_limit' => '10', 'geistm_llms_full_posts_limit' => '50', 'geistm_llms_pages_limit' => '30', 'geistm_llms_include_excerpts' => '1', // Adnet — disabled by default. 'geistm_adnet_enabled' => '0', 'geistm_adnet_host' => 'https://adnet.geistm.com', 'geistm_adnet_auto_enabled' => '0', 'geistm_adnet_auto_format' => 'banner', 'geistm_adnet_auto_paragraph' => '3', 'geistm_adnet_auto_max' => '1', 'geistm_adnet_auto_pages' => '0', 'geistm_adnet_declined' => '0', 'geistm_adnet_placement_choice' => '', ); foreach ( $defaults as $key => $value ) { if ( false === get_option( $key ) ) { update_option( $key, $value ); } } // Register rewrite rules and flush. geistm_ai_discovery_register_rewrites(); flush_rewrite_rules(); // Clear any cached data. geistm_ai_discovery_clear_all_caches(); // The wallet is NOT auto-created here: creating it is a deliberate // onboarding step (its ID stamps content, its Credit receives payouts — // the publisher should know it exists and back it up). A wallet from a // previous install survives deactivate/reactivate untouched. update_option( 'geistm_ai_discovery_version', GEISTM_AI_DISCOVERY_VERSION ); } register_activation_hook( __FILE__, 'geistm_ai_discovery_activate' ); /** * Deactivation: clean up rewrite rules. */ function geistm_ai_discovery_deactivate() { flush_rewrite_rules(); } register_deactivation_hook( __FILE__, 'geistm_ai_discovery_deactivate' ); /** * Upgrade routine — runs once per version change. * * Replacing the plugin via zip upload does not re-fire the activation hook, * so schema/option migrations live here. Hooked on init at priority 20 so * rewrites are registered before the flush. */ function geistm_ai_discovery_maybe_upgrade() { $installed = get_option( 'geistm_ai_discovery_version', '0' ); if ( $installed === GEISTM_AI_DISCOVERY_VERSION ) { return; } // ── Legacy lineage (>= 2.0.0) ─────────────────────────────────────── // The plugin renumbered to 1.x for the Adnet by GeistM release; the // forked lineage shipped as 2.x, with interim 3.x builds before the // renumber. Anything >= 2.0.0 is a legacy install: clean up the removed // features and bring the signature up to the current conventions. if ( '0' !== $installed && version_compare( $installed, '2.0.0', '>=' ) ) { global $wpdb; // Migrate the legacy sector option before deleting it. $legacy_sector = get_option( 'geistm_organization_sector', '' ); if ( ! empty( $legacy_sector ) && '' === get_option( 'geistm_sector', '' ) ) { update_option( 'geistm_sector', $legacy_sector ); } // Options behind features removed from the legacy lineage (content // endpoint, WebMCP, metrics, licensing, registry). $stale_options = array( 'geistm_webmcp_enabled', 'geistm_enable_knowledge', 'geistm_enable_feed', 'geistm_enable_content', 'geistm_content_include_pages', 'geistm_content_include_posts', 'geistm_content_include_custom_types', 'geistm_content_include_media', 'geistm_content_include_full_text', 'geistm_content_posts_limit', 'geistm_content_media_limit', 'geistm_owner_identity', 'geistm_registry_consent', 'geistm_setup_log', 'geistm_authorization_signature', 'geistm_account_status', 'geistm_account_tier', 'geistm_organization_sector', ); foreach ( $stale_options as $option ) { delete_option( $option ); } $stale_transients = array( 'geistm_knowledge_cache', 'geistm_feed_cache', 'geistm_content_cache', 'geistm_license_status', 'geistm_update_check', 'geistm_proxy_usage', 'geistm_proxy_error', ); foreach ( $stale_transients as $transient ) { delete_transient( $transient ); } // Metrics module removed — drop its table and cron. wp_clear_scheduled_hook( 'geistm_metrics_prune' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}geistm_ai_metrics" ); if ( version_compare( $installed, '3.0.0', '<' ) ) { // 2.x snapshots advertise removed endpoints — discard entirely // and drop the removed rewrite rules. delete_option( 'geistm_signed_manifest' ); flush_rewrite_rules(); } if ( version_compare( $installed, '3.2.2', '<' ) ) { // Signature conventions changed after these builds (canonical // key-sorted hashes, digitalName default) — ask for a re-sign. update_option( 'geistm_manifest_needs_signing', '1', false ); } // Legacy builds stored a user-supplied Anthropic API key; generation // is now served by the GeistM AI service — remove the stored secret. delete_option( 'geistm_ai_api_key' ); } // 1.4.0 removed the vestigial "Authorizer Wallet" card — the address was // stored but never published or verified. Organization identity lives in // the spec fields (digitalName / identityContract) on the Identity tab. delete_option( 'geistm_plugin_wallet' ); // 1.5.0 removed the in-plugin balance lookup (it queried public RPC and // price endpoints on admin page load) — drop its cache. delete_transient( 'geistm_credit_balance' ); update_option( 'geistm_ai_discovery_version', GEISTM_AI_DISCOVERY_VERSION ); } add_action( 'init', 'geistm_ai_discovery_maybe_upgrade', 20 ); /** * Register custom rewrite rules — v1.2 URL hierarchy under /.well-known/ai/. */ function geistm_ai_discovery_register_rewrites() { // v1.2: Everything under /.well-known/ai/ hierarchy (RFC 8615). add_rewrite_rule( '\.well-known/ai/?$', 'index.php?geistm_endpoint=discovery', 'top' ); add_rewrite_rule( '\.well-known/ai/policies/?$', 'index.php?geistm_endpoint=policies', 'top' ); // llms.txt and llms-full.txt. add_rewrite_rule( 'llms\.txt$', 'index.php?geistm_endpoint=llms_txt', 'top' ); add_rewrite_rule( 'llms-full\.txt$', 'index.php?geistm_endpoint=llms_full_txt', 'top' ); // Prevent WordPress from redirecting .well-known/ai URLs. add_filter( 'redirect_canonical', 'geistm_ai_discovery_prevent_redirect', 10, 2 ); } add_action( 'init', 'geistm_ai_discovery_register_rewrites' ); /** * Prevent WordPress canonical redirects for our endpoints. * * @param string $redirect_url The redirect URL. * @param string $requested_url The requested URL. * @return string|false The redirect URL or false to cancel. */ function geistm_ai_discovery_prevent_redirect( $redirect_url, $requested_url ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Required by WordPress filter signature. if ( get_query_var( 'geistm_endpoint' ) ) { return false; } return $redirect_url; } /** * Register query vars. * * @param array $vars Existing query vars. * @return array Modified query vars. */ function geistm_ai_discovery_query_vars( $vars ) { $vars[] = 'geistm_endpoint'; return $vars; } add_filter( 'query_vars', 'geistm_ai_discovery_query_vars' ); /** * Serve plain text with standard headers. * * @param string $content The text content to serve. * @param int $cache_seconds Cache duration in seconds. */ function geistm_ai_discovery_serve_text( $content, $cache_seconds = 3600 ) { header( 'Content-Type: text/plain; charset=utf-8' ); header( 'X-Robots-Tag: noindex' ); header( 'Access-Control-Allow-Origin: *' ); header( 'Cache-Control: public, max-age=' . intval( $cache_seconds ) ); header( 'X-GeistM-AI-Discovery: ' . GEISTM_AI_DISCOVERY_VERSION ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain text output, not HTML context. echo $content; exit; } /** * Serve JSON with standard headers. * * @param array $data The data to encode as JSON. * @param int $cache_seconds Cache duration in seconds. */ function geistm_ai_discovery_serve_json( $data, $cache_seconds = 3600 ) { header( 'Content-Type: application/json; charset=utf-8' ); header( 'Access-Control-Allow-Origin: *' ); header( 'Access-Control-Allow-Methods: GET, OPTIONS' ); header( 'Access-Control-Allow-Headers: Content-Type' ); header( 'Cache-Control: public, max-age=' . intval( $cache_seconds ) ); header( 'X-GeistM-AI-Discovery: ' . GEISTM_AI_DISCOVERY_VERSION ); echo wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); exit; } /** * Handle all /.well-known/ai/* requests. */ function geistm_ai_discovery_template_redirect() { $endpoint = get_query_var( 'geistm_endpoint' ); if ( ! $endpoint ) { return; } switch ( $endpoint ) { case 'discovery': $generator = new Geistm_Ai_Json(); geistm_ai_discovery_serve_json( $generator->generate() ); break; case 'policies': $policies = new Geistm_Policies(); geistm_ai_discovery_serve_json( $policies->generate() ); break; case 'llms_txt': if ( '1' !== get_option( 'geistm_enable_llms_txt', '1' ) ) { status_header( 404 ); exit; } $generator = new Geistm_Llms_Txt(); geistm_ai_discovery_serve_text( $generator->generate() ); break; case 'llms_full_txt': if ( '1' !== get_option( 'geistm_enable_llms_full', '0' ) ) { status_header( 404 ); exit; } $generator = new Geistm_Llms_Txt(); geistm_ai_discovery_serve_text( $generator->generate_full() ); break; } } add_action( 'template_redirect', 'geistm_ai_discovery_template_redirect' ); /** * Add and optional SEO meta tags to HTML . */ function geistm_ai_discovery_wp_head() { // AI Discovery link tag (always). $url = home_url( '/.well-known/ai' ); echo '' . "\n"; // llms.txt link tags. if ( '1' === get_option( 'geistm_enable_llms_txt', '1' ) ) { echo '' . "\n"; } if ( '1' === get_option( 'geistm_enable_llms_full', '0' ) ) { echo '' . "\n"; } // SEO tags: meta description, OpenGraph, JSON-LD. // Only output if enabled AND no known SEO plugin is active. if ( '1' !== get_option( 'geistm_enable_seo_tags', '1' ) ) { return; } $seo_plugins = array( 'wordpress-seo/wp-seo.php', 'seo-by-rank-math/rank-math.php', 'all-in-one-seo-pack/all_in_one_seo_pack.php', 'wp-seopress/seopress.php', ); foreach ( $seo_plugins as $plugin ) { if ( is_plugin_active( $plugin ) ) { return; // Let the SEO plugin handle meta tags. } } $org_name = get_option( 'geistm_organization_name', get_bloginfo( 'name' ) ); $description = get_option( 'geistm_ai_summary', '' ); if ( empty( $description ) ) { $description = get_option( 'geistm_organization_tagline', get_bloginfo( 'description' ) ); } // Meta description (all pages). if ( ! empty( $description ) ) { echo '' . "\n"; } // OpenGraph + JSON-LD (front page only to avoid duplication on every page). if ( is_front_page() || is_home() ) { echo '' . "\n"; if ( ! empty( $description ) ) { echo '' . "\n"; } echo '' . "\n"; echo '' . "\n"; $site_icon_id = get_option( 'site_icon' ); if ( $site_icon_id ) { $icon_url = wp_get_attachment_image_url( $site_icon_id, 'full' ); if ( $icon_url ) { echo '' . "\n"; } } // JSON-LD Organization schema. $jsonld = array( '@context' => 'https://schema.org', '@type' => 'Organization', 'name' => $org_name, 'url' => home_url( '/' ), ); if ( ! empty( $description ) ) { $jsonld['description'] = wp_strip_all_tags( $description ); } $sector = get_option( 'geistm_sector', '' ); if ( empty( $sector ) ) { $sector = get_option( 'geistm_organization_sector', '' ); } if ( ! empty( $sector ) ) { $jsonld['knowsAbout'] = array_map( 'trim', explode( ',', $sector ) ); } $contact_email = get_option( 'geistm_contact_email', get_option( 'admin_email', '' ) ); if ( ! empty( $contact_email ) ) { $jsonld['contactPoint'] = array( '@type' => 'ContactPoint', 'email' => $contact_email, ); } if ( $site_icon_id ) { $icon_url = wp_get_attachment_image_url( $site_icon_id, 'full' ); if ( $icon_url ) { $jsonld['logo'] = $icon_url; } } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode output is safe for script context when not using JSON_UNESCAPED_SLASHES. echo '' . "\n"; } } add_action( 'wp_head', 'geistm_ai_discovery_wp_head' ); /** * Add Link HTTP header to all responses. */ function geistm_ai_discovery_send_headers() { if ( headers_sent() ) { return; } $url = home_url( '/.well-known/ai' ); header( 'Link: <' . esc_url( $url ) . '>; rel="ai-discovery"; type="application/json"' ); } add_action( 'send_headers', 'geistm_ai_discovery_send_headers' ); /** * Enqueue frontend scripts. */ function geistm_ai_discovery_enqueue_scripts() { // Adnet client — the slim publisher script served by the Adnet factory // (adnet.geistm.com, or a publisher's first-party _adnet. CNAME). // It derives its API base from its own script URL, asks /client/ads which // campaigns are live, and fills .adnet-entry slots with server-rendered // ads from /render; clicks are recorded server-side via /link. Only loads // when Adnet is enabled and a host is set. $geistm_adnet_host = get_option( 'geistm_adnet_host', 'https://adnet.geistm.com' ); if ( '1' === get_option( 'geistm_adnet_enabled', '0' ) && ! empty( $geistm_adnet_host ) ) { wp_enqueue_script( 'geistm-adnet-client', trailingslashit( esc_url_raw( $geistm_adnet_host ) ) . 'client/client.js', array(), GEISTM_AI_DISCOVERY_VERSION, array( 'in_footer' => true, 'strategy' => 'defer', ) ); } } add_action( 'wp_enqueue_scripts', 'geistm_ai_discovery_enqueue_scripts' ); /** * Automatic Adnet slot placement — inserts ad slots into post content. * * Inserts a slot after every Nth paragraph (up to the configured maximum) * on singular posts, and optionally pages. Skips content that already * contains a manually placed slot so auto-placement never crowds an * editorial decision. Runs at priority 20, after wpautop. * * @param string $content The post content. * @return string Content with ad slots inserted. */ function geistm_adnet_auto_insert( $content ) { if ( is_admin() || is_feed() || ! is_singular() || ! in_the_loop() || ! is_main_query() ) { return $content; } if ( '1' !== get_option( 'geistm_adnet_enabled', '0' ) || '' === get_option( 'geistm_adnet_host', 'https://adnet.geistm.com' ) || '1' !== get_option( 'geistm_adnet_auto_enabled', '0' ) ) { return $content; } if ( is_page() && '1' !== get_option( 'geistm_adnet_auto_pages', '0' ) ) { return $content; } if ( ! is_page() && ! is_singular( 'post' ) ) { return $content; } // Respect manual placements — a hand-placed slot means the author chose. if ( false !== strpos( $content, 'adnet-entry' ) ) { return $content; } $format = get_option( 'geistm_adnet_auto_format', 'banner' ); if ( ! in_array( $format, array( 'banner', 'square', 'card' ), true ) ) { $format = 'banner'; } $every = max( 1, (int) get_option( 'geistm_adnet_auto_paragraph', '3' ) ); $max = max( 1, (int) get_option( 'geistm_adnet_auto_max', '1' ) ); $classes = 'adnet-entry adnet-entry-auto'; if ( 'banner' !== $format ) { $classes .= ' adnet-entry-' . $format; } $slot = '
'; $pieces = preg_split( '/(<\/p>)/i', $content, -1, PREG_SPLIT_DELIM_CAPTURE ); $out = ''; $para = 0; $inserted = 0; foreach ( $pieces as $piece ) { $out .= $piece; if ( 0 === strcasecmp( $piece, '

' ) ) { $para++; if ( $inserted < $max && 0 === $para % $every ) { $out .= $slot; $inserted++; } } } // Short content (fewer paragraphs than the interval): append one slot at the end. if ( 0 === $inserted && $para > 0 ) { $out .= $slot; } return $out; } add_filter( 'the_content', 'geistm_adnet_auto_insert', 20 ); /** * Clear all transient caches. * * Does NOT auto-sign the manifest. Instead, flags it as needing review. * The admin must explicitly approve and sign via the admin notice or Viewer tab. * This prevents FTP-corrupted or injected content from being silently signed. */ function geistm_ai_discovery_clear_all_caches() { delete_transient( 'geistm_ai_json_cache' ); delete_transient( 'geistm_llms_txt_cache' ); delete_transient( 'geistm_llms_full_cache' ); delete_transient( 'geistm_discovered_policies' ); // Flag that content has changed and the manifest needs re-signing. update_option( 'geistm_manifest_needs_signing', '1', false ); } /** * Generate, sign, and store the manifest as an admin-approved snapshot. * Called when an admin triggers a cache clear (settings save, page edit, manual refresh). */ function geistm_ai_discovery_sign_manifest() { $generator = new Geistm_Ai_Json(); $data = $generator->generate_unsigned(); // Sign with plugin wallet. $signer = new Geistm_Signer(); if ( $signer->has_key() && Geistm_Signer::has_gmp() ) { $data['_signature'] = $signer->sign_content( $data ); $data['_signature']['approvedBy'] = 'admin'; } else { $data['_signature'] = array( 'contentHash' => 'sha256:' . Geistm_Signer::content_hash( $data ), 'signedAt' => gmdate( 'c' ), 'method' => 'hash-only', 'authorization' => 'none', 'approvedBy' => 'admin', ); } update_option( 'geistm_signed_manifest', $data, false ); update_option( 'geistm_manifest_signed_at', gmdate( 'c' ), false ); // Clear the needs-signing flag — admin has approved. delete_option( 'geistm_manifest_needs_signing' ); // Clear the ai.json transient so the signed manifest is served immediately. delete_transient( 'geistm_ai_json_cache' ); } /** * Clear caches when relevant options change. * * @param string $option The option name that was updated. */ function geistm_ai_discovery_clear_cache( $option ) { if ( strpos( $option, 'geistm_' ) === 0 || in_array( $option, array( 'blogname', 'blogdescription', 'siteurl', 'home' ), true ) ) { geistm_ai_discovery_clear_all_caches(); } } add_action( 'updated_option', 'geistm_ai_discovery_clear_cache' ); /** * Clear caches on post publish/update. * * @param int $post_id The post ID being saved. */ function geistm_ai_discovery_clear_content_cache( $post_id ) { if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; } geistm_ai_discovery_clear_all_caches(); } add_action( 'save_post', 'geistm_ai_discovery_clear_content_cache' ); /** * Show admin notice when manifest needs signing (content changed since last sign). * * This is the security mechanism: content changes via FTP/hack won't be auto-signed. * The admin must see and approve changes before the manifest is re-signed. */ function geistm_ai_discovery_manifest_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } $needs_signing = get_option( 'geistm_manifest_needs_signing', '0' ); $has_signed = get_option( 'geistm_signed_manifest', false ); $signed_at = get_option( 'geistm_manifest_signed_at', '' ); // No notice needed if manifest is up to date. if ( '1' !== $needs_signing && ! empty( $has_signed ) ) { return; } $sign_url = wp_nonce_url( add_query_arg( array( 'page' => 'geistm-adnet', 'tab' => 'viewer', 'geistm_sign_manifest' => '1', ), admin_url( 'options-general.php' ) ), 'geistm_sign_manifest' ); if ( empty( $has_signed ) ) { // Never signed — first install. ?>

AI Discovery:

AI Discovery:

' . esc_html__( 'Adnet Advertising (if enabled)', 'geistm-adnet' ) . ''; $content .= '

' . esc_html__( 'If this site has enabled Adnet advertising, the following applies to visitors:', 'geistm-adnet' ) . '

'; $content .= ''; $content .= '

' . sprintf( /* translators: %s: URL of the Adnet privacy policy. */ esc_html__( 'Adnet privacy policy: %s', 'geistm-adnet' ), 'https://adnet.geistm.com/privacy' ) . '

'; wp_add_privacy_policy_content( 'GeistM Adnet', wp_kses_post( $content ) ); } add_action( 'admin_init', 'geistm_ai_discovery_privacy_policy_content' ); /** * Load admin settings page. */ if ( is_admin() ) { require_once GEISTM_AI_DISCOVERY_DIR . 'admin/class-geistm-admin.php'; new Geistm_Admin(); }