credentials = $credentials ?? new Credentials( $connection ); $this->client = new Client( $connection ); } /** * Calls `/v1/workspaces/{ws}{path}` with the sync bearer. * * @param string $method HTTP method. * @param string $path Path after the workspace. * @param array|null $body JSON body, or null for none. * * @return array{ok:bool, status:int, data:mixed, code:string, message:string} */ public function api( string $method, string $path, ?array $body = null ): array { $token = $this->credentials->sync_bearer(); if ( ! $token['ok'] ) { return array( 'ok' => false, 'status' => 0, 'data' => null, 'code' => $token['code'], 'message' => $token['message'], ); } $result = $this->send( $method, $path, $body, $token['token'] ); if ( 401 !== (int) $result['status'] ) { return $result; } // The cached token was refused. One fresh attempt, then give up: more // than that and a genuinely revoked key becomes an infinite loop. $this->credentials->forget(); $fresh = $this->credentials->mint(); if ( ! $fresh['ok'] ) { return $result; } return $this->send( $method, $path, $body, $fresh['token'] ); } /** * One attempt. * * @param string $method HTTP method. * @param string $path Path after the workspace. * @param array|null $body JSON body. * @param string $token Bearer to use. * * @return array{ok:bool, status:int, data:mixed, code:string, message:string} */ private function send( string $method, string $path, ?array $body, string $token ): array { $args = array( 'v1' => true, 'headers' => array( 'Authorization' => 'Bearer ' . $token ), 'timeout' => self::TIMEOUT, ); if ( null !== $body ) { $args['body'] = $body; } return $this->client->request( $method, $path, $args ); } /** * Creates one STRING item, describes it, and tags it. * * The order matters and is the reverse of what reads naturally: the item is * created, then its metadata, then its roles. Until the roles land the item * is invisible on an RBAC workspace, which is the right direction to fail — * a page nobody can see beats a page everybody can. * * @param array{title:string, content:string, url:string, language:string} $document The page. * @param string[] $role_ids Sharely role ids. * * @return array{ok:bool, id:string, code:string, message:string} */ public function create( array $document, array $role_ids ): array { $created = $this->api( 'POST', '/knowledge/string', array( 'content' => $document['content'], 'sourceUrl' => $document['url'], ) ); if ( ! $created['ok'] ) { return $this->failure( $created ); } $id = is_array( $created['data'] ) ? (string) ( $created['data']['knowledgeId'] ?? '' ) : ''; if ( '' === $id ) { return array( 'ok' => false, 'id' => '', 'code' => 'NO_KNOWLEDGE_ID', 'message' => __( 'Sharely created the item but returned no id.', 'sharelyai' ), ); } $described = $this->api( 'PUT', '/knowledge/' . rawurlencode( $id ) . '/reservedField', array( 'type' => 'STRING', 'title' => $document['title'], 'language' => $document['language'], 'sourceUrl' => $document['url'], ) ); if ( ! $described['ok'] ) { // The item exists but has no title, so a result would be unlabelled. // Remove it and report: a retry then starts clean. $this->delete( $id ); return $this->failure( $described ); } if ( array() !== $role_ids ) { $tagged = $this->replace_roles( $id, $role_ids ); if ( ! $tagged['ok'] ) { $this->delete( $id ); return $this->failure( $tagged ); } } return array( 'ok' => true, 'id' => $id, 'code' => '', 'message' => '', ); } /** * Sets which roles may see an item — replacing the set, not adding to it. * * PUT replaces; POST only adds. A platform that predates the replace route * answers 404 or 405, where adding is a safe superset. * * @param string $knowledge_id The item. * @param string[] $role_ids Sharely role ids. * * @return array{ok:bool, status:int, data:mixed, code:string, message:string} */ public function replace_roles( string $knowledge_id, array $role_ids ): array { $path = '/knowledge/' . rawurlencode( $knowledge_id ) . '/role'; $body = array( 'roleIds' => array_values( $role_ids ) ); $result = $this->api( 'PUT', $path, $body ); if ( ! $result['ok'] && in_array( (int) $result['status'], array( 404, 405 ), true ) ) { return $this->api( 'POST', $path, $body ); } return $result; } /** * Deletes one item. Already gone is not a failure. * * @param string $knowledge_id The item. * * @return array{ok:bool, status:int, data:mixed, code:string, message:string} */ public function delete( string $knowledge_id ): array { $result = $this->api( 'DELETE', '/knowledge/' . rawurlencode( $knowledge_id ) ); if ( ! $result['ok'] && 404 === (int) $result['status'] ) { $result['ok'] = true; } return $result; } /** * Every item in the workspace, following the pages. * * @param int $page_size How many per request. * * @return array{ok:bool, items:array>, code:string, message:string} */ public function list_all( int $page_size = 500 ): array { $items = array(); $offset = 0; do { $result = $this->api( 'GET', sprintf( '/knowledge?limit=%d&offset=%d', $page_size, $offset ) ); if ( ! $result['ok'] ) { return array( 'ok' => false, 'items' => $items, 'code' => (string) $result['code'], 'message' => (string) $result['message'], ); } $data = $result['data']; if ( is_array( $data ) && array_is_list( $data ) ) { $page = $data; } elseif ( is_array( $data ) ) { $page = (array) ( $data['knowledges'] ?? $data['data'] ?? array() ); } else { $page = array(); } $returned = count( $page ); $items = array_merge( $items, $page ); $offset += $page_size; } while ( $returned >= $page_size ); return array( 'ok' => true, 'items' => $items, 'code' => '', 'message' => '', ); } /** * What the platform holds, for the status screen. * * @return array{ok:bool, status:int, data:mixed, code:string, message:string} */ public function stats(): array { return $this->api( 'GET', '/knowledge/stats' ); } /** * Finds a workspace role by the id this connector knows it as. * * @param string $customer_role_id e.g. `wp:anonymous`. * * @return array{ok:bool, id:string, code:string, message:string} */ public function find_role( string $customer_role_id ): array { $result = $this->api( 'GET', '/role/by-customer-role-id/' . rawurlencode( $customer_role_id ) ); if ( ! $result['ok'] ) { return $this->failure( $result ); } return array( 'ok' => true, 'id' => is_array( $result['data'] ) ? (string) ( $result['data']['id'] ?? '' ) : '', 'code' => '', 'message' => '', ); } /** * Creates a workspace role. * * @param string $name What a person calls it. * @param string $customer_role_id What this connector calls it. * * @return array{ok:bool, id:string, code:string, message:string} */ public function create_role( string $name, string $customer_role_id ): array { $result = $this->api( 'POST', '/role', array( 'name' => $name, 'customerRoleId' => $customer_role_id, ) ); if ( ! $result['ok'] ) { return $this->failure( $result ); } return array( 'ok' => true, 'id' => is_array( $result['data'] ) ? (string) ( $result['data']['id'] ?? '' ) : '', 'code' => '', 'message' => '', ); } /** * Turns a failed request() result into the id-shaped one callers expect. * * @param array $result The failure. * * @return array{ok:bool, id:string, code:string, message:string} */ private function failure( array $result ): array { return array( 'ok' => false, 'id' => '', 'code' => (string) $result['code'], 'message' => (string) $result['message'], ); } }