summaryrefslogtreecommitdiff
path: root/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic
diff options
context:
space:
mode:
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic')
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php391
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractBatchAction.php64
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractCreateAction.php56
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php88
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractGetAction.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractQueryAction.php105
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractUpdateAction.php45
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicBatchAction.php72
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicCreateAction.php64
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetAction.php144
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetFieldsAction.php120
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicReplaceAction.php81
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicUpdateAction.php67
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOCreateAction.php58
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAODeleteAction.php73
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOEntity.php52
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetAction.php21
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetFieldsAction.php53
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOUpdateAction.php31
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Result.php105
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php197
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/CustomValueActionTrait.php83
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/DAOActionTrait.php229
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);
+ }
+ }
+
+}