$verifier, 'challenge' => deltabreen_maps_b64url( hash( 'sha256', $verifier, true ) ), ); } /** * One call to the service. * * @param string $method GET, POST or PATCH. * @param string $path The path on the service, e.g. /api/v1/keys. * @param array|null $body Sent as JSON, or as a form when $form is true. * @param string $bearer An OAuth access token, if any. * @param bool $form Send the body form-encoded, as the OAuth endpoints expect. * @return array{0: int, 1: array}|WP_Error The status and the decoded body. */ function deltabreen_maps_api( $method, $path, $body = null, $bearer = '', $form = false ) { $args = array( 'method' => $method, 'timeout' => 15, 'headers' => array( 'Accept' => 'application/json' ), ); if ( '' !== $bearer ) { $args['headers']['Authorization'] = 'Bearer ' . $bearer; } if ( null !== $body ) { $args['headers']['Content-Type'] = $form ? 'application/x-www-form-urlencoded' : 'application/json'; $args['body'] = $form ? $body : wp_json_encode( $body ); } $res = wp_remote_request( deltabreen_maps_api_url() . $path, $args ); if ( is_wp_error( $res ) ) { return $res; } $json = json_decode( wp_remote_retrieve_body( $res ), true ); return array( (int) wp_remote_retrieve_response_code( $res ), is_array( $json ) ? $json : array() ); } /** The plugin's registration with the service: made once per site, API address and return address. */ function deltabreen_maps_oauth_client() { $api = deltabreen_maps_api_url(); $redirect = deltabreen_maps_connect_redirect_uri(); $saved = get_option( 'deltabreen_maps_oauth', array() ); if ( is_array( $saved ) && ( $saved['api'] ?? '' ) === $api && ( $saved['redirect_uri'] ?? '' ) === $redirect && ! empty( $saved['client_id'] ) ) { return (string) $saved['client_id']; } $r = deltabreen_maps_api( 'POST', '/oauth/register', array( 'client_name' => 'Maps by DeltaBreen for WordPress', 'redirect_uris' => array( $redirect ), 'software_id' => DELTABREEN_MAPS_SOFTWARE_ID, 'token_endpoint_auth_method' => 'none', 'grant_types' => array( 'authorization_code', 'refresh_token' ), ) ); if ( is_wp_error( $r ) || 201 !== $r[0] || empty( $r[1]['client_id'] ) ) { return ''; } update_option( 'deltabreen_maps_oauth', array( 'api' => $api, 'redirect_uri' => $redirect, 'client_id' => (string) $r[1]['client_id'], ), false ); return (string) $r[1]['client_id']; } /** Whether a key's allowed sites cover this host: the host itself, or a *.example.com that matches it. */ function deltabreen_maps_origins_cover( $origins, $host ) { foreach ( (array) $origins as $o ) { $o = strtolower( (string) $o ); if ( $o === $host ) { return true; } if ( 0 === strpos( $o, '*.' ) ) { $suffix = substr( $o, 2 ); if ( $host === $suffix || substr( $host, -strlen( '.' . $suffix ) ) === '.' . $suffix ) { return true; } } } return false; } /** * A publishable key for this site, with an access token that may read and write keys. In order: a key that already * lists this site (a development key on this computer), a new key for this site, or, at the plan's key limit, this site * added to an existing publishable key. * * @param string $access The OAuth access token. * @return array{key?: string, error?: string} The key, or why there is none: owner, limit or unreachable. */ function deltabreen_maps_key_for_site( $access ) { $host = deltabreen_maps_site_host(); $local = deltabreen_maps_is_local_host( $host ); $r = deltabreen_maps_api( 'GET', '/api/v1/keys', null, $access ); if ( is_wp_error( $r ) || 200 !== $r[0] ) { return array( 'error' => ( ! is_wp_error( $r ) && 403 === $r[0] ) ? 'owner' : 'unreachable' ); } $keys = array_values( array_filter( (array) ( $r[1]['keys'] ?? array() ), function ( $k ) { return is_array( $k ) && 'publishable' === ( $k['kind'] ?? '' ) && 'active' === ( $k['status'] ?? '' ) && ! empty( $k['publishable'] ); } ) ); foreach ( $keys as $k ) { if ( $local ? ! empty( $k['dev'] ) : deltabreen_maps_origins_cover( $k['origins'] ?? array(), $host ) ) { return array( 'key' => (string) $k['publishable'] ); } } $made = deltabreen_maps_api( 'POST', '/api/v1/keys', array( 'kind' => 'publishable', 'name' => 'WordPress: ' . $host, 'origins' => $local ? array() : array( $host ), 'dev' => $local, ), $access ); if ( ! is_wp_error( $made ) && 201 === $made[0] && ! empty( $made[1]['key']['publishable'] ) ) { return array( 'key' => (string) $made[1]['key']['publishable'] ); } if ( is_wp_error( $made ) ) { return array( 'error' => 'unreachable' ); } if ( 403 === $made[0] ) { return array( 'error' => 'owner' ); } // At the key limit: this site joins an existing publishable key. A site on this computer never loosens a live key. if ( 402 === $made[0] && ! $local && $keys ) { $k = $keys[0]; $origins = array_values( array_unique( array_merge( (array) ( $k['origins'] ?? array() ), array( $host ) ) ) ); $patched = deltabreen_maps_api( 'PATCH', '/api/v1/keys/' . rawurlencode( (string) $k['id'] ), array( 'origins' => $origins ), $access ); if ( ! is_wp_error( $patched ) && 200 === $patched[0] ) { return array( 'key' => (string) $k['publishable'] ); } return array( 'error' => ( ! is_wp_error( $patched ) && 403 === $patched[0] ) ? 'owner' : 'unreachable' ); } return array( 'error' => 402 === $made[0] ? 'limit' : 'unreachable' ); } // Only our own service's address may be redirected to from the admin. add_filter( 'allowed_redirect_hosts', function ( $hosts ) { $hosts[] = (string) wp_parse_url( deltabreen_maps_api_url(), PHP_URL_HOST ); return $hosts; } ); /** Back to the settings page, saying how the connection went. */ function deltabreen_maps_connect_done( $result ) { wp_safe_redirect( admin_url( 'options-general.php?page=deltabreen-maps&deltabreen_connect=' . rawurlencode( $result ) ) ); exit; } // The Connect button: register once, remember the PKCE verifier and state for this administrator, then go to the service. add_action( 'admin_post_deltabreen_maps_connect', function () { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to connect this site.', 'maps-by-deltabreen' ), 403 ); } check_admin_referer( 'deltabreen_maps_connect' ); if ( ! deltabreen_maps_can_connect() ) { deltabreen_maps_connect_done( 'https' ); } $client = deltabreen_maps_oauth_client(); if ( '' === $client ) { deltabreen_maps_connect_done( 'unreachable' ); } $pkce = deltabreen_maps_pkce(); $state = wp_generate_password( 32, false ); set_transient( 'deltabreen_maps_connect_' . get_current_user_id(), array( 'verifier' => $pkce['verifier'], 'state' => $state, 'client_id' => $client, ), 15 * MINUTE_IN_SECONDS ); $query = http_build_query( array( 'response_type' => 'code', 'client_id' => $client, 'redirect_uri' => deltabreen_maps_connect_redirect_uri(), 'code_challenge' => $pkce['challenge'], 'code_challenge_method' => 'S256', 'scope' => 'keys:read keys:write', 'state' => $state, ), '', '&', PHP_QUERY_RFC3986 ); wp_safe_redirect( deltabreen_maps_api_url() . '/oauth/authorize?' . $query ); exit; } ); /** * The service sends the administrator back here: check the state, trade the code for a token, get the key, end the * sign-in, keep the key. */ function deltabreen_maps_connect_callback() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to connect this site.', 'maps-by-deltabreen' ), 403 ); } $saved = get_transient( 'deltabreen_maps_connect_' . get_current_user_id() ); delete_transient( 'deltabreen_maps_connect_' . get_current_user_id() ); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- the OAuth state below is this request's nonce. $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : ''; $code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : ''; $error = isset( $_GET['error'] ); // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( ! is_array( $saved ) || '' === $state || ! hash_equals( (string) $saved['state'], $state ) ) { deltabreen_maps_connect_done( 'state' ); } if ( $error || '' === $code ) { deltabreen_maps_connect_done( 'denied' ); } $token = deltabreen_maps_api( 'POST', '/oauth/token', array( 'grant_type' => 'authorization_code', 'code' => $code, 'redirect_uri' => deltabreen_maps_connect_redirect_uri(), 'client_id' => (string) $saved['client_id'], 'code_verifier' => (string) $saved['verifier'], ), '', true ); if ( is_wp_error( $token ) || 200 !== $token[0] || empty( $token[1]['access_token'] ) ) { deltabreen_maps_connect_done( 'unreachable' ); } $outcome = deltabreen_maps_key_for_site( (string) $token[1]['access_token'] ); // The plugin keeps only the map key: revoking the refresh token ends the whole sign-in, access token included. if ( ! empty( $token[1]['refresh_token'] ) ) { deltabreen_maps_api( 'POST', '/oauth/revoke', array( 'token' => (string) $token[1]['refresh_token'] ), '', true ); } if ( isset( $outcome['key'] ) ) { $options = deltabreen_maps_options(); $options['key'] = $outcome['key']; update_option( 'deltabreen_maps', $options ); deltabreen_maps_connect_done( 'connected' ); } deltabreen_maps_connect_done( $outcome['error'] ?? 'unreachable' ); } add_action( 'admin_post_deltabreen_maps_connected', 'deltabreen_maps_connect_callback' ); /** What the settings page says after a Connect, by its outcome. */ function deltabreen_maps_connect_message( $result ) { $host = deltabreen_maps_site_host(); $messages = array( /* translators: %s: this site's host name. */ 'connected' => array( 'success', sprintf( __( 'Connected: %s has a map key of its own. Maps show on your pages now.', 'maps-by-deltabreen' ), $host ) ), 'denied' => array( 'warning', __( 'Nothing was connected: the request was not approved.', 'maps-by-deltabreen' ) ), 'state' => array( 'error', __( 'That connection had expired or did not start here. Press Connect again.', 'maps-by-deltabreen' ) ), 'owner' => array( 'error', __( 'Only the workspace owner can make map keys. Ask them to connect this site, or paste a key they give you.', 'maps-by-deltabreen' ) ), 'limit' => array( 'error', __( 'Your plan has no room for another key. Revoke one you no longer use, or upgrade, then connect again.', 'maps-by-deltabreen' ) ), 'https' => array( 'error', __( 'Connecting needs this site to use HTTPS. Paste a publishable key instead.', 'maps-by-deltabreen' ) ), 'unreachable' => array( 'error', __( 'Maps by DeltaBreen could not be reached. Try again in a minute, or paste a key.', 'maps-by-deltabreen' ) ), ); return $messages[ $result ] ?? null; }