From 8bcc0d869e1440985e21f7848f5a8af8ec750550 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sun, 24 Jan 2016 21:24:20 -0500 Subject: [PATCH] CRM-17867 - Api4 core patches Most of api4 is in an extension - these are the changes needed to core. --- CRM/Admin/Page/APIExplorer.php | 2 +- Civi/API/Api3SelectQuery.php | 163 ++++++++++++ Civi/API/Kernel.php | 82 ++++-- Civi/API/Request.php | 101 ++------ Civi/API/SelectQuery.php | 317 ++++++++---------------- Civi/API/Subscriber/ChainSubscriber.php | 10 +- Civi/API/Subscriber/PermissionCheck.php | 6 + Civi/Core/Container.php | 1 + api/api.php | 2 +- api/v3/utils.php | 20 +- 10 files changed, 388 insertions(+), 316 deletions(-) create mode 100644 Civi/API/Api3SelectQuery.php diff --git a/CRM/Admin/Page/APIExplorer.php b/CRM/Admin/Page/APIExplorer.php index 90358db15f..111634b958 100644 --- a/CRM/Admin/Page/APIExplorer.php +++ b/CRM/Admin/Page/APIExplorer.php @@ -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 index 0000000000..cfa39b9281 --- /dev/null +++ b/Civi/API/Api3SelectQuery.php @@ -0,0 +1,163 @@ +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; + } + +} diff --git a/Civi/API/Kernel.php b/Civi/API/Kernel.php index a22c3fcf63..521eb0a32c 100644 --- a/Civi/API/Kernel.php +++ b/Civi/API/Kernel.php @@ -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. * diff --git a/Civi/API/Request.php b/Civi/API/Request.php index 86451eb162..212e18f6bc 100644 --- a/Civi/API/Request.php +++ b/Civi/API/Request.php @@ -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"); } } diff --git a/Civi/API/SelectQuery.php b/Civi/API/SelectQuery.php index c8332fe6d5..3764992baa 100644 --- a/Civi/API/SelectQuery.php +++ b/Civi/API/SelectQuery.php @@ -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(); + } diff --git a/Civi/API/Subscriber/ChainSubscriber.php b/Civi/API/Subscriber/ChainSubscriber.php index dfe9176e04..d89e264d40 100644 --- a/Civi/API/Subscriber/ChainSubscriber.php +++ b/Civi/API/Subscriber/ChainSubscriber.php @@ -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); + } } } diff --git a/Civi/API/Subscriber/PermissionCheck.php b/Civi/API/Subscriber/PermissionCheck.php index b2c7900d4e..d1787876c8 100644 --- a/Civi/API/Subscriber/PermissionCheck.php +++ b/Civi/API/Subscriber/PermissionCheck.php @@ -90,6 +90,12 @@ class PermissionCheck implements EventSubscriberInterface { $event->authorize(); $event->stopPropagation(); } + elseif ($apiRequest['version'] == 4) { + if (!$apiRequest->getCheckPermissions()) { + $event->authorize(); + $event->stopPropagation(); + } + } } /** diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index cad5ff23ed..4acec9fe67 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -1,6 +1,7 @@ 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); } diff --git a/api/v3/utils.php b/api/v3/utils.php index 96f616b32e..42f9f0383b 100644 --- a/api/v3/utils.php +++ b/api/v3/utils.php @@ -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'); } -- 2.25.1