diff options
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4')
123 files changed, 7801 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ACL.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ACL.php new file mode 100644 index 00000000..754a0499 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ACL.php @@ -0,0 +1,19 @@ +<?php + +namespace Civi\Api4; + +/** + * ACL Entity. + * + * This entity holds the ACL informatiom. With this entity you add/update/delete an ACL permission which consists of + * an Operation (e.g. 'View' or 'Edit'), a set of Data that the operation can be performed on (e.g. a group of contacts), + * and a Role that has permission to do this operation. For more info refer to + * https://docs.civicrm.org/user/en/latest/initial-set-up/permissions-and-access-control for more info. + * + * Creating a new ACL requires at minimum a entity table, entity ID and object_table + * + * @package Civi\Api4 + */ +class ACL extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Address/Create.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Address/Create.php new file mode 100644 index 00000000..641b76f6 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Address/Create.php @@ -0,0 +1,43 @@ +<?php + +namespace Civi\Api4\Action\Address; + +use Civi\Api4\Generic\Result; + +/** + * @inheritDoc + */ +class Create extends \Civi\Api4\Generic\DAOCreateAction { + + /** + * Optional param to indicate you want the street_address field parsed into individual params + * + * @var bool + */ + protected $streetParsing = TRUE; + + /** + * Optional param to indicate you want to skip geocoding (useful when importing a lot of addresses at once, the job Geocode and Parse Addresses can execute this task after the import) + * + * @var bool + */ + protected $skipGeocode = FALSE; + + /** + * When true, apply various fixes to the address before insert. + * + * @var bool + */ + protected $fixAddress = TRUE; + + /** + * @inheritDoc + */ + public function _run(Result $result) { + $this->values['street_parsing'] = $this->streetParsing; + $this->values['skip_geocode'] = $this->skipGeocode; + $this->values['fix_address'] = $this->fixAddress; + parent::_run($result); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Address/Update.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Address/Update.php new file mode 100644 index 00000000..862a8b9e --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Address/Update.php @@ -0,0 +1,43 @@ +<?php + +namespace Civi\Api4\Action\Address; + +use Civi\Api4\Generic\Result; + +/** + * @inheritDoc + */ +class Update extends \Civi\Api4\Generic\DAOUpdateAction { + + /** + * Optional param to indicate you want the street_address field parsed into individual params + * + * @var bool + */ + protected $streetParsing = TRUE; + + /** + * Optional param to indicate you want to skip geocoding (useful when importing a lot of addresses at once, the job Geocode and Parse Addresses can execute this task after the import) + * + * @var bool + */ + protected $skipGeocode = FALSE; + + /** + * When true, apply various fixes to the address before insert. + * + * @var bool + */ + protected $fixAddress = TRUE; + + /** + * @inheritDoc + */ + public function _run(Result $result) { + $this->values['street_parsing'] = $this->streetParsing; + $this->values['skip_geocode'] = $this->skipGeocode; + $this->values['fix_address'] = $this->fixAddress; + parent::_run($result); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contact/Create.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contact/Create.php new file mode 100644 index 00000000..1bd0bcec --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contact/Create.php @@ -0,0 +1,22 @@ +<?php + +namespace Civi\Api4\Action\Contact; + +/** + * @inheritDoc + */ +class Create extends \Civi\Api4\Generic\DAOCreateAction { + + protected function fillDefaults(&$params) { + // Guess which type of contact is being created + if (empty($params['contact_type']) && !empty($params['organization_name'])) { + $params['contact_type'] = 'Organization'; + } + if (empty($params['contact_type']) && !empty($params['household_name'])) { + $params['contact_type'] = 'Household'; + } + // Will default to Individual per fieldSpec + parent::fillDefaults($params); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contact/GetFields.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contact/GetFields.php new file mode 100644 index 00000000..1d6ddf66 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contact/GetFields.php @@ -0,0 +1,19 @@ +<?php +namespace Civi\Api4\Action\Contact; + +use Civi\Api4\Generic\DAOGetFieldsAction; + +class GetFields extends DAOGetFieldsAction { + + protected function getRecords() { + $fields = parent::getRecords(); + + $apiKeyPerms = ['edit api keys', 'administer CiviCRM']; + if ($this->checkPermissions && !\CRM_Core_Permission::check([$apiKeyPerms])) { + unset($fields['api_key']); + } + + return $fields; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contribution/Create.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contribution/Create.php new file mode 100644 index 00000000..52ee63c8 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contribution/Create.php @@ -0,0 +1,18 @@ +<?php + +namespace Civi\Api4\Action\Contribution; + +use Civi\Api4\Generic\Result; + +/** + * @inheritDoc + */ +class Create extends \Civi\Api4\Generic\DAOCreateAction { + + public function _run(Result $result) { + // Required by Contribution BAO + $this->values['skipCleanMoney'] = TRUE; + parent::_run($result); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Create.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Create.php new file mode 100644 index 00000000..7d059b24 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Create.php @@ -0,0 +1,11 @@ +<?php + +namespace Civi\Api4\Action\CustomValue; + +/** + * @inheritDoc + */ +class Create extends \Civi\Api4\Generic\DAOCreateAction { + use \Civi\Api4\Generic\Traits\CustomValueActionTrait; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Delete.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Delete.php new file mode 100644 index 00000000..7c521748 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Delete.php @@ -0,0 +1,11 @@ +<?php + +namespace Civi\Api4\Action\CustomValue; + +/** + * Delete one or more items, based on criteria specified in Where param. + */ +class Delete extends \Civi\Api4\Generic\DAODeleteAction { + use \Civi\Api4\Generic\Traits\CustomValueActionTrait; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Get.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Get.php new file mode 100644 index 00000000..47f3f514 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Get.php @@ -0,0 +1,11 @@ +<?php + +namespace Civi\Api4\Action\CustomValue; + +/** + * Get fields for a custom group. + */ +class Get extends \Civi\Api4\Generic\DAOGetAction { + use \Civi\Api4\Generic\Traits\CustomValueActionTrait; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetActions.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetActions.php new file mode 100644 index 00000000..8af9088d --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetActions.php @@ -0,0 +1,11 @@ +<?php + +namespace Civi\Api4\Action\CustomValue; + +/** + * @inheritDoc + */ +class GetActions extends \Civi\Api4\Action\GetActions { + use \Civi\Api4\Generic\Traits\CustomValueActionTrait; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetFields.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetFields.php new file mode 100644 index 00000000..733776b7 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetFields.php @@ -0,0 +1,34 @@ +<?php + +namespace Civi\Api4\Action\CustomValue; + +use Civi\Api4\Service\Spec\SpecGatherer; +use Civi\Api4\Service\Spec\SpecFormatter; + +/** + * Get fields for a custom group. + */ +class GetFields extends \Civi\Api4\Generic\DAOGetFieldsAction { + use \Civi\Api4\Generic\Traits\CustomValueActionTrait; + + protected function getRecords() { + $fields = $this->_itemsToGet('name'); + /** @var SpecGatherer $gatherer */ + $gatherer = \Civi::container()->get('spec_gatherer'); + $spec = $gatherer->getSpec('Custom_' . $this->getCustomGroup(), $this->getAction(), $this->includeCustom); + return SpecFormatter::specToArray($spec->getFields($fields), (array) $this->select, $this->loadOptions); + } + + /** + * @inheritDoc + */ + public function getParamInfo($param = NULL) { + $info = parent::getParamInfo($param); + if (!$param) { + // This param is meaningless here. + unset($info['includeCustom']); + } + return $info; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Replace.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Replace.php new file mode 100644 index 00000000..457be9ca --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Replace.php @@ -0,0 +1,11 @@ +<?php + +namespace Civi\Api4\Action\CustomValue; + +/** + * Given a set of records, will appropriately update the database. + */ +class Replace extends \Civi\Api4\Generic\BasicReplaceAction { + use \Civi\Api4\Generic\Traits\CustomValueActionTrait; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Update.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Update.php new file mode 100644 index 00000000..14f66f29 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Update.php @@ -0,0 +1,11 @@ +<?php + +namespace Civi\Api4\Action\CustomValue; + +/** + * Update one or more records with new values. Use the where clause to select them. + */ +class Update extends \Civi\Api4\Generic\DAOUpdateAction { + use \Civi\Api4\Generic\Traits\CustomValueActionTrait; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Entity/Get.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Entity/Get.php new file mode 100644 index 00000000..020c6398 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Entity/Get.php @@ -0,0 +1,102 @@ +<?php + +namespace Civi\Api4\Action\Entity; + +use Civi\Api4\CustomGroup; +use Civi\Api4\Utils\ReflectionUtils; + +/** + * Get entities + * + * @method $this setIncludeCustom(bool $value) + * @method bool getIncludeCustom() + */ +class Get extends \Civi\Api4\Generic\BasicGetAction { + + /** + * Include custom-field-based pseudo-entities? + * + * @var bool + */ + protected $includeCustom = TRUE; + + /** + * Scan all api directories to discover entities + */ + protected function getRecords() { + $entities = []; + foreach (explode(PATH_SEPARATOR, get_include_path()) as $path) { + $dir = \CRM_Utils_File::addTrailingSlash($path) . 'Civi/Api4'; + if (is_dir($dir)) { + foreach (glob("$dir/*.php") as $file) { + $matches = []; + preg_match('/(\w*).php/', $file, $matches); + $entity = ['name' => $matches[1]]; + if ($this->_isFieldSelected('description') || $this->_isFieldSelected('comment')) { + $this->addDocs($entity); + } + $entities[$matches[1]] = $entity; + } + } + } + unset($entities['CustomValue']); + + if ($this->includeCustom) { + $this->addCustomEntities($entities); + } + + ksort($entities); + return $entities; + } + + /** + * Add custom-field pseudo-entities + * + * @param $entities + * @throws \API_Exception + */ + private function addCustomEntities(&$entities) { + $customEntities = CustomGroup::get() + ->addWhere('is_multiple', '=', 1) + ->addWhere('is_active', '=', 1) + ->setSelect(['name', 'title', 'help_pre', 'help_post', 'extends']) + ->setCheckPermissions(FALSE) + ->execute(); + foreach ($customEntities as $customEntity) { + $fieldName = 'Custom_' . $customEntity['name']; + $entities[$fieldName] = [ + 'name' => $fieldName, + 'description' => $customEntity['title'] . ' custom group - extends ' . $customEntity['extends'], + ]; + if (!empty($customEntity['help_pre'])) { + $entities[$fieldName]['comment'] = $this->plainTextify($customEntity['help_pre']); + } + if (!empty($customEntity['help_post'])) { + $pre = empty($entities[$fieldName]['comment']) ? '' : $entities[$fieldName]['comment'] . "\n\n"; + $entities[$fieldName]['comment'] = $pre . $this->plainTextify($customEntity['help_post']); + } + } + } + + /** + * Convert html to plain text. + * + * @param $input + * @return mixed + */ + private function plainTextify($input) { + return html_entity_decode(strip_tags($input), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } + + /** + * Add info from code docblock. + * + * @param $entity + */ + private function addDocs(&$entity) { + $reflection = new \ReflectionClass("\\Civi\\Api4\\" . $entity['name']); + $entity += ReflectionUtils::getCodeDocs($reflection); + unset($entity['package'], $entity['method']); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Entity/GetLinks.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Entity/GetLinks.php new file mode 100644 index 00000000..ee274bf9 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Entity/GetLinks.php @@ -0,0 +1,51 @@ +<?php + +namespace Civi\Api4\Action\Entity; + +use \CRM_Core_DAO_AllCoreTables as AllCoreTables; + +/** + * Get a list of FK links between entities + */ +class GetLinks extends \Civi\Api4\Generic\BasicGetAction { + + public function getRecords() { + $result = []; + /** @var \Civi\Api4\Service\Schema\SchemaMap $schema */ + $schema = \Civi::container()->get('schema_map'); + foreach ($schema->getTables() as $table) { + $entity = AllCoreTables::getBriefName(AllCoreTables::getClassForTable($table->getName())); + // Since this is an api function, exclude tables that don't have an api + if (class_exists('\Civi\Api4\\' . $entity)) { + $item = [ + 'entity' => $entity, + 'table' => $table->getName(), + 'links' => [], + ]; + foreach ($table->getTableLinks() as $link) { + $link = $link->toArray(); + $link['entity'] = AllCoreTables::getBriefName(AllCoreTables::getClassForTable($link['targetTable'])); + $item['links'][] = $link; + } + $result[] = $item; + } + } + return $result; + } + + public function fields() { + return [ + [ + 'name' => 'entity', + ], + [ + 'name' => 'table', + ], + [ + 'name' => 'links', + 'data_type' => 'Array', + ], + ]; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GetActions.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GetActions.php new file mode 100644 index 00000000..1dc4ebbb --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GetActions.php @@ -0,0 +1,101 @@ +<?php + +namespace Civi\Api4\Action; + +use Civi\API\Exception\NotImplementedException; +use Civi\Api4\Generic\BasicGetAction; +use Civi\Api4\Utils\ActionUtil; +use Civi\Api4\Utils\ReflectionUtils; + +/** + * Get actions for an entity with a list of accepted params + */ +class GetActions extends BasicGetAction { + + private $_actions = []; + + private $_actionsToGet; + + protected function getRecords() { + $this->_actionsToGet = $this->_itemsToGet('name'); + + $entityReflection = new \ReflectionClass('\Civi\Api4\\' . $this->_entityName); + foreach ($entityReflection->getMethods(\ReflectionMethod::IS_STATIC | \ReflectionMethod::IS_PUBLIC) as $method) { + $actionName = $method->getName(); + if ($actionName != 'permissions' && $actionName[0] != '_') { + $this->loadAction($actionName); + } + } + if (!$this->_actionsToGet || count($this->_actionsToGet) > count($this->_actions)) { + $includePaths = array_unique(explode(PATH_SEPARATOR, get_include_path())); + // Search entity-specific actions (including those provided by extensions) + foreach ($includePaths as $path) { + $dir = \CRM_Utils_File::addTrailingSlash($path) . 'Civi/Api4/Action/' . $this->_entityName; + $this->scanDir($dir); + } + } + ksort($this->_actions); + return $this->_actions; + } + + /** + * @param $dir + */ + private function scanDir($dir) { + if (is_dir($dir)) { + foreach (glob("$dir/*.php") as $file) { + $matches = []; + preg_match('/(\w*).php/', $file, $matches); + $actionName = array_pop($matches); + $this->loadAction(lcfirst($actionName)); + } + } + } + + /** + * @param $actionName + */ + private function loadAction($actionName) { + try { + if (!isset($this->_actions[$actionName]) && (!$this->_actionsToGet || in_array($actionName, $this->_actionsToGet))) { + $action = ActionUtil::getAction($this->getEntityName(), $actionName); + if (is_object($action)) { + $this->_actions[$actionName] = ['name' => $actionName]; + if ($this->_isFieldSelected('description') || $this->_isFieldSelected('comment')) { + $actionReflection = new \ReflectionClass($action); + $actionInfo = ReflectionUtils::getCodeDocs($actionReflection); + unset($actionInfo['method']); + $this->_actions[$actionName] += $actionInfo; + } + if ($this->_isFieldSelected('params')) { + $this->_actions[$actionName]['params'] = $action->getParamInfo(); + } + } + } + } + catch (NotImplementedException $e) { + } + } + + public function fields() { + return [ + [ + 'name' => 'name', + 'data_type' => 'String', + ], + [ + 'name' => 'description', + 'data_type' => 'String', + ], + [ + 'name' => 'comment', + 'data_type' => 'String', + ], + [ + 'name' => 'params', + 'data_type' => 'Array', + ], + ]; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GroupContact/Create.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GroupContact/Create.php new file mode 100644 index 00000000..44e61f2e --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GroupContact/Create.php @@ -0,0 +1,38 @@ +<?php + +namespace Civi\Api4\Action\GroupContact; + +use Civi\Api4\Generic\Result; + +/** + * @inheritDoc + * + * @method $this setMethod(string $method) Indicate who added/removed the group. + * @method $this setTracking(string $tracking) Specify ip address or other tracking info. + */ +class Create extends \Civi\Api4\Generic\DAOCreateAction { + + /** + * String to indicate who added/removed the group. + * + * @var string + */ + protected $method = 'API'; + + /** + * IP address or other tracking info about who performed this group subscription. + * + * @var string + */ + protected $tracking = ''; + + /** + * @inheritDoc + */ + public function _run(Result $result) { + $this->values['method'] = $this->method; + $this->values['tracking'] = $this->tracking; + parent::_run($result); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GroupContact/Update.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GroupContact/Update.php new file mode 100644 index 00000000..edb8a902 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GroupContact/Update.php @@ -0,0 +1,38 @@ +<?php + +namespace Civi\Api4\Action\GroupContact; + +use Civi\Api4\Generic\Result; + +/** + * @inheritDoc + * + * @method $this setMethod(string $method) Indicate who added/removed the group. + * @method $this setTracking(string $tracking) Specify ip address or other tracking info. + */ +class Update extends \Civi\Api4\Generic\DAOUpdateAction { + + /** + * String to indicate who added/removed the group. + * + * @var string + */ + protected $method = 'API'; + + /** + * IP address or other tracking info about who performed this group subscription. + * + * @var string + */ + protected $tracking = ''; + + /** + * @inheritDoc + */ + public function _run(Result $result) { + $this->values['method'] = $this->method; + $this->values['tracking'] = $this->tracking; + parent::_run($result); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Navigation/Get.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Navigation/Get.php new file mode 100644 index 00000000..dcecc06a --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Navigation/Get.php @@ -0,0 +1,19 @@ +<?php + +namespace Civi\Api4\Action\Navigation; + +/** + * @inheritDoc + * + * Fetch items from the navigation menu. By default this will fetch items from the current domain. + */ +class Get extends \Civi\Api4\Generic\DAOGetAction { + + /** + * @inheritDoc + */ + protected $where = [ + ['domain_id', '=', 'current_domain'], + ]; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Participant/Get.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Participant/Get.php new file mode 100644 index 00000000..c5efec93 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Participant/Get.php @@ -0,0 +1,18 @@ +<?php + +namespace Civi\Api4\Action\Participant; + +/** + * @inheritDoc + */ +class Get extends \Civi\Api4\Generic\DAOGetAction { + + /** + * @inheritDoc + * $example->addWhere('contact_id.contact_type', 'IN', array('Individual', 'Household')) + */ + protected $where = [ + ['is_test', '=', 0], + ]; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ActionSchedule.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ActionSchedule.php new file mode 100644 index 00000000..a8235f64 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ActionSchedule.php @@ -0,0 +1,18 @@ +<?php + +namespace Civi\Api4; + +/** + * ActionSchedule Entity. + * + * This entity exposes CiviCRM schedule reminders, which allows us to send messages (through email or SMS) + * to contacts when certain criteria are met. Using this API you can create schedule reminder for + * supported entities like Contact, Activity, Event, Membership or Contribution. + * + * Creating a new ActionSchedule requires at minimum a title, mapping_id and entity_value. + * + * @package Civi\Api4 + */ +class ActionSchedule extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Activity.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Activity.php new file mode 100644 index 00000000..23ee4c15 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Activity.php @@ -0,0 +1,20 @@ +<?php + +namespace Civi\Api4; + +/** + * Activity entity. + * + * This entity adds record of any scheduled or completed interaction with one or more contacts. + * Each activity record is tightly linked to other CiviCRM constituents. With this API you can manually + * create an activity of desired type for your organisation or any other contact. + * + * Creating a new Activity requires at minimum a activity_type_id, entity ID and object_table + * + * An activity is a record of some type of interaction with one or more contacts. + * + * @package Civi\Api4 + */ +class Activity extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ActivityContact.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ActivityContact.php new file mode 100644 index 00000000..7ef438a5 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ActivityContact.php @@ -0,0 +1,16 @@ +<?php + +namespace Civi\Api4; + +/** + * ActivityContact Entity. + * + * This entity adds a record which relate a contact to activity. + * + * Creating a new ActivityContact requires at minimum a contact_id and activity_id. + * + * @package Civi\Api4 + */ +class ActivityContact extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Address.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Address.php new file mode 100644 index 00000000..e52b6283 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Address.php @@ -0,0 +1,32 @@ +<?php + +namespace Civi\Api4; + +/** + * Address Entity. + * + * This entity holds the address informatiom of a contact. Each contact may hold + * one or more addresses but must have different location types respectively. + * + * Creating a new address requires at minimum a contact's ID and location type ID + * and other attributes (although optional) like street address, city, country etc. + * + * @package Civi\Api4 + */ +class Address extends Generic\DAOEntity { + + /** + * @return \Civi\Api4\Action\Address\Create + */ + public static function create() { + return new \Civi\Api4\Action\Address\Create(__CLASS__, __FUNCTION__); + } + + /** + * @return \Civi\Api4\Action\Address\Update + */ + public static function update() { + return new \Civi\Api4\Action\Address\Update(__CLASS__, __FUNCTION__); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Contact.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Contact.php new file mode 100644 index 00000000..cca8c335 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Contact.php @@ -0,0 +1,39 @@ +<?php + +namespace Civi\Api4; + +/** + * Contacts - Individuals, Organizations, Households. + * + * This is the central entity in the CiviCRM database, and links to + * many other entities (Email, Phone, Participant, etc.). + * + * Creating a new contact requires at minimum a name or email address. + * + * @package Civi\Api4 + */ +class Contact extends Generic\DAOEntity { + + /** + * @return Action\Contact\Create + */ + public static function create() { + return new Action\Contact\Create(__CLASS__, __FUNCTION__); + } + + /** + * @return \Civi\Api4\Generic\DAOUpdateAction + */ + public static function update() { + // For some reason the contact bao requires this for updating + return new Generic\DAOUpdateAction(__CLASS__, __FUNCTION__, ['id', 'contact_type']); + } + + /** + * @return \Civi\Api4\Action\Contact\GetFields + */ + public static function getFields() { + return new Action\Contact\GetFields(__CLASS__, __FUNCTION__); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ContactType.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ContactType.php new file mode 100644 index 00000000..8ce6c8dd --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ContactType.php @@ -0,0 +1,18 @@ +<?php + +namespace Civi\Api4; + +/** + * ContactType entity. + * + * With this entity you can create or update any new or existing Contact type or a sub type + * In case of updating existing ContactType, id of that particular ContactType must + * be in $params array. + * + * Creating a new contact type requires at minimum a label and parent_id. + * + * @package Civi\Api4 + */ +class ContactType extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Contribution.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Contribution.php new file mode 100644 index 00000000..903c4753 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Contribution.php @@ -0,0 +1,19 @@ +<?php + +namespace Civi\Api4; + +/** + * Contribution entity. + * + * @package Civi\Api4 + */ +class Contribution extends Generic\DAOEntity { + + /** + * @return \Civi\Api4\Action\Contribution\Create + */ + public static function create() { + return new \Civi\Api4\Action\Contribution\Create(__CLASS__, __FUNCTION__); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomField.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomField.php new file mode 100644 index 00000000..245c9f4c --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomField.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * CustomField entity. + * + * @package Civi\Api4 + */ +class CustomField extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomGroup.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomGroup.php new file mode 100644 index 00000000..780ccd2f --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomGroup.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * CustomGroup entity. + * + * @package Civi\Api4 + */ +class CustomGroup extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomValue.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomValue.php new file mode 100644 index 00000000..9cf4da43 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomValue.php @@ -0,0 +1,79 @@ +<?php + +namespace Civi\Api4; + +/** + * CustomGroup entity. + * + * @package Civi\Api4 + */ +class CustomValue extends Generic\AbstractEntity { + + /** + * @param string $customGroup + * @return Action\CustomValue\Get + */ + public static function get($customGroup) { + return new Action\CustomValue\Get($customGroup, __FUNCTION__); + } + + /** + * @param string $customGroup + * @return Action\CustomValue\GetFields + */ + public static function getFields($customGroup = NULL) { + return new Action\CustomValue\GetFields($customGroup, __FUNCTION__); + } + + /** + * @param string $customGroup + * @return Action\CustomValue\Create + */ + public static function create($customGroup) { + return new Action\CustomValue\Create($customGroup, __FUNCTION__); + } + + /** + * @param string $customGroup + * @return Action\CustomValue\Update + */ + public static function update($customGroup) { + return new Action\CustomValue\Update($customGroup, __FUNCTION__); + } + + /** + * @param string $customGroup + * @return Action\CustomValue\Delete + */ + public static function delete($customGroup) { + return new Action\CustomValue\Delete($customGroup, __FUNCTION__); + } + + /** + * @param string $customGroup + * @return Action\CustomValue\Replace + */ + public static function replace($customGroup) { + return new Action\CustomValue\Replace($customGroup, __FUNCTION__); + } + + /** + * @param string $customGroup + * @return Action\CustomValue\GetActions + */ + public static function getActions($customGroup = NULL) { + return new Action\CustomValue\GetActions($customGroup, __FUNCTION__); + } + + /** + * @inheritDoc + */ + public static function permissions() { + $entity = 'contact'; + $permissions = \CRM_Core_Permission::getEntityActionPermissions(); + + // Merge permissions for this entity with the defaults + return \CRM_Utils_Array::value($entity, $permissions, []) + $permissions['default']; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Email.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Email.php new file mode 100644 index 00000000..cb743e3a --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Email.php @@ -0,0 +1,16 @@ +<?php + +namespace Civi\Api4; + +/** + * Email entity. + * + * This entity allows user to add, update, retrieve or delete emails address(es) of a contact. + * + * Creating a new email address requires at minimum a contact's ID and email + * + * @package Civi\Api4 + */ +class Email extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Entity.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Entity.php new file mode 100644 index 00000000..bc759d05 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Entity.php @@ -0,0 +1,48 @@ +<?php + +namespace Civi\Api4; + +/** + * Retrieves information about all Api4 entities. + * + * @package Civi\Api4 + */ +class Entity extends Generic\AbstractEntity { + + /** + * @return Action\Entity\Get + */ + public static function get() { + return new Action\Entity\Get('Entity', __FUNCTION__); + } + + /** + * @return \Civi\Api4\Generic\BasicGetFieldsAction + */ + public static function getFields() { + return new \Civi\Api4\Generic\BasicGetFieldsAction('Entity', __FUNCTION__, function() { + return [ + ['name' => 'name'], + ['name' => 'description'], + ['name' => 'comment'], + ]; + }); + } + + /** + * @return Action\Entity\GetLinks + */ + public static function getLinks() { + return new Action\Entity\GetLinks('Entity', __FUNCTION__); + } + + /** + * @return array + */ + public static function permissions() { + return [ + 'default' => ['access CiviCRM'] + ]; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event.php new file mode 100644 index 00000000..1a07cc3d --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * Event entity. + * + * @package Civi\Api4 + */ +class Event extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Events.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Events.php new file mode 100644 index 00000000..0bf4a993 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Events.php @@ -0,0 +1,25 @@ +<?php + +namespace Civi\Api4\Event; + +class Events { + + /** + * Prepare the specification for a request. Fired from within a request to + * get fields. + * + * @see GetSpecEvent + */ + const GET_SPEC = 'civi.api.get_spec'; + + /** + * Build the database schema, allow adding of custom joins and tables. + */ + const SCHEMA_MAP_BUILD = 'api.schema_map.build'; + + /** + * Alter query results of APIv4 select query + */ + const POST_SELECT_QUERY = 'api.select_query.post'; + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/GetSpecEvent.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/GetSpecEvent.php new file mode 100644 index 00000000..cc247853 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/GetSpecEvent.php @@ -0,0 +1,35 @@ +<?php + +namespace Civi\Api4\Event; + +use Civi\Api4\Generic\AbstractAction; +use Symfony\Component\EventDispatcher\Event as BaseEvent; + +class GetSpecEvent extends BaseEvent { + /** + * @var AbstractAction + */ + protected $request; + + /** + * @param AbstractAction $request + */ + public function __construct(AbstractAction $request) { + $this->request = $request; + } + + /** + * @return AbstractAction + */ + public function getRequest() { + return $this->request; + } + + /** + * @param $request + */ + public function setRequest(AbstractAction $request) { + $this->request = $request; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/PostSelectQueryEvent.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/PostSelectQueryEvent.php new file mode 100644 index 00000000..4489033b --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/PostSelectQueryEvent.php @@ -0,0 +1,64 @@ +<?php + +namespace Civi\Api4\Event; + +use Civi\Api4\Query\Api4SelectQuery; +use Symfony\Component\EventDispatcher\Event; + +class PostSelectQueryEvent extends Event { + + /** + * @var array + */ + protected $results; + + /** + * @var Api4SelectQuery + */ + protected $query; + + /** + * PostSelectQueryEvent constructor. + * @param array $results + * @param Api4SelectQuery $query + */ + public function __construct(array $results, Api4SelectQuery $query) { + $this->results = $results; + $this->query = $query; + } + + /** + * @return array + */ + public function getResults() { + return $this->results; + } + + /** + * @param array $results + * @return $this + */ + public function setResults($results) { + $this->results = $results; + + return $this; + } + + /** + * @return Api4SelectQuery + */ + public function getQuery() { + return $this->query; + } + + /** + * @param Api4SelectQuery $query + * @return $this + */ + public function setQuery($query) { + $this->query = $query; + + return $this; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/SchemaMapBuildEvent.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/SchemaMapBuildEvent.php new file mode 100644 index 00000000..f79f6b4b --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/SchemaMapBuildEvent.php @@ -0,0 +1,39 @@ +<?php + +namespace Civi\Api4\Event; + +use Civi\Api4\Service\Schema\SchemaMap; +use Symfony\Component\EventDispatcher\Event as BaseEvent; + +class SchemaMapBuildEvent extends BaseEvent { + /** + * @var SchemaMap + */ + protected $schemaMap; + + /** + * @param SchemaMap $schemaMap + */ + public function __construct(SchemaMap $schemaMap) { + $this->schemaMap = $schemaMap; + } + + /** + * @return SchemaMap + */ + public function getSchemaMap() { + return $this->schemaMap; + } + + /** + * @param SchemaMap $schemaMap + * + * @return $this + */ + public function setSchemaMap($schemaMap) { + $this->schemaMap = $schemaMap; + + return $this; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/AbstractPrepareSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/AbstractPrepareSubscriber.php new file mode 100644 index 00000000..d4725e0d --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/AbstractPrepareSubscriber.php @@ -0,0 +1,24 @@ +<?php + +namespace Civi\Api4\Event\Subscriber; + +use Civi\API\Event\PrepareEvent; +use Civi\API\Events; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +abstract class AbstractPrepareSubscriber implements EventSubscriberInterface { + /** + * @return array + */ + public static function getSubscribedEvents() { + return [ + Events::PREPARE => 'onApiPrepare', + ]; + } + + /** + * @param PrepareEvent $event + */ + abstract public function onApiPrepare(PrepareEvent $event); + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivityPreCreationSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivityPreCreationSubscriber.php new file mode 100644 index 00000000..1938ce0b --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivityPreCreationSubscriber.php @@ -0,0 +1,40 @@ +<?php + +namespace Civi\Api4\Event\Subscriber; + +use Civi\Api4\Generic\DAOCreateAction; +use Civi\Api4\OptionValue; + +class ActivityPreCreationSubscriber extends PreCreationSubscriber { + /** + * @param DAOCreateAction $request + * @throws \API_Exception + * @throws \Exception + */ + protected function modify(DAOCreateAction $request) { + $activityType = $request->getValue('activity_type'); + if ($activityType) { + $result = OptionValue::get() + ->setCheckPermissions(FALSE) + ->addWhere('name', '=', $activityType) + ->addWhere('option_group.name', '=', 'activity_type') + ->execute(); + + if ($result->count() !== 1) { + throw new \Exception('Activity type must match a *single* type'); + } + + $request->addValue('activity_type_id', $result->first()['value']); + } + } + + /** + * @param DAOCreateAction $request + * + * @return bool + */ + protected function applies(DAOCreateAction $request) { + return $request->getEntityName() === 'Activity'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivitySchemaMapSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivitySchemaMapSubscriber.php new file mode 100644 index 00000000..52d58397 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivitySchemaMapSubscriber.php @@ -0,0 +1,40 @@ +<?php + +namespace Civi\Api4\Event\Subscriber; + +use Civi\Api4\Event\Events; +use Civi\Api4\Event\SchemaMapBuildEvent; +use Civi\Api4\Service\Schema\Joinable\ActivityToActivityContactAssigneesJoinable; +use Civi\Api4\Service\Schema\Joinable\BridgeJoinable; +use Civi\Api4\Service\Schema\Joinable\Joinable; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use \CRM_Utils_String as StringHelper; + +class ActivitySchemaMapSubscriber implements EventSubscriberInterface { + /** + * @return array + */ + public static function getSubscribedEvents() { + return [ + Events::SCHEMA_MAP_BUILD => 'onSchemaBuild', + ]; + } + + /** + * @param SchemaMapBuildEvent $event + */ + public function onSchemaBuild(SchemaMapBuildEvent $event) { + $schema = $event->getSchemaMap(); + $table = $schema->getTableByName('civicrm_activity'); + + $middleAlias = StringHelper::createRandom(10, implode(range('a', 'z'))); + $middleLink = new ActivityToActivityContactAssigneesJoinable($middleAlias); + + $bridge = new BridgeJoinable('civicrm_contact', 'id', 'assignees', $middleLink); + $bridge->setBaseTable('civicrm_activity_contact'); + $bridge->setJoinType(Joinable::JOIN_TYPE_ONE_TO_MANY); + + $table->addTableLink('contact_id', $bridge); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php new file mode 100644 index 00000000..edea3de6 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php @@ -0,0 +1,54 @@ +<?php + +namespace Civi\Api4\Event\Subscriber; + +use Civi\Api4\Event\Events; +use Civi\Api4\Event\SchemaMapBuildEvent; +use Civi\Api4\Service\Schema\Joinable\Joinable; +use Civi\Api4\Service\Schema\Table; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class ContactSchemaMapSubscriber implements EventSubscriberInterface { + /** + * @return array + */ + public static function getSubscribedEvents() { + return [ + Events::SCHEMA_MAP_BUILD => 'onSchemaBuild', + ]; + } + + /** + * @param SchemaMapBuildEvent $event + */ + public function onSchemaBuild(SchemaMapBuildEvent $event) { + $schema = $event->getSchemaMap(); + $table = $schema->getTableByName('civicrm_contact'); + $this->addCreatedActivitiesLink($table); + $this->fixPreferredLanguageAlias($table); + } + + /** + * @param Table $table + */ + private function addCreatedActivitiesLink($table) { + $alias = 'created_activities'; + $joinable = new Joinable('civicrm_activity_contact', 'contact_id', $alias); + $joinable->addCondition($alias . '.record_type_id = 1'); + $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY); + $table->addTableLink('id', $joinable); + } + + /** + * @param Table $table + */ + private function fixPreferredLanguageAlias($table) { + foreach ($table->getExternalLinks() as $link) { + if ($link->getAlias() === 'languages') { + $link->setAlias('preferred_language'); + return; + } + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomFieldPreCreationSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomFieldPreCreationSubscriber.php new file mode 100644 index 00000000..289e1057 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomFieldPreCreationSubscriber.php @@ -0,0 +1,91 @@ +<?php + +namespace Civi\Api4\Event\Subscriber; + +use Civi\Api4\Generic\DAOCreateAction; + +class CustomFieldPreCreationSubscriber extends PreCreationSubscriber { + + const OPTION_TYPE_NEW = 1; + const OPTION_STATUS_ACTIVE = 1; + + /** + * @param DAOCreateAction $request + */ + public function modify(DAOCreateAction $request) { + $this->formatOptionParams($request); + $this->setDefaults($request); + } + + /** + * @param DAOCreateAction $request + * + * @return bool + */ + protected function applies(DAOCreateAction $request) { + return $request->getEntityName() === 'CustomField'; + } + + /** + * Sets defaults required for option group and value creation + * @see CRM_Core_BAO_CustomField::create() + * + * @param DAOCreateAction $request + */ + protected function formatOptionParams(DAOCreateAction $request) { + $options = $request->getValue('options'); + + if (!is_array($options)) { + return; + } + + $dataTypeKey = 'data_type'; + $optionLabelKey = 'option_label'; + $optionWeightKey = 'option_weight'; + $optionStatusKey = 'option_status'; + $optionValueKey = 'option_value'; + $optionTypeKey = 'option_type'; + + $dataType = $request->getValue($dataTypeKey); + $optionLabel = $request->getValue($optionLabelKey); + $optionWeight = $request->getValue($optionWeightKey); + $optionStatus = $request->getValue($optionStatusKey); + $optionValue = $request->getValue($optionValueKey); + $optionType = $request->getValue($optionTypeKey); + + if (!$optionType) { + $request->addValue($optionTypeKey, self::OPTION_TYPE_NEW); + } + + if (!$dataType) { + $request->addValue($dataTypeKey, 'String'); + } + + if (!$optionLabel) { + $request->addValue($optionLabelKey, array_values($options)); + } + + if (!$optionValue) { + $request->addValue($optionValueKey, array_keys($options)); + } + + if (!$optionStatus) { + $statuses = array_fill(0, count($options), self::OPTION_STATUS_ACTIVE); + $request->addValue($optionStatusKey, $statuses); + } + + if (!$optionWeight) { + $request->addValue($optionWeightKey, range(1, count($options))); + } + } + + /** + * @param DAOCreateAction $request + */ + private function setDefaults(DAOCreateAction $request) { + if (!$request->getValue('option_type')) { + $request->addValue('option_type', NULL); + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomGroupPreCreationSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomGroupPreCreationSubscriber.php new file mode 100644 index 00000000..70f6e426 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomGroupPreCreationSubscriber.php @@ -0,0 +1,29 @@ +<?php + +namespace Civi\Api4\Event\Subscriber; + +use Civi\Api4\Generic\DAOCreateAction; + +class CustomGroupPreCreationSubscriber extends PreCreationSubscriber { + /** + * @param DAOCreateAction $request + */ + protected function modify(DAOCreateAction $request) { + $extends = $request->getValue('extends'); + $title = $request->getValue('title'); + $name = $request->getValue('name'); + + if (is_string($extends)) { + $request->addValue('extends', [$extends]); + } + + if (NULL === $title && $name) { + $request->addValue('title', $name); + } + } + + protected function applies(DAOCreateAction $request) { + return $request->getEntityName() === 'CustomGroup'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/OptionValuePreCreationSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/OptionValuePreCreationSubscriber.php new file mode 100644 index 00000000..3e671d61 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/OptionValuePreCreationSubscriber.php @@ -0,0 +1,50 @@ +<?php + +namespace Civi\Api4\Event\Subscriber; + +use Civi\Api4\Generic\DAOCreateAction; +use Civi\Api4\OptionGroup; + +class OptionValuePreCreationSubscriber extends PreCreationSubscriber { + + /** + * @param DAOCreateAction $request + */ + protected function modify(DAOCreateAction $request) { + $this->setOptionGroupId($request); + } + + /** + * @param DAOCreateAction $request + * + * @return bool + */ + protected function applies(DAOCreateAction $request) { + return $request->getEntityName() === 'OptionValue'; + } + + /** + * @param DAOCreateAction $request + * @throws \API_Exception + * @throws \Exception + */ + private function setOptionGroupId(DAOCreateAction $request) { + $optionGroupName = $request->getValue('option_group'); + if (!$optionGroupName || $request->getValue('option_group_id')) { + return; + } + + $optionGroup = OptionGroup::get() + ->setCheckPermissions(FALSE) + ->addSelect('id') + ->addWhere('name', '=', $optionGroupName) + ->execute(); + + if ($optionGroup->count() !== 1) { + throw new \Exception('Option group name must match only a single group'); + } + + $request->addValue('option_group_id', $optionGroup->first()['id']); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PermissionCheckSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PermissionCheckSubscriber.php new file mode 100644 index 00000000..62d542d0 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PermissionCheckSubscriber.php @@ -0,0 +1,65 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 4.7 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2017 | + +--------------------------------------------------------------------+ + | 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\Event\Subscriber; + +use Civi\API\Events; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * For any API requests that correspond to a Doctrine entity + * ($apiRequest['doctrineClass']), check permissions specified in + * Civi\API\Annotation\Permission. + */ +class PermissionCheckSubscriber implements EventSubscriberInterface { + /** + * @return array + */ + public static function getSubscribedEvents() { + return [ + Events::AUTHORIZE => [ + ['onApiAuthorize', Events::W_LATE], + ], + ]; + } + + /** + * @param \Civi\API\Event\AuthorizeEvent $event + * API authorization event. + */ + public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) { + /* @var \Civi\Api4\Generic\AbstractAction $apiRequest */ + $apiRequest = $event->getApiRequest(); + if ($apiRequest['version'] == 4) { + if (!$apiRequest->getCheckPermissions() || $apiRequest->isAuthorized()) { + $event->authorize(); + $event->stopPropagation(); + } + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php new file mode 100644 index 00000000..ff7e6d20 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php @@ -0,0 +1,369 @@ +<?php + +namespace Civi\Api4\Event\Subscriber; + +use Civi\Api4\Event\Events; +use Civi\Api4\Event\PostSelectQueryEvent; +use Civi\Api4\Query\Api4SelectQuery; +use Civi\Api4\Service\Schema\Joinable\Joinable; +use Civi\Api4\Utils\ArrayInsertionUtil; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Changes the results of a select query, doing 1-n joins and unserializing data + */ +class PostSelectQuerySubscriber implements EventSubscriberInterface { + + /** + * @inheritdoc + */ + public static function getSubscribedEvents() { + return [ + Events::POST_SELECT_QUERY => 'onPostQuery' + ]; + } + + /** + * @param PostSelectQueryEvent $event + */ + public function onPostQuery(PostSelectQueryEvent $event) { + $results = $event->getResults(); + $event->setResults($this->postRun($results, $event->getQuery())); + } + + /** + * @param array $results + * @param Api4SelectQuery $query + * + * @return array + */ + protected function postRun(array $results, Api4SelectQuery $query) { + if (empty($results)) { + return $results; + } + + $fieldSpec = $query->getApiFieldSpec(); + $this->unserializeFields($results, $query->getEntity(), $fieldSpec); + + // Group the selects to avoid queries for each field + $groupedSelects = $this->getJoinedDotSelects($query); + foreach ($groupedSelects as $finalAlias => $selects) { + $joinPath = $this->getJoinPathInfo($selects[0], $query); + $selects = $this->formatSelects($finalAlias, $selects, $query); + $joinResults = $this->getJoinResults($query, $finalAlias, $selects); + $this->formatJoinResults($joinResults, $query, $finalAlias); + + // Insert join results into original result + foreach ($results as &$primaryResult) { + $baseId = $primaryResult['id']; + $filtered = array_filter($joinResults, function ($res) use ($baseId) { + return ($res['_base_id'] === $baseId); + }); + $filtered = array_values($filtered); + ArrayInsertionUtil::insert($primaryResult, $joinPath, $filtered); + } + } + + return array_values($results); + } + + /** + * @param array $joinResults + * @param Api4SelectQuery $query + * @param string $alias + */ + private function formatJoinResults(&$joinResults, $query, $alias) { + $join = $query->getJoinedTable($alias); + $fields = []; + foreach ($join->getEntityFields() as $field) { + $name = explode('.', $field->getName()); + $fields[array_pop($name)] = $field->toArray(); + } + if ($fields) { + $this->unserializeFields($joinResults, NULL, $fields); + } + } + + /** + * Unserialize values + * + * @param array $results + * @param string $entity + * @param array $fields + */ + protected function unserializeFields(&$results, $entity, $fields = []) { + if (empty($fields)) { + $params = ['action' => 'get', 'includeCustom' => FALSE]; + $fields = civicrm_api4($entity, 'getFields', $params)->indexBy('name'); + } + + foreach ($results as &$result) { + foreach ($result as $field => &$value) { + if (!empty($fields[$field]['serialize']) && is_string($value)) { + $serializationType = $fields[$field]['serialize']; + $value = \CRM_Core_DAO::unSerializeField($value, $serializationType); + } + } + } + } + + /** + * @param Api4SelectQuery $query + * + * @return array + */ + private function getJoinedDotSelects(Api4SelectQuery $query) { + // Remove selects that are not in a joined table + $fkAliases = $query->getFkSelectAliases(); + $joinedDotSelects = array_filter( + $query->getSelect(), + function ($select) use ($fkAliases) { + return isset($fkAliases[$select]); + } + ); + + $selects = []; + // group related selects by alias so they can be executed in one query + foreach ($joinedDotSelects as $select) { + $parts = explode('.', $select); + $finalAlias = $parts[count($parts) - 2]; + $selects[$finalAlias][] = $select; + } + + // sort by depth, e.g. email selects should be done before email.location + uasort($selects, function ($a, $b) { + $aFirst = $a[0]; + $bFirst = $b[0]; + return substr_count($aFirst, '.') > substr_count($bFirst, '.'); + }); + + return $selects; + } + + + /** + * @param array $selects + * @param $serializationType + * @param Api4SelectQuery $query + * + * @return array + */ + private function getResultsForSerializedField( + array $selects, + $serializationType, + Api4SelectQuery $query + ) { + // Get the alias (Selects are grouped and all target the same table) + $sampleField = current($selects); + $alias = strstr($sampleField, '.', TRUE); + + // Fetch the results with the serialized field + $selects['serialized'] = $query::MAIN_TABLE_ALIAS . '.' . $alias; + $serializedResults = $this->runWithNewSelects($selects, $query); + $newResults = []; + + // Create a new results array, with a separate entry for each option value + foreach ($serializedResults as $result) { + $optionValues = \CRM_Core_DAO::unSerializeField( + $result['serialized'], + $serializationType + ); + unset($result['serialized']); + foreach ($optionValues as $value) { + $newResults[] = array_merge($result, ['value' => $value]); + } + } + + $optionValueValues = array_unique(array_column($newResults, 'value')); + $optionValues = $this->getOptionValuesFromValues( + $selects, + $query, + $optionValueValues + ); + $valueField = $alias . '.value'; + + // Index by value + foreach ($optionValues as $key => $subResult) { + $optionValues[$subResult['value']] = $subResult; + unset($subResult[$key]); + + // Exclude 'value' if not in original selects + if (!in_array($valueField, $selects)) { + unset($optionValues[$subResult['value']]['value']); + } + } + + // Replace serialized with the sub-select results + foreach ($newResults as &$result) { + $result = array_merge($result, $optionValues[$result['value']]); + unset($result['value']); + } + + return $newResults; + } + + + /** + * Prepares selects for the subquery to fetch join results + * + * @param string $alias + * @param array $selects + * @param Api4SelectQuery $query + * + * @return array + */ + private function formatSelects($alias, $selects, Api4SelectQuery $query) { + $mainAlias = $query::MAIN_TABLE_ALIAS; + $selectFields = []; + + foreach ($selects as $select) { + $selectAlias = $query->getFkSelectAliases()[$select]; + $fieldAlias = substr($select, strrpos($select, '.') + 1); + $selectFields[$fieldAlias] = $selectAlias; + } + + $firstSelect = $selects[0]; + $pathParts = explode('.', $firstSelect); + $numParts = count($pathParts); + $parentAlias = $numParts > 2 ? $pathParts[$numParts - 3] : $mainAlias; + + $selectFields['id'] = sprintf('%s.id', $alias); + $selectFields['_parent_id'] = $parentAlias . '.id'; + $selectFields['_base_id'] = $mainAlias . '.id'; + + return $selectFields; + } + + /** + * @param array $selects + * @param Api4SelectQuery $query + * + * @return array + */ + private function runWithNewSelects(array $selects, Api4SelectQuery $query) { + $aliasedSelects = array_map(function ($field, $alias) { + return sprintf('%s as "%s"', $field, $alias); + }, $selects, array_keys($selects)); + + $newSelect = sprintf('SELECT DISTINCT %s', implode(", ", $aliasedSelects)); + $sql = str_replace("\n", ' ', $query->getQuery()->toSQL()); + $originalSelect = substr($sql, 0, strpos($sql, ' FROM')); + $sql = str_replace($originalSelect, $newSelect, $sql); + + $relatedResults = []; + $resultDAO = \CRM_Core_DAO::executeQuery($sql); + while ($resultDAO->fetch()) { + $relatedResult = []; + foreach ($selects as $alias => $column) { + $returnName = $alias; + $alias = str_replace('.', '_', $alias); + if (property_exists($resultDAO, $alias)) { + $relatedResult[$returnName] = $resultDAO->$alias; + } + }; + $relatedResults[] = $relatedResult; + } + + return $relatedResults; + } + + /** + * @param Api4SelectQuery $query + * @param $alias + * @param $selects + * @return array + */ + protected function getJoinResults(Api4SelectQuery $query, $alias, $selects) { + $apiFieldSpec = $query->getApiFieldSpec(); + if (!empty($apiFieldSpec[$alias]['serialize'])) { + $type = $apiFieldSpec[$alias]['serialize']; + $joinResults = $this->getResultsForSerializedField($selects, $type, $query); + } + else { + $joinResults = $this->runWithNewSelects($selects, $query); + } + + // Remove results with no matching entries + $joinResults = array_filter($joinResults, function ($result) { + return !empty($result['id']); + }); + + return $joinResults; + } + + /** + * Separates a string like 'emails.location_type.label' into an array, where + * each value in the array tells whether it is 1-1 or 1-n join type + * + * @param string $pathString + * Dot separated path to the field + * @param Api4SelectQuery $query + * + * @return array + * Index is table alias and value is boolean whether is 1-to-many join + */ + private function getJoinPathInfo($pathString, $query) { + $pathParts = explode('.', $pathString); + array_pop($pathParts); // remove field + $path = []; + $isMultipleChecker = function($alias) use ($query) { + foreach ($query->getJoinedTables() as $table) { + if ($table->getAlias() === $alias) { + return $table->getJoinType() === Joinable::JOIN_TYPE_ONE_TO_MANY; + } + } + return FALSE; + }; + + foreach ($pathParts as $part) { + $path[$part] = $isMultipleChecker($part); + } + + return $path; + } + + /** + * Get all the option_value values required in the query + * + * @param array $selects + * @param Api4SelectQuery $query + * @param array $values + * + * @return array + */ + private function getOptionValuesFromValues( + array $selects, + Api4SelectQuery $query, + array $values + ) { + $sampleField = current($selects); + $alias = strstr($sampleField, '.', TRUE); + + // Get the option value table that was joined + $relatedTable = NULL; + foreach ($query->getJoinedTables() as $joinedTable) { + if ($joinedTable->getAlias() === $alias) { + $relatedTable = $joinedTable; + } + } + + // We only want subselects related to the joined table + $subSelects = array_filter($selects, function ($select) use ($alias) { + return strpos($select, $alias) === 0; + }); + + // Fetch all related option_value entries + $valueField = $alias . '.value'; + $subSelects[] = $valueField; + $tableName = $relatedTable->getTargetTable(); + $conditions = $relatedTable->getExtraJoinConditions(); + $conditions[] = $valueField . ' IN ("' . implode('", "', $values) . '")'; + $subQuery = new \CRM_Utils_SQL_Select($tableName . ' ' . $alias); + $subQuery->where($conditions); + $subQuery->select($subSelects); + $subResults = $subQuery->execute()->fetchAll(); + + return $subResults; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PreCreationSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PreCreationSubscriber.php new file mode 100644 index 00000000..6737a9f3 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PreCreationSubscriber.php @@ -0,0 +1,50 @@ +<?php + +namespace Civi\Api4\Event\Subscriber; + +use Civi\API\Event\PrepareEvent; +use Civi\Api4\Generic\DAOCreateAction; + +abstract class PreCreationSubscriber extends AbstractPrepareSubscriber { + /** + * @param PrepareEvent $event + */ + public function onApiPrepare(PrepareEvent $event) { + $apiRequest = $event->getApiRequest(); + if (!$apiRequest instanceof DAOCreateAction) { + return; + } + + $this->addDefaultCreationValues($apiRequest); + if ($this->applies($apiRequest)) { + $this->modify($apiRequest); + } + } + + /** + * Modify the request + * + * @param DAOCreateAction $request + * + * @return void + */ + abstract protected function modify(DAOCreateAction $request); + + /** + * Check if this subscriber should be applied to the request + * + * @param DAOCreateAction $request + * + * @return bool + */ + abstract protected function applies(DAOCreateAction $request); + + /** + * Sets default values common to all creation requests + * + * @param DAOCreateAction $request + */ + protected function addDefaultCreationValues(DAOCreateAction $request) { + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ValidateFieldsSubscriber.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ValidateFieldsSubscriber.php new file mode 100644 index 00000000..5e2c6f33 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ValidateFieldsSubscriber.php @@ -0,0 +1,97 @@ +<?php +/* + +--------------------------------------------------------------------+ + | CiviCRM version 4.7 | + +--------------------------------------------------------------------+ + | Copyright CiviCRM LLC (c) 2004-2017 | + +--------------------------------------------------------------------+ + | 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\Event\Subscriber; + +use Civi\API\Event\PrepareEvent; + +/** + * Validate field inputs based on annotations in the action class + */ +class ValidateFieldsSubscriber extends AbstractPrepareSubscriber { + + /** + * @param PrepareEvent $event + * @throws \Exception + */ + public function onApiPrepare(PrepareEvent $event) { + /** @var \Civi\Api4\Generic\AbstractAction $apiRequest */ + $apiRequest = $event->getApiRequest(); + if (is_a($apiRequest, 'Civi\Api4\Generic\AbstractAction')) { + $paramInfo = $apiRequest->getParamInfo(); + foreach ($paramInfo as $param => $info) { + $getParam = 'get' . ucfirst($param); + $value = $apiRequest->$getParam(); + // Required fields + if (!empty($info['required']) && (!$value && $value !== 0 && $value !== '0')) { + throw new \API_Exception('Parameter "' . $param . '" is required.'); + } + if (!empty($info['type']) && !self::checkType($value, $info['type'])) { + throw new \API_Exception('Parameter "' . $param . '" is not of the correct type. Expecting ' . implode(' or ', $info['type']) . '.'); + } + } + } + } + + /** + * Validate variable type on input + * + * @param $value + * @param $types + * @return bool + * @throws \API_Exception + */ + public static function checkType($value, $types) { + if ($value === NULL) { + return TRUE; + } + foreach ($types as $type) { + switch ($type) { + case 'array': + case 'bool': + case 'string': + case 'object': + $tester = 'is_' . $type; + if ($tester($value)) { + return TRUE; + } + break; + + case 'int': + if (\CRM_Utils_Rule::integer($value)) { + return TRUE; + } + break; + + default: + throw new \API_Exception('Unknown parameter type: ' . $type); + } + } + return FALSE; + } + +} 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); + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Group.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Group.php new file mode 100644 index 00000000..b82fa982 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Group.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * Group entity. + * + * @package Civi\Api4 + */ +class Group extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/GroupContact.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/GroupContact.php new file mode 100644 index 00000000..8901cd3b --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/GroupContact.php @@ -0,0 +1,29 @@ +<?php + +namespace Civi\Api4; + +/** + * GroupContact entity - link between groups and contacts. + * + * A contact can either be "Added" "Removed" or "Pending" in a group. + * CiviCRM only considers them to be "in" a group if their status is "Added". + * + * @package Civi\Api4 + */ +class GroupContact extends Generic\DAOEntity { + + /** + * @return Action\GroupContact\Create + */ + public static function create() { + return new Action\GroupContact\Create(__CLASS__, __FUNCTION__); + } + + /** + * @return Action\GroupContact\Update + */ + public static function update() { + return new Action\GroupContact\Update(__CLASS__, __FUNCTION__); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/IM.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/IM.php new file mode 100644 index 00000000..514f39a8 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/IM.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * IM entity. + * + * @package Civi\Api4 + */ +class IM extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Navigation.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Navigation.php new file mode 100644 index 00000000..31f2a911 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Navigation.php @@ -0,0 +1,19 @@ +<?php + +namespace Civi\Api4; + +/** + * Navigation entity. + * + * @package Civi\Api4 + */ +class Navigation extends Generic\DAOEntity { + + /** + * @return Action\Navigation\Get + */ + public static function get() { + return new Action\Navigation\Get(__CLASS__, __FUNCTION__); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Note.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Note.php new file mode 100644 index 00000000..55f6b7e6 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Note.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * Note entity. + * + * @package Civi\Api4 + */ +class Note extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OpenID.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OpenID.php new file mode 100644 index 00000000..a0c146aa --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OpenID.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * OpenID entity. + * + * @package Civi\Api4 + */ +class OpenID extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OptionGroup.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OptionGroup.php new file mode 100644 index 00000000..4821348a --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OptionGroup.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * OptionGroup entity. + * + * @package Civi\Api4 + */ +class OptionGroup extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OptionValue.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OptionValue.php new file mode 100644 index 00000000..16e9706c --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OptionValue.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * OptionValue entity. + * + * @package Civi\Api4 + */ +class OptionValue extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Participant.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Participant.php new file mode 100644 index 00000000..0e09c797 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Participant.php @@ -0,0 +1,19 @@ +<?php + +namespace Civi\Api4; + +/** + * Participant entity. + * + * @package Civi\Api4 + */ +class Participant extends Generic\DAOEntity { + + /** + * @return Action\Participant\Get + */ + public static function get() { + return new Action\Participant\Get(__CLASS__, __FUNCTION__); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Phone.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Phone.php new file mode 100644 index 00000000..a02cd7cd --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Phone.php @@ -0,0 +1,16 @@ +<?php + +namespace Civi\Api4; + +/** + * Phone entity. + * + * This entity allows user to add, update, retrieve or delete phone number(s) of a contact. + * + * Creating a new phone of a contact, requires at minimum a contact's ID and phone number + * + * @package Civi\Api4 + */ +class Phone extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Provider/ActionObjectProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Provider/ActionObjectProvider.php new file mode 100644 index 00000000..60596d92 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Provider/ActionObjectProvider.php @@ -0,0 +1,155 @@ +<?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\Provider; + +use Civi\API\Event\ResolveEvent; +use Civi\API\Provider\ProviderInterface; +use Civi\Api4\Generic\AbstractAction; +use Civi\API\Events; +use Civi\Api4\Generic\Result; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Accept $apiRequests based on \Civi\API\Action + */ +class ActionObjectProvider implements EventSubscriberInterface, ProviderInterface { + + /** + * @return array + */ + public static function getSubscribedEvents() { + // Using a high priority allows adhoc implementations + // to override standard implementations -- which is + // handy for testing/mocking. + return [ + Events::RESOLVE => [ + ['onApiResolve', Events::W_EARLY], + ], + ]; + } + /** + * @param ResolveEvent $event + * API resolution event. + */ + public function onApiResolve(ResolveEvent $event) { + $apiRequest = $event->getApiRequest(); + if ($apiRequest instanceof AbstractAction) { + $event->setApiRequest($apiRequest); + $event->setApiProvider($this); + $event->stopPropagation(); + } + } + + /** + * @inheritDoc + * + * @param AbstractAction $action + * + * @return array|mixed + */ + public function invoke($action) { + $result = new Result(); + $result->action = $action->getActionName(); + $result->entity = $action->getEntityName(); + $action->_run($result); + $this->handleChains($action, $result); + return $result; + } + + /** + * Run each chained action once per row + * + * @param AbstractAction $action + * @param Result $result + */ + protected function handleChains($action, $result) { + foreach ($action->getChain() as $name => $request) { + $request += [NULL, NULL, [], NULL]; + $request[2]['checkPermissions'] = $action->getCheckPermissions(); + foreach ($result as &$row) { + $row[$name] = $this->runChain($request, $row); + } + } + } + + /** + * Run a chained action + * + * @param $request + * @param $row + * @return array|\Civi\Api4\Generic\Result|null + * @throws \API_Exception + */ + protected function runChain($request, $row) { + list($entity, $action, $params, $index) = $request; + // Swap out variables in $entity, $action & $params + $this->resolveChainLinks($entity, $row); + $this->resolveChainLinks($action, $row); + $this->resolveChainLinks($params, $row); + return (array) civicrm_api4($entity, $action, $params, $index); + } + + /** + * Swap out variable names + * + * @param mixed $val + * @param array $result + */ + protected function resolveChainLinks(&$val, $result) { + if (is_array($val)) { + foreach ($val as &$v) { + $this->resolveChainLinks($v, $result); + } + } + elseif (is_string($val) && strlen($val) > 1 && substr($val, 0, 1) === '$') { + $val = \CRM_Utils_Array::pathGet($result, explode('.', substr($val, 1))); + } + } + + /** + * @inheritDoc + * @param int $version + * @return array + */ + public function getEntityNames($version) { + /** FIXME */ + return []; + } + + /** + * @inheritDoc + * @param int $version + * @param string $entity + * @return array + */ + public function getActionNames($version, $entity) { + /** FIXME Civi\API\V4\Action\GetActions */ + return []; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Query/Api4SelectQuery.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Query/Api4SelectQuery.php new file mode 100644 index 00000000..a7a912da --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Query/Api4SelectQuery.php @@ -0,0 +1,535 @@ +<?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\Query; + +use Civi\API\SelectQuery; +use Civi\Api4\Event\Events; +use Civi\Api4\Event\PostSelectQueryEvent; +use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable; +use Civi\Api4\Service\Schema\Joinable\Joinable; +use Civi\Api4\Utils\FormattingUtil; +use Civi\Api4\Utils\CoreUtil; +use CRM_Core_DAO_AllCoreTables as TableHelper; +use CRM_Core_DAO_CustomField as CustomFieldDAO; +use CRM_Utils_Array as UtilsArray; + +/** + * A query `node` may be in one of three formats: + * + * * leaf: [$fieldName, $operator, $criteria] + * * negated: ['NOT', $node] + * * branch: ['OR|NOT', [$node, $node, ...]] + * + * Leaf operators are one of: + * + * * '=', '<=', '>=', '>', '<', 'LIKE', "<>", "!=", + * * "NOT LIKE", 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', + * * 'IS NOT NULL', or 'IS NULL'. + */ +class Api4SelectQuery extends SelectQuery { + + /** + * @var int + */ + protected $apiVersion = 4; + + /** + * @var array + * Maps select fields to [<table_alias>, <column_alias>] + */ + protected $fkSelectAliases = []; + + /** + * @var Joinable[] + * The joinable tables that have been joined so far + */ + protected $joinedTables = []; + + /** + * @param string $entity + * @param bool $checkPermissions + */ + public function __construct($entity, $checkPermissions) { + require_once 'api/v3/utils.php'; + $this->entity = $entity; + $this->checkPermissions = $checkPermissions; + + $baoName = CoreUtil::getDAOFromApiName($entity); + $bao = new $baoName(); + + $this->entityFieldNames = _civicrm_api3_field_names(_civicrm_api3_build_fields_array($bao)); + $this->apiFieldSpec = $this->getFields(); + + \CRM_Utils_SQL_Select::from($this->getTableName($baoName) . ' ' . self::MAIN_TABLE_ALIAS); + + // Add ACLs first to avoid redundant subclauses + $this->query->where($this->getAclClause(self::MAIN_TABLE_ALIAS, $baoName)); + } + + /** + * Why walk when you can + * + * @return array|int + */ + public function run() { + $this->preRun(); + $baseResults = parent::run(); + $event = new PostSelectQueryEvent($baseResults, $this); + \Civi::dispatcher()->dispatch(Events::POST_SELECT_QUERY, $event); + + return $event->getResults(); + } + + /** + * Gets all FK fields and does the required joins + */ + protected function preRun() { + $allFields = array_merge($this->select, array_keys($this->orderBy)); + $recurse = function($clauses) use (&$allFields, &$recurse) { + foreach ($clauses as $clause) { + if ($clause[0] === 'NOT' && is_string($clause[1][0])) { + $recurse($clause[1][1]); + } + elseif (in_array($clause[0], ['AND', 'OR', 'NOT'])) { + $recurse($clause[1]); + } + elseif (is_array($clause[0])) { + array_walk($clause, $recurse); + } + else { + $allFields[] = $clause[0]; + } + } + }; + $recurse($this->where); + $dotFields = array_unique(array_filter($allFields, function ($field) { + return strpos($field, '.') !== FALSE; + })); + + foreach ($dotFields as $dotField) { + $this->joinFK($dotField); + } + } + + /** + * Populate $this->selectFields + * + * @throws \Civi\API\Exception\UnauthorizedException + */ + protected function buildSelectFields() { + $return_all_fields = (empty($this->select) || !is_array($this->select)); + $return = $return_all_fields ? $this->entityFieldNames : $this->select; + if ($return_all_fields || in_array('custom', $this->select)) { + foreach (array_keys($this->apiFieldSpec) as $fieldName) { + if (strpos($fieldName, 'custom_') === 0) { + $return[] = $fieldName; + } + } + } + + // Always select the ID if the table has one. + if (array_key_exists('id', $this->apiFieldSpec) || strstr($this->entity, 'Custom_')) { + $this->selectFields[self::MAIN_TABLE_ALIAS . ".id"] = "id"; + } + + // core return fields + foreach ($return as $fieldName) { + $field = $this->getField($fieldName); + if ($field && in_array($field['name'], $this->entityFieldNames)) { + $this->selectFields[self::MAIN_TABLE_ALIAS . "." . UtilsArray::value('column_name', $field, $field['name'])] = $field['name']; + } + elseif (strpos($fieldName, '.')) { + $fkField = $this->addFkField($fieldName, 'LEFT'); + if ($fkField) { + $this->selectFields[implode('.', $fkField)] = $fieldName; + } + } + elseif ($field && strpos($fieldName, 'custom_') === 0) { + list($table_name, $column_name) = $this->addCustomField($field, 'LEFT'); + + if ($field['data_type'] != 'ContactReference') { + // 'ordinary' custom field. We will select the value as custom_XX. + $this->selectFields["$table_name.$column_name"] = $fieldName; + } + else { + // contact reference custom field. The ID will be stored in custom_XX_id. + // custom_XX will contain the sort name of the contact. + $this->query->join("c_$fieldName", "LEFT JOIN civicrm_contact c_$fieldName ON c_$fieldName.id = `$table_name`.`$column_name`"); + $this->selectFields["$table_name.$column_name"] = $fieldName . "_id"; + // We will call the contact table for the join c_XX. + $this->selectFields["c_$fieldName.sort_name"] = $fieldName; + } + } + } + } + + /** + * @inheritDoc + */ + protected function buildWhereClause() { + foreach ($this->where as $clause) { + $sql_clause = $this->treeWalkWhereClause($clause); + $this->query->where($sql_clause); + } + } + + /** + * @inheritDoc + */ + protected function buildOrderBy() { + foreach ($this->orderBy as $field => $dir) { + if ($dir !== 'ASC' && $dir !== 'DESC') { + throw new \API_Exception("Invalid sort direction. Cannot order by $field $dir"); + } + if ($this->getField($field)) { + $this->query->orderBy(self::MAIN_TABLE_ALIAS . '.' . $field . " $dir"); + } + // TODO: Handle joined fields, custom fields, etc. + else { + throw new \API_Exception("Invalid sort field. Cannot order by $field $dir"); + } + } + } + + /** + * Recursively validate and transform a branch or leaf clause array to SQL. + * + * @param array $clause + * @return string SQL where clause + * + * @uses validateClauseAndComposeSql() to generate the SQL etc. + * @todo if an 'and' is nested within and 'and' (or or-in-or) then should + * flatten that to be a single list of clauses. + */ + protected function treeWalkWhereClause($clause) { + switch ($clause[0]) { + case 'OR': + case 'AND': + // handle branches + if (count($clause[1]) === 1) { + // a single set so AND|OR is immaterial + return $this->treeWalkWhereClause($clause[1][0]); + } + else { + $sql_subclauses = []; + foreach ($clause[1] as $subclause) { + $sql_subclauses[] = $this->treeWalkWhereClause($subclause); + } + return '(' . implode("\n" . $clause[0], $sql_subclauses) . ')'; + } + + case 'NOT': + // If we get a group of clauses with no operator, assume AND + if (!is_string($clause[1][0])) { + $clause[1] = ['AND', $clause[1]]; + } + return 'NOT (' . $this->treeWalkWhereClause($clause[1]) . ')'; + + default: + return $this->validateClauseAndComposeSql($clause); + } + } + + /** + * Validate and transform a leaf clause array to SQL. + * @param array $clause [$fieldName, $operator, $criteria] + * @return string SQL + * @throws \API_Exception + * @throws \Exception + */ + protected function validateClauseAndComposeSql($clause) { + // Pad array for unary operators + list($key, $operator, $value) = array_pad($clause, 3, NULL); + $fieldSpec = $this->getField($key); + // derive table and column: + $table_name = NULL; + $column_name = NULL; + if (in_array($key, $this->entityFieldNames)) { + $table_name = self::MAIN_TABLE_ALIAS; + $column_name = $key; + } + elseif (strpos($key, '.') && isset($this->fkSelectAliases[$key])) { + list($table_name, $column_name) = explode('.', $this->fkSelectAliases[$key]); + } + + if (!$table_name || !$column_name) { + throw new \API_Exception("Invalid field '$key' in where clause."); + } + + FormattingUtil::formatValue($value, $fieldSpec, $this->getEntity()); + + $sql_clause = \CRM_Core_DAO::createSQLFilter("`$table_name`.`$column_name`", [$operator => $value]); + if ($sql_clause === NULL) { + throw new \API_Exception("Invalid value in where clause for field '$key'"); + } + return $sql_clause; + } + + /** + * @inheritDoc + */ + protected function getFields() { + $fields = civicrm_api4($this->entity, 'getFields', ['action' => 'get', 'checkPermissions' => $this->checkPermissions, 'includeCustom' => FALSE])->indexBy('name'); + return (array) $fields; + } + + /** + * Fetch a field from the getFields list + * + * @param string $fieldName + * + * @return string|null + */ + protected function getField($fieldName) { + if ($fieldName) { + $fieldPath = explode('.', $fieldName); + if (count($fieldPath) > 1) { + $fieldName = implode('.', array_slice($fieldPath, -2)); + } + return UtilsArray::value($fieldName, $this->apiFieldSpec); + } + return NULL; + } + + /** + * @param $key + * @throws \API_Exception + */ + protected function joinFK($key) { + $pathArray = explode('.', $key); + + if (count($pathArray) < 2) { + return; + } + + /** @var \Civi\Api4\Service\Schema\Joiner $joiner */ + $joiner = \Civi::container()->get('joiner'); + $field = array_pop($pathArray); + $pathString = implode('.', $pathArray); + + if (!$joiner->canJoin($this, $pathString)) { + return; + } + + $joinPath = $joiner->join($this, $pathString); + /** @var Joinable $lastLink */ + $lastLink = array_pop($joinPath); + + // Cache field info for retrieval by $this->getField() + $prefix = array_pop($pathArray) . '.'; + if (!isset($this->apiFieldSpec[$prefix . $field])) { + $joinEntity = $lastLink->getEntity(); + // Custom fields are already prefixed + if ($lastLink instanceof CustomGroupJoinable) { + $prefix = ''; + } + foreach ($lastLink->getEntityFields() as $fieldObject) { + $this->apiFieldSpec[$prefix . $fieldObject->getName()] = $fieldObject->toArray() + ['entity' => $joinEntity]; + } + } + + if (!$lastLink->getField($field)) { + throw new \API_Exception('Invalid join'); + } + + // custom groups use aliases for field names + if ($lastLink instanceof CustomGroupJoinable) { + $field = $lastLink->getSqlColumn($field); + } + + $this->fkSelectAliases[$key] = sprintf('%s.%s', $lastLink->getAlias(), $field); + } + + /** + * @param Joinable $joinable + * + * @return $this + */ + public function addJoinedTable(Joinable $joinable) { + $this->joinedTables[] = $joinable; + + return $this; + } + + /** + * @return FALSE|string + */ + public function getFrom() { + return TableHelper::getTableForClass(TableHelper::getFullName($this->entity)); + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @return array + */ + public function getSelect() { + return $this->select; + } + + /** + * @return array + */ + public function getWhere() { + return $this->where; + } + + /** + * @return array + */ + public function getOrderBy() { + return $this->orderBy; + } + + /** + * @return mixed + */ + public function getLimit() { + return $this->limit; + } + + /** + * @return mixed + */ + public function getOffset() { + return $this->offset; + } + + /** + * @return array + */ + public function getSelectFields() { + return $this->selectFields; + } + + /** + * @return bool + */ + public function isFillUniqueFields() { + return $this->isFillUniqueFields; + } + + /** + * @return \CRM_Utils_SQL_Select + */ + public function getQuery() { + return $this->query; + } + + /** + * @return array + */ + public function getJoins() { + return $this->joins; + } + + /** + * @return array + */ + public function getApiFieldSpec() { + return $this->apiFieldSpec; + } + + /** + * @return array + */ + public function getEntityFieldNames() { + return $this->entityFieldNames; + } + + /** + * @return array + */ + public function getAclFields() { + return $this->aclFields; + } + + /** + * @return bool|string + */ + public function getCheckPermissions() { + return $this->checkPermissions; + } + + /** + * @return int + */ + public function getApiVersion() { + return $this->apiVersion; + } + + /** + * @return array + */ + public function getFkSelectAliases() { + return $this->fkSelectAliases; + } + + /** + * @return Joinable[] + */ + public function getJoinedTables() { + return $this->joinedTables; + } + + /** + * @return Joinable + */ + public function getJoinedTable($alias) { + foreach ($this->joinedTables as $join) { + if ($join->getAlias() == $alias) { + return $join; + } + } + } + + /** + * Get table name on basis of entity + * + * @param string $baoName + * + * @return void + */ + public function getTableName($baoName) { + if (strstr($this->entity, 'Custom_')) { + $this->query = \CRM_Utils_SQL_Select::from(CoreUtil::getCustomTableByName(str_replace('Custom_', '', $this->entity)) . ' ' . self::MAIN_TABLE_ALIAS); + $this->entityFieldNames = array_keys($this->apiFieldSpec); + } + else { + $bao = new $baoName(); + $this->query = \CRM_Utils_SQL_Select::from($bao->tableName() . ' ' . self::MAIN_TABLE_ALIAS); + $bao->free(); + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Relationship.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Relationship.php new file mode 100644 index 00000000..5161f05f --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Relationship.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * Relationship entity. + * + * @package Civi\Api4 + */ +class Relationship extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/RelationshipType.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/RelationshipType.php new file mode 100644 index 00000000..1cd335cd --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/RelationshipType.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * RelationshipType entity. + * + * @package Civi\Api4 + */ +class RelationshipType extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php new file mode 100644 index 00000000..191f4389 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php @@ -0,0 +1,40 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +class ActivityToActivityContactAssigneesJoinable extends Joinable { + /** + * @var string + */ + protected $baseTable = 'civicrm_activity'; + + /** + * @var string + */ + protected $baseColumn = 'id'; + + /** + * @param $alias + */ + public function __construct($alias) { + $optionValueTable = 'civicrm_option_value'; + $optionGroupTable = 'civicrm_option_group'; + + $subSubSelect = sprintf( + 'SELECT id FROM %s WHERE name = "%s"', + $optionGroupTable, + 'activity_contacts' + ); + + $subSelect = sprintf( + 'SELECT value FROM %s WHERE name = "%s" AND option_group_id = (%s)', + $optionValueTable, + 'Activity Assignees', + $subSubSelect + ); + + $this->addCondition(sprintf('%s.record_type_id = (%s)', $alias, $subSelect)); + parent::__construct('civicrm_activity_contact', 'activity_id', $alias); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php new file mode 100644 index 00000000..370c5898 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php @@ -0,0 +1,23 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +class BridgeJoinable extends Joinable { + /** + * @var Joinable + */ + protected $middleLink; + + public function __construct($targetTable, $targetColumn, $alias, Joinable $middleLink) { + parent::__construct($targetTable, $targetColumn, $alias); + $this->middleLink = $middleLink; + } + + /** + * @return Joinable + */ + public function getMiddleLink() { + return $this->middleLink; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php new file mode 100644 index 00000000..a1dd1a1d --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php @@ -0,0 +1,71 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +use Civi\Api4\CustomField; + +class CustomGroupJoinable extends Joinable { + + /** + * @var string + */ + protected $joinSide = self::JOIN_SIDE_LEFT; + + /** + * @var string + * + * Name of the custom field column. + */ + protected $columns; + + /** + * @param $targetTable + * @param $alias + * @param bool $isMultiRecord + * @param null $entity + */ + public function __construct($targetTable, $alias, $isMultiRecord, $entity, $columns) { + $this->entity = $entity; + $this->columns = $columns; + parent::__construct($targetTable, 'entity_id', $alias); + $this->joinType = $isMultiRecord ? + self::JOIN_TYPE_ONE_TO_MANY : self::JOIN_TYPE_ONE_TO_ONE; + } + + /** + * @inheritDoc + */ + public function getEntityFields() { + if (!$this->entityFields) { + $fields = CustomField::get() + ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_required', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value']) + ->addWhere('custom_group.table_name', '=', $this->getTargetTable()) + ->execute(); + foreach ($fields as $field) { + $this->entityFields[] = \Civi\Api4\Service\Spec\SpecFormatter::arrayToField($field, $this->getEntity()); + } + } + return $this->entityFields; + } + + /** + * @inheritDoc + */ + public function getField($fieldName) { + foreach ($this->getEntityFields() as $field) { + $name = $field->getName(); + if ($name === $fieldName || strrpos($name, '.' . $fieldName) === strlen($name) - strlen($fieldName) - 1) { + return $field; + } + } + return NULL; + } + + /** + * @return string + */ + public function getSqlColumn($fieldName) { + return $this->columns[$fieldName]; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php new file mode 100644 index 00000000..0e92e3ab --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php @@ -0,0 +1,277 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +use Civi\Api4\Service\Spec\FieldSpec; +use CRM_Core_DAO_AllCoreTables as Tables; + +class Joinable { + + const JOIN_SIDE_LEFT = 'LEFT'; + const JOIN_SIDE_INNER = 'INNER'; + + const JOIN_TYPE_ONE_TO_ONE = '1_to_1'; + const JOIN_TYPE_MANY_TO_ONE = 'n_to_1'; + const JOIN_TYPE_ONE_TO_MANY = '1_to_n'; + + /** + * @var string + */ + protected $baseTable; + + /** + * @var string + */ + protected $baseColumn; + + /** + * @var string + */ + protected $targetTable; + + /** + * @var string + * + * Name (or alias) of the target column) + */ + protected $targetColumn; + + /** + * @var string + */ + protected $alias; + + /** + * @var array + */ + protected $conditions = []; + + /** + * @var string + */ + protected $joinSide = self::JOIN_SIDE_LEFT; + + /** + * @var int + */ + protected $joinType = self::JOIN_TYPE_ONE_TO_ONE; + + /** + * @var string + */ + protected $entity; + + /** + * @var array + */ + protected $entityFields; + + /** + * @param $targetTable + * @param $targetColumn + * @param string|null $alias + */ + public function __construct($targetTable, $targetColumn, $alias = NULL) { + $this->targetTable = $targetTable; + $this->targetColumn = $targetColumn; + if (!$this->entity) { + $this->entity = Tables::getBriefName(Tables::getClassForTable($targetTable)); + } + $this->alias = $alias ?: str_replace('civicrm_', '', $targetTable); + } + + /** + * Gets conditions required when joining to a base table + * + * @param string|null $baseTableAlias + * Name of the base table, if aliased. + * + * @return array + */ + public function getConditionsForJoin($baseTableAlias = NULL) { + $baseCondition = sprintf( + '%s.%s = %s.%s', + $baseTableAlias ?: $this->baseTable, + $this->baseColumn, + $this->getAlias(), + $this->targetColumn + ); + + return array_merge([$baseCondition], $this->conditions); + } + + /** + * @return string + */ + public function getBaseTable() { + return $this->baseTable; + } + + /** + * @param string $baseTable + * + * @return $this + */ + public function setBaseTable($baseTable) { + $this->baseTable = $baseTable; + + return $this; + } + + /** + * @return string + */ + public function getBaseColumn() { + return $this->baseColumn; + } + + /** + * @param string $baseColumn + * + * @return $this + */ + public function setBaseColumn($baseColumn) { + $this->baseColumn = $baseColumn; + + return $this; + } + + /** + * @return string + */ + public function getAlias() { + return $this->alias; + } + + /** + * @param string $alias + * + * @return $this + */ + public function setAlias($alias) { + $this->alias = $alias; + + return $this; + } + + /** + * @return string + */ + public function getTargetTable() { + return $this->targetTable; + } + + /** + * @return string + */ + public function getTargetColumn() { + return $this->targetColumn; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @param $condition + * + * @return $this + */ + public function addCondition($condition) { + $this->conditions[] = $condition; + + return $this; + } + + /** + * @return array + */ + public function getExtraJoinConditions() { + return $this->conditions; + } + + /** + * @param array $conditions + * + * @return $this + */ + public function setConditions($conditions) { + $this->conditions = $conditions; + + return $this; + } + + /** + * @return string + */ + public function getJoinSide() { + return $this->joinSide; + } + + /** + * @param string $joinSide + * + * @return $this + */ + public function setJoinSide($joinSide) { + $this->joinSide = $joinSide; + + return $this; + } + + /** + * @return int + */ + public function getJoinType() { + return $this->joinType; + } + + /** + * @param int $joinType + * + * @return $this + */ + public function setJoinType($joinType) { + $this->joinType = $joinType; + + return $this; + } + + /** + * @return array + */ + public function toArray() { + return get_object_vars($this); + } + + /** + * @return \Civi\Api4\Service\Spec\FieldSpec[] + */ + public function getEntityFields() { + if (!$this->entityFields) { + $bao = Tables::getClassForTable($this->getTargetTable()); + if ($bao) { + foreach ($bao::fields() as $field) { + $this->entityFields[] = \Civi\Api4\Service\Spec\SpecFormatter::arrayToField($field, $this->getEntity()); + } + } + } + return $this->entityFields; + } + + /** + * @return \Civi\Api4\Service\Spec\FieldSpec|NULL + */ + public function getField($fieldName) { + foreach ($this->getEntityFields() as $field) { + if ($field->getName() === $fieldName) { + return $field; + } + } + return NULL; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php new file mode 100644 index 00000000..96f65488 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php @@ -0,0 +1,61 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +class OptionValueJoinable extends Joinable { + /** + * @var string + */ + protected $optionGroupName; + + /** + * @param string $optionGroup + * Can be either the option group name or ID + * @param string|null $alias + * The join alias + * @param string $keyColumn + * Which column to use to join, defaults to "value" + */ + public function __construct($optionGroup, $alias = NULL, $keyColumn = 'value') { + $this->optionGroupName = $optionGroup; + $optionValueTable = 'civicrm_option_value'; + + // default join alias to option group name, e.g. activity_type + if (!$alias && !is_numeric($optionGroup)) { + $alias = $optionGroup; + } + + parent::__construct($optionValueTable, $keyColumn, $alias); + + if (!is_numeric($optionGroup)) { + $subSelect = 'SELECT id FROM civicrm_option_group WHERE name = "%s"'; + $subQuery = sprintf($subSelect, $optionGroup); + $condition = sprintf('%s.option_group_id = (%s)', $alias, $subQuery); + } + else { + $condition = sprintf('%s.option_group_id = %d', $alias, $optionGroup); + } + + $this->addCondition($condition); + } + + /** + * The existing condition must also be re-aliased + * + * @param string $alias + * + * @return $this + */ + public function setAlias($alias) { + foreach ($this->conditions as $index => $condition) { + $search = $this->alias . '.'; + $replace = $alias . '.'; + $this->conditions[$index] = str_replace($search, $replace, $condition); + } + + parent::setAlias($alias); + + return $this; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php new file mode 100644 index 00000000..cb30ab57 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php @@ -0,0 +1,98 @@ +<?php + +namespace Civi\Api4\Service\Schema; + +use Civi\Api4\Query\Api4SelectQuery; +use Civi\Api4\Service\Schema\Joinable\Joinable; + +class Joiner { + /** + * @var SchemaMap + */ + protected $schemaMap; + + /** + * @var Joinable[][] + */ + protected $cache = []; + + /** + * @param SchemaMap $schemaMap + */ + public function __construct(SchemaMap $schemaMap) { + $this->schemaMap = $schemaMap; + } + + /** + * @param Api4SelectQuery $query + * The query object to do the joins on + * @param string $joinPath + * A path of aliases in dot notation, e.g. contact.phone + * @param string $side + * Can be LEFT or INNER + * + * @throws \Exception + * @return Joinable[] + * The path used to make the join + */ + public function join(Api4SelectQuery $query, $joinPath, $side = 'LEFT') { + $fullPath = $this->getPath($query->getFrom(), $joinPath); + $baseTable = $query::MAIN_TABLE_ALIAS; + + foreach ($fullPath as $link) { + $target = $link->getTargetTable(); + $alias = $link->getAlias(); + $conditions = $link->getConditionsForJoin($baseTable); + + $query->join($side, $target, $alias, $conditions); + $query->addJoinedTable($link); + + $baseTable = $link->getAlias(); + } + + return $fullPath; + } + + /** + * @param Api4SelectQuery $query + * @param $joinPath + * + * @return bool + */ + public function canJoin(Api4SelectQuery $query, $joinPath) { + return !empty($this->getPath($query->getFrom(), $joinPath)); + } + + /** + * @param string $baseTable + * @param string $joinPath + * + * @return array + * @throws \Exception + */ + protected function getPath($baseTable, $joinPath) { + $cacheKey = sprintf('%s.%s', $baseTable, $joinPath); + if (!isset($this->cache[$cacheKey])) { + $stack = explode('.', $joinPath); + $fullPath = []; + + foreach ($stack as $key => $targetAlias) { + $links = $this->schemaMap->getPath($baseTable, $targetAlias); + + if (empty($links)) { + throw new \Exception(sprintf('Cannot join %s to %s', $baseTable, $targetAlias)); + } + else { + $fullPath = array_merge($fullPath, $links); + $lastLink = end($links); + $baseTable = $lastLink->getTargetTable(); + } + } + + $this->cache[$cacheKey] = $fullPath; + } + + return $this->cache[$cacheKey]; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php new file mode 100644 index 00000000..3989afeb --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php @@ -0,0 +1,140 @@ +<?php + +namespace Civi\Api4\Service\Schema; + +use Civi\Api4\Service\Schema\Joinable\BridgeJoinable; +use Civi\Api4\Service\Schema\Joinable\Joinable; + +class SchemaMap { + + const MAX_JOIN_DEPTH = 3; + + /** + * @var Table[] + */ + protected $tables = []; + + /** + * @param $baseTableName + * @param $targetTableAlias + * + * @return Joinable[] + * Array of links to the target table, empty if no path found + */ + public function getPath($baseTableName, $targetTableAlias) { + $table = $this->getTableByName($baseTableName); + $path = []; + + if (!$table) { + return $path; + } + + $this->findPaths($table, $targetTableAlias, 1, $path); + + foreach ($path as $index => $pathLink) { + if ($pathLink instanceof BridgeJoinable) { + $start = array_slice($path, 0, $index); + $middle = [$pathLink->getMiddleLink()]; + $end = array_slice($path, $index, count($path) - $index); + $path = array_merge($start, $middle, $end); + } + } + + return $path; + } + + /** + * @return Table[] + */ + public function getTables() { + return $this->tables; + } + + /** + * @param $name + * + * @return Table|null + */ + public function getTableByName($name) { + foreach ($this->tables as $table) { + if ($table->getName() === $name) { + return $table; + } + } + + return NULL; + } + + /** + * Adds a table to the schema map if it has not already been added + * + * @param Table $table + * + * @return $this + */ + public function addTable(Table $table) { + if (!$this->getTableByName($table->getName())) { + $this->tables[] = $table; + } + + return $this; + } + + /** + * @param array $tables + */ + public function addTables(array $tables) { + foreach ($tables as $table) { + $this->addTable($table); + } + } + + /** + * Recursive function to traverse the schema looking for a path + * + * @param Table $table + * The current table to base fromm + * @param string $target + * The target joinable table alias + * @param int $depth + * The current level of recursion which reflects the number of joins needed + * @param Joinable[] $path + * (By-reference) The possible paths to the target table + * @param Joinable[] $currentPath + * For internal use only to track the path to reach the target table + */ + private function findPaths(Table $table, $target, $depth, &$path, $currentPath = [] + ) { + static $visited = []; + + // reset if new call + if ($depth === 1) { + $visited = []; + } + + $canBeShorter = empty($path) || count($currentPath) + 1 < count($path); + $tooFar = $depth > self::MAX_JOIN_DEPTH; + $beenHere = in_array($table->getName(), $visited); + + if ($tooFar || $beenHere || !$canBeShorter) { + return; + } + + // prevent circular reference + $visited[] = $table->getName(); + + foreach ($table->getExternalLinks() as $link) { + if ($link->getAlias() === $target) { + $path = array_merge($currentPath, [$link]); + } + else { + $linkTable = $this->getTableByName($link->getTargetTable()); + if ($linkTable) { + $nextStep = array_merge($currentPath, [$link]); + $this->findPaths($linkTable, $target, $depth + 1, $path, $nextStep); + } + } + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php new file mode 100644 index 00000000..b578b73a --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php @@ -0,0 +1,217 @@ +<?php + +namespace Civi\Api4\Service\Schema; + +use Civi\Api4\Entity; +use Civi\Api4\Event\Events; +use Civi\Api4\Event\SchemaMapBuildEvent; +use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable; +use Civi\Api4\Service\Schema\Joinable\Joinable; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Civi\Api4\Service\Schema\Joinable\OptionValueJoinable; +use CRM_Core_DAO_AllCoreTables as TableHelper; +use CRM_Utils_Array as UtilsArray; + +class SchemaMapBuilder { + /** + * @var EventDispatcherInterface + */ + protected $dispatcher; + /** + * @var array + */ + protected $apiEntities; + + /** + * @param EventDispatcherInterface $dispatcher + */ + public function __construct(EventDispatcherInterface $dispatcher) { + $this->dispatcher = $dispatcher; + $this->apiEntities = array_keys((array) Entity::get()->setCheckPermissions(FALSE)->addSelect('name')->execute()->indexBy('name')); + } + + /** + * @return SchemaMap + */ + public function build() { + $map = new SchemaMap(); + $this->loadTables($map); + + $event = new SchemaMapBuildEvent($map); + $this->dispatcher->dispatch(Events::SCHEMA_MAP_BUILD, $event); + + return $map; + } + + /** + * Add all tables and joins + * + * @param SchemaMap $map + */ + private function loadTables(SchemaMap $map) { + /** @var \CRM_Core_DAO $daoName */ + foreach (TableHelper::get() as $daoName => $data) { + $table = new Table($data['table']); + foreach ($daoName::fields() as $field => $fieldData) { + $this->addJoins($table, $field, $fieldData); + } + $map->addTable($table); + if (in_array($data['name'], $this->apiEntities)) { + $this->addCustomFields($map, $table, $data['name']); + } + } + + $this->addBackReferences($map); + } + + /** + * @param Table $table + * @param string $field + * @param array $data + */ + private function addJoins(Table $table, $field, array $data) { + $fkClass = UtilsArray::value('FKClassName', $data); + + // can there be multiple methods e.g. pseudoconstant and fkclass + if ($fkClass) { + $tableName = TableHelper::getTableForClass($fkClass); + $fkKey = UtilsArray::value('FKKeyColumn', $data, 'id'); + $alias = str_replace('_id', '', $field); + $joinable = new Joinable($tableName, $fkKey, $alias); + $joinable->setJoinType($joinable::JOIN_TYPE_MANY_TO_ONE); + $table->addTableLink($field, $joinable); + } + elseif (UtilsArray::value('pseudoconstant', $data)) { + $this->addPseudoConstantJoin($table, $field, $data); + } + } + + /** + * @param Table $table + * @param string $field + * @param array $data + */ + private function addPseudoConstantJoin(Table $table, $field, array $data) { + $pseudoConstant = UtilsArray::value('pseudoconstant', $data); + $tableName = UtilsArray::value('table', $pseudoConstant); + $optionGroupName = UtilsArray::value('optionGroupName', $pseudoConstant); + $keyColumn = UtilsArray::value('keyColumn', $pseudoConstant, 'id'); + + if ($tableName) { + $alias = str_replace('civicrm_', '', $tableName); + $joinable = new Joinable($tableName, $keyColumn, $alias); + $condition = UtilsArray::value('condition', $pseudoConstant); + if ($condition) { + $joinable->addCondition($condition); + } + $table->addTableLink($field, $joinable); + } + elseif ($optionGroupName) { + $keyColumn = UtilsArray::value('keyColumn', $pseudoConstant, 'value'); + $joinable = new OptionValueJoinable($optionGroupName, NULL, $keyColumn); + + if (!empty($data['serialize'])) { + $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY); + } + + $table->addTableLink($field, $joinable); + } + } + + /** + * Loop through existing links and provide link from the other side + * + * @param SchemaMap $map + */ + private function addBackReferences(SchemaMap $map) { + foreach ($map->getTables() as $table) { + foreach ($table->getTableLinks() as $link) { + // there are too many possible joins from option value so skip + if ($link instanceof OptionValueJoinable) { + continue; + } + + $target = $map->getTableByName($link->getTargetTable()); + $tableName = $link->getBaseTable(); + $plural = str_replace('civicrm_', '', $this->getPlural($tableName)); + $joinable = new Joinable($tableName, $link->getBaseColumn(), $plural); + $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY); + $target->addTableLink($link->getTargetColumn(), $joinable); + } + } + } + + /** + * Simple implementation of pluralization. + * Could be replaced with symfony/inflector + * + * @param string $singular + * + * @return string + */ + private function getPlural($singular) { + $last_letter = substr($singular, -1); + switch ($last_letter) { + case 'y': + return substr($singular, 0, -1) . 'ies'; + + case 's': + return $singular . 'es'; + + default: + return $singular . 's'; + } + } + + /** + * @param \Civi\Api4\Service\Schema\SchemaMap $map + * @param \Civi\Api4\Service\Schema\Table $baseTable + * @param string $entity + */ + private function addCustomFields(SchemaMap $map, Table $baseTable, $entity) { + // Don't be silly + if (!array_key_exists($entity, \CRM_Core_SelectValues::customGroupExtends())) { + return; + } + $queryEntity = (array) $entity; + if ($entity == 'Contact') { + $queryEntity = ['Contact', 'Individual', 'Organization', 'Household']; + } + $fieldData = \CRM_Utils_SQL_Select::from('civicrm_custom_field f') + ->join('custom_group', 'INNER JOIN civicrm_custom_group g ON g.id = f.custom_group_id') + ->select(['g.name as custom_group_name', 'g.table_name', 'g.is_multiple', 'f.name', 'label', 'column_name', 'option_group_id']) + ->where('g.extends IN (@entity)', ['@entity' => $queryEntity]) + ->where('g.is_active') + ->where('f.is_active') + ->execute(); + + $links = []; + + while ($fieldData->fetch()) { + $tableName = $fieldData->table_name; + + $customTable = $map->getTableByName($tableName); + if (!$customTable) { + $customTable = new Table($tableName); + } + + if (!empty($fieldData->option_group_id)) { + $optionValueJoinable = new OptionValueJoinable($fieldData->option_group_id, $fieldData->label); + $customTable->addTableLink($fieldData->column_name, $optionValueJoinable); + } + + $map->addTable($customTable); + + $alias = $fieldData->custom_group_name; + $links[$alias]['tableName'] = $tableName; + $links[$alias]['isMultiple'] = !empty($fieldData->is_multiple); + $links[$alias]['columns'][$fieldData->name] = $fieldData->column_name; + } + + foreach ($links as $alias => $link) { + $joinable = new CustomGroupJoinable($link['tableName'], $alias, $link['isMultiple'], $entity, $link['columns']); + $baseTable->addTableLink('id', $joinable); + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php new file mode 100644 index 00000000..1f464a45 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php @@ -0,0 +1,128 @@ +<?php + +namespace Civi\Api4\Service\Schema; + +use Civi\Api4\Service\Schema\Joinable\Joinable; + +class Table { + + /** + * @var string + */ + protected $name; + + /** + * @var Joinable[] + * Array of links to other tables + */ + protected $tableLinks = []; + + /** + * @param $name + */ + public function __construct($name) { + $this->name = $name; + } + + /** + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) { + $this->name = $name; + + return $this; + } + + /** + * @return Joinable[] + */ + public function getTableLinks() { + return $this->tableLinks; + } + + /** + * @return Joinable[] + * Only those links that are not joining the table to itself + */ + public function getExternalLinks() { + return array_filter($this->tableLinks, function (Joinable $joinable) { + return $joinable->getTargetTable() !== $this->getName(); + }); + } + + /** + * @param Joinable $linkToRemove + */ + public function removeLink(Joinable $linkToRemove) { + foreach ($this->tableLinks as $index => $link) { + if ($link === $linkToRemove) { + unset($this->tableLinks[$index]); + } + } + } + + /** + * @param string $baseColumn + * @param Joinable $joinable + * + * @return $this + */ + public function addTableLink($baseColumn, Joinable $joinable) { + $target = $joinable->getTargetTable(); + $targetCol = $joinable->getTargetColumn(); + $alias = $joinable->getAlias(); + + if (!$this->hasLink($target, $targetCol, $alias)) { + if (!$joinable->getBaseTable()) { + $joinable->setBaseTable($this->getName()); + } + if (!$joinable->getBaseColumn()) { + $joinable->setBaseColumn($baseColumn); + } + $this->tableLinks[] = $joinable; + } + + return $this; + } + + /** + * @param mixed $tableLinks + * + * @return $this + */ + public function setTableLinks($tableLinks) { + $this->tableLinks = $tableLinks; + + return $this; + } + + /** + * @param $target + * @param $targetCol + * @param $alias + * + * @return bool + */ + private function hasLink($target, $targetCol, $alias) { + foreach ($this->tableLinks as $link) { + if ($link->getTargetTable() === $target + && $link->getTargetColumn() === $targetCol + && $link->getAlias() === $alias + ) { + return TRUE; + } + } + + return FALSE; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php new file mode 100644 index 00000000..2c689344 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php @@ -0,0 +1,118 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +class CustomFieldSpec extends FieldSpec { + /** + * @var int + */ + protected $customFieldId; + + /** + * @var int + */ + protected $customGroup; + + /** + * @var string + */ + protected $tableName; + + /** + * @var string + */ + protected $columnName; + + /** + * @inheritDoc + */ + public function setDataType($dataType) { + switch ($dataType) { + case 'ContactReference': + $this->setFkEntity('Contact'); + $dataType = 'Integer'; + break; + + case 'File': + case 'StateProvince': + case 'Country': + $this->setFkEntity($dataType); + $dataType = 'Integer'; + break; + } + return parent::setDataType($dataType); + } + + /** + * @return int + */ + public function getCustomFieldId() { + return $this->customFieldId; + } + + /** + * @param int $customFieldId + * + * @return $this + */ + public function setCustomFieldId($customFieldId) { + $this->customFieldId = $customFieldId; + + return $this; + } + + /** + * @return int + */ + public function getCustomGroupName() { + return $this->customGroup; + } + + /** + * @param string $customGroupName + * + * @return $this + */ + public function setCustomGroupName($customGroupName) { + $this->customGroup = $customGroupName; + + return $this; + } + + /** + * @return string + */ + public function getCustomTableName() { + return $this->tableName; + } + + /** + * @param string $customFieldColumnName + * + * @return $this + */ + public function setCustomTableName($customFieldColumnName) { + $this->tableName = $customFieldColumnName; + + return $this; + } + + /** + * @return string + */ + public function getCustomFieldColumnName() { + return $this->columnName; + } + + /** + * @param string $customFieldColumnName + * + * @return $this + */ + public function setCustomFieldColumnName($customFieldColumnName) { + $this->columnName = $customFieldColumnName; + + return $this; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php new file mode 100644 index 00000000..1db2941e --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php @@ -0,0 +1,320 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +use Civi\Api4\Utils\CoreUtil; + +class FieldSpec { + /** + * @var mixed + */ + protected $defaultValue; + + /** + * @var string + */ + protected $name; + + /** + * @var string + */ + protected $title; + + /** + * @var string + */ + protected $entity; + + /** + * @var string + */ + protected $description; + + /** + * @var bool + */ + protected $required = FALSE; + + /** + * @var bool + */ + protected $requiredIf; + + /** + * @var array|boolean + */ + protected $options; + + /** + * @var string + */ + protected $dataType; + + /** + * @var string + */ + protected $fkEntity; + + /** + * @var int + */ + protected $serialize; + + /** + * Aliases for the valid data types + * + * @var array + */ + public static $typeAliases = [ + 'Int' => 'Integer', + 'Link' => 'Url', + 'Memo' => 'Text', + ]; + + /** + * @param string $name + * @param string $entity + * @param string $dataType + */ + public function __construct($name, $entity, $dataType = 'String') { + $this->entity = $entity; + $this->setName($name); + $this->setDataType($dataType); + } + + /** + * @return mixed + */ + public function getDefaultValue() { + return $this->defaultValue; + } + + /** + * @param mixed $defaultValue + * + * @return $this + */ + public function setDefaultValue($defaultValue) { + $this->defaultValue = $defaultValue; + + return $this; + } + + /** + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getTitle() { + return $this->title; + } + + /** + * @param string $title + * + * @return $this + */ + public function setTitle($title) { + $this->title = $title; + + return $this; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @return string + */ + public function getDescription() { + return $this->description; + } + + /** + * @param string $description + * + * @return $this + */ + public function setDescription($description) { + $this->description = $description; + + return $this; + } + + /** + * @return bool + */ + public function isRequired() { + return $this->required; + } + + /** + * @param bool $required + * + * @return $this + */ + public function setRequired($required) { + $this->required = $required; + + return $this; + } + + /** + * @return bool + */ + public function getRequiredIf() { + return $this->requiredIf; + } + + /** + * @param bool $required + * + * @return $this + */ + public function setRequiredIf($requiredIf) { + $this->requiredIf = $requiredIf; + + return $this; + } + + /** + * @return string + */ + public function getDataType() { + return $this->dataType; + } + + /** + * @param $dataType + * + * @return $this + * @throws \Exception + */ + public function setDataType($dataType) { + if (array_key_exists($dataType, self::$typeAliases)) { + $dataType = self::$typeAliases[$dataType]; + } + + if (!in_array($dataType, $this->getValidDataTypes())) { + throw new \Exception(sprintf('Invalid data type "%s', $dataType)); + } + + $this->dataType = $dataType; + + return $this; + } + + /** + * @return int + */ + public function getSerialize() { + return $this->serialize; + } + + /** + * @param int|null $serialize + */ + public function setSerialize($serialize) { + $this->serialize = $serialize; + } + + /** + * Add valid types that are not not part of \CRM_Utils_Type::dataTypes + * + * @return array + */ + private function getValidDataTypes() { + $extraTypes = ['Boolean', 'Text', 'Float', 'Url']; + $extraTypes = array_combine($extraTypes, $extraTypes); + + return array_merge(\CRM_Utils_Type::dataTypes(), $extraTypes); + } + + /** + * @return array + */ + public function getOptions() { + if (!isset($this->options) || $this->options === TRUE) { + $fieldName = $this->getName(); + + if ($this instanceof CustomFieldSpec) { + // buildOptions relies on the custom_* type of field names + $fieldName = sprintf('custom_%d', $this->getCustomFieldId()); + } + + $dao = CoreUtil::getDAOFromApiName($this->getEntity()); + $options = $dao::buildOptions($fieldName); + + if (!is_array($options) || !$options) { + $options = FALSE; + } + + $this->setOptions($options); + } + return $this->options; + } + + /** + * @param array|bool $options + * + * @return $this + */ + public function setOptions($options) { + $this->options = $options; + return $this; + } + + /** + * @return string + */ + public function getFkEntity() { + return $this->fkEntity; + } + + /** + * @param string $fkEntity + * + * @return $this + */ + public function setFkEntity($fkEntity) { + $this->fkEntity = $fkEntity; + + return $this; + } + + /** + * @param array $values + * @return array + */ + public function toArray($values = []) { + $ret = []; + foreach (get_object_vars($this) as $key => $val) { + $key = strtolower(preg_replace('/(?=[A-Z])/', '_$0', $key)); + if (!$values || in_array($key, $values)) { + $ret[$key] = $val; + } + } + return $ret; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php new file mode 100644 index 00000000..660bfec9 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php @@ -0,0 +1,27 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\FieldSpec; +use Civi\Api4\Service\Spec\RequestSpec; + +class ActionScheduleCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('title')->setRequired(TRUE); + $spec->getFieldByName('mapping_id')->setRequired(TRUE); + $spec->getFieldByName('entity_value')->setRequired(TRUE); + $spec->getFieldByName('start_action_date')->setRequiredIf('empty($values.absolute_date)'); + $spec->getFieldByName('absolute_date')->setRequiredIf('empty($values.start_action_date)'); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'ActionSchedule' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php new file mode 100644 index 00000000..dc254342 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php @@ -0,0 +1,27 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\FieldSpec; +use Civi\Api4\Service\Spec\RequestSpec; + +class ActivityCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $sourceContactField = new FieldSpec('source_contact_id', 'Activity', 'Integer'); + $sourceContactField->setRequired(TRUE); + $sourceContactField->setFkEntity('Contact'); + + $spec->addFieldSpec($sourceContactField); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Activity' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php new file mode 100644 index 00000000..afba9c79 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php @@ -0,0 +1,29 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\FieldSpec; +use Civi\Api4\Service\Spec\RequestSpec; + + +class AddressCreationSpecProvider implements SpecProviderInterface { + + /** + * @param RequestSpec $spec + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('contact_id')->setRequired(TRUE); + $spec->getFieldByName('location_type_id')->setRequired(TRUE); + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'Address' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php new file mode 100644 index 00000000..94c68d9d --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php @@ -0,0 +1,32 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class ContactCreationSpecProvider implements SpecProviderInterface { + + /** + * @param RequestSpec $spec + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('contact_type') + ->setRequired(TRUE) + ->setDefaultValue('Individual'); + + $spec->getFieldByName('is_opt_out')->setRequired(FALSE); + $spec->getFieldByName('is_deleted')->setRequired(FALSE); + + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'Contact' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php new file mode 100644 index 00000000..f55deb1c --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php @@ -0,0 +1,29 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class ContactTypeCreationSpecProvider implements SpecProviderInterface { + + /** + * @param RequestSpec $spec + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('label')->setRequired(TRUE); + $spec->getFieldByName('name')->setRequired(TRUE); + $spec->getFieldByName('parent_id')->setRequired(TRUE); + + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'ContactType' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php new file mode 100644 index 00000000..14861871 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php @@ -0,0 +1,23 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class ContributionCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('financial_type_id')->setRequired(TRUE); + $spec->getFieldByName('receive_date')->setDefaultValue('now'); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Contribution' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php new file mode 100644 index 00000000..cd033754 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php @@ -0,0 +1,22 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class CustomGroupCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + return $spec->getFieldByName('extends')->setRequired(TRUE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'CustomGroup' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php new file mode 100644 index 00000000..cd82d438 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php @@ -0,0 +1,34 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\FieldSpec; +use Civi\Api4\Service\Spec\RequestSpec; + +class CustomValueSpecProvider implements SpecProviderInterface { + + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $action = $spec->getAction(); + if ($action !== 'create') { + $idField = new FieldSpec('id', $spec->getEntity(), 'Integer'); + $idField->setTitle(ts('Custom Value ID')); + $spec->addFieldSpec($idField); + } + $entityField = new FieldSpec('entity_id', $spec->getEntity(), 'Integer'); + $entityField->setTitle(ts('Entity ID')); + $entityField->setRequired($action === 'create'); + $entityField->setFkEntity('Contact'); + $spec->addFieldSpec($entityField); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return strstr($entity, 'Custom_'); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php new file mode 100644 index 00000000..136b0e54 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php @@ -0,0 +1,25 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class EmailCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('contact_id')->setRequired(TRUE); + $spec->getFieldByName('email')->setRequired(TRUE); + $spec->getFieldByName('on_hold')->setRequired(FALSE); + $spec->getFieldByName('is_bulkmail')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Email' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php new file mode 100644 index 00000000..42b74a6f --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php @@ -0,0 +1,22 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class EventCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('is_template')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Event' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php new file mode 100644 index 00000000..8af69a0a --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php @@ -0,0 +1,22 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class GroupCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('title')->setRequired(TRUE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Group' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php new file mode 100644 index 00000000..7d5fc270 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php @@ -0,0 +1,22 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class NavigationCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('domain_id')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Navigation' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php new file mode 100644 index 00000000..f12e592c --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php @@ -0,0 +1,28 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + + +class NoteCreationSpecProvider implements SpecProviderInterface { + + /** + * @param RequestSpec $spec + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('note')->setRequired(TRUE); + $spec->getFieldByName('entity_table')->setDefaultValue('civicrm_contact'); + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'Note' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php new file mode 100644 index 00000000..4ea634c1 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php @@ -0,0 +1,23 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class OptionValueCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('weight')->setRequired(FALSE); + $spec->getFieldByName('value')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'OptionValue' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php new file mode 100644 index 00000000..bb757d43 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php @@ -0,0 +1,24 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class PhoneCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('contact_id')->setRequired(TRUE); + $spec->getFieldByName('location_type_id')->setRequired(TRUE); + $spec->getFieldByName('phone')->setRequired(TRUE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Phone' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php new file mode 100644 index 00000000..8be77e68 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php @@ -0,0 +1,23 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +interface SpecProviderInterface { + /** + * @param RequestSpec $spec + * + * @return void + */ + public function modifySpec(RequestSpec $spec); + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action); + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php new file mode 100644 index 00000000..9437d930 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php @@ -0,0 +1,110 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +class RequestSpec { + + /** + * @var string + */ + protected $entity; + + /** + * @var string + */ + protected $action; + + /** + * @var FieldSpec[] + */ + protected $fields = []; + + /** + * @param string $entity + * @param string $action + */ + public function __construct($entity, $action) { + $this->entity = $entity; + $this->action = $action; + } + + public function addFieldSpec(FieldSpec $field) { + $this->fields[] = $field; + } + + /** + * @param $name + * + * @return FieldSpec|null + */ + public function getFieldByName($name) { + foreach ($this->fields as $field) { + if ($field->getName() === $name) { + return $field; + } + } + + return NULL; + } + + /** + * @return array + * Gets all the field names currently part of the specification + */ + public function getFieldNames() { + return array_map(function(FieldSpec $field) { + return $field->getName(); + }, $this->fields); + } + + /** + * @return array|FieldSpec[] + */ + public function getRequiredFields() { + return array_filter($this->fields, function (FieldSpec $field) { + return $field->isRequired(); + }); + } + + /** + * @return array|FieldSpec[] + */ + public function getConditionalRequiredFields() { + return array_filter($this->fields, function (FieldSpec $field) { + return $field->getRequiredIf(); + }); + } + + /** + * @param array $fieldNames + * Optional array of fields to return + * @return FieldSpec[] + */ + public function getFields($fieldNames = NULL) { + if (!$fieldNames) { + return $this->fields; + } + $fields = []; + foreach ($this->fields as $field) { + if (in_array($field->getName(), $fieldNames)) { + $fields[] = $field; + } + } + return $fields; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @return string + */ + public function getAction() { + return $this->action; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php new file mode 100644 index 00000000..c8e4d3da --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php @@ -0,0 +1,117 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +use CRM_Utils_Array as ArrayHelper; +use CRM_Core_DAO_AllCoreTables as TableHelper; + +class SpecFormatter { + /** + * @param FieldSpec[] $fields + * @param array $return + * @param bool $includeFieldOptions + * + * @return array + */ + public static function specToArray($fields, $return = [], $includeFieldOptions = FALSE) { + $fieldArray = []; + + foreach ($fields as $field) { + if ($includeFieldOptions || in_array('options', $return)) { + $field->getOptions(); + } + $fieldArray[$field->getName()] = $field->toArray($return); + } + + return $fieldArray; + } + + /** + * @param array $data + * @param string $entity + * + * @return FieldSpec + */ + public static function arrayToField(array $data, $entity) { + $dataTypeName = self::getDataType($data); + + if (!empty($data['custom_group_id'])) { + $field = new CustomFieldSpec($data['name'], $entity, $dataTypeName); + if (strpos($entity, 'Custom_') !== 0) { + $field->setName($data['custom_group']['name'] . '.' . $data['name']); + } + else { + $field->setCustomTableName($data['custom_group']['table_name']); + $field->setCustomFieldColumnName($data['column_name']); + } + $field->setCustomFieldId(ArrayHelper::value('id', $data)); + $field->setCustomGroupName($data['custom_group']['name']); + $field->setTitle(ArrayHelper::value('label', $data)); + $field->setOptions(self::customFieldHasOptions($data)); + if (\CRM_Core_BAO_CustomField::isSerialized($data)) { + $field->setSerialize(\CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND); + } + } + else { + $name = ArrayHelper::value('name', $data); + $field = new FieldSpec($name, $entity, $dataTypeName); + $field->setRequired((bool) ArrayHelper::value('required', $data, FALSE)); + $field->setTitle(ArrayHelper::value('title', $data)); + $field->setOptions(!empty($data['pseudoconstant'])); + $field->setSerialize(ArrayHelper::value('serialize', $data)); + } + + $field->setDefaultValue(ArrayHelper::value('default', $data)); + $field->setDescription(ArrayHelper::value('description', $data)); + + $fkAPIName = ArrayHelper::value('FKApiName', $data); + $fkClassName = ArrayHelper::value('FKClassName', $data); + if ($fkAPIName || $fkClassName) { + $field->setFkEntity($fkAPIName ?: TableHelper::getBriefName($fkClassName)); + } + + return $field; + } + + /** + * Does this custom field have options + * + * @param array $field + * @return bool + */ + private static function customFieldHasOptions($field) { + // This will include boolean fields with Yes/No options. + if (in_array($field['html_type'], ['Radio', 'CheckBox'])) { + return TRUE; + } + // Do this before the "Select" string search because date fields have a "Select Date" html_type + // and contactRef fields have an "Autocomplete-Select" html_type - contacts are an FK not an option list. + if (in_array($field['data_type'], ['ContactReference', 'Date'])) { + return FALSE; + } + if (strpos($field['html_type'], 'Select')) { + return TRUE; + } + return !empty($field['option_group_id']); + } + + /** + * Get the data type from an array. Defaults to 'data_type' with fallback to + * mapping for the integer value 'type' + * + * @param array $data + * + * @return string + */ + private static function getDataType(array $data) { + if (isset($data['data_type'])) { + return $data['data_type']; + } + + $dataTypeInt = ArrayHelper::value('type', $data); + $dataTypeName = \CRM_Utils_Type::typeToString($dataTypeInt); + + return $dataTypeName; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php new file mode 100644 index 00000000..b1c83c89 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php @@ -0,0 +1,131 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +use Civi\Api4\CustomField; +use Civi\Api4\Service\Spec\Provider\SpecProviderInterface; +use Civi\Api4\Utils\CoreUtil; + +class SpecGatherer { + + /** + * @var SpecProviderInterface[] + */ + protected $specProviders = []; + + /** + * A cache of DAOs based on entity + * + * @var \CRM_Core_DAO[] + */ + protected $DAONames; + + /** + * Returns a RequestSpec with all the fields available. Uses spec providers + * to add or modify field specifications. + * For an example @see CustomFieldSpecProvider. + * + * @param string $entity + * @param string $action + * @param $includeCustom + * + * @return \Civi\Api4\Service\Spec\RequestSpec + */ + public function getSpec($entity, $action, $includeCustom) { + $specification = new RequestSpec($entity, $action); + + // Real entities + if (strpos($entity, 'Custom_') !== 0) { + $this->addDAOFields($entity, $action, $specification); + if ($includeCustom && array_key_exists($entity, \CRM_Core_SelectValues::customGroupExtends())) { + $this->addCustomFields($entity, $specification); + } + } + // Custom pseudo-entities + else { + $this->getCustomGroupFields(substr($entity, 7), $specification); + } + + foreach ($this->specProviders as $provider) { + if ($provider->applies($entity, $action)) { + $provider->modifySpec($specification); + } + } + + return $specification; + } + + /** + * @param SpecProviderInterface $provider + */ + public function addSpecProvider(SpecProviderInterface $provider) { + $this->specProviders[] = $provider; + } + + /** + * @param string $entity + * @param RequestSpec $specification + */ + private function addDAOFields($entity, $action, RequestSpec $specification) { + $DAOFields = $this->getDAOFields($entity); + + foreach ($DAOFields as $DAOField) { + if ($DAOField['name'] == 'id' && $action == 'create') { + continue; + } + if ($action !== 'create' || isset($DAOField['default'])) { + $DAOField['required'] = FALSE; + } + if ($DAOField['name'] == 'is_active' && empty($DAOField['default'])) { + $DAOField['default'] = '1'; + } + $field = SpecFormatter::arrayToField($DAOField, $entity); + $specification->addFieldSpec($field); + } + } + + /** + * @param string $entity + * @param RequestSpec $specification + */ + private function addCustomFields($entity, RequestSpec $specification) { + $extends = ($entity == 'Contact') ? ['Contact', 'Individual', 'Organization', 'Household'] : [$entity]; + $customFields = CustomField::get() + ->addWhere('custom_group.extends', 'IN', $extends) + ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value']) + ->execute(); + + foreach ($customFields as $fieldArray) { + $field = SpecFormatter::arrayToField($fieldArray, $entity); + $specification->addFieldSpec($field); + } + } + + /** + * @param string $customGroup + * @param RequestSpec $specification + */ + private function getCustomGroupFields($customGroup, RequestSpec $specification) { + $customFields = CustomField::get() + ->addWhere('custom_group.name', '=', $customGroup) + ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value', 'custom_group.table_name', 'column_name']) + ->execute(); + + foreach ($customFields as $fieldArray) { + $field = SpecFormatter::arrayToField($fieldArray, 'Custom_' . $customGroup); + $specification->addFieldSpec($field); + } + } + + /** + * @param string $entityName + * + * @return array + */ + private function getDAOFields($entityName) { + $dao = CoreUtil::getDAOFromApiName($entityName); + + return $dao::fields(); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/UFGroup.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/UFGroup.php new file mode 100644 index 00000000..aeea02c1 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/UFGroup.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * UFGroup entity - AKA profiles. + * + * @package Civi\Api4 + */ +class UFGroup extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/UFJoin.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/UFJoin.php new file mode 100644 index 00000000..4d68fc7a --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/UFJoin.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * UFJoin entity - links profiles to the components/extensions they are used for. + * + * @package Civi\Api4 + */ +class UFJoin extends Generic\DAOEntity { + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ActionUtil.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ActionUtil.php new file mode 100644 index 00000000..628bc6fa --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ActionUtil.php @@ -0,0 +1,27 @@ +<?php + +namespace Civi\Api4\Utils; + +class ActionUtil { + + /** + * @param $entityName + * @param $actionName + * @return \Civi\Api4\Generic\AbstractAction + * @throws \Civi\API\Exception\NotImplementedException + */ + public static function getAction($entityName, $actionName) { + // For custom pseudo-entities + if (strpos($entityName, 'Custom_') === 0) { + return \Civi\Api4\CustomValue::$actionName(substr($entityName, 7)); + } + else { + $callable = ["\\Civi\\Api4\\$entityName", $actionName]; + if (!is_callable($callable)) { + throw new \Civi\API\Exception\NotImplementedException("API ($entityName, $actionName) does not exist (join the API team and implement it!)"); + } + return call_user_func($callable); + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ArrayInsertionUtil.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ArrayInsertionUtil.php new file mode 100644 index 00000000..54e3944b --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ArrayInsertionUtil.php @@ -0,0 +1,73 @@ +<?php + +namespace Civi\Api4\Utils; + +use CRM_Utils_Array as UtilsArray; + +class ArrayInsertionUtil { + /** + * If the values to be inserted contain a key _parent_id they will only be + * inserted if the parent node ID matches their ID + * + * @param $array + * The array to insert the value in + * @param array $parts + * Path to insertion point with structure: + * [[ name => is_multiple ], ..] + * @param mixed $values + * The value to be inserted + */ + public static function insert(&$array, $parts, $values) { + $key = key($parts); + $isMulti = array_shift($parts); + if (!isset($array[$key])) { + $array[$key] = $isMulti ? [] : NULL; + } + if (empty($parts)) { + $values = self::filterValues($array, $isMulti, $values); + $array[$key] = $values; + } + else { + if ($isMulti) { + foreach ($array[$key] as &$subArray) { + self::insert($subArray, $parts, $values); + } + } + else { + self::insert($array[$key], $parts, $values); + } + } + } + + /** + * @param $parentArray + * @param $isMulti + * @param $values + * + * @return array|mixed + */ + private static function filterValues($parentArray, $isMulti, $values) { + $parentID = UtilsArray::value('id', $parentArray); + + if ($parentID) { + $values = array_filter($values, function ($value) use ($parentID) { + return UtilsArray::value('_parent_id', $value) == $parentID; + }); + } + + $unsets = ['_parent_id', '_base_id']; + array_walk($values, function (&$value) use ($unsets) { + foreach ($unsets as $unset) { + if (isset($value[$unset])) { + unset($value[$unset]); + } + } + }); + + if (!$isMulti) { + $values = array_shift($values); + } + return $values; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/CoreUtil.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/CoreUtil.php new file mode 100644 index 00000000..b43e62ca --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/CoreUtil.php @@ -0,0 +1,42 @@ +<?php + +namespace Civi\Api4\Utils; + +use Civi\Api4\CustomGroup; + +require_once 'api/v3/utils.php'; + +class CoreUtil { + + /** + * todo this class should not rely on api3 code + * + * @param $entityName + * + * @return \CRM_Core_DAO|string + * The DAO name for use in static calls. Return doc block is hacked to allow + * auto-completion of static methods + */ + public static function getDAOFromApiName($entityName) { + if ($entityName === 'CustomValue' || strpos($entityName, 'Custom_') === 0) { + return 'CRM_Contact_BAO_Contact'; + } + return \_civicrm_api3_get_DAO($entityName); + } + + /** + * Get table name of given Custom group + * + * @param string $customGroupName + * + * @return string + */ + public static function getCustomTableByName($customGroupName) { + return CustomGroup::get() + ->addSelect('table_name') + ->addWhere('name', '=', $customGroupName) + ->execute() + ->first()['table_name']; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/FormattingUtil.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/FormattingUtil.php new file mode 100644 index 00000000..3a7cdae5 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/FormattingUtil.php @@ -0,0 +1,106 @@ +<?php + +namespace Civi\Api4\Utils; + +use CRM_Utils_Array as UtilsArray; + +require_once 'api/v3/utils.php'; + +class FormattingUtil { + + /** + * Massage values into the format the BAO expects for a write operation + * + * @param $params + * @param $entity + * @param $fields + * @throws \API_Exception + */ + public static function formatWriteParams(&$params, $entity, $fields) { + foreach ($fields as $name => $field) { + if (!empty($params[$name])) { + $value =& $params[$name]; + // Hack for null values -- see comment below + if ($value === 'null') { + $value = 'Null'; + } + FormattingUtil::formatValue($value, $field, $entity); + // Ensure we have an array for serialized fields + if (!empty($field['serialize'] && !is_array($value))) { + $value = (array) $value; + } + } + /* + * Because of the wacky way that database values are saved we need to format + * some of the values here. In this strange world the string 'null' is used to + * unset values. Hence if we encounter true null we change it to string 'null'. + * + * If we encounter the string 'null' then we assume the user actually wants to + * set the value to string null. However since the string null is reserved for + * unsetting values we must change it. Another quirk of the DB_DataObject is + * that it allows 'Null' to be set, but any other variation of string 'null' + * will be converted to true null, e.g. 'nuLL', 'NUlL' etc. so we change it to + * 'Null'. + */ + elseif (array_key_exists($name, $params) && $params[$name] === NULL) { + $params[$name] = 'null'; + } + + if (strstr($entity, 'Custom_')) { + if ($name == 'entity_id') { + $params['entityID'] = $params['entity_id']; + unset($params['entity_id']); + } + elseif (!empty($field['custom_field_id'])) { + $params['custom_' . $field['custom_field_id']] = $params[$name]; + unset($params[$name]); + } + } + } + } + + /** + * Transform raw api input to appropriate format for use in a SQL query. + * + * This is used by read AND write actions (Get, Create, Update, Replace) + * + * @param $value + * @param $fieldSpec + * @throws \API_Exception + */ + public static function formatValue(&$value, $fieldSpec, $entity) { + if (is_array($value)) { + foreach ($value as &$val) { + self::formatValue($val, $fieldSpec, $entity); + } + return; + } + $fk = UtilsArray::value('fk_entity', $fieldSpec); + if ($fieldSpec['name'] == 'id') { + $fk = $entity; + } + $dataType = UtilsArray::value('data_type', $fieldSpec); + + if ($fk === 'Domain' && $value === 'current_domain') { + $value = \CRM_Core_Config::domainID(); + } + + if ($fk === 'Contact' && !is_numeric($value)) { + $value = \_civicrm_api3_resolve_contactID($value); + if ('unknown-user' === $value) { + throw new \API_Exception("\"{$fieldSpec['name']}\" \"{$value}\" cannot be resolved to a contact ID", 2002, ['error_field' => $fieldSpec['name'], "type" => "integer"]); + } + } + + switch ($dataType) { + case 'Timestamp': + $value = date('Y-m-d H:i:s', strtotime($value)); + break; + + case 'Date': + $value = date('Ymd', strtotime($value)); + break; + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ReflectionUtils.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ReflectionUtils.php new file mode 100644 index 00000000..76662647 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ReflectionUtils.php @@ -0,0 +1,119 @@ +<?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\Utils; + +/** + * Just another place to put static functions... + */ +class ReflectionUtils { + /** + * @param \Reflector|\ReflectionClass $reflection + * @param string $type + * If we are not reflecting the class itself, specify "Method", "Property", etc. + * + * @return array + */ + public static function getCodeDocs($reflection, $type = NULL) { + $docs = self::parseDocBlock($reflection->getDocComment()); + + // Recurse into parent functions + if (isset($docs['inheritDoc']) || isset($docs['inheritdoc'])) { + unset($docs['inheritDoc'], $docs['inheritdoc']); + $newReflection = NULL; + try { + if ($type) { + $name = $reflection->getName(); + $reflectionClass = $reflection->getDeclaringClass()->getParentClass(); + if ($reflectionClass) { + $getItem = "get$type"; + $newReflection = $reflectionClass->$getItem($name); + } + } + else { + $newReflection = $reflection->getParentClass(); + } + } + catch (\ReflectionException $e) {} + if ($newReflection) { + // Mix in + $additionalDocs = self::getCodeDocs($newReflection, $type); + if (!empty($docs['comment']) && !empty($additionalDocs['comment'])) { + $docs['comment'] .= "\n\n" . $additionalDocs['comment']; + } + $docs += $additionalDocs; + } + } + return $docs; + } + + /** + * @param string $comment + * @return array + */ + public static function parseDocBlock($comment) { + $info = []; + foreach (preg_split("/((\r?\n)|(\r\n?))/", $comment) as $num => $line) { + if (!$num || strpos($line, '*/') !== FALSE) { + continue; + } + $line = ltrim(trim($line), '* '); + if (strpos($line, '@') === 0) { + $words = explode(' ', $line); + $key = substr($words[0], 1); + if ($key == 'var') { + $info['type'] = explode('|', $words[1]); + } + elseif ($key == 'options') { + $val = str_replace(', ', ',', implode(' ', array_slice($words, 1))); + $info['options'] = explode(',', $val); + } + else { + // Unrecognized annotation, but we'll duly add it to the info array + $val = implode(' ', array_slice($words, 1)); + $info[$key] = strlen($val) ? $val : TRUE; + } + } + elseif ($num == 1) { + $info['description'] = $line; + } + elseif (!$line) { + if (isset($info['comment'])) { + $info['comment'] .= "\n"; + } + } + else { + $info['comment'] = isset($info['comment']) ? "{$info['comment']}\n$line" : $line; + } + } + if (isset($info['comment'])) { + $info['comment'] = trim($info['comment']); + } + return $info; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Website.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Website.php new file mode 100644 index 00000000..fb890a0f --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Website.php @@ -0,0 +1,12 @@ +<?php + +namespace Civi\Api4; + +/** + * Website entity. + * + * @package Civi\Api4 + */ +class Website extends Generic\DAOEntity { + +} |