CRM-17867 - Api4 core patches
authorColeman Watts <coleman@civicrm.org>
Mon, 25 Jan 2016 02:24:20 +0000 (21:24 -0500)
committerColeman Watts <coleman@civicrm.org>
Mon, 10 Oct 2016 18:15:50 +0000 (14:15 -0400)
Most of api4 is in an extension - these are the changes needed to core.

CRM/Admin/Page/APIExplorer.php
Civi/API/Api3SelectQuery.php [new file with mode: 0644]
Civi/API/Kernel.php
Civi/API/Request.php
Civi/API/SelectQuery.php
Civi/API/Subscriber/ChainSubscriber.php
Civi/API/Subscriber/PermissionCheck.php
Civi/Core/Container.php
api/api.php
api/v3/utils.php

index 90358db15f801cd12e04a94025c542847e30aae0..111634b958a92069c9b46edfc1afbab04b3bd7e6 100644 (file)
@@ -46,7 +46,7 @@ class CRM_Admin_Page_APIExplorer extends CRM_Core_Page {
       ->addScriptFile('civicrm', 'templates/CRM/Admin/Page/APIExplorer.js')
       ->addScriptFile('civicrm', 'bower_components/google-code-prettify/bin/prettify.min.js', 99)
       ->addStyleFile('civicrm', 'bower_components/google-code-prettify/bin/prettify.min.css', 99)
-      ->addVars('explorer', array('max_joins' => \Civi\API\SelectQuery::MAX_JOINS));
+      ->addVars('explorer', array('max_joins' => \Civi\API\Api3SelectQuery::MAX_JOINS));
 
     $this->assign('operators', CRM_Core_DAO::acceptedSQLOperators());
 
diff --git a/Civi/API/Api3SelectQuery.php b/Civi/API/Api3SelectQuery.php
new file mode 100644 (file)
index 0000000..cfa39b9
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.7                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2015                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\API;
+
+/**
+ */
+class Api3SelectQuery extends SelectQuery {
+
+  protected $apiVersion = 3;
+
+  /**
+   * @inheritDoc
+   */
+  protected function buildWhereClause() {
+    foreach ($this->where as $key => $value) {
+      $table_name = NULL;
+      $column_name = NULL;
+
+      if (substr($key, 0, 7) == 'filter.') {
+        // Legacy support for old filter syntax per the test contract.
+        // (Convert the style to the later one & then deal with them).
+        $filterArray = explode('.', $key);
+        $value = array($filterArray[1] => $value);
+        $key = 'filters';
+      }
+
+      // Legacy support for 'filter's construct.
+      if ($key == 'filters') {
+        foreach ($value as $filterKey => $filterValue) {
+          if (substr($filterKey, -4, 4) == 'high') {
+            $key = substr($filterKey, 0, -5);
+            $value = array('<=' => $filterValue);
+          }
+
+          if (substr($filterKey, -3, 3) == 'low') {
+            $key = substr($filterKey, 0, -4);
+            $value = array('>=' => $filterValue);
+          }
+
+          if ($filterKey == 'is_current' || $filterKey == 'isCurrent') {
+            // Is current is almost worth creating as a 'sql filter' in the DAO function since several entities have the concept.
+            $todayStart = date('Ymd000000', strtotime('now'));
+            $todayEnd = date('Ymd235959', strtotime('now'));
+            $a = self::MAIN_TABLE_ALIAS;
+            $this->query->where("($a.start_date <= '$todayStart' OR $a.start_date IS NULL)
+              AND ($a.end_date >= '$todayEnd' OR $a.end_date IS NULL)
+              AND a.is_active = 1");
+          }
+        }
+      }
+      // Ignore the "options" param if it is referring to api options and not a field in this entity
+      if (
+        $key === 'options' && is_array($value)
+        && !in_array(\CRM_Utils_Array::first(array_keys($value)), \CRM_Core_DAO::acceptedSQLOperators())
+      ) {
+        continue;
+      }
+      $field = $this->getField($key);
+      if ($field) {
+        $key = $field['name'];
+      }
+      if (in_array($key, $this->entityFieldNames)) {
+        $table_name = self::MAIN_TABLE_ALIAS;
+        $column_name = $key;
+      }
+      elseif (($cf_id = \CRM_Core_BAO_CustomField::getKeyID($key)) != FALSE) {
+        list($table_name, $column_name) = $this->addCustomField($this->apiFieldSpec['custom_' . $cf_id], 'INNER');
+      }
+      elseif (strpos($key, '.')) {
+        $fkInfo = $this->addFkField($key, 'INNER');
+        if ($fkInfo) {
+          list($table_name, $column_name) = $fkInfo;
+          $this->validateNestedInput($key, $value);
+        }
+      }
+      // I don't know why I had to specifically exclude 0 as a key - wouldn't the others have caught it?
+      // We normally silently ignore null values passed in - if people want IS_NULL they can use acceptedSqlOperator syntax.
+      if ((!$table_name) || empty($key) || is_null($value)) {
+        // No valid filter field. This might be a chained call or something.
+        // Just ignore this for the $where_clause.
+        continue;
+      }
+      if (!is_array($value)) {
+        $this->query->where(array("`$table_name`.`$column_name` = @value"), array(
+          "@value" => $value,
+        ));
+      }
+      else {
+        // We expect only one element in the array, of the form
+        // "operator" => "rhs".
+        $operator = \CRM_Utils_Array::first(array_keys($value));
+        if (!in_array($operator, \CRM_Core_DAO::acceptedSQLOperators())) {
+          $this->query->where(array("{$table_name}.{$column_name} = @value"), array("@value" => $value));
+        }
+        else {
+          $this->query->where(\CRM_Core_DAO::createSQLFilter("{$table_name}.{$column_name}", $value));
+        }
+      }
+    }
+  }
+
+  /**
+   * @inheritDoc
+   */
+  protected function getFields() {
+    require_once 'api/v3/Generic.php';
+    // Call this function directly instead of using the api wrapper to force unique field names off
+    $apiSpec = \civicrm_api3_generic_getfields(array(
+      'entity' => $this->entity,
+      'version' => 3,
+      'params' => array('action' => 'get'),
+    ), FALSE);
+    return $apiSpec['values'];
+  }
+
+  /**
+   * Fetch a field from the getFields list
+   *
+   * Searches by name, uniqueName, and api.aliases
+   */
+  protected function getField($fieldName) {
+    if (!$fieldName) {
+      return NULL;
+    }
+    if (isset($this->apiFieldSpec[$fieldName])) {
+      return $this->apiFieldSpec[$fieldName];
+    }
+    foreach ($this->apiFieldSpec as $field) {
+      if (
+        $fieldName == \CRM_Utils_Array::value('uniqueName', $field) ||
+        array_search($fieldName, \CRM_Utils_Array::value('api.aliases', $field, array())) !== FALSE
+      ) {
+        return $field;
+      }
+    }
+    return NULL;
+  }
+
+}
index a22c3fcf63116fe0c68f85f9be953a8427c9616f..521eb0a32c7069deedf445ba9ec3911d05e06d01 100644 (file)
@@ -61,6 +61,18 @@ class Kernel {
   }
 
   /**
+   * @deprecated
+   * @return array|int
+   * @see runSafe
+   */
+  public function run($entity, $action, $params, $extra = NULL) {
+    return $this->runSafe($entity, $action, $params, $extra);
+  }
+
+  /**
+   * Parse and execute an API request. Any errors will be converted to
+   * normal format.
+   *
    * @param string $entity
    *   Type of entities to deal with.
    * @param string $action
@@ -68,38 +80,27 @@ class Kernel {
    * @param array $params
    *   Array to be passed to API function.
    * @param mixed $extra
-   *   Who knows.
+   *   Unused/deprecated.
    *
    * @return array|int
+   * @throws \API_Exception
    */
-  public function run($entity, $action, $params, $extra = NULL) {
+  public function runSafe($entity, $action, $params, $extra = NULL) {
+    // TODO Define alternative calling convention makes it easier to construct $apiRequest
+    // without the ambiguity of "data" vs "options"
+    $apiRequest = Request::create($entity, $action, $params, $extra);
+
     /**
      * @var $apiProvider \Civi\API\Provider\ProviderInterface|NULL
      */
     $apiProvider = NULL;
 
-    // TODO Define alternative calling convention makes it easier to construct $apiRequest
-    // without the ambiguity of "data" vs "options"
-    $apiRequest = Request::create($entity, $action, $params, $extra);
-
     try {
-      if (!is_array($params)) {
-        throw new \API_Exception('Input variable `params` is not an array', 2000);
-      }
-
-      $this->boot();
-      $errorScope = \CRM_Core_TemporaryErrorScope::useException();
-
-      list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
-      $this->authorize($apiProvider, $apiRequest);
-      $apiRequest = $this->prepare($apiProvider, $apiRequest);
-      $result = $apiProvider->invoke($apiRequest);
-
-      $apiResponse = $this->respond($apiProvider, $apiRequest, $result);
+      $apiResponse = $this->runRequest($apiRequest);
       return $this->formatResult($apiRequest, $apiResponse);
     }
     catch (\Exception $e) {
-      $this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, $apiProvider, $apiRequest, $this));
+      $this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, NULL, $apiRequest, $this));
 
       if ($e instanceof \PEAR_Exception) {
         $err = $this->formatPearException($e, $apiRequest);
@@ -125,7 +126,8 @@ class Kernel {
    * @param array $params
    *   Array to be passed to function.
    * @param mixed $extra
-   *   Who knows.
+   *   Unused/deprecated.
+   *
    * @return bool
    *   TRUE if authorization would succeed.
    * @throws \Exception
@@ -145,6 +147,32 @@ class Kernel {
     }
   }
 
+  /**
+   * Execute an API request.
+   *
+   * The request must be in canonical format. Exceptions will be propagated out.
+   *
+   * @param $apiRequest
+   * @return array
+   * @throws \API_Exception
+   * @throws \Civi\API\Exception\NotImplementedException
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  public function runRequest($apiRequest) {
+    $this->validate($apiRequest);
+
+    $this->boot();
+    $errorScope = \CRM_Core_TemporaryErrorScope::useException();
+
+    list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
+    $this->authorize($apiProvider, $apiRequest);
+    $apiRequest = $this->prepare($apiProvider, $apiRequest);
+    $result = $apiProvider->invoke($apiRequest);
+
+    $apiResponse = $this->respond($apiProvider, $apiRequest, $result);
+    return $apiResponse;
+  }
+
   /**
    * Bootstrap - Load basic dependencies.
    */
@@ -154,6 +182,18 @@ class Kernel {
     _civicrm_api3_initialize();
   }
 
+  /**
+   * @param $apiRequest
+   * @throws \API_Exception
+   */
+  protected function validate($apiRequest) {
+    if ($apiRequest['version'] === 3) {
+      if (!is_array($apiRequest['params'])) {
+        throw new \API_Exception('Input variable `params` is not an array', 2000);
+      }
+    }
+  }
+
   /**
    * Determine which, if any, service will execute the API request.
    *
index 86451eb1626aecf4ed75629ac945104193f4ca2a..212e18f6bc839e85a96505044d5063264d2d840b 100644 (file)
@@ -58,76 +58,36 @@ class Request {
    *   - data: \CRM_Utils_OptionBag derived from params [v4-only]
    *   - chains: unspecified derived from params [v4-only]
    */
-  public static function create($entity, $action, $params, $extra) {
-    $apiRequest = array(); // new \Civi\API\Request();
-    $apiRequest['id'] = self::$nextId++;
-    $apiRequest['version'] = self::parseVersion($params);
-    $apiRequest['params'] = $params;
-    $apiRequest['extra'] = $extra;
-    $apiRequest['fields'] = NULL;
+  public static function create($entity, $action, $params, $extra = NULL) {
+    $version = self::parseVersion($params);
 
-    $apiRequest['entity'] = $entity = self::normalizeEntityName($entity, $apiRequest['version']);
-    $apiRequest['action'] = $action = self::normalizeActionName($action, $apiRequest['version']);
+    switch ($version) {
+      case 2:
+      case 3:
+        $apiRequest = array(); // new \Civi\API\Request();
+        $apiRequest['id'] = self::$nextId++;
+        $apiRequest['version'] = $version;
+        $apiRequest['params'] = $params;
+        $apiRequest['extra'] = $extra;
+        $apiRequest['fields'] = NULL;
 
-    // APIv1-v3 mix data+options in $params which means that each API callback is responsible
-    // for splitting the two. In APIv4, the split is done systematically so that we don't
-    // so much parsing logic spread around.
-    if ($apiRequest['version'] >= 4) {
-      $options = array();
-      $data = array();
-      $chains = array();
-      foreach ($params as $key => $value) {
-        if ($key == 'options') {
-          $options = array_merge($options, $value);
-        }
-        elseif ($key == 'return') {
-          if (!isset($options['return'])) {
-            $options['return'] = array();
-          }
-          $options['return'] = array_merge($options['return'], $value);
-        }
-        elseif (preg_match('/^option\.(.*)$/', $key, $matches)) {
-          $options[$matches[1]] = $value;
-        }
-        elseif (preg_match('/^return\.(.*)$/', $key, $matches)) {
-          if ($value) {
-            if (!isset($options['return'])) {
-              $options['return'] = array();
-            }
-            $options['return'][] = $matches[1];
-          }
-        }
-        elseif (preg_match('/^format\.(.*)$/', $key, $matches)) {
-          if ($value) {
-            if (!isset($options['format'])) {
-              $options['format'] = $matches[1];
-            }
-            else {
-              throw new \API_Exception("Too many API formats specified");
-            }
-          }
-        }
-        elseif (preg_match('/^api\./', $key)) {
-          // FIXME: represent subrequests as instances of "Request"
-          $chains[$key] = $value;
-        }
-        elseif ($key == 'debug') {
-          $options['debug'] = $value;
-        }
-        elseif ($key == 'version') {
-          // ignore
-        }
-        else {
-          $data[$key] = $value;
+        $apiRequest['entity'] = $entity = self::normalizeEntityName($entity, $apiRequest['version']);
+        $apiRequest['action'] = $action = self::normalizeActionName($action, $apiRequest['version']);
 
+        return $apiRequest;
+
+      case 4:
+        $apiCall = call_user_func(array("Civi\\Api4\\$entity", $action));
+        unset($params['version']);
+        foreach ($params as $name => $param) {
+          $setter = 'set' . ucfirst($name);
+          $apiCall->$setter($param);
         }
-      }
-      $apiRequest['options'] = new \CRM_Utils_OptionBag($options);
-      $apiRequest['data'] = new \CRM_Utils_OptionBag($data);
-      $apiRequest['chains'] = $chains;
+        return $apiCall;
+
+      default:
     }
 
-    return $apiRequest;
   }
 
   /**
@@ -145,11 +105,7 @@ class Request {
       return \CRM_Utils_String::convertStringToCamel(\CRM_Utils_String::munge($entity));
     }
     else {
-      // APIv4 requires exact spelling & capitalization of entity/action name; deviations should cause errors
-      if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $entity)) {
-        throw new \API_Exception("Malformed entity");
-      }
-      return $entity;
+      throw new \API_Exception("Unknown api version");
     }
   }
 
@@ -160,12 +116,7 @@ class Request {
       return strtolower(\CRM_Utils_String::munge($action));
     }
     else {
-      // APIv4 requires exact spelling & capitalization of entity/action name; deviations should cause errors
-      if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $action)) {
-        throw new \API_Exception("Malformed action");
-      }
-      // TODO: Not sure about camelCase actions - in v3 they are all lowercase.
-      return strtolower($action{0}) . substr($action, 1);
+      throw new \API_Exception("Unknown api version");
     }
   }
 
index c8332fe6d54d47e044e6577fdebeb1d72c227f30..3764992baa362adc06f74840efcc429d399998b6 100644 (file)
@@ -40,7 +40,7 @@ use Civi\API\Exception\UnauthorizedException;
  *
  * @package Civi\API
  */
-class SelectQuery {
+abstract class SelectQuery {
 
   const
     MAX_JOINS = 4,
@@ -50,18 +50,19 @@ class SelectQuery {
    * @var string
    */
   protected $entity;
+  public $select = array();
+  public $where = array();
+  public $orderBy = array();
+  public $limit;
+  public $offset;
   /**
    * @var array
    */
-  protected $params;
-  /**
-   * @var array
-   */
-  protected $options;
+  protected $selectFields = array();
   /**
    * @var bool
    */
-  protected $isFillUniqueFields;
+  public $isFillUniqueFields = FALSE;
   /**
    * @var \CRM_Utils_SQL_Select
    */
@@ -69,7 +70,7 @@ class SelectQuery {
   /**
    * @var array
    */
-  private $joins = array();
+  protected $joins = array();
   /**
    * @var array
    */
@@ -85,29 +86,21 @@ class SelectQuery {
   /**
    * @var string|bool
    */
-  protected $checkPermissions;
+  public $checkPermissions;
+
+  protected $apiVersion;
 
   /**
-   * @param string $baoName
-   *   Name of BAO
-   * @param array $params
-   *   As passed into api get function.
-   * @param bool $isFillUniqueFields
-   *   Do we need to ensure unique fields continue to be populated for this api? (backward compatibility).
+   * @param string $entity
    */
-  public function __construct($baoName, $params, $isFillUniqueFields) {
+  public function __construct($entity) {
+    $this->entity = $entity;
+    require_once 'api/v3/utils.php';
+    $baoName = _civicrm_api3_get_BAO($entity);
     $bao = new $baoName();
-    $this->entity = _civicrm_api_get_entity_name_from_dao($bao);
-    $this->params = $params;
-    $this->isFillUniqueFields = $isFillUniqueFields;
-    $this->checkPermissions = \CRM_Utils_Array::value('check_permissions', $this->params, FALSE);
-    $this->options = _civicrm_api3_get_options_from_params($this->params);
 
     $this->entityFieldNames = _civicrm_api3_field_names(_civicrm_api3_build_fields_array($bao));
-    // Call this function directly instead of using the api wrapper to force unique field names off
-    require_once 'api/v3/Generic.php';
-    $apiSpec = \civicrm_api3_generic_getfields(array('entity' => $this->entity, 'version' => 3, 'params' => array('action' => 'get')), FALSE);
-    $this->apiFieldSpec = $apiSpec['values'];
+    $this->apiFieldSpec = $this->getFields();
 
     $this->query = \CRM_Utils_SQL_Select::from($bao->tableName() . ' ' . self::MAIN_TABLE_ALIAS);
     $bao->free();
@@ -125,177 +118,36 @@ class SelectQuery {
    * @throws \Exception
    */
   public function run() {
-    // $select_fields maps column names to the field names of the result values.
-    $select_fields = $custom_fields = array();
+    $this->buildSelectFields();
 
-    // populate $select_fields
-    $return_all_fields = (empty($this->options['return']) || !is_array($this->options['return']));
-    $return = $return_all_fields ? array_fill_keys($this->entityFieldNames, 1) : $this->options['return'];
-
-    // core return fields
-    foreach ($return as $field_name => $include) {
-      if ($include) {
-        $field = $this->getField($field_name);
-        if ($field && in_array($field['name'], $this->entityFieldNames)) {
-          $select_fields[self::MAIN_TABLE_ALIAS . ".{$field['name']}"] = $field['name'];
-        }
-        elseif ($include && strpos($field_name, '.')) {
-          $fkField = $this->addFkField($field_name, 'LEFT');
-          if ($fkField) {
-            $select_fields[implode('.', $fkField)] = $field_name;
-          }
-        }
-      }
-    }
-
-    // Do custom fields IF the params contain the word "custom" or we are returning *
-    if ($return_all_fields || strpos(json_encode($this->params), 'custom')) {
-      $custom_fields = _civicrm_api3_custom_fields_for_entity($this->entity);
-      foreach ($custom_fields as $cf_id => $custom_field) {
-        $field_name = "custom_$cf_id";
-        if ($return_all_fields || !empty($this->options['return'][$field_name])
-          ||
-          // This is a tested format so we support it.
-          !empty($this->options['return']['custom'])
-        ) {
-          list($table_name, $column_name) = $this->addCustomField($custom_field, 'LEFT');
-
-          if ($custom_field["data_type"] != "ContactReference") {
-            // 'ordinary' custom field. We will select the value as custom_XX.
-            $select_fields["$table_name.$column_name"] = $field_name;
-          }
-          else {
-            // contact reference custom field. The ID will be stored in custom_XX_id.
-            // custom_XX will contain the sort name of the contact.
-            $this->query->join("c_$cf_id", "LEFT JOIN civicrm_contact c_$cf_id ON c_$cf_id.id = `$table_name`.`$column_name`");
-            $select_fields["$table_name.$column_name"] = $field_name . "_id";
-            // We will call the contact table for the join c_XX.
-            $select_fields["c_$cf_id.sort_name"] = $field_name;
-          }
-        }
-      }
-    }
-    // Always select the ID.
-    $select_fields[self::MAIN_TABLE_ALIAS . ".id"] = "id";
-
-    // populate where_clauses
-    foreach ($this->params as $key => $value) {
-      $table_name = NULL;
-      $column_name = NULL;
-
-      if (substr($key, 0, 7) == 'filter.') {
-        // Legacy support for old filter syntax per the test contract.
-        // (Convert the style to the later one & then deal with them).
-        $filterArray = explode('.', $key);
-        $value = array($filterArray[1] => $value);
-        $key = 'filters';
-      }
-
-      // Legacy support for 'filter's construct.
-      if ($key == 'filters') {
-        foreach ($value as $filterKey => $filterValue) {
-          if (substr($filterKey, -4, 4) == 'high') {
-            $key = substr($filterKey, 0, -5);
-            $value = array('<=' => $filterValue);
-          }
+    $this->buildWhereClause();
 
-          if (substr($filterKey, -3, 3) == 'low') {
-            $key = substr($filterKey, 0, -4);
-            $value = array('>=' => $filterValue);
-          }
-
-          if ($filterKey == 'is_current' || $filterKey == 'isCurrent') {
-            // Is current is almost worth creating as a 'sql filter' in the DAO function since several entities have the concept.
-            $todayStart = date('Ymd000000', strtotime('now'));
-            $todayEnd = date('Ymd235959', strtotime('now'));
-            $a = self::MAIN_TABLE_ALIAS;
-            $this->query->where("($a.start_date <= '$todayStart' OR $a.start_date IS NULL)
-              AND ($a.end_date >= '$todayEnd' OR $a.end_date IS NULL)
-              AND a.is_active = 1");
-          }
-        }
-      }
-      // Ignore the "options" param if it is referring to api options and not a field in this entity
-      if (
-        $key === 'options' && is_array($value)
-        && !in_array(\CRM_Utils_Array::first(array_keys($value)), \CRM_Core_DAO::acceptedSQLOperators())
-      ) {
-        continue;
-      }
-      $field = $this->getField($key);
-      if ($field) {
-        $key = $field['name'];
-      }
-      if (in_array($key, $this->entityFieldNames)) {
-        $table_name = self::MAIN_TABLE_ALIAS;
-        $column_name = $key;
-      }
-      elseif (($cf_id = \CRM_Core_BAO_CustomField::getKeyID($key)) != FALSE) {
-        list($table_name, $column_name) = $this->addCustomField($custom_fields[$cf_id], 'INNER');
-      }
-      elseif (strpos($key, '.')) {
-        $fkInfo = $this->addFkField($key, 'INNER');
-        if ($fkInfo) {
-          list($table_name, $column_name) = $fkInfo;
-          $this->validateNestedInput($key, $value);
-        }
-      }
-      // I don't know why I had to specifically exclude 0 as a key - wouldn't the others have caught it?
-      // We normally silently ignore null values passed in - if people want IS_NULL they can use acceptedSqlOperator syntax.
-      if ((!$table_name) || empty($key) || is_null($value)) {
-        // No valid filter field. This might be a chained call or something.
-        // Just ignore this for the $where_clause.
-        continue;
-      }
-      if (!is_array($value)) {
-        $this->query->where(array("`$table_name`.`$column_name` = @value"), array(
-          "@value" => $value,
-        ));
-      }
-      else {
-        // We expect only one element in the array, of the form
-        // "operator" => "rhs".
-        $operator = \CRM_Utils_Array::first(array_keys($value));
-        if (!in_array($operator, \CRM_Core_DAO::acceptedSQLOperators())) {
-          $this->query->where(array(
-            "{$table_name}.{$column_name} = @value"), array("@value" => $value)
-          );
-        }
-        else {
-          $this->query->where(\CRM_Core_DAO::createSQLFilter("{$table_name}.{$column_name}", $value));
-        }
-      }
+    if (in_array('count', $this->select)) {
+      $this->query->select("count(*) as c");
     }
-
-    if (!$this->options['is_count']) {
-      foreach ($select_fields as $column => $alias) {
+    else {
+      foreach ($this->selectFields as $column => $alias) {
         $this->query->select("$column as `$alias`");
       }
-    }
-    else {
-      $this->query->select("count(*) as c");
-    }
-
-    // Order by
-    if (!empty($this->options['sort'])) {
-      $this->orderBy($this->options['sort']);
+      // Order by
+      $this->buildOrderBy();
     }
 
     // Limit
-    if (!empty($this->options['limit']) || !empty($this->options['offset'])) {
-      $this->query->limit($this->options['limit'], $this->options['offset']);
+    if (!empty($this->limit) || !empty($this->offset)) {
+      $this->query->limit($this->limit, $this->offset);
     }
 
     $result_entities = array();
     $result_dao = \CRM_Core_DAO::executeQuery($this->query->toSQL());
 
     while ($result_dao->fetch()) {
-      if ($this->options['is_count']) {
+      if (in_array('count', $this->select)) {
         $result_dao->free();
         return (int) $result_dao->c;
       }
       $result_entities[$result_dao->id] = array();
-      foreach ($select_fields as $column => $alias) {
+      foreach ($this->selectFields as $column => $alias) {
         $returnName = $alias;
         $alias = str_replace('.', '_', $alias);
         if (property_exists($result_dao, $alias) && $result_dao->$alias != NULL) {
@@ -340,7 +192,7 @@ class SelectQuery {
    * @throws \API_Exception
    * @throws \Civi\API\Exception\UnauthorizedException
    */
-  private function addFkField($fkFieldName, $side) {
+  protected function addFkField($fkFieldName, $side) {
     $stack = explode('.', $fkFieldName);
     if (count($stack) < 2) {
       return NULL;
@@ -415,7 +267,7 @@ class SelectQuery {
    * @return array
    *   Returns the table and field name for adding this field to a SELECT or WHERE clause
    */
-  private function addCustomField($customField, $side, $baseTable = self::MAIN_TABLE_ALIAS) {
+  protected function addCustomField($customField, $side, $baseTable = self::MAIN_TABLE_ALIAS) {
     $tableName = $customField["table_name"];
     $columnName = $customField["column_name"];
     $tableAlias = "{$baseTable}_to_$tableName";
@@ -426,28 +278,10 @@ class SelectQuery {
   /**
    * Fetch a field from the getFields list
    *
-   * Searches by name, uniqueName, and api.aliases
-   *
    * @param string $fieldName
    * @return array|null
    */
-  private function getField($fieldName) {
-    if (!$fieldName) {
-      return NULL;
-    }
-    if (isset($this->apiFieldSpec[$fieldName])) {
-      return $this->apiFieldSpec[$fieldName];
-    }
-    foreach ($this->apiFieldSpec as $field) {
-      if (
-        $fieldName == \CRM_Utils_Array::value('uniqueName', $field) ||
-        array_search($fieldName, \CRM_Utils_Array::value('api.aliases', $field, array())) !== FALSE
-      ) {
-        return $field;
-      }
-    }
-    return NULL;
-  }
+  abstract protected function getField($fieldName);
 
   /**
    * Perform input validation on params that use the join syntax
@@ -459,7 +293,7 @@ class SelectQuery {
    * @param $value
    * @throws \Exception
    */
-  private function validateNestedInput($fieldName, &$value) {
+  protected function validateNestedInput($fieldName, &$value) {
     $stack = explode('.', $fieldName);
     $spec = $this->apiFieldSpec;
     $fieldName = array_pop($stack);
@@ -480,7 +314,7 @@ class SelectQuery {
    *   The stack of fields leading up to this join
    * @return bool
    */
-  private function checkPermissionToJoin($entity, $fieldStack) {
+  protected function checkPermissionToJoin($entity, $fieldStack) {
     if (!$this->checkPermissions) {
       return TRUE;
     }
@@ -492,12 +326,12 @@ class SelectQuery {
     );
     $prefix = implode('.', $fieldStack) . '.';
     $len = strlen($prefix);
-    foreach ($this->options['return'] as $key => $ret) {
+    foreach ($this->select as $key => $ret) {
       if (strpos($key, $prefix) === 0) {
         $params['return'][substr($key, $len)] = $ret;
       }
     }
-    foreach ($this->params as $key => $param) {
+    foreach ($this->where as $key => $param) {
       if (strpos($key, $prefix) === 0) {
         $params[substr($key, $len)] = $param;
       }
@@ -514,7 +348,7 @@ class SelectQuery {
    * @param array $stack
    * @return array
    */
-  private function getAclClause($tableAlias, $baoName, $stack = array()) {
+  protected function getAclClause($tableAlias, $baoName, $stack = array()) {
     if (!$this->checkPermissions) {
       return array();
     }
@@ -535,18 +369,13 @@ class SelectQuery {
   /**
    * Orders the query by one or more fields
    *
-   * e.g.
-   * @code
-   *   $this->orderBy(array('last_name DESC', 'birth_date'));
-   * @endcode
-   *
-   * @param string|array $sortParams
    * @throws \API_Exception
    * @throws \Civi\API\Exception\UnauthorizedException
    */
-  public function orderBy($sortParams) {
+  protected function buildOrderBy() {
     $orderBy = array();
-    foreach (is_array($sortParams) ? $sortParams : explode(',', $sortParams) as $item) {
+    $sortParams = is_string($this->orderBy) ? explode(',', $this->orderBy) : (array) $this->orderBy;
+    foreach ($sortParams as $item) {
       $words = preg_split("/[\s]+/", trim($item));
       if ($words) {
         // Direction defaults to ASC unless DESC is specified
@@ -583,4 +412,68 @@ class SelectQuery {
     }
   }
 
+  /**
+   * Populate where clauses
+   *
+   * @throws \Civi\API\Exception\UnauthorizedException
+   * @throws \Exception
+   */
+  abstract protected function buildWhereClause();
+
+  /**
+   * Populate $this->selectFields
+   *
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  protected function buildSelectFields() {
+    $return_all_fields = (empty($this->select) || !is_array($this->select));
+    $return = $return_all_fields ? $this->entityFieldNames : $this->select;
+    if ($return_all_fields || in_array('custom', $this->select)) {
+      foreach (array_keys($this->apiFieldSpec) as $fieldName) {
+        if (strpos($fieldName, 'custom_') === 0) {
+          $return[] = $fieldName;
+        }
+      }
+    }
+
+    // Always select the ID.
+    $this->selectFields[self::MAIN_TABLE_ALIAS . ".id"] = "id";
+
+    // core return fields
+    foreach ($return as $fieldName) {
+      $field = $this->getField($fieldName);
+      if ($field && in_array($field['name'], $this->entityFieldNames)) {
+        $this->selectFields[self::MAIN_TABLE_ALIAS . ".{$field['name']}"] = $field['name'];
+      }
+      elseif (strpos($fieldName, '.')) {
+        $fkField = $this->addFkField($fieldName, 'LEFT');
+        if ($fkField) {
+          $this->selectFields[implode('.', $fkField)] = $fieldName;
+        }
+      }
+      elseif ($field && strpos($fieldName, 'custom_') === 0) {
+        list($table_name, $column_name) = $this->addCustomField($field, 'LEFT');
+
+        if ($field['data_type'] != 'ContactReference') {
+          // 'ordinary' custom field. We will select the value as custom_XX.
+          $this->selectFields["$table_name.$column_name"] = $fieldName;
+        }
+        else {
+          // contact reference custom field. The ID will be stored in custom_XX_id.
+          // custom_XX will contain the sort name of the contact.
+          $this->query->join("c_$fieldName", "LEFT JOIN civicrm_contact c_$fieldName ON c_$fieldName.id = `$table_name`.`$column_name`");
+          $this->selectFields["$table_name.$column_name"] = $fieldName . "_id";
+          // We will call the contact table for the join c_XX.
+          $this->selectFields["c_$fieldName.sort_name"] = $fieldName;
+        }
+      }
+    }
+  }
+
+  /**
+   * Load entity fields
+   * @return array
+   */
+  abstract protected function getFields();
+
 }
index dfe9176e04e9a2a17a9e75b363d273751b7fb04b..d89e264d40f921aafdc7f459255a9b67ff0e77ba 100644 (file)
@@ -67,10 +67,12 @@ class ChainSubscriber implements EventSubscriberInterface {
    */
   public function onApiRespond(\Civi\API\Event\RespondEvent $event) {
     $apiRequest = $event->getApiRequest();
-    $result = $event->getResponse();
-    if (\CRM_Utils_Array::value('is_error', $result, 0) == 0) {
-      $this->callNestedApi($event->getApiKernel(), $apiRequest['params'], $result, $apiRequest['action'], $apiRequest['entity'], $apiRequest['version']);
-      $event->setResponse($result);
+    if ($apiRequest['version'] < 4) {
+      $result = $event->getResponse();
+      if (\CRM_Utils_Array::value('is_error', $result, 0) == 0) {
+        $this->callNestedApi($event->getApiKernel(), $apiRequest['params'], $result, $apiRequest['action'], $apiRequest['entity'], $apiRequest['version']);
+        $event->setResponse($result);
+      }
     }
   }
 
index b2c7900d4e5d376bbb0985cb64c88a5f98b70c8f..d1787876c89d4eff80814403d1eaa71b33bb7dd0 100644 (file)
@@ -90,6 +90,12 @@ class PermissionCheck implements EventSubscriberInterface {
       $event->authorize();
       $event->stopPropagation();
     }
+    elseif ($apiRequest['version'] == 4) {
+      if (!$apiRequest->getCheckPermissions()) {
+        $event->authorize();
+        $event->stopPropagation();
+      }
+    }
   }
 
   /**
index cad5ff23edb2842b7c913bbff7aac247229bbb7e..4acec9fe67b8235298b02fd753df94afab2bbf37 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace Civi\Core;
 
+use Civi\API\Provider\ActionObjectProvider;
 use Civi\Core\Event\SystemInstallEvent;
 use Civi\Core\Lock\LockManager;
 use Doctrine\Common\Annotations\AnnotationReader;
index 32b0c2297fe22bcec60c0d76c1658f75f570f894..3ab522232f7dd41c5985de2eedac1c5e058e048a 100644 (file)
@@ -40,7 +40,7 @@ function civicrm_api($entity, $action, $params, $extra = NULL) {
  */
 function civicrm_api3($entity, $action, $params = array()) {
   $params['version'] = 3;
-  $result = civicrm_api($entity, $action, $params);
+  $result = \Civi::service('civi_api_kernel')->run($entity, $action, $params);
   if (is_array($result) && !empty($result['is_error'])) {
     throw new CiviCRM_API3_Exception($result['error_message'], CRM_Utils_Array::value('error_code', $result, 'undefined'), $result);
   }
index 96f616b32e60236b1a41c4e78ce17eb6cfbb0b30..42f9f0383b369fa8af5b3e4390e9e3e32844d193 100644 (file)
@@ -382,7 +382,7 @@ function _civicrm_api3_get_DAO($name) {
  *   return the DAO name to manipulate this function
  *   eg. "civicrm_contact_create" or "Contact" will return "CRM_Contact_BAO_Contact"
  *
- * @return mixed
+ * @return string|null
  */
 function _civicrm_api3_get_BAO($name) {
   // FIXME: DAO should be renamed CRM_Badge_DAO_BadgeLayout
@@ -1336,9 +1336,25 @@ function _civicrm_api3_check_required_fields($params, $daoName, $return = FALSE)
  * @return array
  */
 function _civicrm_api3_basic_get($bao_name, $params, $returnAsSuccess = TRUE, $entity = "", $sql = NULL, $uniqueFields = FALSE) {
-  $query = new \Civi\API\SelectQuery($bao_name, $params, $uniqueFields);
+  $entity = CRM_Core_DAO_AllCoreTables::getBriefName(str_replace('_BAO_', '_DAO_', $bao_name));
+  $options = _civicrm_api3_get_options_from_params($params);
+
+  $query = new \Civi\API\Api3SelectQuery($entity);
+  $query->where = $params;
+  if ($options['is_count']) {
+    $query->select = array('count');
+  }
+  else {
+    $query->select = array_keys(array_filter($options['return']));
+    $query->orderBy = $options['sort'];
+    $query->isFillUniqueFields = $uniqueFields;
+  }
+  $query->limit = $options['limit'];
+  $query->offset = $options['offset'];
+  $query->checkPermissions = CRM_Utils_Array::value('check_permissions', $params, FALSE);
   $query->merge($sql);
   $result = $query->run();
+
   if ($returnAsSuccess) {
     return civicrm_api3_create_success($result, $params, $entity, 'get');
   }