class-evf-ai-ajax.php
1 month ago
class-evf-ai-api.php
1 month ago
class-evf-ai-form-builder.php
1 month ago
class-evf-ai-loader.php
1 month ago
class-evf-ai-registration.php
1 week ago
class-evf-ai-registration.php
216 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 | $is_local = ( function_exists( 'wp_get_environment_type' ) && 'local' === wp_get_environment_type() ) |
| 63 | || in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true ); |
| 64 | |
| 65 | if ( ! $is_local ) { |
| 66 | foreach ( array( '.local', '.test', '.localhost' ) as $suffix ) { |
| 67 | if ( '' !== $host && substr( $host, -strlen( $suffix ) ) === $suffix ) { |
| 68 | $is_local = true; |
| 69 | break; |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Filter whether the current site is treated as local for AI features — the final say |
| 76 | * over the heuristics above, e.g. to test the live gateway from a .local domain (set |
| 77 | * this to __return_false), or to block a host the heuristics miss. |
| 78 | * |
| 79 | * @param bool $is_local Whether the built-in heuristics consider this site local. |
| 80 | * @param string $host The detected site host. |
| 81 | */ |
| 82 | return (bool) apply_filters( 'everest_forms_ai_is_local_site', $is_local, $host ); |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Clear stored credentials and registration lock. |
| 87 | * Called automatically when the gateway returns "Invalid token" so the |
| 88 | * site re-registers transparently on the next request. |
| 89 | */ |
| 90 | public static function clear_credentials(): void { |
| 91 | delete_option( self::OPTION_KEY ); |
| 92 | delete_transient( self::LOCK_TRANSIENT ); |
| 93 | } |
| 94 | |
| 95 | /** |
| 96 | * Register this site with the gateway (called silently on first AI use). |
| 97 | * Uses a transient lock to prevent concurrent registration attempts. |
| 98 | * |
| 99 | * @return bool True on success. |
| 100 | */ |
| 101 | public static function register(): bool { |
| 102 | // Prevent duplicate registration |
| 103 | if ( self::is_registered() ) { |
| 104 | return true; |
| 105 | } |
| 106 | |
| 107 | // Transient lock — only one registration attempt per 60 seconds |
| 108 | if ( get_transient( self::LOCK_TRANSIENT ) ) { |
| 109 | return false; |
| 110 | } |
| 111 | set_transient( self::LOCK_TRANSIENT, true, 60 ); |
| 112 | |
| 113 | // Generate a one-time ownership token the gateway will call back to verify. |
| 114 | // The gateway hits /wp-json/everest-forms/v1/gateway-verify?token=... and we |
| 115 | // confirm the transient exists — proving this WordPress install issued the request. |
| 116 | $verify_token = wp_generate_password( 32, false ); |
| 117 | set_transient( self::VERIFY_PREFIX . $verify_token, 1, self::VERIFY_TTL ); |
| 118 | |
| 119 | $response = EVF_AI_API::register_site( $verify_token ); |
| 120 | |
| 121 | if ( is_wp_error( $response ) || empty( $response['site_token'] ) ) { |
| 122 | delete_transient( self::VERIFY_PREFIX . $verify_token ); |
| 123 | return false; |
| 124 | } |
| 125 | |
| 126 | update_option( |
| 127 | self::OPTION_KEY, |
| 128 | array( |
| 129 | 'site_token' => sanitize_text_field( $response['site_token'] ), |
| 130 | 'tier' => sanitize_key( $response['tier'] ?? 'free' ), |
| 131 | 'registered_at' => time(), |
| 132 | ) |
| 133 | ); |
| 134 | |
| 135 | delete_transient( self::LOCK_TRANSIENT ); |
| 136 | return true; |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * Activate pro tier using the EVF Pro license key. |
| 141 | * Called automatically when EVF Pro license is activated. |
| 142 | * |
| 143 | * @param string $license_key |
| 144 | * @return bool |
| 145 | */ |
| 146 | public static function activate_pro( string $license_key ): bool { |
| 147 | $token = self::get_site_token(); |
| 148 | |
| 149 | // Register first if needed |
| 150 | if ( ! $token ) { |
| 151 | if ( ! self::register() ) { |
| 152 | return false; |
| 153 | } |
| 154 | $token = self::get_site_token(); |
| 155 | } |
| 156 | |
| 157 | $response = EVF_AI_API::activate_license( $token, $license_key ); |
| 158 | |
| 159 | if ( is_wp_error( $response ) || empty( $response['tier'] ) ) { |
| 160 | return false; |
| 161 | } |
| 162 | |
| 163 | $creds = get_option( self::OPTION_KEY, array() ); |
| 164 | $creds['tier'] = sanitize_key( $response['tier'] ); |
| 165 | $creds['plan'] = sanitize_key( $response['plan'] ?? '' ); |
| 166 | update_option( self::OPTION_KEY, $creds ); |
| 167 | |
| 168 | return 'pro' === $creds['tier']; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Register the public REST endpoint the gateway calls to verify domain ownership. |
| 173 | * Hooked to rest_api_init from class-evf-ai-loader.php. |
| 174 | */ |
| 175 | public static function register_verify_endpoint(): void { |
| 176 | register_rest_route( |
| 177 | 'everest-forms/v1', |
| 178 | '/gateway-verify', |
| 179 | array( |
| 180 | 'methods' => WP_REST_Server::READABLE, |
| 181 | 'callback' => array( __CLASS__, 'handle_verify_request' ), |
| 182 | 'permission_callback' => '__return_true', |
| 183 | ) |
| 184 | ); |
| 185 | } |
| 186 | |
| 187 | /** |
| 188 | * Respond to the gateway's ownership callback. |
| 189 | * |
| 190 | * The gateway sends the same verify_token we generated in register(). |
| 191 | * We check the transient (one-time, 5-minute TTL) and delete it on success |
| 192 | * so the token cannot be reused. |
| 193 | * |
| 194 | * @param WP_REST_Request $request Incoming REST request. |
| 195 | * @return WP_REST_Response |
| 196 | */ |
| 197 | public static function handle_verify_request( WP_REST_Request $request ): WP_REST_Response { |
| 198 | $token = sanitize_text_field( $request->get_param( 'token' ) ); |
| 199 | |
| 200 | if ( ! $token ) { |
| 201 | return new WP_REST_Response( array( 'valid' => false ), 400 ); |
| 202 | } |
| 203 | |
| 204 | $stored = get_transient( self::VERIFY_PREFIX . $token ); |
| 205 | |
| 206 | if ( ! $stored ) { |
| 207 | return new WP_REST_Response( array( 'valid' => false ), 403 ); |
| 208 | } |
| 209 | |
| 210 | // One-time use — delete immediately so the token cannot be replayed. |
| 211 | delete_transient( self::VERIFY_PREFIX . $token ); |
| 212 | |
| 213 | return new WP_REST_Response( array( 'valid' => true ), 200 ); |
| 214 | } |
| 215 | } |
| 216 |