class-evf-ai-ajax.php
3 days ago
class-evf-ai-api.php
3 days ago
class-evf-ai-form-builder.php
3 days ago
class-evf-ai-loader.php
2 months ago
class-evf-ai-registration.php
3 days ago
class-evf-ai-registration.php
239 lines
| 1 | <?php |
| 2 | /** |
| 3 | * EVF AI Registration — manages site token lifecycle. |
| 4 | * |
| 5 | * Mirrors WPForms' LiteConnect pattern: the plugin silently registers with |
| 6 | * the gateway on first use and stores a site_token in wp_options. |
| 7 | * Pro users additionally activate their EVF license key to upgrade limits. |
| 8 | * |
| 9 | * Options used: |
| 10 | * evf_ai_credentials { site_token, tier, registered_at } |
| 11 | */ |
| 12 | |
| 13 | defined( 'ABSPATH' ) || exit; |
| 14 | |
| 15 | class EVF_AI_Registration { |
| 16 | |
| 17 | const OPTION_KEY = 'evf_ai_credentials'; |
| 18 | const LOCK_TRANSIENT = 'evf_ai_registration_lock'; |
| 19 | const VERIFY_PREFIX = 'evf_ai_verify_'; // transient prefix for ownership tokens |
| 20 | const VERIFY_TTL = 300; // 5 minutes |
| 21 | |
| 22 | /** |
| 23 | * Get the stored site token, or null if not yet registered. |
| 24 | */ |
| 25 | public static function get_site_token(): ?string { |
| 26 | $creds = get_option( self::OPTION_KEY ); |
| 27 | return ! empty( $creds['site_token'] ) ? $creds['site_token'] : null; |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * Get the stored tier ("free" or "pro"). |
| 32 | */ |
| 33 | public static function get_tier(): string { |
| 34 | $creds = get_option( self::OPTION_KEY ); |
| 35 | return $creds['tier'] ?? 'free'; |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * True if the site has been registered with the gateway. |
| 40 | */ |
| 41 | public static function is_registered(): bool { |
| 42 | return (bool) self::get_site_token(); |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * True if this is a local / development site where the AI features |
| 47 | * (Create with AI, in-builder AI Assistant) should be disabled. |
| 48 | * |
| 49 | * The gateway cannot verify domain ownership for non-public hosts, so the |
| 50 | * AI features can never work on local installs — we hide them entirely |
| 51 | * instead of letting the user hit a "not available" error. |
| 52 | * |
| 53 | * Detection: WP environment type "local" OR a non-public host |
| 54 | * (localhost / loopback IP / .local / .test / .localhost TLD). |
| 55 | * |
| 56 | * @return bool |
| 57 | */ |
| 58 | public static function is_local_site(): bool { |
| 59 | $host = wp_parse_url( site_url(), PHP_URL_HOST ); |
| 60 | $host = $host ? strtolower( $host ) : ''; |
| 61 | |
| 62 | // An explicit local-gateway override (see the themegrill-ai-cloud README's "Setup — |
| 63 | // WordPress Plugin" instructions: `define('TG_AI_GATEWAY_URL', 'http://localhost:8000')`) |
| 64 | // means the developer has deliberately pointed this install at a gateway running on |
| 65 | // their own machine to test AI features end-to-end, so the heuristics below don't apply |
| 66 | // — without this the documented local-dev setup could never work on ANY local WP |
| 67 | // environment (Local by Flywheel, Valet, MAMP, …), since `WP_ENVIRONMENT_TYPE=local` and |
| 68 | // `.local`/`.test` domains are exactly what those tools use by default. |
| 69 | if ( defined( 'TG_AI_GATEWAY_URL' ) && self::is_loopback_url( TG_AI_GATEWAY_URL ) ) { |
| 70 | $is_local = false; |
| 71 | } else { |
| 72 | $is_local = ( function_exists( 'wp_get_environment_type' ) && 'local' === wp_get_environment_type() ) |
| 73 | || in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true ); |
| 74 | |
| 75 | if ( ! $is_local ) { |
| 76 | foreach ( array( '.local', '.test', '.localhost' ) as $suffix ) { |
| 77 | if ( '' !== $host && substr( $host, -strlen( $suffix ) ) === $suffix ) { |
| 78 | $is_local = true; |
| 79 | break; |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Filter whether the current site is treated as local for AI features — the final say |
| 87 | * over the heuristics above, e.g. to test the live gateway from a .local domain (set |
| 88 | * this to __return_false), or to block a host the heuristics miss. |
| 89 | * |
| 90 | * @param bool $is_local Whether the built-in heuristics consider this site local. |
| 91 | * @param string $host The detected site host. |
| 92 | */ |
| 93 | return (bool) apply_filters( 'everest_forms_ai_is_local_site', $is_local, $host ); |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Whether a URL's host is a loopback address (localhost / 127.0.0.1 / ::1) — i.e. a gateway |
| 98 | * that can only possibly be running on this same machine, never a real production endpoint. |
| 99 | * |
| 100 | * @param string $url URL to inspect. |
| 101 | * @return bool |
| 102 | */ |
| 103 | private static function is_loopback_url( string $url ): bool { |
| 104 | $host = wp_parse_url( $url, PHP_URL_HOST ); |
| 105 | return in_array( strtolower( (string) $host ), array( 'localhost', '127.0.0.1', '::1' ), true ); |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * Clear stored credentials and registration lock. |
| 110 | * Called automatically when the gateway returns "Invalid token" so the |
| 111 | * site re-registers transparently on the next request. |
| 112 | */ |
| 113 | public static function clear_credentials(): void { |
| 114 | delete_option( self::OPTION_KEY ); |
| 115 | delete_transient( self::LOCK_TRANSIENT ); |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Register this site with the gateway (called silently on first AI use). |
| 120 | * Uses a transient lock to prevent concurrent registration attempts. |
| 121 | * |
| 122 | * @return bool True on success. |
| 123 | */ |
| 124 | public static function register(): bool { |
| 125 | // Prevent duplicate registration |
| 126 | if ( self::is_registered() ) { |
| 127 | return true; |
| 128 | } |
| 129 | |
| 130 | // Transient lock — only one registration attempt per 60 seconds |
| 131 | if ( get_transient( self::LOCK_TRANSIENT ) ) { |
| 132 | return false; |
| 133 | } |
| 134 | set_transient( self::LOCK_TRANSIENT, true, 60 ); |
| 135 | |
| 136 | // Generate a one-time ownership token the gateway will call back to verify. |
| 137 | // The gateway hits /wp-json/everest-forms/v1/gateway-verify?token=... and we |
| 138 | // confirm the transient exists — proving this WordPress install issued the request. |
| 139 | $verify_token = wp_generate_password( 32, false ); |
| 140 | set_transient( self::VERIFY_PREFIX . $verify_token, 1, self::VERIFY_TTL ); |
| 141 | |
| 142 | $response = EVF_AI_API::register_site( $verify_token ); |
| 143 | |
| 144 | if ( is_wp_error( $response ) || empty( $response['site_token'] ) ) { |
| 145 | delete_transient( self::VERIFY_PREFIX . $verify_token ); |
| 146 | return false; |
| 147 | } |
| 148 | |
| 149 | update_option( |
| 150 | self::OPTION_KEY, |
| 151 | array( |
| 152 | 'site_token' => sanitize_text_field( $response['site_token'] ), |
| 153 | 'tier' => sanitize_key( $response['tier'] ?? 'free' ), |
| 154 | 'registered_at' => time(), |
| 155 | ) |
| 156 | ); |
| 157 | |
| 158 | delete_transient( self::LOCK_TRANSIENT ); |
| 159 | return true; |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * Activate pro tier using the EVF Pro license key. |
| 164 | * Called automatically when EVF Pro license is activated. |
| 165 | * |
| 166 | * @param string $license_key |
| 167 | * @return bool |
| 168 | */ |
| 169 | public static function activate_pro( string $license_key ): bool { |
| 170 | $token = self::get_site_token(); |
| 171 | |
| 172 | // Register first if needed |
| 173 | if ( ! $token ) { |
| 174 | if ( ! self::register() ) { |
| 175 | return false; |
| 176 | } |
| 177 | $token = self::get_site_token(); |
| 178 | } |
| 179 | |
| 180 | $response = EVF_AI_API::activate_license( $token, $license_key ); |
| 181 | |
| 182 | if ( is_wp_error( $response ) || empty( $response['tier'] ) ) { |
| 183 | return false; |
| 184 | } |
| 185 | |
| 186 | $creds = get_option( self::OPTION_KEY, array() ); |
| 187 | $creds['tier'] = sanitize_key( $response['tier'] ); |
| 188 | $creds['plan'] = sanitize_key( $response['plan'] ?? '' ); |
| 189 | update_option( self::OPTION_KEY, $creds ); |
| 190 | |
| 191 | return 'pro' === $creds['tier']; |
| 192 | } |
| 193 | |
| 194 | /** |
| 195 | * Register the public REST endpoint the gateway calls to verify domain ownership. |
| 196 | * Hooked to rest_api_init from class-evf-ai-loader.php. |
| 197 | */ |
| 198 | public static function register_verify_endpoint(): void { |
| 199 | register_rest_route( |
| 200 | 'everest-forms/v1', |
| 201 | '/gateway-verify', |
| 202 | array( |
| 203 | 'methods' => WP_REST_Server::READABLE, |
| 204 | 'callback' => array( __CLASS__, 'handle_verify_request' ), |
| 205 | 'permission_callback' => '__return_true', |
| 206 | ) |
| 207 | ); |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * Respond to the gateway's ownership callback. |
| 212 | * |
| 213 | * The gateway sends the same verify_token we generated in register(). |
| 214 | * We check the transient (one-time, 5-minute TTL) and delete it on success |
| 215 | * so the token cannot be reused. |
| 216 | * |
| 217 | * @param WP_REST_Request $request Incoming REST request. |
| 218 | * @return WP_REST_Response |
| 219 | */ |
| 220 | public static function handle_verify_request( WP_REST_Request $request ): WP_REST_Response { |
| 221 | $token = sanitize_text_field( $request->get_param( 'token' ) ); |
| 222 | |
| 223 | if ( ! $token ) { |
| 224 | return new WP_REST_Response( array( 'valid' => false ), 400 ); |
| 225 | } |
| 226 | |
| 227 | $stored = get_transient( self::VERIFY_PREFIX . $token ); |
| 228 | |
| 229 | if ( ! $stored ) { |
| 230 | return new WP_REST_Response( array( 'valid' => false ), 403 ); |
| 231 | } |
| 232 | |
| 233 | // One-time use — delete immediately so the token cannot be replayed. |
| 234 | delete_transient( self::VERIFY_PREFIX . $token ); |
| 235 | |
| 236 | return new WP_REST_Response( array( 'valid' => true ), 200 ); |
| 237 | } |
| 238 | } |
| 239 |