diff options
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic')
23 files changed, 2222 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php new file mode 100644 index 00000000..1b0786ea --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php @@ -0,0 +1,391 @@ +<?php +namespace Civi\Api4\Generic; + +use Civi\API\Exception\UnauthorizedException; +use Civi\API\Kernel; +use Civi\Api4\Generic\Result; +use Civi\Api4\Utils\ReflectionUtils; + +/** + * Base class for all api actions. + * + * @method $this setCheckPermissions(bool $value) + * @method bool getCheckPermissions() + * @method $this setChain(array $chain) + * @method array getChain() + */ +abstract class AbstractAction implements \ArrayAccess { + + /** + * Api version number; cannot be changed. + * + * @var int + */ + protected $version = 4; + + /** + * Additional api requests - will be called once per result. + * + * Keys can be any string - this will be the name given to the output. + * + * You can reference other values in the api results in this call by prefixing them with $ + * + * For example, you could create a contact and place them in a group by chaining the + * GroupContact api to the Contact api: + * + * Contact::create() + * ->setValue('first_name', 'Hello') + * ->addChain('add_to_a_group', GroupContact::create()->setValue('contact_id', '$id')->setValue('group_id', 123)) + * + * This will substitute the id of the newly created contact with $id. + * + * @var array + */ + protected $chain = []; + + /** + * Whether to enforce acl permissions based on the current user. + * + * Setting to FALSE will disable permission checks and override ACLs. + * In REST/javascript this cannot be disabled. + * + * @var bool + */ + protected $checkPermissions = TRUE; + + /* @var string */ + protected $_entityName; + + /* @var string */ + protected $_actionName; + + /* @var \ReflectionClass */ + private $thisReflection; + + /* @var array */ + private $thisParamInfo; + + /* @var array */ + private $entityFields; + + /* @var array */ + private $thisArrayStorage; + + /** + * Action constructor. + * + * @param string $entityName + * @param string $actionName + * @throws \API_Exception + */ + public function __construct($entityName, $actionName) { + // If a namespaced class name is passed in + if (strpos($entityName, '\\') !== FALSE) { + $entityName = substr($entityName, strrpos($entityName, '\\') + 1); + } + $this->_entityName = $entityName; + $this->_actionName = $actionName; + } + + /** + * Strictly enforce api parameters + * @param $name + * @param $value + * @throws \Exception + */ + public function __set($name, $value) { + throw new \API_Exception('Unknown api parameter'); + } + + /** + * @param int $val + * @return $this + * @throws \API_Exception + */ + public function setVersion($val) { + if ($val != 4) { + throw new \API_Exception('Cannot modify api version'); + } + return $this; + } + + /** + * @param string $name + * Unique name for this chained request + * @param \Civi\Api4\Generic\AbstractAction $apiRequest + * @param string|int $index + * Either a string for how the results should be indexed e.g. 'name' + * or the index of a single result to return e.g. 0 for the first result. + * @return $this + */ + public function addChain($name, AbstractAction $apiRequest, $index = NULL) { + $this->chain[$name] = [$apiRequest->getEntityName(), $apiRequest->getActionName(), $apiRequest->getParams(), $index]; + return $this; + } + + /** + * Magic function to provide addFoo, getFoo and setFoo for params. + * + * @param $name + * @param $arguments + * @return static|mixed + * @throws \API_Exception + */ + public function __call($name, $arguments) { + $param = lcfirst(substr($name, 3)); + if (!$param || $param[0] == '_') { + throw new \API_Exception('Unknown api parameter: ' . $name); + } + $mode = substr($name, 0, 3); + // Handle plural when adding to e.g. $values with "addValue" method. + if ($mode == 'add' && $this->paramExists($param . 's')) { + $param .= 's'; + } + if ($this->paramExists($param)) { + switch ($mode) { + case 'get': + return $this->$param; + + case 'set': + $this->$param = $arguments[0]; + return $this; + + case 'add': + if (!is_array($this->$param)) { + throw new \API_Exception('Cannot add to non-array param'); + } + if (array_key_exists(1, $arguments)) { + $this->{$param}[$arguments[0]] = $arguments[1]; + } + else { + $this->{$param}[] = $arguments[0]; + } + return $this; + } + } + throw new \API_Exception('Unknown api parameter: ' . $name); + } + + /** + * Invoke api call. + * + * At this point all the params have been sent in and we initiate the api call & return the result. + * This is basically the outer wrapper for api v4. + * + * @return Result|array + * @throws UnauthorizedException + */ + final public function execute() { + /** @var Kernel $kernel */ + $kernel = \Civi::service('civi_api_kernel'); + + return $kernel->runRequest($this); + } + + /** + * @param \Civi\Api4\Generic\Result $result + */ + abstract public function _run(Result $result); + + /** + * Serialize this object's params into an array + * @return array + */ + public function getParams() { + $params = []; + foreach ($this->getReflection()->getProperties(\ReflectionProperty::IS_PROTECTED) as $property) { + $name = $property->getName(); + // Skip variables starting with an underscore + if ($name[0] != '_') { + $params[$name] = $this->$name; + } + } + return $params; + } + + /** + * Get documentation for one or all params + * + * @param string $param + * @return array of arrays [description, type, default, (comment)] + */ + public function getParamInfo($param = NULL) { + if (!isset($this->thisParamInfo)) { + $defaults = $this->getParamDefaults(); + foreach ($this->getReflection()->getProperties(\ReflectionProperty::IS_PROTECTED) as $property) { + $name = $property->getName(); + if ($name != 'version' && $name[0] != '_') { + $this->thisParamInfo[$name] = ReflectionUtils::getCodeDocs($property, 'Property'); + $this->thisParamInfo[$name]['default'] = $defaults[$name]; + } + } + } + return $param ? $this->thisParamInfo[$param] : $this->thisParamInfo; + } + + /** + * @return string + */ + public function getEntityName() { + return $this->_entityName; + } + + /** + * + * @return string + */ + public function getActionName() { + return $this->_actionName; + } + + /** + * @param string $param + * @return bool + */ + protected function paramExists($param) { + return array_key_exists($param, $this->getParams()); + } + + /** + * @return array + */ + protected function getParamDefaults() { + return array_intersect_key($this->getReflection()->getDefaultProperties(), $this->getParams()); + } + + /** + * @inheritDoc + */ + public function offsetExists($offset) { + return in_array($offset, ['entity', 'action', 'params', 'version', 'check_permissions']) || isset($this->thisArrayStorage[$offset]); + } + + /** + * @inheritDoc + */ + public function &offsetGet($offset) { + $val = NULL; + if (in_array($offset, ['entity', 'action'])) { + $offset .= 'Name'; + } + if (in_array($offset, ['entityName', 'actionName', 'params', 'version'])) { + $getter = 'get' . ucfirst($offset); + $val = $this->$getter(); + return $val; + } + if ($offset == 'check_permissions') { + return $this->checkPermissions; + } + if (isset ($this->thisArrayStorage[$offset])) { + return $this->thisArrayStorage[$offset]; + } + return $val; + } + + /** + * @inheritDoc + */ + public function offsetSet($offset, $value) { + if (in_array($offset, ['entity', 'action', 'entityName', 'actionName', 'params', 'version'])) { + throw new \API_Exception('Cannot modify api4 state via array access'); + } + if ($offset == 'check_permissions') { + $this->setCheckPermissions($value); + } + else { + $this->thisArrayStorage[$offset] = $value; + } + } + + /** + * @inheritDoc + */ + public function offsetUnset($offset) { + if (in_array($offset, ['entity', 'action', 'entityName', 'actionName', 'params', 'check_permissions', 'version'])) { + throw new \API_Exception('Cannot modify api4 state via array access'); + } + unset($this->thisArrayStorage[$offset]); + } + + /** + * Is this api call permitted? + * + * This function is called if checkPermissions is set to true. + * + * @return bool + */ + public function isAuthorized() { + $permissions = $this->getPermissions(); + return \CRM_Core_Permission::check($permissions); + } + + public function getPermissions() { + $permissions = call_user_func(["\\Civi\\Api4\\" . $this->_entityName, 'permissions']); + $permissions += [ + // applies to getFields, getActions, etc. + 'meta' => ['access CiviCRM'], + // catch-all, applies to create, get, delete, etc. + 'default' => ['administer CiviCRM'], + ]; + $action = $this->getActionName(); + if (isset($permissions[$action])) { + return $permissions[$action]; + } + elseif (in_array($action, ['getActions', 'getFields'])) { + return $permissions['meta']; + } + return $permissions['default']; + } + + /** + * Returns schema fields for this entity & action. + * + * @return array + * @throws \API_Exception + */ + protected function getEntityFields() { + if (!$this->entityFields) { + $params = [ + 'action' => $this->getActionName(), + 'checkPermissions' => $this->checkPermissions, + ]; + if (method_exists($this, 'getBaoName')) { + $params['includeCustom'] = FALSE; + } + $this->entityFields = (array) civicrm_api4($this->getEntityName(), 'getFields', $params, 'name'); + } + return $this->entityFields; + } + + /** + * @return \ReflectionClass + */ + protected function getReflection() { + if (!$this->thisReflection) { + $this->thisReflection = new \ReflectionClass($this); + } + return $this->thisReflection; + } + + /** + * This function is used internally for evaluating field annotations. + * + * It should never be passed raw user input. + * + * @param string $expr + * Conditional in php format e.g. $foo > $bar + * @param array $vars + * Variable name => value + * @return bool + * @throws \API_Exception + * @throws \Exception + */ + protected function evaluateCondition($expr, $vars) { + if (strpos($expr, '}') !== FALSE || strpos($expr, '{') !== FALSE) { + throw new \API_Exception('Illegal character in expression'); + } + $tpl = "{if $expr}1{else}0{/if}"; + return (bool) trim(\CRM_Core_Smarty::singleton()->fetchWith('string:' . $tpl, $vars)); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractBatchAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractBatchAction.php new file mode 100644 index 00000000..91c3b461 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractBatchAction.php @@ -0,0 +1,64 @@ +<?php + +namespace Civi\Api4\Generic; + +/** + * Base class for all batch actions (Update, Delete, Replace). + * + * This differs from the AbstractQuery class in that the "Where" clause is required. + * + * @package Civi\Api4\Generic + */ +abstract class AbstractBatchAction extends AbstractQueryAction { + + /** + * Criteria for selecting items to process. + * + * @required + * @var array + */ + protected $where = []; + + /** + * @var array + */ + private $select; + + /** + * QueryAction constructor. + * @param string $entityName + * @param string $actionName + * @param string|array $select + * One or more fields to load for each item. + */ + public function __construct($entityName, $actionName, $select = 'id') { + $this->select = (array) $select; + parent::__construct($entityName, $actionName); + } + + /** + * @return array + */ + protected function getBatchRecords() { + $params = [ + 'checkPermissions' => $this->checkPermissions, + 'where' => $this->where, + 'orderBy' => $this->orderBy, + 'limit' => $this->limit, + 'offset' => $this->offset, + ]; + if (empty($this->reload)) { + $params['select'] = $this->select; + } + + return (array) civicrm_api4($this->getEntityName(), 'get', $params); + } + + /** + * @return array + */ + protected function getSelect() { + return $this->select; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractCreateAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractCreateAction.php new file mode 100644 index 00000000..0cb55d10 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractCreateAction.php @@ -0,0 +1,56 @@ +<?php + +namespace Civi\Api4\Generic; + +/** + * Base class for all "Create" api actions. + * + * @method $this setValues(array $values) Set all field values from an array of key => value pairs. + * @method $this addValue($field, $value) Set field value. + * @method array getValues() Get field values. + * + * @package Civi\Api4\Generic + */ +abstract class AbstractCreateAction extends AbstractAction { + + /** + * Field values to set + * + * @var array + */ + protected $values = []; + + /** + * @param string $key + * + * @return mixed|null + */ + public function getValue($key) { + return isset($this->values[$key]) ? $this->values[$key] : NULL; + } + + /** + * @throws \API_Exception + */ + protected function validateValues() { + $unmatched = []; + $params = NULL; + foreach ($this->getEntityFields() as $fieldName => $fieldInfo) { + if (!$this->getValue($fieldName)) { + if (!empty($fieldInfo['required']) && !isset($fieldInfo['default_value'])) { + $unmatched[] = $fieldName; + } + elseif (!empty($fieldInfo['required_if'])) { + $params = $params ?: $this->getParams(); + if ($this->evaluateCondition($fieldInfo['required_if'], $params)) { + $unmatched[] = $fieldName; + } + } + } + } + if ($unmatched) { + throw new \API_Exception("Mandatory values missing from Api4 {$this->getEntityName()}::{$this->getActionName()}: '" . implode("', '", $unmatched) . "'", "mandatory_missing", ["fields" => $unmatched]); + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php new file mode 100644 index 00000000..e774baca --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php @@ -0,0 +1,88 @@ +<?php +namespace Civi\Api4\Generic; + +use Civi\API\Exception\NotImplementedException; + +/** + * Base class for all api entities. + * + * When adding your own api from an extension, extend this class only + * if your entity does not have an associated DAO. Otherwise extend DAOEntity. + * + * The recommended way to create a non-DAO-based api is to extend this class + * and then add a getFields function and any other actions you wish, e.g. + * - a get() function which returns BasicGetAction using your custom getter callback + * - a create() function which returns BasicCreateAction using your custom setter callback + * - an update() function which returns BasicUpdateAction using your custom setter callback + * - a delete() function which returns BasicBatchAction using your custom delete callback + * - a replace() function which returns BasicReplaceAction (no callback needed but + * depends on the existence of get, create, update & delete actions) + * + * Note that you can use the same setter callback function for update as create - + * that function can distinguish between new & existing records by checking if the + * unique identifier has been set (identifier field defaults to "id" but you can change + * that when constructing BasicUpdateAction) + */ +abstract class AbstractEntity { + + /** + * @return \Civi\Api4\Action\GetActions + */ + public static function getActions() { + return new \Civi\Api4\Action\GetActions(self::getEntityName(), __FUNCTION__); + } + + /** + * Should return \Civi\Api4\Generic\BasicGetFieldsAction + * @todo make this function abstract when we require php 7. + * @throws \Civi\API\Exception\NotImplementedException + */ + public static function getFields() { + throw new NotImplementedException(self::getEntityName() . ' should implement getFields action.'); + } + + /** + * Returns a list of permissions needed to access the various actions in this api. + * + * @return array + */ + public static function permissions() { + $permissions = \CRM_Core_Permission::getEntityActionPermissions(); + + // For legacy reasons the permissions are keyed by lowercase entity name + $lcentity = _civicrm_api_get_entity_name_from_camel(self::getEntityName()); + // Merge permissions for this entity with the defaults + return \CRM_Utils_Array::value($lcentity, $permissions, []) + $permissions['default']; + } + + /** + * Get entity name from called class + * + * @return string + */ + protected static function getEntityName() { + return substr(static::class, strrpos(static::class, '\\') + 1); + } + + /** + * Magic method to return the action object for an api. + * + * @param string $action + * @param null $args + * @return AbstractAction + * @throws NotImplementedException + */ + public static function __callStatic($action, $args) { + $entity = self::getEntityName(); + // Find class for this action + $entityAction = "\\Civi\\Api4\\Action\\$entity\\" . ucfirst($action); + if (class_exists($entityAction)) { + $actionObject = new $entityAction($entity, $action); + } + else { + throw new NotImplementedException("Api $entity $action version 4 does not exist."); + } + return $actionObject; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractGetAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractGetAction.php new file mode 100644 index 00000000..f8374cf4 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractGetAction.php @@ -0,0 +1,23 @@ +<?php + +namespace Civi\Api4\Generic; + +/** + * Base class for all "Get" api actions. + * + * @package Civi\Api4\Generic + * + * @method $this addSelect(string $select) + * @method $this setSelect(array $selects) + * @method array getSelect() + */ +abstract class AbstractGetAction extends AbstractQueryAction { + + /** + * Fields to return. Defaults to all non-custom fields. + * + * @var array + */ + protected $select = []; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractQueryAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractQueryAction.php new file mode 100644 index 00000000..993383dc --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractQueryAction.php @@ -0,0 +1,105 @@ +<?php + +namespace Civi\Api4\Generic; + +/** + * Base class for all actions that need to fetch records (Get, Update, Delete, etc) + * + * @package Civi\Api4\Generic + * + * @method $this setWhere(array $wheres) + * @method array getWhere() + * @method $this setOrderBy(array $order) + * @method array getOrderBy() + * @method $this setLimit(int $limit) + * @method int getLimit() + * @method $this setOffset(int $offset) + * @method int getOffset() + */ +abstract class AbstractQueryAction extends AbstractAction { + + /** + * Criteria for selecting items. + * + * $example->addWhere('contact_type', 'IN', array('Individual', 'Household')) + * + * @var array + */ + protected $where = []; + + /** + * Array of field(s) to use in ordering the results + * + * Defaults to id ASC + * + * $example->addOrderBy('sort_name', 'ASC') + * + * @var array + */ + protected $orderBy = []; + + /** + * Maximum number of results to return. + * + * Defaults to unlimited. + * + * Note: the Api Explorer sets this to 25 by default to avoid timeouts. + * Change or remove this default for your application code. + * + * @var int + */ + protected $limit = 0; + + /** + * Zero-based index of first result to return. + * + * Defaults to "0" - first record. + * + * @var int + */ + protected $offset = 0; + + /** + * @param string $field + * @param string $op + * @param mixed $value + * @return $this + * @throws \API_Exception + */ + public function addWhere($field, $op, $value = NULL) { + if (!in_array($op, \CRM_Core_DAO::acceptedSQLOperators())) { + throw new \API_Exception('Unsupported operator'); + } + $this->where[] = [$field, $op, $value]; + return $this; + } + + /** + * Adds one or more AND/OR/NOT clause groups + * + * @param string $operator + * @param mixed $condition1 ... $conditionN + * Either a nested array of arguments, or a variable number of arguments passed to this function. + * + * @return $this + * @throws \API_Exception + */ + public function addClause($operator, $condition1) { + if (!is_array($condition1[0])) { + $condition1 = array_slice(func_get_args(), 1); + } + $this->where[] = [$operator, $condition1]; + return $this; + } + + /** + * @param string $field + * @param string $direction + * @return $this + */ + public function addOrderBy($field, $direction = 'ASC') { + $this->orderBy[$field] = $direction; + return $this; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractUpdateAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractUpdateAction.php new file mode 100644 index 00000000..a1904970 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractUpdateAction.php @@ -0,0 +1,45 @@ +<?php + +namespace Civi\Api4\Generic; + +/** + * Base class for all "Update" api actions + * + * @method $this setValues(array $values) Set all field values from an array of key => value pairs. + * @method $this addValue($field, $value) Set field value. + * @method array getValues() Get field values. + * @method $this setReload(bool $reload) Specify whether complete objects will be returned after saving. + * @method bool getReload() + * + * @package Civi\Api4\Generic + */ +abstract class AbstractUpdateAction extends AbstractBatchAction { + + /** + * Field values to update. + * + * @required + * @var array + */ + protected $values = []; + + /** + * Reload objects after saving. + * + * Setting to TRUE will load complete records and return them as the api result. + * If FALSE the api usually returns only the fields specified to be updated. + * + * @var bool + */ + protected $reload = FALSE; + + /** + * @param string $key + * + * @return mixed|null + */ + public function getValue($key) { + return isset($this->values[$key]) ? $this->values[$key] : NULL; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicBatchAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicBatchAction.php new file mode 100644 index 00000000..2f39cf23 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicBatchAction.php @@ -0,0 +1,72 @@ +<?php + +namespace Civi\Api4\Generic; +use Civi\API\Exception\NotImplementedException; + +/** + * Basic action for deleting or performing some other task with a set of records. Ex: + * + * $myAction = new BasicBatchAction('Entity', 'action', function($item) { + * // Do something with $item + * $return $item; + * }); + * + * @package Civi\Api4\Generic + */ +class BasicBatchAction extends AbstractBatchAction { + + /** + * @var callable + * + * Function(array $item, BasicBatchAction $thisAction) => array + */ + private $doer; + + /** + * BasicBatchAction constructor. + * + * @param string $entityName + * @param string $actionName + * @param string|array $select + * One or more fields to select from each matching item. + * @param callable $doer + * Function(array $item, BasicBatchAction $thisAction) => array + */ + public function __construct($entityName, $actionName, $select = 'id', $doer = NULL) { + parent::__construct($entityName, $actionName, $select); + $this->doer = $doer; + } + + /** + * We pass the doTask function an array representing one item to update. + * We expect to get the same format back. + * + * @param \Civi\Api4\Generic\Result $result + */ + public function _run(Result $result) { + foreach ($this->getBatchRecords() as $item) { + $result[] = $this->doTask($item); + } + } + + /** + * This Basic Batch class can be used in one of two ways: + * + * 1. Use this class directly by passing a callable ($doer) to the constructor. + * 2. Extend this class and override this function. + * + * Either way, this function should return an array with an output record + * for the item. + * + * @param array $item + * @return array + * @throws \Civi\API\Exception\NotImplementedException + */ + protected function doTask($item) { + if (is_callable($this->doer)) { + return call_user_func($this->doer, $item, $this); + } + throw new NotImplementedException('Doer function not found for api4 ' . $this->getEntityName() . '::' . $this->getActionName()); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicCreateAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicCreateAction.php new file mode 100644 index 00000000..ddd238f4 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicCreateAction.php @@ -0,0 +1,64 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\API\Exception\NotImplementedException; + +/** + * Create a new object from supplied values. + * + * This function will create 1 new object. It cannot be used to update existing objects. Use the Update or Replace actions for that. + */ +class BasicCreateAction extends AbstractCreateAction { + + /** + * @var callable + * + * Function(array $item, BasicCreateAction $thisAction) => array + */ + private $setter; + + /** + * Basic Create constructor. + * + * @param string $entityName + * @param string $actionName + * @param callable $setter + * Function(array $item, BasicCreateAction $thisAction) => array + */ + public function __construct($entityName, $actionName, $setter = NULL) { + parent::__construct($entityName, $actionName); + $this->setter = $setter; + } + + /** + * We pass the writeRecord function an array representing one item to write. + * We expect to get the same format back. + * + * @param \Civi\Api4\Generic\Result $result + */ + public function _run(Result $result) { + $this->validateValues(); + $result->exchangeArray([$this->writeRecord($this->values)]); + } + + /** + * This Basic Create class can be used in one of two ways: + * + * 1. Use this class directly by passing a callable ($setter) to the constructor. + * 2. Extend this class and override this function. + * + * Either way, this function should return an array representing the one new object. + * + * @param array $item + * @return array + * @throws \Civi\API\Exception\NotImplementedException + */ + protected function writeRecord($item) { + if (is_callable($this->setter)) { + return call_user_func($this->setter, $item, $this); + } + throw new NotImplementedException('Setter function not found for api4 ' . $this->getEntityName() . '::' . $this->getActionName()); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetAction.php new file mode 100644 index 00000000..23d47a13 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetAction.php @@ -0,0 +1,144 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\API\Exception\NotImplementedException; + +/** + * Retrieve items based on criteria specified in the 'where' param. + * + * Use the 'select' param to determine which fields are returned, defaults to *. + */ +class BasicGetAction extends AbstractGetAction { + use Traits\ArrayQueryActionTrait; + + /** + * @var callable + * + * Function(BasicGetAction $thisAction) => array<array> + */ + private $getter; + + /** + * Basic Get constructor. + * + * @param string $entityName + * @param string $actionName + * @param callable $getter + */ + public function __construct($entityName, $actionName, $getter = NULL) { + parent::__construct($entityName, $actionName); + $this->getter = $getter; + } + + /** + * Fetch results from the getter then apply filter/sort/select/limit. + * + * @param \Civi\Api4\Generic\Result $result + */ + public function _run(Result $result) { + $values = $this->getRecords(); + $result->exchangeArray($this->queryArray($values)); + } + + /** + * This Basic Get class is a general-purpose api for non-DAO-based entities. + * + * Useful for fetching records from files or other places. + * You can specify any php function to retrieve the records, and this class will + * automatically filter, sort, select & limit the raw data from your callback. + * + * You can implement this action in one of two ways: + * 1. Use this class directly by passing a callable ($getter) to the constructor. + * 2. Extend this class and override this function. + * + * Either way, this function should return an array of arrays, each representing one retrieved object. + * + * The simplest thing for your getter function to do is return every full record + * and allow this class to automatically do the sorting and filtering. + * + * Sometimes however that may not be practical for performance reasons. + * To optimize your getter, it can use the following helpers from $this: + * + * Use this->_itemsToGet() to match records to field values in the WHERE clause. + * Note the WHERE clause can potentially be very complex and it is not recommended + * to parse $this->where yourself. + * + * Use $this->_isFieldSelected() to check if a field value is called for - useful + * if loading the field involves expensive calculations. + * + * Be careful not to make assumptions, e.g. if LIMIT 100 is specified and your getter "helpfully" truncates the list + * at 100 without accounting for WHERE, ORDER BY and LIMIT clauses, the final filtered result may be very incorrect. + * + * @return array + * @throws \Civi\API\Exception\NotImplementedException + */ + protected function getRecords() { + if (is_callable($this->getter)) { + return call_user_func($this->getter, $this); + } + throw new NotImplementedException('Getter function not found for api4 ' . $this->getEntityName() . '::' . $this->getActionName()); + } + + /** + * Helper to parse the WHERE param for getRecords to perform simple pre-filtering. + * + * This is intended to optimize some common use-cases e.g. calling the api to get + * one or more records by name or id. + * + * Ex: If getRecords fetches a long list of items each with a unique name, + * but the user has specified a single record to retrieve, you can optimize the call + * by checking $this->_itemsToGet('name') and only fetching the item(s) with that name. + * + * @param string $field + * @return array|null + */ + public function _itemsToGet($field) { + foreach ($this->where as $clause) { + if ($clause[0] == $field && in_array($clause[1], ['=', 'IN'])) { + return (array) $clause[2]; + } + } + return NULL; + } + + /** + * Helper to see if a field should be selected by the getRecords function. + * + * Checks the SELECT, WHERE and ORDER BY params to see what fields are needed. + * + * Note that if no SELECT clause has been set then all fields should be selected + * and this function will always return TRUE. + * + * @param string $field + * @return bool + */ + public function _isFieldSelected($field) { + if (!$this->select || in_array($field, $this->select) || isset($this->orderBy[$field])) { + return TRUE; + } + return $this->_whereContains($field, $this->where); + } + + /** + * Walk through the where clause and check if a field is in use. + * + * @param string $field + * @param array $clauses + * @return bool + */ + private function _whereContains($field, $clauses) { + foreach ($clauses as $clause) { + if (is_array($clause) && is_string($clause[0])) { + if ($clause[0] == $field) { + return TRUE; + } + elseif (is_array($clause[1])) { + return $this->_whereContains($field, $clause[1]); + } + } + } + return FALSE; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetFieldsAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetFieldsAction.php new file mode 100644 index 00000000..c9869d5e --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetFieldsAction.php @@ -0,0 +1,120 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\Api4\Utils\ActionUtil; + +/** + * Get fields for an entity. + * + * @method $this setLoadOptions(bool $value) + * @method bool getLoadOptions() + * @method $this setAction(string $value) + */ +class BasicGetFieldsAction extends BasicGetAction { + + /** + * Fetch option lists for fields? + * + * @var bool + */ + protected $loadOptions = FALSE; + + /** + * @var string + */ + protected $action = 'get'; + + /** + * To implement getFields for your own entity: + * + * 1. From your entity class add a static getFields method. + * 2. That method should construct and return this class. + * 3. The 3rd argument passed to this constructor should be a function that returns an + * array of fields for your entity's CRUD actions. + * 4. For non-crud actions that need a different set of fields, you can override the + * list from step 3 on a per-action basis by defining a fields() method in that action. + * See for example BasicGetFieldsAction::fields() or GetActions::fields(). + * + * @param Result $result + * @throws \Civi\API\Exception\NotImplementedException + */ + public function _run(Result $result) { + $actionClass = ActionUtil::getAction($this->getEntityName(), $this->action); + if (method_exists($actionClass, 'fields')) { + $values = $actionClass->fields(); + } + else { + $values = $this->getRecords(); + } + $this->padResults($values); + $result->exchangeArray($this->queryArray($values)); + } + + /** + * @param array $values + */ + private function padResults(&$values) { + foreach ($values as &$field) { + $field += [ + 'title' => ucwords(str_replace('_', ' ', $field['name'])), + 'entity' => $this->getEntityName(), + 'required' => FALSE, + 'options' => FALSE, + 'data_type' => 'String', + ]; + if (!$this->loadOptions) { + $field['options'] = (bool) $field['options']; + } + } + } + + /** + * @return string + */ + public function getAction() { + return $this->action; + } + + public function fields() { + return [ + [ + 'name' => 'name', + 'data_type' => 'String', + ], + [ + 'name' => 'title', + 'data_type' => 'String', + ], + [ + 'name' => 'description', + 'data_type' => 'String', + ], + [ + 'name' => 'default_value', + 'data_type' => 'String', + ], + [ + 'name' => 'required', + 'data_type' => 'Boolean', + ], + [ + 'name' => 'options', + 'data_type' => 'Array', + ], + [ + 'name' => 'data_type', + 'data_type' => 'String', + ], + [ + 'name' => 'fk_entity', + 'data_type' => 'String', + ], + [ + 'name' => 'serialize', + 'data_type' => 'Integer', + ], + ]; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicReplaceAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicReplaceAction.php new file mode 100644 index 00000000..8e0dd22e --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicReplaceAction.php @@ -0,0 +1,81 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\Api4\Generic\Result; + +/** + * Given a set of records, will appropriately update the database. + * + * @method $this setRecords(array $records) Array of records. + * @method $this addRecord($record) Add a record to update. + * @method array getRecords() + * @method $this setReload(bool $reload) Specify whether complete objects will be returned after saving. + * @method bool getReload() + */ +class BasicReplaceAction extends AbstractBatchAction { + + /** + * Array of records. + * + * Should be in the same format as returned by Get. + * + * @required + * @var array + */ + protected $records = []; + + /** + * Reload objects after saving. + * + * Setting to TRUE will load complete records and return them as the api result. + * If FALSE the api usually returns only the fields specified to be updated. + * + * @var bool + */ + protected $reload = FALSE; + + /** + * @inheritDoc + */ + public function _run(Result $result) { + $items = $this->getBatchRecords(); + + // Copy params from where clause if the operator is = + $paramsFromWhere = []; + foreach ($this->where as $clause) { + if (is_array($clause) && $clause[1] === '=') { + $paramsFromWhere[$clause[0]] = $clause[2]; + } + } + + $idField = $this->getSelect()[0]; + $toDelete = array_column($items, NULL, $idField); + + foreach ($this->records as $record) { + $record += $paramsFromWhere; + if (!empty($record[$idField])) { + $id = $record[$idField]; + unset($toDelete[$id], $record[$idField]); + $result[] = civicrm_api4($this->getEntityName(), 'update', [ + 'reload' => $this->reload, + 'where' => [[$idField, '=', $id]], + 'values' => $record, + ])->first(); + } + else { + $result[] = civicrm_api4($this->getEntityName(), 'create', [ + 'values' => $record, + ])->first(); + } + } + + $result->deleted = []; + if ($toDelete) { + $result->deleted = (array) civicrm_api4($this->getEntityName(), 'delete', [ + 'where' => [[$idField, 'IN', array_keys($toDelete)]], + ]); + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicUpdateAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicUpdateAction.php new file mode 100644 index 00000000..40c93624 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicUpdateAction.php @@ -0,0 +1,67 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\API\Exception\NotImplementedException; + +/** + * Update one or more records with new values. + * + * Use the where clause (required) to select them. + */ +class BasicUpdateAction extends AbstractUpdateAction { + + /** + * @var callable + * + * Function(array $item, BasicUpdateAction $thisAction) => array + */ + private $setter; + + /** + * BasicUpdateAction constructor. + * + * @param string $entityName + * @param string $actionName + * @param string|array $select + * One or more fields to select from each matching item. + * @param callable $setter + * Function(array $item, BasicUpdateAction $thisAction) => array + */ + public function __construct($entityName, $actionName, $select = 'id', $setter = NULL) { + parent::__construct($entityName, $actionName, $select); + $this->setter = $setter; + } + + /** + * We pass the writeRecord function an array representing one item to update. + * We expect to get the same format back. + * + * @param \Civi\Api4\Generic\Result $result + */ + public function _run(Result $result) { + foreach ($this->getBatchRecords() as $item) { + $result[] = $this->writeRecord($this->values + $item); + } + } + + /** + * This Basic Update class can be used in one of two ways: + * + * 1. Use this class directly by passing a callable ($setter) to the constructor. + * 2. Extend this class and override this function. + * + * Either way, this function should return an array representing the one modified object. + * + * @param array $item + * @return array + * @throws \Civi\API\Exception\NotImplementedException + */ + protected function writeRecord($item) { + if (is_callable($this->setter)) { + return call_user_func($this->setter, $item, $this); + } + throw new NotImplementedException('Setter function not found for api4 ' . $this->getEntityName() . '::' . $this->getActionName()); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOCreateAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOCreateAction.php new file mode 100644 index 00000000..d7a0e869 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOCreateAction.php @@ -0,0 +1,58 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\Api4\Generic\Result; + +/** + * Create a new object from supplied values. + * + * This function will create 1 new object. It cannot be used to update existing objects. Use the Update or Replace actions for that. + */ +class DAOCreateAction extends AbstractCreateAction { + use Traits\DAOActionTrait; + + /** + * @inheritDoc + */ + public function _run(Result $result) { + $this->validateValues(); + $params = $this->values; + $this->fillDefaults($params); + + $resultArray = $this->writeObjects([$params]); + + $result->exchangeArray($resultArray); + } + + /** + * @throws \API_Exception + */ + protected function validateValues() { + if (!empty($this->values['id'])) { + throw new \API_Exception('Cannot pass id to Create action. Use Update action instead.'); + } + parent::validateValues(); + } + + /** + * Fill field defaults which were declared by the api. + * + * Note: default values from core are ignored because the BAO or database layer will supply them. + * + * @param array $params + */ + protected function fillDefaults(&$params) { + $fields = $this->getEntityFields(); + $bao = $this->getBaoName(); + $coreFields = array_column($bao::fields(), NULL, 'name'); + + foreach ($fields as $name => $field) { + // If a default value is set in the api but not in core, the api should supply it. + if (!isset($params[$name]) && !empty($field['default_value']) && empty($coreFields[$name]['default'])) { + $params[$name] = $field['default_value']; + } + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAODeleteAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAODeleteAction.php new file mode 100644 index 00000000..f61af3f1 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAODeleteAction.php @@ -0,0 +1,73 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\Api4\Generic\Result; + +/** + * Delete one or more items, based on criteria specified in Where param (required). + */ +class DAODeleteAction extends AbstractBatchAction { + use Traits\DAOActionTrait; + + /** + * Batch delete function + */ + public function _run(Result $result) { + $defaults = $this->getParamDefaults(); + if ($defaults['where'] && !array_diff_key($this->where, $defaults['where'])) { + throw new \API_Exception('Cannot delete with no "where" parameter specified'); + } + + $items = $this->getObjects(); + + $ids = $this->deleteObjects($items); + + $result->exchangeArray($ids); + } + + /** + * @param $items + * @return array + * @throws \API_Exception + */ + protected function deleteObjects($items) { + $ids = []; + $baoName = $this->getBaoName(); + + if ($this->getCheckPermissions()) { + foreach ($items as $item) { + $this->checkContactPermissions($baoName, $item); + } + } + + if ($this->getEntityName() !== 'EntityTag' && method_exists($baoName, 'del')) { + foreach ($items as $item) { + $args = [$item['id']]; + $bao = call_user_func_array([$baoName, 'del'], $args); + if ($bao !== FALSE) { + $ids[] = $item['id']; + } + else { + throw new \API_Exception("Could not delete {$this->getEntityName()} id {$item['id']}"); + } + } + } + else { + foreach ($items as $item) { + $bao = new $baoName(); + $bao->id = $item['id']; + // delete it + $action_result = $bao->delete(); + if ($action_result) { + $ids[] = $item['id']; + } + else { + throw new \API_Exception("Could not delete {$this->getEntityName()} id {$item['id']}"); + } + } + } + return $ids; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOEntity.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOEntity.php new file mode 100644 index 00000000..1ad175da --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOEntity.php @@ -0,0 +1,52 @@ +<?php + +namespace Civi\Api4\Generic; + +/** + * Base class for DAO-based entities. + */ +abstract class DAOEntity extends AbstractEntity { + + /** + * @return DAOGetAction + */ + public static function get() { + return new DAOGetAction(static::class, __FUNCTION__); + } + + /** + * @return DAOGetFieldsAction + */ + public static function getFields() { + return new DAOGetFieldsAction(static::class, __FUNCTION__); + } + + /** + * @return DAOCreateAction + */ + public static function create() { + return new DAOCreateAction(static::class, __FUNCTION__); + } + + /** + * @return DAOUpdateAction + */ + public static function update() { + return new DAOUpdateAction(static::class, __FUNCTION__); + } + + /** + * @return DAODeleteAction + */ + public static function delete() { + return new DAODeleteAction(static::class, __FUNCTION__); + } + + /** + * @return BasicReplaceAction + */ + public static function replace() { + return new BasicReplaceAction(static::class, __FUNCTION__); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetAction.php new file mode 100644 index 00000000..0216f0da --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetAction.php @@ -0,0 +1,21 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\Api4\Generic\Result; + +/** + * Retrieve items based on criteria specified in the 'where' param. + * + * Use the 'select' param to determine which fields are returned, defaults to *. + * + * Perform joins on other related entities using a dot notation. + */ +class DAOGetAction extends AbstractGetAction { + use Traits\DAOActionTrait; + + public function _run(Result $result) { + $result->exchangeArray($this->getObjects()); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetFieldsAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetFieldsAction.php new file mode 100644 index 00000000..e86d99bc --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetFieldsAction.php @@ -0,0 +1,53 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\Api4\Service\Spec\SpecGatherer; +use Civi\Api4\Service\Spec\SpecFormatter; + +/** + * Get fields for a DAO-based entity. + * + * @method $this setIncludeCustom(bool $value) + * @method bool getIncludeCustom() + */ +class DAOGetFieldsAction extends BasicGetFieldsAction { + + /** + * Include custom fields for this entity, or only core fields? + * + * @var bool + */ + protected $includeCustom = TRUE; + + /** + * Get fields for a DAO-based entity + * + * @return array + */ + protected function getRecords() { + $fields = $this->_itemsToGet('name'); + /** @var SpecGatherer $gatherer */ + $gatherer = \Civi::container()->get('spec_gatherer'); + // Any fields name with a dot in it is custom + if ($fields) { + $this->includeCustom = strpos(implode('', $fields), '.') !== FALSE; + } + $spec = $gatherer->getSpec($this->getEntityName(), $this->action, $this->includeCustom); + return SpecFormatter::specToArray($spec->getFields($fields), (array) $this->select, $this->loadOptions); + } + + public function fields() { + $fields = parent::fields(); + $fields[] = [ + 'name' => 'custom_field_id', + 'data_type' => 'Integer', + ]; + $fields[] = [ + 'name' => 'custom_group_id', + 'data_type' => 'Integer', + ]; + return $fields; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOUpdateAction.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOUpdateAction.php new file mode 100644 index 00000000..62da8796 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOUpdateAction.php @@ -0,0 +1,31 @@ +<?php + +namespace Civi\Api4\Generic; + +use Civi\Api4\Generic\Result; + +/** + * Update one or more records with new values. + * + * Use the where clause (required) to select them. + */ +class DAOUpdateAction extends AbstractUpdateAction { + use Traits\DAOActionTrait; + + /** + * @inheritDoc + */ + public function _run(Result $result) { + if (!empty($this->values['id'])) { + throw new \Exception("Cannot update the id of an existing " . $this->getEntityName() . '.'); + } + + $items = $this->getObjects(); + foreach ($items as &$item) { + $item = $this->values + $item; + } + + $result->exchangeArray($this->writeObjects($items)); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Result.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Result.php new file mode 100644 index 00000000..35fb6fb0 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Result.php @@ -0,0 +1,105 @@ +<?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\Api4\Generic; + +/** + * Container for api results. + */ +class Result extends \ArrayObject { + /** + * @var string + */ + public $entity; + /** + * @var string + */ + public $action; + /** + * Api version + * @var int + */ + public $version = 4; + + /** + * Return first result. + * @return array|null + */ + public function first() { + foreach ($this as $values) { + return $values; + } + return NULL; + } + + /** + * Return last result. + * @return array|null + */ + public function last() { + $items = $this->getArrayCopy(); + return array_pop($items); + } + + /** + * @param int $index + * @return array|null + */ + public function itemAt($index) { + $length = $index < 0 ? 0 - $index : $index + 1; + if ($length > count($this)) { + return NULL; + } + return array_slice(array_values($this->getArrayCopy()), $index, 1)[0]; + } + + /** + * Re-index the results array (which by default is non-associative) + * + * Drops any item from the results that does not contain the specified key + * + * @param string $key + * @return $this + * @throws \API_Exception + */ + public function indexBy($key) { + if (count($this)) { + $newResults = []; + foreach ($this as $values) { + if (isset($values[$key])) { + $newResults[$values[$key]] = $values; + } + } + if (!$newResults) { + throw new \API_Exception("Key $key not found in api results"); + } + $this->exchangeArray($newResults); + } + return $this; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php new file mode 100644 index 00000000..1d223f1b --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php @@ -0,0 +1,197 @@ +<?php + +namespace Civi\Api4\Generic\Traits; +use Civi\API\Exception\NotImplementedException; + +/** + * Helper functions for performing api queries on arrays of data. + * + * @package Civi\Api4\Generic + */ +trait ArrayQueryActionTrait { + + /** + * @param array $values + * List of all rows + * @return array + * Filtered list of rows + */ + protected function queryArray($values) { + $values = $this->filterArray($values); + $values = $this->sortArray($values); + $values = $this->selectArray($values); + $values = $this->limitArray($values); + return $values; + } + + /** + * @param array $values + * @return array + */ + protected function filterArray($values) { + if ($this->getWhere()) { + $values = array_filter($values, [$this, 'evaluateFilters']); + } + return array_values($values); + } + + /** + * @param array $row + * @return bool + */ + private function evaluateFilters($row) { + $where = $this->getWhere(); + $allConditions = in_array($where[0], ['AND', 'OR', 'NOT']) ? $where : ['AND', $where]; + return $this->walkFilters($row, $allConditions); + } + + /** + * @param array $row + * @param array $filters + * @return bool + * @throws \Civi\API\Exception\NotImplementedException + */ + private function walkFilters($row, $filters) { + switch ($filters[0]) { + case 'AND': + case 'NOT': + $result = TRUE; + foreach ($filters[1] as $filter) { + if (!$this->walkFilters($row, $filter)) { + $result = FALSE; + break; + } + } + return $result == ($filters[0] == 'AND'); + + case 'OR': + $result = !count($filters[1]); + foreach ($filters[1] as $filter) { + if ($this->walkFilters($row, $filter)) { + return TRUE; + } + } + return $result; + + default: + return $this->filterCompare($row, $filters); + } + } + + /** + * @param array $row + * @param array $condition + * @return bool + * @throws \Civi\API\Exception\NotImplementedException + */ + private function filterCompare($row, $condition) { + if (!is_array($condition)) { + throw new NotImplementedException('Unexpected where syntax; expecting array.'); + } + $value = isset($row[$condition[0]]) ? $row[$condition[0]] : NULL; + $operator = $condition[1]; + $expected = isset($condition[2]) ? $condition[2] : NULL; + switch ($operator) { + case '=': + case '!=': + case '<>': + $equal = $value == $expected; + // PHP is too imprecise about comparing the number 0 + if ($expected === 0 || $expected === '0') { + $equal = ($value === 0 || $value === '0'); + } + // PHP is too imprecise about comparing empty strings + if ($expected === '') { + $equal = ($value === ''); + } + return $equal == ($operator == '='); + + case 'IS NULL': + case 'IS NOT NULL': + return is_null($value) == ($operator == 'IS NULL'); + + case '>': + return $value > $expected; + + case '>=': + return $value >= $expected; + + case '<': + return $value < $expected; + + case '<=': + return $value <= $expected; + + case 'BETWEEN': + case 'NOT BETWEEN': + $between = ($value >= $expected[0] && $value <= $expected[1]); + return $between == ($operator == 'BETWEEN'); + + case 'LIKE': + case 'NOT LIKE': + $pattern = '/^' . str_replace('%', '.*', preg_quote($expected, '/')) . '$/i'; + return !preg_match($pattern, $value) == ($operator != 'LIKE'); + + case 'IN': + return in_array($value, $expected); + + case 'NOT IN': + return !in_array($value, $expected); + + default: + throw new NotImplementedException("Unsupported operator: '$operator' cannot be used with array data"); + } + } + + /** + * @param $values + * @return array + */ + protected function sortArray($values) { + if ($this->getOrderBy()) { + usort($values, [$this, 'sortCompare']); + } + return $values; + } + + private function sortCompare($a, $b) { + foreach ($this->getOrderBy() as $field => $dir) { + $modifier = $dir == 'ASC' ? 1 : -1; + if (isset($a[$field]) && isset($b[$field])) { + if ($a[$field] == $b[$field]) { + continue; + } + return (strnatcasecmp($a[$field], $b[$field]) * $modifier); + } + elseif (isset($a[$field]) || isset($b[$field])) { + return ((isset($a[$field]) ? 1 : -1) * $modifier); + } + } + return 0; + } + + /** + * @param $values + * @return array + */ + protected function selectArray($values) { + if ($this->getSelect()) { + foreach ($values as &$value) { + $value = array_intersect_key($value, array_flip($this->getSelect())); + } + } + return $values; + } + + /** + * @param $values + * @return array + */ + protected function limitArray($values) { + if ($this->getOffset() || $this->getLimit()) { + $values = array_slice($values, $this->getOffset() ?: 0, $this->getLimit() ?: NULL); + } + return $values; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/CustomValueActionTrait.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/CustomValueActionTrait.php new file mode 100644 index 00000000..6a765b40 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/CustomValueActionTrait.php @@ -0,0 +1,83 @@ +<?php + +namespace Civi\Api4\Generic\Traits; + +use Civi\Api4\Utils\FormattingUtil; +use Civi\Api4\Utils\CoreUtil; + +/** + * Helper functions for working with custom values + * + * @package Civi\Api4\Generic + */ +trait CustomValueActionTrait { + + function __construct($customGroup, $actionName) { + $this->customGroup = $customGroup; + parent::__construct('CustomValue', $actionName, ['id', 'entity_id']); + } + + /** + * Custom Group name if this is a CustomValue pseudo-entity. + * + * @var string + */ + private $customGroup; + + /** + * @inheritDoc + */ + public function getEntityName() { + return 'Custom_' . $this->getCustomGroup(); + } + + /** + * @inheritDoc + */ + protected function writeObjects($items) { + $result = []; + foreach ($items as $item) { + FormattingUtil::formatWriteParams($item, $this->getEntityName(), $this->getEntityFields()); + + $result[] = \CRM_Core_BAO_CustomValueTable::setValues($item); + } + return $result; + } + + /** + * @inheritDoc + */ + protected function deleteObjects($items) { + $customTable = CoreUtil::getCustomTableByName($this->getCustomGroup()); + $ids = []; + foreach ($items as $item) { + \CRM_Utils_Hook::pre('delete', $this->getEntityName(), $item['id'], \CRM_Core_DAO::$_nullArray); + \CRM_Utils_SQL_Delete::from($customTable) + ->where('id = #value') + ->param('#value', $item['id']) + ->execute(); + \CRM_Utils_Hook::post('delete', $this->getEntityName(), $item['id'], \CRM_Core_DAO::$_nullArray); + $ids[] = $item['id']; + } + return $ids; + } + + /** + * @inheritDoc + */ + protected function fillDefaults(&$params) { + foreach ($this->getEntityFields() as $name => $field) { + if (!isset($params[$name]) && isset($field['default_value'])) { + $params[$name] = $field['default_value']; + } + } + } + + /** + * @return string + */ + public function getCustomGroup() { + return $this->customGroup; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/DAOActionTrait.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/DAOActionTrait.php new file mode 100644 index 00000000..1c92906b --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/DAOActionTrait.php @@ -0,0 +1,229 @@ +<?php +namespace Civi\Api4\Generic\Traits; + +use CRM_Utils_Array as UtilsArray; +use Civi\Api4\Utils\FormattingUtil; +use Civi\Api4\Query\Api4SelectQuery; + +trait DAOActionTrait { + + /** + * @return \CRM_Core_DAO|string + */ + protected function getBaoName() { + require_once 'api/v3/utils.php'; + return \_civicrm_api3_get_BAO($this->getEntityName()); + } + + /** + * Extract the true fields from a BAO + * + * (Used by create and update actions) + * @param object $bao + * @return array + */ + public static function baoToArray($bao) { + $fields = $bao->fields(); + $values = []; + foreach ($fields as $key => $field) { + $name = $field['name']; + if (property_exists($bao, $name)) { + $values[$name] = $bao->$name; + } + } + return $values; + } + + /** + * @return array|int + */ + protected function getObjects() { + $query = new Api4SelectQuery($this->getEntityName(), $this->getCheckPermissions()); + $query->select = $this->getSelect(); + $query->where = $this->getWhere(); + $query->orderBy = $this->getOrderBy(); + $query->limit = $this->getLimit(); + $query->offset = $this->getOffset(); + return $query->run(); + } + + /** + * Write a bao object as part of a create/update action. + * + * @param array $items + * The record to write to the DB. + * @return array + * The record after being written to the DB (e.g. including newly assigned "id"). + * @throws \API_Exception + */ + protected function writeObjects($items) { + $baoName = $this->getBaoName(); + + // Some BAOs are weird and don't support a straightforward "create" method. + $oddballs = [ + 'Address' => 'add', + 'GroupContact' => 'add', + 'Website' => 'add', + ]; + $method = UtilsArray::value($this->getEntityName(), $oddballs, 'create'); + if (!method_exists($baoName, $method)) { + $method = 'add'; + } + + $result = []; + + foreach ($items as $item) { + $entityId = UtilsArray::value('id', $item); + FormattingUtil::formatWriteParams($item, $this->getEntityName(), $this->getEntityFields()); + $this->formatCustomParams($item, $entityId); + $item['check_permissions'] = $this->getCheckPermissions(); + + $apiKeyPermission = $this->getEntityName() != 'Contact' || !$this->getCheckPermissions() || array_key_exists('api_key', $this->getEntityFields()) + || ($entityId && \CRM_Core_Permission::check('edit own api keys') && \CRM_Core_Session::getLoggedInContactID() == $entityId); + + if (!$apiKeyPermission && array_key_exists('api_key', $item)) { + throw new \Civi\API\Exception\UnauthorizedException('Permission denied to modify api key'); + } + + // For some reason the contact bao requires this + if ($entityId && $this->getEntityName() == 'Contact') { + $item['contact_id'] = $entityId; + } + + if ($this->getCheckPermissions() && $entityId) { + $this->checkContactPermissions($baoName, $item); + } + + if (method_exists($baoName, $method)) { + $createResult = $baoName::$method($item); + } + else { + $createResult = $this->genericCreateMethod($item); + } + + if (!$createResult) { + $errMessage = sprintf('%s write operation failed', $this->getEntityName()); + throw new \API_Exception($errMessage); + } + + if (!empty($this->reload) && is_a($createResult, 'CRM_Core_DAO')) { + $createResult->find(TRUE); + } + + // trim back the junk and just get the array: + $resultArray = $this->baoToArray($createResult); + + if (!$apiKeyPermission && array_key_exists('api_key', $resultArray)) { + unset($resultArray['api_key']); + } + + $result[] = $resultArray; + } + return $result; + } + + /** + * Fallback when a BAO does not contain create or add functions + * + * @param $params + * @return mixed + */ + private function genericCreateMethod($params) { + $baoName = $this->getBaoName(); + $hook = empty($params['id']) ? 'create' : 'edit'; + + \CRM_Utils_Hook::pre($hook, $this->getEntityName(), UtilsArray::value('id', $params), $params); + /** @var \CRM_Core_DAO $instance */ + $instance = new $baoName(); + $instance->copyValues($params, TRUE); + $instance->save(); + \CRM_Utils_Hook::post($hook, $this->getEntityName(), $instance->id, $instance); + + return $instance; + } + + /** + * @param array $params + * @param int $entityId + * @return mixed + */ + private function formatCustomParams(&$params, $entityId) { + $customParams = []; + + // $customValueID is the ID of the custom value in the custom table for this + // entity (i guess this assumes it's not a multi value entity) + foreach ($params as $name => $value) { + if (strpos($name, '.') === FALSE) { + continue; + } + + list($customGroup, $customField) = explode('.', $name); + + $customFieldId = \CRM_Core_BAO_CustomField::getFieldValue( + \CRM_Core_DAO_CustomField::class, + $customField, + 'id', + 'name' + ); + $customFieldType = \CRM_Core_BAO_CustomField::getFieldValue( + \CRM_Core_DAO_CustomField::class, + $customField, + 'html_type', + 'name' + ); + $customFieldExtends = \CRM_Core_BAO_CustomGroup::getFieldValue( + \CRM_Core_DAO_CustomGroup::class, + $customGroup, + 'extends', + 'name' + ); + + // todo are we sure we don't want to allow setting to NULL? need to test + if ($customFieldId && NULL !== $value) { + + if ($customFieldType == 'CheckBox') { + // this function should be part of a class + formatCheckBoxField($value, 'custom_' . $customFieldId, $this->getEntityName()); + } + + \CRM_Core_BAO_CustomField::formatCustomField( + $customFieldId, + $customParams, + $value, + $customFieldExtends, + NULL, // todo check when this is needed + $entityId, + FALSE, + FALSE, + TRUE + ); + } + } + + if ($customParams) { + $params['custom'] = $customParams; + } + } + + /** + * Check edit/delete permissions for contacts and related entities. + * + * @param $baoName + * @param $item + * @throws \Civi\API\Exception\UnauthorizedException + */ + protected function checkContactPermissions($baoName, $item) { + if ($baoName == 'CRM_Contact_BAO_Contact') { + $permission = $this->getActionName() == 'delete' ? \CRM_Core_Permission::DELETE : \CRM_Core_Permission::EDIT; + if (!\CRM_Contact_BAO_Contact_Permission::allow($item['id'], $permission)) { + throw new \Civi\API\Exception\UnauthorizedException('Permission denied to modify contact record'); + } + } + else { + // Fixme: decouple from v3 + require_once 'api/v3/utils.php'; + _civicrm_api3_check_edit_permissions($baoName, ['check_permissions' => 1] + $item); + } + } + +} |