'GET', 'callback' => array( self::class, 'status' ), 'permission_callback' => $edit, ) ); register_rest_route( self::NS, '/modules', array( 'methods' => 'GET', 'callback' => array( self::class, 'modules' ), 'permission_callback' => $edit, ) ); register_rest_route( self::NS, '/modules/(?P[a-z]{1,4}_[A-Za-z0-9_-]{1,64})', array( 'methods' => 'GET', 'callback' => array( self::class, 'module' ), 'permission_callback' => $edit, ) ); register_rest_route( self::NS, '/account', array( 'methods' => 'GET', 'callback' => array( self::class, 'account' ), 'permission_callback' => $admin, ) ); // The OAuth return (decision 0009): public, secured by the state and the PKCE verifier kept on the server. register_rest_route( self::NS, '/connect/return', array( 'methods' => 'GET', 'callback' => array( OAuth::class, 'handle_return' ), 'permission_callback' => '__return_true', ) ); register_rest_route( self::NS, '/start/(?P(ek|gk)_[a-z0-9]{6,64})', array( 'methods' => 'GET', 'callback' => array( self::class, 'start' ), 'permission_callback' => $edit, ) ); } /** * The connection's state for the editor. * * @return \WP_REST_Response */ public static function status() { $connection = Connection::get(); $out = array( 'status' => Connection::current_state(), 'canConnect' => current_user_can( 'manage_options' ), ); if ( $connection && current_user_can( 'manage_options' ) ) { $out['account'] = array( 'id' => $connection->account_id(), 'owner' => $connection->connected_by(), ); } return rest_ensure_response( $out ); } /** * The picker's list: modules and pages. * * @return \WP_REST_Response|\WP_Error */ public static function modules() { $answer = self::modules_answer(); if ( is_wp_error( $answer ) ) { return self::error( $answer ); } return rest_ensure_response( $answer ); } /** * One module, to place it: with its layout. Fills the module's start cache on the way. * * @param \WP_REST_Request $request The request. * @return \WP_REST_Response|\WP_Error */ public static function module( \WP_REST_Request $request ) { $id = (string) $request['id']; if ( ! Keys::is_module_id( $id ) ) { return new \WP_Error( 'sranywhere_bad_id', __( 'Unknown module.', 'sr-anywhere' ), array( 'status' => 400 ) ); } // Only ids from the account's list reach the API (no call for made-up ids; decision 0019's budget). $list = self::modules_answer(); if ( is_wp_error( $list ) ) { return self::error( $list ); } if ( ! in_array( $id, array_column( array_merge( $list['modules'], $list['pages'] ), 'id' ), true ) ) { return self::error( new \WP_Error( 'sranywhere_not_found', '', array( 'status' => 404 ) ) ); } // A page (a one-page site) has no detail route in the API (it answers 404): the list has all it needs, and a page // always runs the page's full width. foreach ( $list['pages'] as $page ) { if ( $page['id'] === $id ) { return rest_ensure_response( $page + array( 'layout' => 'fullwidth' ) ); } } $answer = Cache::remember( 'module:' . $id, MINUTE_IN_SECONDS, static function () use ( $id ) { return Api::get( '/api/connect/modules/' . rawurlencode( $id ) ); } ); if ( is_wp_error( $answer ) ) { return self::error( $answer ); } $body = isset( $answer['module'] ) && is_array( $answer['module'] ) ? $answer['module'] : $answer; $item = self::item( $body ); if ( null === $item ) { return self::error( new \WP_Error( 'sranywhere_not_found', '', array( 'status' => 404 ) ) ); } $item['layout'] = in_array( $body['layout'] ?? '', array( 'fullwidth', 'fullscreen', 'boxed' ), true ) ? $body['layout'] : 'fullwidth'; if ( $item['embedKey'] && ! Keys::is_group_key( $item['embedKey'] ) ) { Start::refresh( $item['embedKey'] ); } return rest_ensure_response( $item ); } /** * The plan line and its links, for administrators. * * @return \WP_REST_Response|\WP_Error */ public static function account() { $b = self::account_answer(); if ( is_wp_error( $b ) ) { return self::error( $b ); } $links = array(); foreach ( (array) ( $b['links'] ?? array() ) as $name => $url ) { if ( Config::is_portal_url( $url ) ) { $links[ sanitize_key( $name ) ] = esc_url_raw( $url ); } } return rest_ensure_response( array( 'plan' => is_string( $b['plan'] ?? null ) ? sanitize_key( $b['plan'] ) : '', 'status' => is_string( $b['status'] ?? null ) ? sanitize_key( $b['status'] ) : '', 'watermark' => ! empty( $b['watermark'] ), 'limits' => array_map( 'intval', (array) ( $b['limits'] ?? array() ) ), 'used' => array_map( 'intval', (array) ( $b['used'] ?? array() ) ), 'links' => $links, ) ); } /** * What the site will print for a module: a start, the plain line (and why, when paused), or nothing (and why), for * the editor's states. * * @param \WP_REST_Request $request The request. * @return \WP_REST_Response */ public static function start( \WP_REST_Request $request ) { $key = (string) $request['key']; if ( ! Placeable::allows( $key ) ) { return rest_ensure_response( array( 'state' => 'foreign' ) ); } if ( Keys::is_group_key( $key ) ) { return rest_ensure_response( array( 'state' => 'line' ) ); } $start = Start::for_editor( $key ); if ( Start::GONE === $start ) { return rest_ensure_response( array( 'state' => 'gone', 'reason' => Start::reason( $key ), ) ); } $answer = array( 'state' => is_array( $start ) ? 'start' : 'line' ); if ( Start::LINE === $start && '' !== Start::reason( $key ) ) { $answer['reason'] = Start::reason( $key ); // paused: the line shows the module's still until the 1st } return rest_ensure_response( $answer ); } /** * One entry of the picker, with only the fields it needs, every URL checked. * * @param mixed $m An entry of the API's list. * @return array|null */ private static function item( $m ) { if ( ! is_array( $m ) || ! Keys::is_module_id( $m['id'] ?? null ) ) { return null; } $links = array(); foreach ( array( 'preview', 'publish', 'edit' ) as $name ) { if ( isset( $m['links'][ $name ] ) && Config::is_portal_url( $m['links'][ $name ] ) ) { $links[ $name ] = esc_url_raw( $m['links'][ $name ] ); } } $thumb = isset( $m['thumb'] ) && is_string( $m['thumb'] ) && Config::is_delivery_url( $m['thumb'] ) ? esc_url_raw( $m['thumb'] ) : ''; $key = isset( $m['embedKey'] ) && Keys::is_embed_key( $m['embedKey'] ) ? $m['embedKey'] : ''; return array( 'id' => $m['id'], 'name' => is_string( $m['name'] ?? null ) ? sanitize_text_field( $m['name'] ) : '', // plain text: Elementor prints select labels unescaped 'kind' => 'page' === ( $m['kind'] ?? '' ) ? 'page' : 'module', 'status' => in_array( $m['status'] ?? '', array( 'draft', 'changed', 'published' ), true ) ? $m['status'] : 'draft', 'thumb' => $thumb, 'embedKey' => $key, 'updatedAt' => (int) ( $m['updatedAt'] ?? 0 ), 'modal' => ! empty( $m['modal'] ), 'sections' => (int) ( $m['sections'] ?? 0 ), 'links' => $links, ); } /** * The account's modules and pages, checked and trimmed to what the picker needs, cached for a minute (also used by * the settings page's shortcode list). * * @return array|\WP_Error `{ modules, pages }`. */ public static function modules_answer() { $answer = Cache::remember( 'modules', MINUTE_IN_SECONDS, static function () { return Api::get( '/api/connect/modules' ); } ); if ( is_wp_error( $answer ) ) { return $answer; } $out = array( 'modules' => array_values( array_filter( array_map( array( self::class, 'item' ), (array) ( $answer['modules'] ?? array() ) ) ) ), 'pages' => array_values( array_filter( array_map( array( self::class, 'item' ), (array) ( $answer['pages'] ?? array() ) ) ) ), ); // The keys of the connected account are the ones this site may print (Placeable). Placeable::remember( array_column( array_merge( $out['modules'], $out['pages'] ), 'embedKey' ) ); return $out; } /** * The account's answer, cached for five minutes (also used by the settings page). * * @return array|\WP_Error */ public static function account_answer() { return Cache::remember( 'account', 5 * MINUTE_IN_SECONDS, static function () { return Api::get( '/api/connect/account' ); } ); } /** * An API failure as a REST error the editor understands; a lost connection carries its state. * * @param \WP_Error $error The API's error. * @return \WP_Error */ private static function error( \WP_Error $error ) { $status = (int) ( $error->get_error_data()['status'] ?? 0 ); if ( 401 === $status ) { return new \WP_Error( 'sranywhere_not_connected', __( 'SR Anywhere is not connected.', 'sr-anywhere' ), array( 'status' => 409, 'state' => Connection::current_state(), ) ); } if ( 404 === $status ) { return new \WP_Error( 'sranywhere_gone', __( 'This module is no longer in your SR Anywhere account.', 'sr-anywhere' ), array( 'status' => 404 ) ); } return new \WP_Error( 'sranywhere_unreachable', __( 'SR Anywhere could not be reached.', 'sr-anywhere' ), array( 'status' => 502 ) ); } }