20, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'code' => $code, 'site_url' => home_url(), 'plugin_version' => AIM_BE_VERSION, // Lets AiM ask this site to sync the moment a post is // ready, instead of waiting for WP-Cron to be triggered // by a visitor. 'wake_token' => aim_be_wake_token(), ) ), ) ); $data = $this->parse( $response ); if ( is_wp_error( $data ) ) { return $data; } if ( empty( $data['site_key'] ) ) { return new WP_Error( 'aim_be_no_key', __( 'The Connect Code was not accepted. Generate a new one in your AiM account and try again.', 'aim-blog-engine' ) ); } update_option( AIM_BE_OPT_SITE_KEY, sanitize_text_field( $data['site_key'] ) ); if ( ! empty( $data['site_id'] ) ) { update_option( AIM_BE_OPT_SITE_ID, sanitize_text_field( $data['site_id'] ) ); } update_option( AIM_BE_OPT_STATUS, 'connected' ); return true; } /** * Fetch blog posts queued for this site. * * @return array|WP_Error List of post payloads, or WP_Error. */ public function get_pending() { $site_key = get_option( AIM_BE_OPT_SITE_KEY, '' ); if ( empty( $site_key ) ) { return new WP_Error( 'aim_be_not_connected', __( 'Not connected.', 'aim-blog-engine' ) ); } $response = wp_remote_get( AIM_BE_API_BASE . '/pending', array( 'timeout' => 20, 'headers' => $this->auth_headers( $site_key ), ) ); $data = $this->parse( $response ); if ( is_wp_error( $data ) ) { return $data; } return isset( $data['posts'] ) && is_array( $data['posts'] ) ? $data['posts'] : array(); } /** * Report the local result of publishing a post back to AiM. * * @param array $result { blog_id, post_id, post_url, status }. * @return void */ public function ack( $result ) { $site_key = get_option( AIM_BE_OPT_SITE_KEY, '' ); if ( empty( $site_key ) || empty( $result['blog_id'] ) ) { return; } wp_remote_post( AIM_BE_API_BASE . '/ack', array( 'timeout' => 15, 'headers' => $this->auth_headers( $site_key ), 'body' => wp_json_encode( $result ), ) ); } /** * Report liveness + versions to AiM so the dashboard shows connection health * and the installed plugin/WordPress versions. Best-effort. * * @return void */ public function heartbeat() { $site_key = get_option( AIM_BE_OPT_SITE_KEY, '' ); if ( empty( $site_key ) ) { return; } wp_remote_post( AIM_BE_API_BASE . '/heartbeat', array( 'timeout' => 15, 'headers' => $this->auth_headers( $site_key ), 'body' => wp_json_encode( array( 'plugin_version' => AIM_BE_VERSION, 'wp_version' => get_bloginfo( 'version' ), // Sent every time, so a site that connected before the // wake route existed starts using it after updating, // with nothing for the member to do. 'wake_token' => aim_be_wake_token(), ) ), ) ); } /** * Authorization headers for signed requests. * * @param string $site_key The stored site key. * @return array */ private function auth_headers( $site_key ) { return array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $site_key, 'X-AiM-Site-Url' => home_url(), 'X-AiM-Plugin-Ver' => AIM_BE_VERSION, ); } /** * Decode a WordPress HTTP response into an array, or a WP_Error. * * @param array|WP_Error $response Raw wp_remote_* return. * @return array|WP_Error */ private function parse( $response ) { if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code < 200 || $code >= 300 ) { $message = is_array( $body ) && ! empty( $body['error'] ) ? $body['error'] : sprintf( /* translators: %d: HTTP status code. */ __( 'AiM request failed (HTTP %d).', 'aim-blog-engine' ), (int) $code ); return new WP_Error( 'aim_be_http', $message ); } return is_array( $body ) ? $body : array(); } }