Merge pull request #24117 from civicrm/5.52
[civicrm-core.git] / api / v3 / Profile.php
CommitLineData
6a488035 1<?php
6a488035 2/*
a30c801b
TO
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
e70a7fc0 10 */
6a488035
TO
11
12/**
244bbdd8
CW
13 * This api exposes CiviCRM profiles.
14 *
15 * Profiles are collections of fields used as forms, listings, search columns, etc.
6a488035
TO
16 *
17 * @package CiviCRM_APIv3
6a488035
TO
18 */
19
6a488035
TO
20/**
21 * Retrieve Profile field values.
22 *
f01ce56b 23 * NOTE this api is not standard & since it is tested we need to honour that
24 * but the correct behaviour is for it to return an id indexed array as this supports
6a386447 25 * multiple instances - if a single profile is passed in we will not return a normal api result array
26 * in order to avoid breaking code. (This could still be confusing :-( but we have to keep the tested behaviour working
40a60af6 27 *
28 * Note that if contact_id is empty an array of defaults is returned
a130e045
DG
29 *
30 * @param array $params
31 * Associative array of property name/value.
32 * pairs to get profile field values
33 *
a130e045 34 * @return array
58159c3b 35 * @throws \CRM_Core_Exception
36 * @throws API_Exception
6a488035
TO
37 */
38function civicrm_api3_profile_get($params) {
1699214f 39 $nonStandardLegacyBehaviour = is_numeric($params['profile_id']);
cf8f0fff 40 if (!empty($params['check_permissions']) && !empty($params['contact_id']) && !1 === civicrm_api3('contact', 'getcount', ['contact_id' => $params['contact_id'], 'check_permissions' => 1])) {
c85e32fc 41 throw new API_Exception('permission denied');
42 }
f01ce56b 43 $profiles = (array) $params['profile_id'];
cf8f0fff 44 $values = [];
0d5cc439 45 $ufGroupBAO = new CRM_Core_BAO_UFGroup();
f01ce56b 46 foreach ($profiles as $profileID) {
6a386447 47 $profileID = _civicrm_api3_profile_getProfileID($profileID);
cf8f0fff 48 $values[$profileID] = [];
f01ce56b 49 if (strtolower($profileID) == 'billing') {
50 $values[$profileID] = _civicrm_api3_profile_getbillingpseudoprofile($params);
51 continue;
52 }
22e263ad 53 if (!CRM_Core_DAO::getFieldValue('CRM_Core_DAO_UFGroup', $profileID, 'is_active')) {
f01ce56b 54 throw new API_Exception('Invalid value for profile_id : ' . $profileID);
55 }
6a488035 56
f01ce56b 57 $isContactActivityProfile = CRM_Core_BAO_UFField::checkContactActivityProfileType($profileID);
6a488035 58
f01ce56b 59 $profileFields = CRM_Core_BAO_UFGroup::getFields($profileID,
60 FALSE,
61 NULL,
62 NULL,
63 NULL,
64 FALSE,
65 NULL,
c40f0e57 66 empty($params['check_permissions']),
f01ce56b 67 NULL,
68 CRM_Core_Permission::EDIT
69 );
6a488035 70
35671d00 71 if ($isContactActivityProfile) {
cf8f0fff 72 civicrm_api3_verify_mandatory($params, NULL, ['activity_id']);
6a488035 73
35671d00 74 $errors = CRM_Profile_Form::validateContactActivityProfile($params['activity_id'],
6a488035
TO
75 $params['contact_id'],
76 $params['profile_id']
35671d00
TO
77 );
78 if (!empty($errors)) {
79 throw new API_Exception(array_pop($errors));
6a488035 80 }
35671d00 81
cf8f0fff 82 $contactFields = $activityFields = [];
35671d00
TO
83 foreach ($profileFields as $fieldName => $field) {
84 if (CRM_Utils_Array::value('field_type', $field) == 'Activity') {
85 $activityFields[$fieldName] = $field;
86 }
87 else {
88 $contactFields[$fieldName] = $field;
c3d3e837
E
89 // we should return 'Primary' with & without capitalisation. it is more consistent with api to not
90 // capitalise, but for form support we need it for now. Hopefully we can move away from it
91 $contactFields[strtolower($fieldName)] = $field;
92 }
6a488035 93 }
6a488035 94
35671d00 95 $ufGroupBAO->setProfileDefaults($params['contact_id'], $contactFields, $values[$profileID], TRUE);
6a488035 96
35671d00
TO
97 if ($params['activity_id']) {
98 $ufGroupBAO->setComponentDefaults($activityFields, $params['activity_id'], 'Activity', $values[$profileID], TRUE);
99 }
6a488035 100 }
bed98343 101 elseif (!empty($params['contact_id'])) {
35671d00 102 $ufGroupBAO->setProfileDefaults($params['contact_id'], $profileFields, $values[$profileID], TRUE);
9b873358 103 foreach ($values[$profileID] as $fieldName => $field) {
c3d3e837
E
104 // we should return 'Primary' with & without capitalisation. it is more consistent with api to not
105 // capitalise, but for form support we need it for now. Hopefully we can move away from it
106 $values[$profileID][strtolower($fieldName)] = $field;
107 }
108 }
92e4c2a5 109 else {
c3d3e837
E
110 $values[$profileID] = array_fill_keys(array_keys($profileFields), '');
111 }
f01ce56b 112 }
22e263ad 113 if ($nonStandardLegacyBehaviour) {
f01ce56b 114 $result = civicrm_api3_create_success();
115 $result['values'] = $values[$profileID];
116 return $result;
117 }
118 else {
119 return civicrm_api3_create_success($values, $params, 'Profile', 'Get');
6a488035 120 }
6a488035
TO
121}
122
aa1b1481 123/**
9d32e6f7
EM
124 * Adjust profile get function metadata.
125 *
c490a46a 126 * @param array $params
aa1b1481 127 */
f01ce56b 128function _civicrm_api3_profile_get_spec(&$params) {
129 $params['profile_id']['api.required'] = TRUE;
4c41ecb2 130 $params['profile_id']['title'] = 'Profile ID';
40a60af6 131 $params['contact_id']['description'] = 'If no contact is specified an array of defaults will be returned';
4c41ecb2 132 $params['contact_id']['title'] = 'Contact ID';
f01ce56b 133}
6a386447 134
6a488035 135/**
6a386447 136 * Submit a set of fields against a profile.
1747ab99 137 *
6a386447 138 * Note choice of submit versus create is discussed CRM-13234 & related to the fact
139 * 'profile' is being treated as a data-entry entity
0d5cc439 140 *
6a386447 141 * @param array $params
0d5cc439
E
142 *
143 * @throws API_Exception
a6c01b45 144 * @return array
72b3a70c 145 * API result array
6a488035 146 */
6a386447 147function civicrm_api3_profile_submit($params) {
c1b19e8a 148 $profileID = _civicrm_api3_profile_getProfileID($params['profile_id']);
4bcfd71f 149 if (!CRM_Core_DAO::getFieldValue('CRM_Core_DAO_UFGroup', $profileID, 'is_active')) {
150 //@todo declare pseudoconstant & let api do this
f01ce56b 151 throw new API_Exception('Invalid value for profile_id');
6a488035
TO
152 }
153
4bcfd71f 154 $isContactActivityProfile = CRM_Core_BAO_UFField::checkContactActivityProfileType($profileID);
6a488035 155
4bcfd71f 156 if (!empty($params['id']) && CRM_Core_BAO_UFField::checkProfileType($profileID) && !$isContactActivityProfile) {
157 throw new API_Exception('Update profiles including more than one entity not currently supported');
6a488035
TO
158 }
159
cf8f0fff 160 $contactParams = $activityParams = $missingParams = [];
6a488035 161
cf8f0fff 162 $profileFields = civicrm_api3('Profile', 'getfields', ['action' => 'submit', 'profile_id' => $profileID]);
c3d3e837 163 $profileFields = $profileFields['values'];
6a488035 164 if ($isContactActivityProfile) {
cf8f0fff 165 civicrm_api3_verify_mandatory($params, NULL, ['activity_id']);
6a488035 166
6a488035
TO
167 $errors = CRM_Profile_Form::validateContactActivityProfile($params['activity_id'],
168 $params['contact_id'],
4bcfd71f 169 $profileID
6a488035
TO
170 );
171 if (!empty($errors)) {
6a386447 172 throw new API_Exception(array_pop($errors));
6a488035
TO
173 }
174 }
175
93878a99
CW
176 // Add custom greeting fields
177 $greetingFields = ['email_greeting', 'postal_greeting', 'addressee'];
178 foreach ($greetingFields as $greetingField) {
179 if (isset($profileFields[$greetingField]) && !isset($profileFields["{$greetingField}_custom"])) {
180 $profileFields["{$greetingField}_custom"] = ['name' => "{$greetingField}_custom"];
181 }
182 }
183
6a488035 184 foreach ($profileFields as $fieldName => $field) {
6a488035
TO
185 if (!isset($params[$fieldName])) {
186 continue;
187 }
188
189 $value = $params[$fieldName];
190 if ($params[$fieldName] && isset($params[$fieldName . '_id'])) {
191 $value = $params[$fieldName . '_id'];
192 }
cf8f0fff
CW
193 $contactEntities = ['contact', 'individual', 'organization', 'household'];
194 $locationEntities = ['email', 'address', 'phone', 'website', 'im'];
c3d3e837 195
7a702ae6 196 $entity = strtolower(CRM_Utils_Array::value('entity', $field, ''));
22e263ad 197 if ($entity && !in_array($entity, array_merge($contactEntities, $locationEntities))) {
d9bbb948
CW
198 switch ($entity) {
199 case 'note':
200 if ($value) {
201 $contactParams['api.Note.create'] = [
202 'note' => $value,
203 'contact_id' => 'user_contact_id',
204 ];
205 }
206 break;
207
208 case 'entity_tag':
209 if (!is_array($value)) {
210 $value = $value ? explode(',', $value) : [];
211 }
212 $contactParams['api.entity_tag.replace'] = [
213 'tag_id' => $value,
214 ];
215 break;
216
217 default:
218 $contactParams['api.' . $entity . '.create'][$fieldName] = $value;
219 //@todo we are not currently declaring this option
220 if (isset($params['batch_id']) && strtolower($entity) == 'contribution') {
221 $contactParams['api.' . $entity . '.create']['batch_id'] = $params['batch_id'];
222 }
223 if (isset($params[$entity . '_id'])) {
224 //todo possibly declare $entity_id in getfields ?
225 $contactParams['api.' . $entity . '.create']['id'] = $params[$entity . '_id'];
226 }
c3d3e837 227 }
6a488035
TO
228 }
229 else {
c3d3e837 230 $contactParams[_civicrm_api3_profile_translate_fieldnames_for_bao($fieldName)] = $value;
6a488035
TO
231 }
232 }
22e263ad 233 if (isset($contactParams['api.contribution.create']) && isset($contactParams['api.membership.create'])) {
cf8f0fff 234 $contactParams['api.membership_payment.create'] = [
599c61ac 235 'contribution_id' => '$value.api.contribution.create.id',
21dfd5f5 236 'membership_id' => '$value.api.membership.create.id',
cf8f0fff 237 ];
599c61ac
E
238 }
239
22e263ad 240 if (isset($contactParams['api.contribution.create']) && isset($contactParams['api.participant.create'])) {
cf8f0fff 241 $contactParams['api.participant_payment.create'] = [
599c61ac 242 'contribution_id' => '$value.api.contribution.create.id',
21dfd5f5 243 'participant_id' => '$value.api.participant.create.id',
cf8f0fff 244 ];
599c61ac 245 }
6a488035 246
d9bbb948 247 $contactParams['contact_id'] = empty($params['contact_id']) ? CRM_Utils_Array::value('id', $params) : $params['contact_id'];
4bcfd71f 248 $contactParams['profile_id'] = $profileID;
6a488035
TO
249 $contactParams['skip_custom'] = 1;
250
251 $contactProfileParams = civicrm_api3_profile_apply($contactParams);
6a488035
TO
252
253 // Contact profile fields
254 $profileParams = $contactProfileParams['values'];
255
256 // If profile having activity fields
257 if ($isContactActivityProfile && !empty($activityParams)) {
258 $activityParams['id'] = $params['activity_id'];
259 $profileParams['api.activity.create'] = $activityParams;
260 }
261
f01ce56b 262 return civicrm_api3('contact', 'create', $profileParams);
6a386447 263}
c3d3e837
E
264
265/**
1747ab99
EM
266 * Translate field names for BAO.
267 *
c3d3e837
E
268 * The api standards expect field names to be lower case but the BAO uses mixed case
269 * so we accept 'email-primary' but pass 'email-Primary' to the BAO
270 * we could make the BAO handle email-primary but this would alter the fieldname seen by hooks
271 * & we would need to consider that change
1747ab99 272 *
cf470720
TO
273 * @param string $fieldName
274 * API field name.
c3d3e837 275 *
a6c01b45 276 * @return string
72b3a70c 277 * BAO Field Name
c3d3e837 278 */
9b873358 279function _civicrm_api3_profile_translate_fieldnames_for_bao($fieldName) {
c3d3e837
E
280 $fieldName = str_replace('url', 'URL', $fieldName);
281 return str_replace('primary', 'Primary', $fieldName);
282}
dc64d047 283
6a386447 284/**
dc64d047
EM
285 * Metadata for submit action.
286 *
6a386447 287 * @param array $params
288 * @param array $apirequest
289 */
290function _civicrm_api3_profile_submit_spec(&$params, $apirequest) {
22e263ad 291 if (isset($apirequest['params']['profile_id'])) {
6a386447 292 // we will return what is required for this profile
293 // note the problem with simply over-riding getfields & then calling generic if needbe is we don't have the
294 // api request array to pass to it.
295 //@todo - it may make more sense just to pass the apiRequest to getfields
296 //@todo get_options should take an array - @ the moment it is only takes 'all' - which is supported
297 // by other getfields fn
298 // we don't resolve state, country & county for performance reasons
1699214f 299 $resolveOptions = ($apirequest['params']['get_options'] ?? NULL) == 'all';
6a386447 300 $profileID = _civicrm_api3_profile_getProfileID($apirequest['params']['profile_id']);
174dbdd5
E
301 $params = _civicrm_api3_buildprofile_submitfields($profileID, $resolveOptions, CRM_Utils_Array::value('cache_clear', $params));
302 }
303 elseif (isset($apirequest['params']['cache_clear'])) {
35671d00 304 _civicrm_api3_buildprofile_submitfields(FALSE, FALSE, TRUE);
6a386447 305 }
306 $params['profile_id']['api.required'] = TRUE;
4c41ecb2 307 $params['profile_id']['title'] = 'Profile ID';
d9bbb948
CW
308 // Profile forms submit tag values as a string; hack to get past api wrapper validation
309 if (!empty($params['tag_id'])) {
310 unset($params['tag_id']['pseudoconstant']);
311 $params['tag_id']['type'] = CRM_Utils_Type::T_STRING;
312 }
6a386447 313}
314
315/**
244bbdd8 316 * Update Profile field values.
d1b0d05e 317 *
6a386447 318 * @deprecated - calling this function directly is deprecated as 'set' is not a clear action
319 * use submit
6a386447 320 *
cf470720 321 * @param array $params
d1b0d05e 322 * Array of property name/value.
244bbdd8 323 * pairs to update profile field values
6a386447 324 *
a6c01b45 325 * @return array
72b3a70c 326 * Updated Contact/ Activity object|CRM_Error
6a386447 327 */
328function civicrm_api3_profile_set($params) {
329 return civicrm_api3('profile', 'submit', $params);
6a488035
TO
330}
331
332/**
244bbdd8 333 * Apply profile.
d1b0d05e 334 *
6a386447 335 * @deprecated - appears to be an internal function - should not be accessible via api
6a488035
TO
336 * Provide formatted values for profile fields.
337 *
cf470720 338 * @param array $params
d1b0d05e 339 * Array of property name/value.
244bbdd8 340 * pairs to profile field values
6a488035 341 *
c3d3e837 342 * @throws API_Exception
488e7aba 343 * @return array
6a488035
TO
344 *
345 * @todo add example
346 * @todo add test cases
6a488035
TO
347 */
348function civicrm_api3_profile_apply($params) {
6a488035
TO
349 $profileFields = CRM_Core_BAO_UFGroup::getFields($params['profile_id'],
350 FALSE,
351 NULL,
352 NULL,
353 NULL,
354 FALSE,
355 NULL,
356 TRUE,
357 NULL,
358 CRM_Core_Permission::EDIT
359 );
360
361 list($data, $contactDetails) = CRM_Contact_BAO_Contact::formatProfileContactParams($params,
362 $profileFields,
363 CRM_Utils_Array::value('contact_id', $params),
364 $params['profile_id'],
365 CRM_Utils_Array::value('contact_type', $params),
366 CRM_Utils_Array::value('skip_custom', $params, FALSE)
367 );
368
369 if (empty($data)) {
4f94e3fa 370 throw new API_Exception('Unable to format profile parameters.');
6a488035
TO
371 }
372
373 return civicrm_api3_create_success($data);
374}
375
4f94e3fa
MM
376/**
377 * Adjust Metadata for Apply action.
378 *
379 * The metadata is used for setting defaults, documentation & validation.
380 *
381 * @param array $params
382 * Array of parameters determined by getfields.
383 */
384function _civicrm_api3_profile_apply_spec(&$params) {
385 $params['profile_id']['api.required'] = 1;
86939032 386 $params['profile_id']['title'] = 'Profile ID';
4f94e3fa 387}
6a488035 388
f01ce56b 389/**
d1b0d05e
EM
390 * Get pseudo profile 'billing'.
391 *
f01ce56b 392 * This is a function to help us 'pretend' billing is a profile & treat it like it is one.
393 * It gets standard credit card address fields etc
394 * Note this is 'better' that the inbuilt version as it will pull in fallback values
395 * billing location -> is_billing -> primary
40a60af6 396 *
397 * Note that that since the existing code for deriving a blank profile is not easily accessible our
398 * interim solution is just to return an empty array
f1a0080c 399 *
c490a46a 400 * @param array $params
f1a0080c
E
401 *
402 * @return array
f01ce56b 403 */
404function _civicrm_api3_profile_getbillingpseudoprofile(&$params) {
5a9e1452 405
b576d770 406 $locationTypeID = CRM_Core_BAO_LocationType::getBilling();
40a60af6 407
22e263ad 408 if (empty($params['contact_id'])) {
5a9e1452 409 $config = CRM_Core_Config::singleton();
cf8f0fff 410 $blanks = [
40a60af6 411 'billing_first_name' => '',
412 'billing_middle_name' => '',
413 'billing_last_name' => '',
5a9e1452 414 'email-' . $locationTypeID => '',
415 'billing_email-' . $locationTypeID => '',
416 'billing_city-' . $locationTypeID => '',
417 'billing_postal_code-' . $locationTypeID => '',
418 'billing_street_address-' . $locationTypeID => '',
419 'billing_country_id-' . $locationTypeID => $config->defaultContactCountry,
420 'billing_state_province_id-' . $locationTypeID => $config->defaultContactStateProvince,
cf8f0fff 421 ];
40a60af6 422 return $blanks;
423 }
5a9e1452 424
cf8f0fff
CW
425 $addressFields = ['street_address', 'city', 'state_province_id', 'country_id', 'postal_code'];
426 $result = civicrm_api3('contact', 'getsingle', [
f01ce56b 427 'id' => $params['contact_id'],
cf8f0fff 428 'api.address.get.1' => ['location_type_id' => 'Billing', 'return' => $addressFields],
f01ce56b 429 // getting the is_billing required or not is an extra db call but probably cheap enough as this isn't an import api
cf8f0fff
CW
430 'api.address.get.2' => ['is_billing' => TRUE, 'return' => $addressFields],
431 'api.email.get.1' => ['location_type_id' => 'Billing'],
432 'api.email.get.2' => ['is_billing' => TRUE],
e4f09afe 433 'return' => 'api.email.get, api.address.get, api.address.getoptions, country, state_province, email, first_name, last_name, middle_name, ' . implode(',', $addressFields),
7c31ae57 434 ]
f01ce56b 435 );
f01ce56b 436
cf8f0fff 437 $values = [
f01ce56b 438 'billing_first_name' => $result['first_name'],
439 'billing_middle_name' => $result['middle_name'],
440 'billing_last_name' => $result['last_name'],
cf8f0fff 441 ];
f01ce56b 442
22e263ad 443 if (!empty($result['api.address.get.1']['count'])) {
f01ce56b 444 foreach ($addressFields as $fieldname) {
2e1f50d6 445 $values['billing_' . $fieldname . '-' . $locationTypeID] = $result['api.address.get.1']['values'][0][$fieldname] ?? '';
f01ce56b 446 }
447 }
bed98343 448 elseif (!empty($result['api.address.get.2']['count'])) {
f01ce56b 449 foreach ($addressFields as $fieldname) {
2e1f50d6 450 $values['billing_' . $fieldname . '-' . $locationTypeID] = $result['api.address.get.2']['values'][0][$fieldname] ?? '';
f01ce56b 451 }
452 }
92e4c2a5 453 else {
f01ce56b 454 foreach ($addressFields as $fieldname) {
2e1f50d6 455 $values['billing_' . $fieldname . '-' . $locationTypeID] = $result[$fieldname] ?? '';
f01ce56b 456 }
457 }
458
22e263ad 459 if (!empty($result['api.email.get.1']['count'])) {
86bfa4f6 460 $values['billing-email' . '-' . $locationTypeID] = $result['api.email.get.1']['values'][0]['email'];
f01ce56b 461 }
bed98343 462 elseif (!empty($result['api.email.get.2']['count'])) {
86bfa4f6 463 $values['billing-email' . '-' . $locationTypeID] = $result['api.email.get.2']['values'][0]['email'];
f01ce56b 464 }
92e4c2a5 465 else {
86bfa4f6 466 $values['billing-email' . '-' . $locationTypeID] = $result['email'];
f01ce56b 467 }
40a60af6 468 // return both variants of email to reflect inconsistencies in form layer
86bfa4f6 469 $values['email' . '-' . $locationTypeID] = $values['billing-email' . '-' . $locationTypeID];
f01ce56b 470 return $values;
471}
6a386447 472
473/**
dc64d047
EM
474 * Here we will build up getfields type data for all the fields in the profile.
475 *
476 * Because the integration with the form layer in core is so hard-coded we are not going to attempt to re-use it
6a386447 477 * However, as this function is unit-tested & hence 'locked in' we can aspire to extract sharable
478 * code out of the form-layer over time.
479 *
480 * The function deciphers which fields belongs to which entites & retrieves metadata about the entities
481 * Unfortunately we have inconsistencies such as 'contribution' uses contribution_status_id
482 * & participant has 'participant_status' so we have to standardise from the outside in here -
483 * find the oddities, 'mask them' at this layer, add tests & work to standardise over time so we can remove this handling
484 *
cf470720
TO
485 * @param int $profileID
486 * @param int $optionsBehaviour
487 * 0 = don't resolve, 1 = resolve non-aggressively, 2 = resolve aggressively - ie include country & state.
2884d956 488 * @param bool $is_flush
7c3f2c03 489 *
bed98343 490 * @return array|void
6a386447 491 */
e9cde327 492function _civicrm_api3_buildprofile_submitfields($profileID, $optionsBehaviour, $is_flush) {
cf8f0fff 493 static $profileFields = [];
22e263ad 494 if ($is_flush) {
cf8f0fff 495 $profileFields = [];
22e263ad 496 if (empty($profileID)) {
bed98343 497 return NULL;
174dbdd5
E
498 }
499 }
22e263ad 500 if (isset($profileFields[$profileID])) {
6a386447 501 return $profileFields[$profileID];
502 }
589d69c8 503 $fields = civicrm_api3('uf_field', 'get', ['uf_group_id' => $profileID, 'options' => ['limit' => 0]]);
cf8f0fff 504 $entities = [];
c1fec147 505 foreach ($fields['values'] as $field) {
22e263ad 506 if (!$field['is_active']) {
6a386447 507 continue;
508 }
509 list($entity, $fieldName) = _civicrm_api3_map_profile_fields_to_entity($field);
cf8f0fff 510 $aliasArray = [];
22e263ad 511 if (strtolower($fieldName) != $fieldName) {
cf8f0fff 512 $aliasArray['api.aliases'] = [$fieldName];
c3d3e837
E
513 $fieldName = strtolower($fieldName);
514 }
cf8f0fff 515 $profileFields[$profileID][$fieldName] = array_merge([
6a386447 516 'api.required' => $field['is_required'],
517 'title' => $field['label'],
6b409353
CW
518 'help_pre' => $field['help_pre'] ?? NULL,
519 'help_post' => $field['help_post'] ?? NULL,
c3d3e837 520 'entity' => $entity,
6b409353 521 'weight' => $field['weight'] ?? NULL,
cf8f0fff 522 ], $aliasArray);
6a386447 523
f5c68f3c 524 $ufFieldTaleFieldName = $field['field_name'];
22e263ad 525 if (isset($entity[$ufFieldTaleFieldName]['name'])) {
f5c68f3c
E
526 // in the case where we are dealing with an alias we map back to a name
527 // this will be tested by 'membership_type_id' field
528 $ufFieldTaleFieldName = $entity[$ufFieldTaleFieldName]['name'];
529 }
6a386447 530 //see function notes
b0b44427 531 // as we build up a list of these we should be able to determine a generic approach
532 //
cf8f0fff 533 $hardCodedEntityFields = [
6a386447 534 'state_province' => 'state_province_id',
535 'country' => 'country_id',
f1d2a871 536 'county' => 'county_id',
6a386447 537 'participant_status' => 'status_id',
538 'gender' => 'gender_id',
b0b44427 539 'financial_type' => 'financial_type_id',
540 'soft_credit' => 'soft_credit_to',
541 'group' => 'group_id',
542 'tag' => 'tag_id',
3ebd4b5c 543 'soft_credit_type' => 'soft_credit_type_id',
cf8f0fff 544 ];
b0b44427 545
22e263ad 546 if (array_key_exists($ufFieldTaleFieldName, $hardCodedEntityFields)) {
f5c68f3c 547 $ufFieldTaleFieldName = $hardCodedEntityFields[$ufFieldTaleFieldName];
6a386447 548 }
b0b44427 549
f5c68f3c 550 $entities[$entity][$fieldName] = $ufFieldTaleFieldName;
6a386447 551 }
552
553 foreach ($entities as $entity => $entityFields) {
cf8f0fff 554 $result = civicrm_api3($entity, 'getfields', ['action' => 'create']);
b0b44427 555 $entityGetFieldsResult = _civicrm_api3_profile_appendaliases($result['values'], $entity);
6a386447 556 foreach ($entityFields as $entityfield => $realName) {
7c3f2c03 557 $fieldName = strtolower($entityfield);
22e263ad
TO
558 if (!strstr($fieldName, '-')) {
559 if (strtolower($realName) != $fieldName) {
35671d00
TO
560 // we want to keep the '-' pattern for locations but otherwise
561 // we are going to make the api-standard field the main / preferred name but support the db name
562 // in future naming the fields in the DB to reflect the way the rest of the api / BAO / metadata works would
563 // reduce code
564 $fieldName = strtolower($realName);
29fbb90a 565 }
22e263ad 566 if (isset($entityGetFieldsResult[$realName]['uniqueName'])) {
f5c68f3c
E
567 // we won't alias the field name on here are we are using uniqueNames for the possibility of needing to differentiate
568 // which entity 'status_id' belongs to
29fbb90a
E
569 $fieldName = $entityGetFieldsResult[$realName]['uniqueName'];
570 }
92e4c2a5 571 else {
22e263ad 572 if (isset($entityGetFieldsResult[$realName]['name'])) {
f5c68f3c
E
573 // this will sort out membership_type_id vs membership_type
574 $fieldName = $entityGetFieldsResult[$realName]['name'];
575 }
576 }
7c3f2c03 577 }
f5c68f3c 578 $profileFields[$profileID][$fieldName] = array_merge($entityGetFieldsResult[$realName], $profileFields[$profileID][$entityfield]);
22e263ad 579 if (!isset($profileFields[$profileID][$fieldName]['api.aliases'])) {
cf8f0fff 580 $profileFields[$profileID][$fieldName]['api.aliases'] = [];
29fbb90a 581 }
22e263ad 582 if ($optionsBehaviour && !empty($entityGetFieldsResult[$realName]['pseudoconstant'])) {
cf8f0fff
CW
583 if ($optionsBehaviour > 1 || !in_array($realName, ['state_province_id', 'county_id', 'country_id'])) {
584 $options = civicrm_api3($entity, 'getoptions', ['field' => $realName]);
7c3f2c03 585 $profileFields[$profileID][$fieldName]['options'] = $options['values'];
6a386447 586 }
587 }
c3d3e837 588
22e263ad
TO
589 if ($entityfield != $fieldName) {
590 if (isset($profileFields[$profileID][$entityfield])) {
7c3f2c03
E
591 unset($profileFields[$profileID][$entityfield]);
592 }
22e263ad 593 if (!in_array($entityfield, $profileFields[$profileID][$fieldName]['api.aliases'])) {
dd9d7b83
EM
594 // we will make the mixed case version (e.g. of 'Primary') an alias
595 $profileFields[$profileID][$fieldName]['api.aliases'][] = $entityfield;
596 }
c3d3e837 597 }
6a386447 598 /**
599 * putting this on hold -this would cause the api to set the default - but could have unexpected behaviour
7c31ae57
SL
600 * if (isset($result['values'][$realName]['default_value'])) {
601 * //this would be the case for a custom field with a configured default
602 * $profileFields[$profileID][$entityfield]['api.default'] = $result['values'][$realName]['default_value'];
603 * }
c3d3e837 604 */
6a386447 605 }
606 }
3413e582 607 $profileFields[$profileID] = $profileFields[$profileID] ?? [];
f5c68f3c 608 uasort($profileFields[$profileID], "_civicrm_api3_order_by_weight");
6a386447 609 return $profileFields[$profileID];
610}
611
aa1b1481 612/**
e71c1326
CW
613 * @param array $a
614 * @param array $b
aa1b1481
EM
615 *
616 * @return bool
617 */
f5c68f3c 618function _civicrm_api3_order_by_weight($a, $b) {
11d593ed 619 return ($b['weight'] ?? 0) < ($a['weight'] ?? 0) ? 1 : -1;
f5c68f3c 620}
f1a0080c 621
6a386447 622/**
623 * Here we map the profile fields as stored in the uf_field table to their 'real entity'
624 * we also return the profile fieldname
625 *
2884d956 626 * @param array $field
f1a0080c
E
627 *
628 * @return array
6a386447 629 */
630function _civicrm_api3_map_profile_fields_to_entity(&$field) {
7c3f2c03 631 $entity = $field['field_type'];
cf8f0fff 632 $contactTypes = civicrm_api3('contact', 'getoptions', ['field' => 'contact_type']);
22e263ad 633 if (in_array($entity, $contactTypes['values'])) {
174dbdd5 634 $entity = 'contact';
6a386447 635 }
7c3f2c03 636 $entity = _civicrm_api_get_entity_name_from_camel($entity);
8cb596a5 637 $locationFields = ['email', 'phone'];
6a386447 638 $fieldName = $field['field_name'];
22e263ad 639 if (!empty($field['location_type_id'])) {
8cb596a5
CW
640 if (in_array($fieldName, $locationFields)) {
641 $entity = $fieldName;
6a386447 642 }
92e4c2a5 643 else {
174dbdd5 644 $entity = 'address';
6a386447 645 }
646 $fieldName .= '-' . $field['location_type_id'];
647 }
8cb596a5
CW
648 elseif (in_array($fieldName, $locationFields)) {
649 $entity = $fieldName;
c3d3e837 650 $fieldName .= '-Primary';
c3d3e837 651 }
22e263ad 652 if (!empty($field['phone_type_id'])) {
8cb596a5 653 $fieldName .= '-' . $field['phone_type_id'];
174dbdd5 654 $entity = 'phone';
6a386447 655 }
c3d3e837 656
b0b44427 657 // @todo - sort this out!
6a386447 658 //here we do a hard-code list of known fields that don't map to where they are mapped to
b0b44427 659 // not a great solution but probably if we looked in the BAO we'd find a scary switch statement
660 // in a perfect world the uf_field table would hold the correct entity for each item
661 // & only the relationships between entities would need to be coded
cf8f0fff 662 $hardCodedEntityMappings = [
174dbdd5
E
663 'street_address' => 'address',
664 'street_number' => 'address',
665 'supplemental_address_1' => 'address',
666 'supplemental_address_2' => 'address',
667 'supplemental_address_3' => 'address',
668 'postal_code' => 'address',
669 'city' => 'address',
670 'email' => 'email',
671 'state_province' => 'address',
672 'country' => 'address',
673 'county' => 'address',
b0b44427 674 //note that in discussions about how to restructure the api we discussed making these membership
675 // fields into 'membership_payment' fields - which would entail declaring them in getfields
676 // & renaming them in existing profiles
174dbdd5
E
677 'financial_type' => 'contribution',
678 'total_amount' => 'contribution',
679 'receive_date' => 'contribution',
680 'payment_instrument' => 'contribution',
7bc1d4da 681 'contribution_check_number' => 'contribution',
174dbdd5
E
682 'contribution_status_id' => 'contribution',
683 'soft_credit' => 'contribution',
3ebd4b5c 684 'soft_credit_type' => 'contribution_soft',
174dbdd5
E
685 'group' => 'group_contact',
686 'tag' => 'entity_tag',
d9bbb948 687 'note' => 'note',
cf8f0fff 688 ];
22e263ad 689 if (array_key_exists($fieldName, $hardCodedEntityMappings)) {
6a386447 690 $entity = $hardCodedEntityMappings[$fieldName];
691 }
cf8f0fff 692 return [$entity, $fieldName];
6a386447 693}
694
695/**
696 * @todo this should be handled by the api wrapper using getfields info - need to check
23fb5e08
EM
697 * how we add a a pseudoconstant to this pseudo api to make that work
698 *
100fef9d 699 * @param int $profileID
23fb5e08 700 *
df8d3074 701 * @return int|string
23fb5e08 702 * @throws CiviCRM_API3_Exception
6a386447 703 */
704function _civicrm_api3_profile_getProfileID($profileID) {
22e263ad 705 if (!empty($profileID) && strtolower($profileID) != 'billing' && !is_numeric($profileID)) {
cf8f0fff 706 $profileID = civicrm_api3('uf_group', 'getvalue', ['return' => 'id', 'name' => $profileID]);
6a386447 707 }
708 return $profileID;
b0b44427 709}
710
711/**
712 * helper function to add all aliases as keys to getfields response so we can look for keys within it
713 * since the relationship between profile fields & api / metadata based fields is a bit inconsistent
0d5cc439 714 *
b0b44427 715 * @param array $values
716 *
717 * e.g getfields response incl 'membership_type_id' - with api.aliases = 'membership_type'
718 * returned array will include both as keys (with the same values)
2884d956 719 * @param string $entity
0d5cc439
E
720 *
721 * @return array
b0b44427 722 */
723function _civicrm_api3_profile_appendaliases($values, $entity) {
724 foreach ($values as $field => $spec) {
22e263ad 725 if (!empty($spec['api.aliases'])) {
b0b44427 726 foreach ($spec['api.aliases'] as $alias) {
727 $values[$alias] = $spec;
728 }
729 }
22e263ad 730 if (!empty($spec['uniqueName'])) {
b0b44427 731 $values[$spec['uniqueName']] = $spec;
732 }
733 }
734 //special case on membership & contribution - can't see how to handle in a generic way
cf8f0fff 735 if (in_array($entity, ['membership', 'contribution'])) {
e71c1326 736 $values['send_receipt'] = ['title' => 'Send Receipt', 'type' => 16];
b0b44427 737 }
738 return $values;
0d5cc439 739}
a14e9d08
CW
740
741/**
742 * @deprecated api notice
a6c01b45 743 * @return array
16b10e64 744 * Array of deprecated actions
a14e9d08
CW
745 */
746function _civicrm_api3_profile_deprecation() {
cf8f0fff 747 return [
a14e9d08
CW
748 'set' => 'Profile api "set" action is deprecated in favor of "submit".',
749 'apply' => 'Profile api "apply" action is deprecated in favor of "submit".',
cf8f0fff 750 ];
a14e9d08 751}