<?php /** * Rest controller class. * * @since 0.1 */ namespace CiviCRM_WP_REST\Controller; class Rest extends Base { /** * The base route. * * @since 0.1 * @var string */ protected $rest_base = 'rest'; /** * Registers routes. * * @since 0.1 */ public function register_routes() { register_rest_route( $this->get_namespace(), $this->get_rest_base(), [ [ 'methods' => \WP_REST_Server::ALLMETHODS, 'callback' => [ $this, 'get_items' ], 'permission_callback' => [ $this, 'permissions_check' ], 'args' => $this->get_item_args() ], 'schema' => [ $this, 'get_item_schema' ] ] ); } /** * Check get permission. * * @since 0.1 * @param WP_REST_Request $request * @return bool */ public function permissions_check( $request ) { /** * Opportunity to bypass CiviCRM's * authentication ('api_key' and 'site_key'), * return 'true' or 'false' to grant * or deny access to this endpoint. * * To deny and throw an error, return either * a string, an array, or a \WP_Error. * * NOTE: if you use your won authentication, * you still must log in the user in order * to respect/apply CiviCRM ACLs. * * @since 0.1 * @param null|bool|string|array|\WP_Error $grant_auth Grant, deny, or error * @param \WP_REST_Request $request The request */ $grant_auth = apply_filters( 'civi_wp_rest/controller/rest/permissions_check', null, $request ); if ( is_bool( $grant_auth ) ) { return $grant_auth; } elseif ( is_string( $grant_auth ) ) { return $this->civi_rest_error( $grant_auth ); } elseif ( is_array( $grant_auth ) ) { return $this->civi_rest_error( __( 'CiviCRM WP REST permission check error.', 'civicrm' ), $grant_auth ); } elseif ( $grant_auth instanceof \WP_Error ) { return $grant_auth; } else { if ( ! $this->is_valid_api_key( $request ) ) return $this->civi_rest_error( __( 'Param api_key is not valid.', 'civicrm' ) ); if ( ! $this->is_valid_site_key() ) return $this->civi_rest_error( __( 'Param key is not valid.', 'civicrm' ) ); return true; } } /** * Get items. * * @since 0.1 * @param WP_REST_Request $request */ public function get_items( $request ) { /** * Filter formatted api params. * * @since 0.1 * @param array $params * @param WP_REST_Request $request */ $params = apply_filters( 'civi_wp_rest/controller/rest/api_params', $this->get_formatted_api_params( $request ), $request ); try { $items = civicrm_api3( ...$params ); } catch ( \CiviCRM_API3_Exception $e ) { $items = $this->civi_rest_error( $e ); } if ( ! isset( $items ) || empty( $items ) ) return rest_ensure_response( [] ); /** * Filter civi api result. * * @since 0.1 * @param array $items * @param WP_REST_Request $request */ $data = apply_filters( 'civi_wp_rest/controller/rest/api_result', $items, $params, $request ); // only collections of items, ie any action but 'getsingle' if ( isset( $data['values'] ) ) { $data['values'] = array_reduce( $items['values'] ?? $items, function( $items, $item ) use ( $request ) { $response = $this->prepare_item_for_response( $item, $request ); $items[] = $this->prepare_response_for_collection( $response ); return $items; }, [] ); } $response = rest_ensure_response( $data ); // check wheather we need to serve xml or json if ( ! in_array( 'json', array_keys( $request->get_params() ) ) ) { /** * Adds our response holding Civi data before dispatching. * * @since 0.1 * @param WP_HTTP_Response $result Result to send to client * @param WP_REST_Server $server The REST server * @param WP_REST_Request $request The request * @return WP_HTTP_Response $result Result to send to client */ add_filter( 'rest_post_dispatch', function( $result, $server, $request ) use ( $response ) { return $response; }, 10, 3 ); // serve xml add_filter( 'rest_pre_serve_request', [ $this, 'serve_xml_response' ], 10, 4 ); } else { // return json return $response; } } /** * Get formatted api params. * * @since 0.1 * @param WP_REST_Resquest $request * @return array $params */ public function get_formatted_api_params( $request ) { $args = $request->get_params(); $entity = $args['entity']; $action = $args['action']; // unset unnecessary args unset( $args['entity'], $args['action'], $args['key'], $args['api_key'] ); if ( ! isset( $args['json'] ) || is_numeric( $args['json'] ) ) { $params = $args; } else { $params = is_string( $args['json'] ) ? json_decode( $args['json'], true ) : []; } // ensure check permissions is enabled $params['check_permissions'] = true; return [ $entity, $action, $params ]; } /** * Matches the item data to the schema. * * @since 0.1 * @param object $item * @param WP_REST_Request $request */ public function prepare_item_for_response( $item, $request ) { return rest_ensure_response( $item ); } /** * Serves XML response. * * @since 0.1 * @param bool $served Whether the request has already been served * @param WP_REST_Response $result * @param WP_REST_Request $request * @param WP_REST_Server $server */ public function serve_xml_response( $served, $result, $request, $server ) { // get xml from response $xml = $this->get_xml_formatted_data( $result->get_data() ); // set content type header $server->send_header( 'Content-Type', 'text/xml' ); echo $xml; return true; } /** * Formats CiviCRM API result to XML. * * @since 0.1 * @param array $data The CiviCRM api result * @return string $xml The formatted xml */ protected function get_xml_formatted_data( array $data ) { // xml document $xml = new \DOMDocument(); // result set element <ResultSet> $result_set = $xml->createElement( 'ResultSet' ); // xmlns:xsi attribute $result_set->setAttribute( 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance' ); // count attribute if ( isset( $data['count'] ) ) $result_set->setAttribute( 'count', $data['count'] ); // build result from result => values if ( isset( $data['values'] ) ) { array_map( function( $item ) use ( $result_set, $xml ) { // result element <Result> $result = $xml->createElement( 'Result' ); // format item $result = $this->get_xml_formatted_item( $item, $result, $xml ); // append result to result set $result_set->appendChild( $result ); }, $data['values'] ); } else { // result element <Result> $result = $xml->createElement( 'Result' ); // format item $result = $this->get_xml_formatted_item( $data, $result, $xml ); // append result to result set $result_set->appendChild( $result ); } // append result set $xml->appendChild( $result_set ); return $xml->saveXML(); } /** * Formats a single api result to xml. * * @since 0.1 * @param array $item The single api result * @param DOMElement $parent The parent element to append to * @param DOMDocument $doc The document * @return DOMElement $parent The parent element */ public function get_xml_formatted_item( array $item, \DOMElement $parent, \DOMDocument $doc ) { // build field => values array_map( function( $field, $value ) use ( $parent, $doc ) { // entity field element $element = $doc->createElement( $field ); // handle array values if ( is_array( $value ) ) { array_map( function( $key, $val ) use ( $element, $doc ) { // child element, append underscore '_' otherwise createElement // will throw an Invalid character exception as elements cannot start with a number $child = $doc->createElement( '_' . $key, $val ); // append child $element->appendChild( $child ); }, array_keys( $value ), $value ); } else { // assign value $element->nodeValue = $value; } // append element $parent->appendChild( $element ); }, array_keys( $item ), $item ); return $parent; } /** * Item schema. * * @since 0.1 * @return array $schema */ public function get_item_schema() { return [ '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'civicrm/v3/rest', 'description' => __( 'CiviCRM API3 WP rest endpoint wrapper', 'civicrm' ), 'type' => 'object', 'required' => [ 'entity', 'action', 'params' ], 'properties' => [ 'is_error' => [ 'type' => 'integer' ], 'version' => [ 'type' => 'integer' ], 'count' => [ 'type' => 'integer' ], 'values' => [ 'type' => 'array' ] ] ]; } /** * Item arguments. * * @since 0.1 * @return array $arguments */ public function get_item_args() { return [ 'key' => [ 'type' => 'string', 'required' => false, 'validate_callback' => function( $value, $request, $key ) { return $this->is_valid_site_key(); } ], 'api_key' => [ 'type' => 'string', 'required' => false, 'validate_callback' => function( $value, $request, $key ) { return $this->is_valid_api_key( $request ); } ], 'entity' => [ 'type' => 'string', 'required' => true, 'validate_callback' => function( $value, $request, $key ) { return is_string( $value ); } ], 'action' => [ 'type' => 'string', 'required' => true, 'validate_callback' => function( $value, $request, $key ) { return is_string( $value ); } ], 'json' => [ 'type' => ['integer', 'string', 'array'], 'required' => false, 'validate_callback' => function( $value, $request, $key ) { return is_numeric( $value ) || is_array( $value ) || $this->is_valid_json( $value ); } ] ]; } /** * Checks if string is a valid json. * * @since 0.1 * @param string $param * @return bool */ protected function is_valid_json( $param ) { $param = json_decode( $param, true ); if ( ! is_array( $param ) ) return false; return ( json_last_error() == JSON_ERROR_NONE ); } /** * Validates the site key. * * @since 0.1 * @return bool $is_valid_site_key */ private function is_valid_site_key() { return \CRM_Utils_System::authenticateKey( false ); } /** * Validates the api key. * * @since 0.1 * @param WP_REST_Resquest $request * @return bool $is_valid_api_key */ private function is_valid_api_key( $request ) { $api_key = $request->get_param( 'api_key' ); if ( ! $api_key ) return false; $contact_id = \CRM_Core_DAO::getFieldValue( 'CRM_Contact_DAO_Contact', $api_key, 'id', 'api_key' ); // validate contact and login if ( $contact_id ) { $wp_user = $this->get_wp_user( $contact_id ); $this->do_user_login( $wp_user ); return true; } return false; } /** * Get WordPress user data. * * @since 0.1 * @param int $contact_id The contact id * @return bool|WP_User $user The WordPress user data */ protected function get_wp_user( int $contact_id ) { try { // Get CiviCRM domain group ID from constant, if set. $domain_id = defined( 'CIVICRM_DOMAIN_ID' ) ? CIVICRM_DOMAIN_ID : 0; // If this fails, get it from config. if ( $domain_id === 0 ) { $domain_id = \CRM_Core_Config::domainID(); } // Call API. $uf_match = civicrm_api3( 'UFMatch', 'getsingle', [ 'contact_id' => $contact_id, 'domain_id' => $domain_id, ] ); } catch ( \CiviCRM_API3_Exception $e ) { return $this->civi_rest_error( $e->getMessage() ); } $wp_user = get_userdata( $uf_match['uf_id'] ); return $wp_user; } /** * Logs in the WordPress user, needed to respect CiviCRM ACL and permissions. * * @since 0.1 * @param WP_User $user */ protected function do_user_login( \WP_User $user ) { if ( is_user_logged_in() ) return; wp_set_current_user( $user->ID, $user->user_login ); wp_set_auth_cookie( $user->ID ); do_action( 'wp_login', $user->user_login, $user ); } }