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