Skip to content
Snippets Groups Projects
Rest.php 11.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • <?php
    /**
     * Rest controller class.
     *
     * @since 0.1
     */
    
    namespace CiviCRM_WP_REST\Controller;
    
    class Rest extends Base {
    
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      /**
       * @var string
       * The base route.
       * @since 0.1
       */
      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);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        if (is_bool($grant_auth)) {
    
          return $grant_auth;
    
        }
    
        elseif (is_string($grant_auth)) {
    
          return $this->civi_rest_error($grant_auth);
    
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        elseif (is_array($grant_auth)) {
    
          return $this->civi_rest_error(__('CiviCRM WP REST permission check error.', 'civicrm'), $grant_auth);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        elseif ($grant_auth instanceof \WP_Error) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          return $grant_auth;
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        else {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          if (!$this->is_valid_api_key($request)) {
            return $this->civi_rest_error(__('Param api_key is not valid.', 'civicrm'));
          }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          if (!$this->is_valid_site_key()) {
            return $this->civi_rest_error(__('Param key is not valid.', 'civicrm'));
          }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          return TRUE;
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      /**
       * Get items.
       *
       * @since 0.1
       * @param WP_REST_Request $request
       */
      public function get_items($request) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        /**
         * 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);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        try {
          $items = civicrm_api3(...$params);
        }
        catch (\CiviCRM_API3_Exception $e) {
          $items = $this->civi_rest_error($e);
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        if (!isset($items) || empty($items)) {
          return rest_ensure_response([]);
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        /**
         * 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);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // only collections of items, ie any action but 'getsingle'
        if (isset($data['values'])) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          $data['values'] = array_reduce($items['values'] ?? $items, function($items, $item) use ($request) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            $response = $this->prepare_item_for_response($item, $request);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            $items[] = $this->prepare_response_for_collection($response);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            return $items;
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          }, []);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $response = rest_ensure_response($data);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // check wheather we need to serve xml or json
        if (!in_array('json', array_keys($request->get_params()))) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          /**
           * 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) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            return $response;
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          }, 10, 3);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // serve xml
          add_filter('rest_pre_serve_request', [$this, 'serve_xml_response'], 10, 4);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        else {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // return json
          return $response;
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      /**
       * Get formatted api params.
       *
       * @since 0.1
       * @param WP_REST_Resquest $request
       * @return array $params
       */
      public function get_formatted_api_params($request) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $args = $request->get_params();
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $entity = $args['entity'];
        $action = $args['action'];
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // unset unnecessary args
        unset($args['entity'], $args['action'], $args['key'], $args['api_key']);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        if (!isset($args['json']) || is_numeric($args['json'])) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          $params = $args;
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        else {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          $params = is_string($args['json']) ? json_decode($args['json'], TRUE) : [];
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // ensure check permissions is enabled
        $params['check_permissions'] = TRUE;
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        return [$entity, $action, $params];
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      /**
       * 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) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        return rest_ensure_response($item);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      /**
       * 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) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // get xml from response
        $xml = $this->get_xml_formatted_data($result->get_data());
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // set content type header
        $server->send_header('Content-Type', 'text/xml');
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        echo $xml;
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        return TRUE;
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      /**
       * 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) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // xml document
        $xml = new \DOMDocument();
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // result set element <ResultSet>
        $result_set = $xml->createElement('ResultSet');
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // xmlns:xsi attribute
        $result_set->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // count attribute
        if (isset($data['count'])) {
          $result_set->setAttribute('count', $data['count']);
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // build result from result => values
        if (isset($data['values'])) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          array_map(function($item) use ($result_set, $xml) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            // result element <Result>
            $result = $xml->createElement('Result');
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            // format item
            $result = $this->get_xml_formatted_item($item, $result, $xml);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            // append result to result set
            $result_set->appendChild($result);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          }, $data['values']);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
        else {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // result element <Result>
          $result = $xml->createElement('Result');
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // format item
          $result = $this->get_xml_formatted_item($data, $result, $xml);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // append result to result set
          $result_set->appendChild($result);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // append result set
        $xml->appendChild($result_set);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        return $xml->saveXML();
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      /**
       * 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
       */
      public 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
       */
      public 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
       */
      public 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');
    
        if (!$contact_id) {
          return FALSE;
        }
    
        return TRUE;
    
      }