Merge pull request #17008 from ivan-compucorp/CPS-70-fix-radio-value
[civicrm-core.git] / CRM / Core / BAO / Block.php
1 <?php
2 /*
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 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 *
17 * Add static functions to include some common functionality used across location sub object BAO classes.
18 */
19 class CRM_Core_BAO_Block {
20
21 /**
22 * Fields that are required for a valid block.
23 * @var array
24 */
25 public static $requiredBlockFields = [
26 'email' => ['email'],
27 'phone' => ['phone'],
28 'im' => ['name'],
29 'openid' => ['openid'],
30 ];
31
32 /**
33 * Given the list of params in the params array, fetch the object
34 * and store the values in the values array
35 *
36 * @param string $blockName
37 * Name of the above object.
38 * @param array $params
39 * Input parameters to find object.
40 *
41 * @return array
42 * Array of $block objects.
43 * @throws CRM_Core_Exception
44 */
45 public static function &getValues($blockName, $params) {
46 if (empty($params)) {
47 return NULL;
48 }
49 $BAOString = 'CRM_Core_BAO_' . $blockName;
50 $block = new $BAOString();
51
52 $blocks = [];
53 if (!isset($params['entity_table'])) {
54 $block->contact_id = $params['contact_id'];
55 if (!$block->contact_id) {
56 throw new CRM_Core_Exception('Invalid Contact ID parameter passed');
57 }
58 $blocks = self::retrieveBlock($block, $blockName);
59 }
60 else {
61 $blockIds = self::getBlockIds($blockName, NULL, $params);
62
63 if (empty($blockIds)) {
64 return $blocks;
65 }
66
67 $count = 1;
68 foreach ($blockIds as $blockId) {
69 $block = new $BAOString();
70 $block->id = $blockId['id'];
71 $getBlocks = self::retrieveBlock($block, $blockName);
72 $blocks[$count++] = array_pop($getBlocks);
73 }
74 }
75
76 return $blocks;
77 }
78
79 /**
80 * Given the list of params in the params array, fetch the object
81 * and store the values in the values array
82 *
83 * @param Object $block
84 * Typically a Phone|Email|IM|OpenID object.
85 * @param string $blockName
86 * Name of the above object.
87 *
88 * @return array
89 * Array of $block objects.
90 */
91 public static function retrieveBlock(&$block, $blockName) {
92 // we first get the primary location due to the order by clause
93 $block->orderBy('is_primary desc, id');
94 $block->find();
95
96 $count = 1;
97 $blocks = [];
98 while ($block->fetch()) {
99 CRM_Core_DAO::storeValues($block, $blocks[$count]);
100 //unset is_primary after first block. Due to some bug in earlier version
101 //there might be more than one primary blocks, hence unset is_primary other than first
102 if ($count > 1) {
103 unset($blocks[$count]['is_primary']);
104 }
105 $count++;
106 }
107
108 return $blocks;
109 }
110
111 /**
112 * Check if the current block object has any valid data.
113 *
114 * @param array $blockFields
115 * Array of fields that are of interest for this object.
116 * @param array $params
117 * Associated array of submitted fields.
118 *
119 * @return bool
120 * true if the block has data, otherwise false
121 */
122 public static function dataExists($blockFields, &$params) {
123 foreach ($blockFields as $field) {
124 if (CRM_Utils_System::isNull(CRM_Utils_Array::value($field, $params))) {
125 return FALSE;
126 }
127 }
128 return TRUE;
129 }
130
131 /**
132 * Check if the current block exits.
133 *
134 * @param string $blockName
135 * Bloack name.
136 * @param array $params
137 * Associated array of submitted fields.
138 *
139 * @return bool
140 * true if the block exits, otherwise false
141 */
142 public static function blockExists($blockName, &$params) {
143 // return if no data present
144 if (empty($params[$blockName]) || !is_array($params[$blockName])) {
145 return FALSE;
146 }
147
148 return TRUE;
149 }
150
151 /**
152 * Get all block ids for a contact.
153 *
154 * @param string $blockName
155 * Block name.
156 * @param int $contactId
157 * Contact id.
158 *
159 * @param null $entityElements
160 * @param bool $updateBlankLocInfo
161 *
162 * @return array
163 * formatted array of block ids
164 *
165 */
166 public static function getBlockIds($blockName, $contactId = NULL, $entityElements = NULL, $updateBlankLocInfo = FALSE) {
167 $allBlocks = [];
168
169 $name = ucfirst($blockName);
170 if ($blockName == 'im') {
171 $name = 'IM';
172 }
173 elseif ($blockName == 'openid') {
174 $name = 'OpenID';
175 }
176
177 $baoString = 'CRM_Core_BAO_' . $name;
178 if ($contactId) {
179 //@todo a cleverer way to do this would be to use the same fn name on each
180 // BAO rather than constructing the fn
181 // it would also be easier to grep for
182 // e.g $bao = new $baoString;
183 // $bao->getAllBlocks()
184 $baoFunction = 'all' . $name . 's';
185 $allBlocks = $baoString::$baoFunction($contactId, $updateBlankLocInfo);
186 }
187 elseif (!empty($entityElements) && $blockName != 'openid') {
188 $baoFunction = 'allEntity' . $name . 's';
189 $allBlocks = $baoString::$baoFunction($entityElements);
190 }
191
192 return $allBlocks;
193 }
194
195 /**
196 * Takes an associative array and creates a block.
197 *
198 * @param string $blockName
199 * Block name.
200 * @param array $params
201 * (reference ) an assoc array of name/value pairs.
202 * @param null $entity
203 * @param int $contactId
204 *
205 * @return object
206 * CRM_Core_BAO_Block object on success, null otherwise
207 */
208 public static function create($blockName, &$params, $entity = NULL, $contactId = NULL) {
209 if (!self::blockExists($blockName, $params)) {
210 return NULL;
211 }
212
213 $name = ucfirst($blockName);
214 $isPrimary = $isBilling = TRUE;
215 $entityElements = $blocks = [];
216 $resetPrimaryId = NULL;
217 $primaryId = FALSE;
218
219 if ($entity) {
220 $entityElements = [
221 'entity_table' => $params['entity_table'],
222 'entity_id' => $params['entity_id'],
223 ];
224 }
225 else {
226 $contactId = $params['contact_id'];
227 }
228
229 $updateBlankLocInfo = CRM_Utils_Array::value('updateBlankLocInfo', $params, FALSE);
230 $isIdSet = CRM_Utils_Array::value('isIdSet', $params[$blockName], FALSE);
231
232 //get existing block ids.
233 $blockIds = self::getBlockIds($blockName, $contactId, $entityElements);
234 foreach ($params[$blockName] as $count => $value) {
235 $blockId = $value['id'] ?? NULL;
236 if ($blockId) {
237 if (is_array($blockIds) && array_key_exists($blockId, $blockIds)) {
238 unset($blockIds[$blockId]);
239 }
240 else {
241 unset($value['id']);
242 }
243 }
244 //lets allow to update primary w/ more cleanly.
245 if (!$resetPrimaryId && !empty($value['is_primary'])) {
246 $primaryId = TRUE;
247 if (is_array($blockIds)) {
248 foreach ($blockIds as $blockId => $blockValue) {
249 if (!empty($blockValue['is_primary'])) {
250 $resetPrimaryId = $blockId;
251 break;
252 }
253 }
254 }
255 if ($resetPrimaryId) {
256 $baoString = 'CRM_Core_BAO_' . $blockName;
257 $block = new $baoString();
258 $block->selectAdd();
259 $block->selectAdd("id, is_primary");
260 $block->id = $resetPrimaryId;
261 if ($block->find(TRUE)) {
262 $block->is_primary = FALSE;
263 $block->save();
264 }
265 }
266 }
267 }
268
269 foreach ($params[$blockName] as $count => $value) {
270 if (!is_array($value)) {
271 continue;
272 }
273 // if in some cases (eg. email used in Online Conribution Page, Profiles, etc.) id is not set
274 // lets try to add using the previous method to avoid any false creation of existing data.
275 foreach ($blockIds as $blockId => $blockValue) {
276 if (empty($value['id']) && $blockValue['locationTypeId'] == CRM_Utils_Array::value('location_type_id', $value) && !$isIdSet) {
277 $valueId = FALSE;
278 if ($blockName == 'phone') {
279 $phoneTypeBlockValue = $blockValue['phoneTypeId'] ?? NULL;
280 if ($phoneTypeBlockValue == CRM_Utils_Array::value('phone_type_id', $value)) {
281 $valueId = TRUE;
282 }
283 }
284 elseif ($blockName == 'im') {
285 $providerBlockValue = $blockValue['providerId'] ?? NULL;
286 if (!empty($value['provider_id']) && $providerBlockValue == $value['provider_id']) {
287 $valueId = TRUE;
288 }
289 }
290 else {
291 $valueId = TRUE;
292 }
293 if ($valueId) {
294 $value['id'] = $blockValue['id'];
295 if (!$primaryId && !empty($blockValue['is_primary'])) {
296 $value['is_primary'] = $blockValue['is_primary'];
297 }
298 break;
299 }
300 }
301 }
302 $dataExists = self::dataExists(self::$requiredBlockFields[$blockName], $value);
303 // Note there could be cases when block info already exist ($value[id] is set) for a contact/entity
304 // BUT info is not present at this time, and therefore we should be really careful when deleting the block.
305 // $updateBlankLocInfo will help take appropriate decision. CRM-5969
306 if (!empty($value['id']) && !$dataExists && $updateBlankLocInfo) {
307 //delete the existing record
308 self::blockDelete($blockName, ['id' => $value['id']]);
309 continue;
310 }
311 elseif (!$dataExists) {
312 continue;
313 }
314 $contactFields = [
315 'contact_id' => $contactId,
316 'location_type_id' => $value['location_type_id'] ?? NULL,
317 ];
318
319 $contactFields['is_primary'] = 0;
320 if ($isPrimary && !empty($value['is_primary'])) {
321 $contactFields['is_primary'] = $value['is_primary'];
322 $isPrimary = FALSE;
323 }
324
325 $contactFields['is_billing'] = 0;
326 if ($isBilling && !empty($value['is_billing'])) {
327 $contactFields['is_billing'] = $value['is_billing'];
328 $isBilling = FALSE;
329 }
330
331 $blockFields = array_merge($value, $contactFields);
332 if ($name === 'Email') {
333 // @todo ideally all would call the api which is our main tested function,
334 // and towards that call the create rather than add which is preferred by the
335 // api. In order to be careful with change only email is swapped over here because it
336 // is specifically tested in testImportParserWithUpdateWithContactID
337 // and the primary handling is otherwise bypassed on importing an email update.
338 $blocks[] = CRM_Core_BAO_Email::create($blockFields);
339 }
340 else {
341 $baoString = 'CRM_Core_BAO_' . $name;
342 $blocks[] = $baoString::add($blockFields);
343 }
344 }
345
346 return $blocks;
347 }
348
349 /**
350 * Delete block.
351 *
352 * @param string $blockName
353 * Block name.
354 * @param int $params
355 * Associates array.
356 */
357 public static function blockDelete($blockName, $params) {
358 $name = ucfirst($blockName);
359 if ($blockName == 'im') {
360 $name = 'IM';
361 }
362 elseif ($blockName == 'openid') {
363 $name = 'OpenID';
364 }
365
366 $baoString = 'CRM_Core_DAO_' . $name;
367 $block = new $baoString();
368
369 $block->copyValues($params);
370
371 // CRM-11006 add call to pre and post hook for delete action
372 CRM_Utils_Hook::pre('delete', $name, $block->id, CRM_Core_DAO::$_nullArray);
373 $block->delete();
374 CRM_Utils_Hook::post('delete', $name, $block->id, $block);
375 }
376
377 /**
378 * Handling for is_primary.
379 * $params is_primary could be
380 * # 1 - find other entries with is_primary = 1 & reset them to 0
381 * # 0 - make sure at least one entry is set to 1
382 * - if no other entry is 1 change to 1
383 * - if one other entry exists change that to 1
384 * - if more than one other entry exists change first one to 1
385 * @fixme - perhaps should choose by location_type
386 * # empty - same as 0 as once we have checked first step
387 * we know if it should be 1 or 0
388 *
389 * if $params['id'] is set $params['contact_id'] may need to be retrieved
390 *
391 * @param array $params
392 * @param $class
393 *
394 * @throws API_Exception
395 */
396 public static function handlePrimary(&$params, $class) {
397 $table = CRM_Core_DAO_AllCoreTables::getTableForClass($class);
398 if (!$table) {
399 throw new API_Exception("Failed to locate table for class [$class]");
400 }
401
402 // contact_id in params might be empty or the string 'null' so cast to integer
403 $contactId = (int) ($params['contact_id'] ?? 0);
404 // If id is set & we haven't been passed a contact_id, retrieve it
405 if (!empty($params['id']) && !isset($params['contact_id'])) {
406 $entity = new $class();
407 $entity->id = $params['id'];
408 $entity->find(TRUE);
409 $contactId = $entity->contact_id;
410 }
411 // If entity is not associated with contact, concept of is_primary not relevant
412 if (!$contactId) {
413 return;
414 }
415
416 // if params is_primary then set all others to not be primary & exit out
417 // if is_primary = 1
418 if (!empty($params['is_primary'])) {
419 $sql = "UPDATE $table SET is_primary = 0 WHERE contact_id = %1";
420 $sqlParams = [1 => [$contactId, 'Integer']];
421 // we don't want to create unnecessary entries in the log_ tables so exclude the one we are working on
422 if (!empty($params['id'])) {
423 $sql .= " AND id <> %2";
424 $sqlParams[2] = [$params['id'], 'Integer'];
425 }
426 CRM_Core_DAO::executeQuery($sql, $sqlParams);
427 return;
428 }
429
430 //Check what other emails exist for the contact
431 $existingEntities = new $class();
432 $existingEntities->contact_id = $contactId;
433 $existingEntities->orderBy('is_primary DESC');
434 if (!$existingEntities->find(TRUE) || (!empty($params['id']) && $existingEntities->id == $params['id'])) {
435 // ie. if no others is set to be primary then this has to be primary set to 1 so change
436 $params['is_primary'] = 1;
437 return;
438 }
439 else {
440 /*
441 * If the only existing email is the one we are editing then we must set
442 * is_primary to 1
443 * CRM-10451
444 */
445 if ($existingEntities->N == 1 && $existingEntities->id == CRM_Utils_Array::value('id', $params)) {
446 $params['is_primary'] = 1;
447 return;
448 }
449
450 if ($existingEntities->is_primary == 1) {
451 return;
452 }
453 // so at this point we are only dealing with ones explicity setting is_primary to 0
454 // since we have reverse sorted by email we can either set the first one to
455 // primary or return if is already is
456 $existingEntities->is_primary = 1;
457 $existingEntities->save();
458 }
459 }
460
461 /**
462 * Sort location array so primary element is first.
463 *
464 * @param array $locations
465 */
466 public static function sortPrimaryFirst(&$locations) {
467 uasort($locations, 'self::primaryComparison');
468 }
469
470 /**
471 * compare 2 locations to see which should go first based on is_primary
472 * (sort function for sortPrimaryFirst)
473 * @param array $location1
474 * @param array $location2
475 * @return int
476 */
477 public static function primaryComparison($location1, $location2) {
478 $l1 = $location1['is_primary'] ?? NULL;
479 $l2 = $location2['is_primary'] ?? NULL;
480 if ($l1 == $l2) {
481 return 0;
482 }
483 return ($l1 < $l2) ? -1 : 1;
484 }
485
486 }