'GET', 'permission_callback' => '__return_true', 'callback' => function () { global $wpdb; // Straight from the table, not get_option(): this request runs // in a different PHP process from the one that just wrote the // challenge, and a persistent object cache that is not shared // between web servers can still hold "no such option" here. $challenge = (string) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- bypassing the object cache is the point. $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", CITOLUME_CLAIM_OPTION ) ); if ( '' === $challenge ) { $response = new WP_REST_Response( array( 'code' => 'citolume_no_claim', 'message' => 'No claim in progress.', ), 404 ); } else { $response = new WP_REST_Response( array( 'challenge' => $challenge ) ); } // Host caches sit in front of the REST API (GoDaddy's gateway // caches /wp-json/); a cached answer here fails the claim. $response->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0' ); return $response; }, ) ); } /** * Registers this site for the free plan — the wizard's "Connect" click. * * Same proof as a claim, no purchase required: the service hands us a * challenge, we publish it on the public route above, the service fetches it * back off the live site, and a free-tier key comes back. The user never * sees the key, and re-registering a previously uninstalled site gets its * old site — history intact — rather than a stranger. * * **The owner is optional and the caller decides** (ticket 124). WordPress * already knows the signed-in user's address and name, so the wizard offers * them and the service creates a real account instead of a placeholder. Send * nothing and the old behaviour is unchanged — which is also what happens if * the person clears the field, because the button is the consent and an * address nobody confirmed is not consent. * * @param array{email?: string, first_name?: string, last_name?: string} $owner Who is setting this up. * @return array{owner_status: string, owner_email: string, owner_matches: bool, reconnected: bool}|WP_Error */ function citolume_register_free_site( $owner = array() ) { $domain = citolume_site_host(); $started = citolume_api_public_post( '/v1/sites/register/start', array( 'domain' => $domain ) ); if ( is_wp_error( $started ) ) { return $started; } if ( empty( $started['challenge'] ) ) { return new WP_Error( 'citolume_register_bad', __( 'The service did not hand back a confirmation code.', 'citolume' ) ); } update_option( CITOLUME_CLAIM_OPTION, (string) $started['challenge'], false ); $payload = array( 'domain' => $domain, 'brand' => get_bloginfo( 'name' ), 'client' => 'wordpress', ); // Only ever what the wizard showed the person before they pressed the // button. Never silently sourced from get_option( 'admin_email' ), which // is often a shared address nobody reads. foreach ( array( 'email', 'first_name', 'last_name' ) as $field ) { if ( ! empty( $owner[ $field ] ) ) { $payload[ $field ] = (string) $owner[ $field ]; } } $finished = citolume_api_public_post( '/v1/sites/register/finish', $payload ); delete_option( CITOLUME_CLAIM_OPTION ); if ( is_wp_error( $finished ) ) { return $finished; } if ( empty( $finished['license_key'] ) ) { return new WP_Error( 'citolume_register_bad', __( 'The service sent a reply we could not read.', 'citolume' ) ); } update_option( 'citolume_license_key', trim( (string) $finished['license_key'] ), false ); citolume_clear_status_cache(); return array( 'owner_status' => (string) ( $finished['owner_status'] ?? 'none' ), 'owner_email' => (string) ( $finished['owner_email'] ?? '' ), 'owner_matches' => ! empty( $finished['owner_matches'] ), // A reinstall came back to its kept site (ticket 146): same key, // questions and history, so there is nothing left to set up. 'reconnected' => ! empty( $finished['reconnected'] ), ); } /** * Runs the whole claim: challenge, publish, verify, store the key. * * @param string $email The email address used at checkout. * @return true|WP_Error */ function citolume_claim_license_key( $email ) { $domain = citolume_site_host(); $started = citolume_api_public_post( '/v1/claim/start', array( 'domain' => $domain, 'email' => $email, ) ); if ( is_wp_error( $started ) ) { return $started; } if ( empty( $started['challenge'] ) ) { return new WP_Error( 'citolume_claim_bad', __( 'The service did not hand back a confirmation code.', 'citolume' ) ); } // Publish, then ask the service to come and look. update_option( CITOLUME_CLAIM_OPTION, (string) $started['challenge'], false ); $finished = citolume_api_public_post( '/v1/claim/finish', array( 'domain' => $domain, 'email' => $email, ) ); // The challenge has done its job either way; leaving it published invites // confusion about whether a claim is still running. delete_option( CITOLUME_CLAIM_OPTION ); if ( is_wp_error( $finished ) ) { return $finished; } if ( empty( $finished['license_key'] ) ) { return new WP_Error( 'citolume_claim_bad', __( 'The service sent a reply we could not read.', 'citolume' ) ); } update_option( 'citolume_license_key', trim( (string) $finished['license_key'] ) ); citolume_clear_status_cache(); return true; }