Skip to content
Snippets Groups Projects
Rest.php 11.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • <?php
    /**
     * Rest controller class.
     *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
     * @since 5.25
    
     */
    
    namespace CiviCRM_WP_REST\Controller;
    
    class Rest extends Base {
    
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
      /**
       * @var string
       * The base route.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       */
      protected $rest_base = 'rest';
    
      /**
       * Registers routes.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       */
      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.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @param WP_REST_Request $request
       * @return bool
       */
      public function permissions_check($request) {
    
        /**
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         * 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.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         * NOTE: if you use your own authentication, you still must log in the user
         * in order to respect/apply CiviCRM ACLs.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         * @since 5.25
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         *
         * @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.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @param WP_REST_Request $request
       */
      public function get_items($request) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        /**
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         * Filter formatted API params.
         *
         * @since 5.25
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         *
         * @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 (\CRM_Core_Exception $e) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          $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
        /**
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         * Filter CiviCRM API result.
         *
         * @since 5.25
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
         *
         * @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'.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        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 whether we need to serve xml or json.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        if (!in_array('json', array_keys($request->get_params()))) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          /**
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
           * Adds our response holding CiviCRM data before dispatching.
           *
           * @since 5.25
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
           *
           * @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
      /**
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * Get formatted API params.
       *
       * @since 5.25
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       *
       * @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.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $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.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @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.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @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.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $xml = $this->get_xml_formatted_data($result->get_data());
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // Set content type header.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $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.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
       * @param array $data The CiviCRM API result.
       * @return string $xml The formatted XML.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       */
      protected function get_xml_formatted_data(array $data) {
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // XML document.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $xml = new \DOMDocument();
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // Result set element <ResultSet>.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $result_set = $xml->createElement('ResultSet');
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // The xmlns:xsi attribute.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $result_set->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // Count attributes.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        if (isset($data['count'])) {
          $result_set->setAttribute('count', $data['count']);
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // Build result from result => values.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        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>.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            $result = $xml->createElement('Result');
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            // Format item.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            $result = $this->get_xml_formatted_item($item, $result, $xml);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            // Append result to result set.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            $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>.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          $result = $xml->createElement('Result');
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // Format item.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          $result = $this->get_xml_formatted_item($data, $result, $xml);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // Append result to result set.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          $result_set->appendChild($result);
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        }
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // Append result set.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        $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
      /**
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * Formats a single API result to XML.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
       * @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.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       */
      public function get_xml_formatted_item(array $item, \DOMElement $parent, \DOMDocument $doc) {
    
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        // Build field => values.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
        array_map(function($field, $value) use ($parent, $doc) {
    
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // Entity field element.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          $element = $doc->createElement($field);
    
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // Handle array values.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          if (is_array($value)) {
    
            array_map(function($key, $val) use ($element, $doc) {
    
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
              /*
               * Child element - append underscore '_' otherwise createElement will
               * throw an Invalid character exception as elements cannot start with
               * a number.
               */
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
              $child = $doc->createElement('_' . $key, $val);
    
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
              // Append child.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
              $element->appendChild($child);
    
            }, array_keys($value), $value);
    
          }
          else {
    
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            // Assign value.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
            $element->nodeValue = $value;
    
          }
    
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          // Append element.
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
          $parent->appendChild($element);
    
        }, array_keys($item), $item);
    
        return $parent;
    
      }
    
      /**
       * Item schema.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @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.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @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.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @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.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @return bool $is_valid_site_key
       */
      public function is_valid_site_key() {
    
        return \CRM_Utils_System::authenticateKey(FALSE);
    
      }
    
      /**
       * Validates the api key.
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @since 5.25
       *
    
    Kevin Cristiano's avatar
    Kevin Cristiano committed
       * @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;
    
      }