summaryrefslogtreecommitdiff
path: root/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi
diff options
context:
space:
mode:
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi')
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ACL.php19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Address/Create.php43
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Address/Update.php43
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contact/Create.php22
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contact/GetFields.php19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Contribution/Create.php18
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Create.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Delete.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Get.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetActions.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/GetFields.php34
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Replace.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/CustomValue/Update.php11
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Entity/Get.php102
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Entity/GetLinks.php51
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GetActions.php101
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GroupContact/Create.php38
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/GroupContact/Update.php38
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Navigation/Get.php19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Action/Participant/Get.php18
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ActionSchedule.php18
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Activity.php20
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ActivityContact.php16
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Address.php32
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Contact.php39
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/ContactType.php18
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Contribution.php19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomField.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomGroup.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/CustomValue.php79
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Email.php16
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Entity.php48
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Events.php25
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/GetSpecEvent.php35
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/PostSelectQueryEvent.php64
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/SchemaMapBuildEvent.php39
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/AbstractPrepareSubscriber.php24
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivityPreCreationSubscriber.php40
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ActivitySchemaMapSubscriber.php40
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ContactSchemaMapSubscriber.php54
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomFieldPreCreationSubscriber.php91
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/CustomGroupPreCreationSubscriber.php29
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/OptionValuePreCreationSubscriber.php50
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PermissionCheckSubscriber.php65
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php369
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/PreCreationSubscriber.php50
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Event/Subscriber/ValidateFieldsSubscriber.php97
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php391
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractBatchAction.php64
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractCreateAction.php56
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php88
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractGetAction.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractQueryAction.php105
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/AbstractUpdateAction.php45
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicBatchAction.php72
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicCreateAction.php64
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetAction.php144
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicGetFieldsAction.php120
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicReplaceAction.php81
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/BasicUpdateAction.php67
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOCreateAction.php58
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAODeleteAction.php73
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOEntity.php52
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetAction.php21
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOGetFieldsAction.php53
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/DAOUpdateAction.php31
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Result.php105
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/ArrayQueryActionTrait.php197
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/CustomValueActionTrait.php83
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Generic/Traits/DAOActionTrait.php229
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Group.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/GroupContact.php29
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/IM.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Navigation.php19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Note.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OpenID.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OptionGroup.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/OptionValue.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Participant.php19
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Phone.php16
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Provider/ActionObjectProvider.php155
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Query/Api4SelectQuery.php535
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Relationship.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/RelationshipType.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php40
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php71
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php277
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php61
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php98
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php140
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php217
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php128
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php118
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php320
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php27
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php27
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php29
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php32
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php29
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php22
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php34
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php25
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php22
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php22
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php22
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php28
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php24
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php110
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php117
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php131
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/UFGroup.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/UFJoin.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ActionUtil.php27
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ArrayInsertionUtil.php73
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/CoreUtil.php42
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/FormattingUtil.php106
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Utils/ReflectionUtils.php119
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Website.php12
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 {
+
+}