$verifier, 'state' => $state, 'return_url' => self::return_url(), ), self::PENDING_TTL ); $name = wp_strip_all_tags( html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES, 'UTF-8' ) ); return add_query_arg( rawurlencode_deep( array( 'platform' => 'wordpress', 'site_name' => '' !== $name ? $name : wp_parse_url( home_url(), PHP_URL_HOST ), 'site_url' => self::site_url(), 'return_url' => self::return_url(), 'state' => $state, 'code_challenge' => self::base64url( hash( 'sha256', $verifier, true ) ), 'code_challenge_method' => 'S256', ) ), Api::app_url( '/connect' ) ); } /** * The browser is back from Ojelio with a code (or "access_denied"). Checks the * state against the one kept for this user, trades the code, and keeps the * connection. Answers true, or a WP_Error to show on the settings page. * * @param array $query The return address's query: code, state, error. * @return true|WP_Error */ public static function complete( array $query ) { $key = self::PENDING . get_current_user_id(); $pending = get_transient( $key ); $state = isset( $query['state'] ) ? (string) $query['state'] : ''; if ( ! is_array( $pending ) || '' === $state || ! hash_equals( $pending['state'], $state ) ) { return new WP_Error( 'ojelio_state', __( 'This connection request expired or was started in another window. Press "Connect to Ojelio" again.', 'ojelio' ) ); } // The state is used once, whatever happens next. delete_transient( $key ); if ( isset( $query['error'] ) ) { return new WP_Error( 'ojelio_access_denied', __( 'Connecting was cancelled in Ojelio. Nothing was changed.', 'ojelio' ) ); } if ( empty( $query['code'] ) ) { return new WP_Error( 'ojelio_code', __( 'Ojelio did not send a code back. Press "Connect to Ojelio" again.', 'ojelio' ) ); } $traded = Api::call( 'POST', '/connections/token', array( 'body' => array( 'code' => (string) $query['code'], 'code_verifier' => $pending['verifier'], 'return_url' => $pending['return_url'], 'webhook_url' => self::webhook_url(), ), ) ); if ( is_wp_error( $traded ) ) { return $traded; } // Kept out of the options loaded on every page. update_option( self::OPTION, array( 'token' => $traded['token'], 'id' => $traded['connection']['id'], 'site_url' => $traded['connection']['site_url'], 'workspace' => $traded['workspace']['name'], 'webhook_url' => isset( $traded['webhook']['url'] ) ? $traded['webhook']['url'] : null, 'webhook_secret' => isset( $traded['webhook']['secret'] ) ? $traded['webhook']['secret'] : null, 'connected_at' => time(), 'connected_by' => get_current_user_id(), ), false ); delete_transient( Settings::OPTIONS_CACHE ); return true; } /** Disconnects: tells Ojelio (whatever it answers) and forgets the token. */ public static function disconnect() { $token = self::token(); if ( $token ) { Api::call( 'DELETE', '/connection', array( 'token' => $token ) ); } self::forget(); } /** Forgets the connection without telling Ojelio: it is already gone there. */ public static function forget() { delete_option( self::OPTION ); delete_transient( Settings::OPTIONS_CACHE ); } /** * A call with the connection's token. A 401 means the site was disconnected in * Ojelio: the token is forgotten, so the settings page offers to connect again. * * @return array|WP_Error */ public static function call( $method, $path, array $args = array() ) { $token = self::token(); if ( ! $token ) { return new WP_Error( 'ojelio_not_connected', __( 'This site is not connected to Ojelio.', 'ojelio' ) ); } $answer = Api::call( $method, $path, array_merge( $args, array( 'token' => $token ) ) ); if ( is_wp_error( $answer ) && 'ojelio_unauthorized' === $answer->get_error_code() ) { self::forget(); return new WP_Error( 'ojelio_not_connected', __( 'This site was disconnected in Ojelio. An administrator can connect it again in Settings → Ojelio.', 'ojelio' ) ); } return $answer; } private static function random( $bytes ) { return self::base64url( random_bytes( $bytes ) ); } private static function base64url( $raw ) { return rtrim( strtr( base64_encode( $raw ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } }