Merge pull request #16837 from tunbola/case-api-case-clients
[civicrm-core.git] / CRM / Core / BAO / EntityTag.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
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 |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 * This class contains functions for managing Tag(tag) for a contact
14 *
15 * @package CRM
ca5cec67 16 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
17 */
18class CRM_Core_BAO_EntityTag extends CRM_Core_DAO_EntityTag {
19
20 /**
192d36c5 21 * Given a contact id, it returns an array of tag id's the contact belongs to.
6a488035 22 *
6a0b768e
TO
23 * @param int $entityID
24 * Id of the entity usually the contactID.
25 * @param string $entityTable
26 * Name of the entity table usually 'civicrm_contact'.
6a488035 27 *
8d7a9d07
CB
28 * @return array
29 * reference $tag array of category id's the contact belongs to.
6a488035 30 */
24602943 31 public static function getTag($entityID, $entityTable = 'civicrm_contact') {
be2fb01f 32 $tags = [];
6a488035
TO
33
34 $entityTag = new CRM_Core_BAO_EntityTag();
35 $entityTag->entity_id = $entityID;
36 $entityTag->entity_table = $entityTable;
37 $entityTag->find();
38
39 while ($entityTag->fetch()) {
40 $tags[$entityTag->tag_id] = $entityTag->tag_id;
41 }
42 return $tags;
43 }
44
45 /**
fe482240 46 * Takes an associative array and creates a entityTag object.
6a488035
TO
47 *
48 * the function extract all the params it needs to initialize the create a
49 * group object. the params array could contain additional unused name/value
50 * pairs
51 *
6a0b768e
TO
52 * @param array $params
53 * (reference ) an assoc array of name/value pairs.
6a488035 54 *
16b10e64 55 * @return CRM_Core_BAO_EntityTag
6a488035 56 */
00be9182 57 public static function add(&$params) {
6a488035
TO
58 $dataExists = self::dataExists($params);
59 if (!$dataExists) {
60 return NULL;
61 }
62
63 $entityTag = new CRM_Core_BAO_EntityTag();
64 $entityTag->copyValues($params);
65
66 // dont save the object if it already exists, CRM-1276
67 if (!$entityTag->find(TRUE)) {
8f4db603
KJ
68 //invoke pre hook
69 CRM_Utils_Hook::pre('create', 'EntityTag', $params['tag_id'], $params);
70
6a488035
TO
71 $entityTag->save();
72
73 //invoke post hook on entityTag
74 // we are using this format to keep things consistent between the single and bulk operations
75 // so a bit different from other post hooks
be2fb01f 76 $object = [0 => [0 => $params['entity_id']], 1 => $params['entity_table']];
6a488035
TO
77 CRM_Utils_Hook::post('create', 'EntityTag', $params['tag_id'], $object);
78 }
79 return $entityTag;
80 }
81
82 /**
fe482240 83 * Check if there is data to create the object.
6a488035 84 *
6a0b768e
TO
85 * @param array $params
86 * An assoc array of name/value pairs.
dd244018 87 *
8d7a9d07 88 * @return bool
6a488035 89 */
00be9182 90 public static function dataExists($params) {
c490a46a 91 return !($params['tag_id'] == 0);
6a488035
TO
92 }
93
94 /**
fe482240 95 * Delete the tag for a contact.
6a488035 96 *
6a0b768e
TO
97 * @param array $params
98 * (reference ) an assoc array of name/value pairs.
6a488035 99 */
00be9182 100 public static function del(&$params) {
a77c0f8c
KJ
101 //invoke pre hook
102 if (!empty($params['tag_id'])) {
103 CRM_Utils_Hook::pre('delete', 'EntityTag', $params['tag_id'], $params);
104 }
105
6a488035
TO
106 $entityTag = new CRM_Core_BAO_EntityTag();
107 $entityTag->copyValues($params);
355b20b1 108 $entityTag->delete();
6a488035 109
355b20b1 110 //invoke post hook on entityTag
5fb2e445 111 if (!empty($params['tag_id'])) {
be2fb01f 112 $object = [0 => [0 => $params['entity_id']], 1 => $params['entity_table']];
5fb2e445 113 CRM_Utils_Hook::post('delete', 'EntityTag', $params['tag_id'], $object);
114 }
6a488035
TO
115 }
116
117 /**
f836c635 118 * Given an array of entity ids and entity table, add all the entity to the tags.
6a488035 119 *
6a0b768e
TO
120 * @param array $entityIds
121 * (reference ) the array of entity ids to be added.
122 * @param int $tagId
123 * The id of the tag.
124 * @param string $entityTable
125 * Name of entity table default:civicrm_contact.
424616b8 126 * @param bool $applyPermissions
127 * Should permissions be applied in this function.
6a488035 128 *
a6c01b45 129 * @return array
424616b8 130 * (total, added, notAdded) count of entities added to tag
6a488035 131 */
424616b8 132 public static function addEntitiesToTag(&$entityIds, $tagId, $entityTable, $applyPermissions) {
353ffa53 133 $numEntitiesAdded = 0;
6a488035 134 $numEntitiesNotAdded = 0;
be2fb01f 135 $entityIdsAdded = [];
6a488035 136
8f4db603 137 //invoke pre hook for entityTag
be2fb01f 138 $preObject = [$entityIds, $entityTable];
8f4db603
KJ
139 CRM_Utils_Hook::pre('create', 'EntityTag', $tagId, $preObject);
140
6a488035 141 foreach ($entityIds as $entityId) {
f836c635
J
142 // CRM-17350 - check if we have permission to edit the contact
143 // that this tag belongs to.
424616b8 144 if ($applyPermissions && !self::checkPermissionOnEntityTag($entityId, $entityTable)) {
f836c635
J
145 $numEntitiesNotAdded++;
146 continue;
147 }
6a488035
TO
148 $tag = new CRM_Core_DAO_EntityTag();
149
353ffa53
TO
150 $tag->entity_id = $entityId;
151 $tag->tag_id = $tagId;
6a488035
TO
152 $tag->entity_table = $entityTable;
153 if (!$tag->find()) {
154 $tag->save();
155 $entityIdsAdded[] = $entityId;
156 $numEntitiesAdded++;
157 }
158 else {
159 $numEntitiesNotAdded++;
160 }
161 }
162
163 //invoke post hook on entityTag
be2fb01f 164 $object = [$entityIdsAdded, $entityTable];
6a488035
TO
165 CRM_Utils_Hook::post('create', 'EntityTag', $tagId, $object);
166
2b68a50c 167 CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
6a488035 168
be2fb01f 169 return [count($entityIds), $numEntitiesAdded, $numEntitiesNotAdded];
6a488035
TO
170 }
171
172 /**
424616b8 173 * Basic check for ACL permission on editing/creating/removing a tag.
174 *
175 * In the absence of something better contacts get a proper check and other entities
176 * default to 'edit all contacts'. This is currently only accessed from the api which previously
177 * applied edit all contacts to all - so while still too restrictive it represents a loosening.
178 *
179 * Current possible entities are attachments, activities, cases & contacts.
180 *
181 * @param int $entityID
182 * @param string $entityTable
183 *
184 * @return bool
185 */
186 public static function checkPermissionOnEntityTag($entityID, $entityTable) {
187 if ($entityTable == 'civicrm_contact') {
188 return CRM_Contact_BAO_Contact_Permission::allow($entityID, CRM_Core_Permission::EDIT);
189 }
190 else {
191 return CRM_Core_Permission::check('edit all contacts');
192 }
193 }
194
195 /**
196 * Given an array of entity ids and entity table, remove entity(s)tags.
6a488035 197 *
6a0b768e
TO
198 * @param array $entityIds
199 * (reference ) the array of entity ids to be removed.
200 * @param int $tagId
201 * The id of the tag.
202 * @param string $entityTable
203 * Name of entity table default:civicrm_contact.
424616b8 204 * @param bool $applyPermissions
205 * Should permissions be applied in this function.
6a488035 206 *
a6c01b45
CW
207 * @return array
208 * (total, removed, notRemoved) count of entities removed from tags
6a488035 209 */
424616b8 210 public static function removeEntitiesFromTag(&$entityIds, $tagId, $entityTable, $applyPermissions) {
6a488035
TO
211 $numEntitiesRemoved = 0;
212 $numEntitiesNotRemoved = 0;
be2fb01f 213 $entityIdsRemoved = [];
6a488035 214
d5c46f2a 215 //invoke pre hook for entityTag
be2fb01f 216 $preObject = [$entityIds, $entityTable];
d5c46f2a
KJ
217 CRM_Utils_Hook::pre('delete', 'EntityTag', $tagId, $preObject);
218
6a488035 219 foreach ($entityIds as $entityId) {
f836c635
J
220 // CRM-17350 - check if we have permission to edit the contact
221 // that this tag belongs to.
424616b8 222 if ($applyPermissions && !self::checkPermissionOnEntityTag($entityId, $entityTable)) {
223 $numEntitiesNotRemoved++;
f836c635
J
224 continue;
225 }
6a488035
TO
226 $tag = new CRM_Core_DAO_EntityTag();
227
353ffa53
TO
228 $tag->entity_id = $entityId;
229 $tag->tag_id = $tagId;
6a488035
TO
230 $tag->entity_table = $entityTable;
231 if ($tag->find()) {
232 $tag->delete();
233 $entityIdsRemoved[] = $entityId;
234 $numEntitiesRemoved++;
235 }
236 else {
237 $numEntitiesNotRemoved++;
238 }
239 }
240
241 //invoke post hook on entityTag
be2fb01f 242 $object = [$entityIdsRemoved, $entityTable];
6a488035
TO
243 CRM_Utils_Hook::post('delete', 'EntityTag', $tagId, $object);
244
2b68a50c 245 CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
6a488035 246
be2fb01f 247 return [count($entityIds), $numEntitiesRemoved, $numEntitiesNotRemoved];
6a488035
TO
248 }
249
250 /**
fe482240 251 * Takes an associative array and creates tag entity record for all tag entities.
6a488035 252 *
6a0b768e
TO
253 * @param array $params
254 * (reference) an assoc array of name/value pairs.
192d36c5 255 * @param string $entityTable
100fef9d 256 * @param int $entityID
6a488035 257 */
00be9182 258 public static function create(&$params, $entityTable, $entityID) {
6a488035
TO
259 // get categories for the entity id
260 $entityTag = CRM_Core_BAO_EntityTag::getTag($entityID, $entityTable);
261
262 // get the list of all the categories
263 $allTag = CRM_Core_BAO_Tag::getTags($entityTable);
264
265 // this fix is done to prevent warning generated by array_key_exits incase of empty array is given as input
266 if (!is_array($params)) {
be2fb01f 267 $params = [];
6a488035
TO
268 }
269
270 // this fix is done to prevent warning generated by array_key_exits incase of empty array is given as input
271 if (!is_array($entityTag)) {
be2fb01f 272 $entityTag = [];
6a488035
TO
273 }
274
275 // check which values has to be inserted/deleted for contact
276 foreach ($allTag as $key => $varValue) {
277 $tagParams['entity_table'] = $entityTable;
278 $tagParams['entity_id'] = $entityID;
279 $tagParams['tag_id'] = $key;
280
281 if (array_key_exists($key, $params) && !array_key_exists($key, $entityTag)) {
282 // insert a new record
283 CRM_Core_BAO_EntityTag::add($tagParams);
284 }
285 elseif (!array_key_exists($key, $params) && array_key_exists($key, $entityTag)) {
286 // delete a record for existing contact
287 CRM_Core_BAO_EntityTag::del($tagParams);
288 }
289 }
290 }
291
292 /**
fe482240 293 * This function returns all entities assigned to a specific tag.
6a488035 294 *
6a0b768e
TO
295 * @param object $tag
296 * An object of a tag.
6a488035 297 *
a6c01b45
CW
298 * @return array
299 * array of entity ids
6a488035 300 */
00be9182 301 public function getEntitiesByTag($tag) {
be2fb01f 302 $entityIds = [];
6a488035
TO
303 $entityTagDAO = new CRM_Core_DAO_EntityTag();
304 $entityTagDAO->tag_id = $tag->id;
305 $entityTagDAO->find();
306 while ($entityTagDAO->fetch()) {
07fc2fd3 307 $entityIds[] = $entityTagDAO->entity_id;
6a488035 308 }
07fc2fd3 309 return $entityIds;
6a488035
TO
310 }
311
312 /**
fe482240 313 * Get contact tags.
54957108 314 *
315 * @param int $contactID
316 * @param bool $count
317 *
318 * @return array
6a488035 319 */
00be9182 320 public static function getContactTags($contactID, $count = FALSE) {
be2fb01f 321 $contactTags = [];
6a488035 322 if (!$count) {
bff5783c 323 $select = "SELECT ct.id, ct.name ";
6a488035
TO
324 }
325 else {
326 $select = "SELECT count(*) as cnt";
327 }
328
8ef12e64 329 $query = "{$select}
330 FROM civicrm_tag ct
6a488035
TO
331 INNER JOIN civicrm_entity_tag et ON ( ct.id = et.tag_id AND
332 et.entity_id = {$contactID} AND
333 et.entity_table = 'civicrm_contact' AND
334 ct.is_tagset = 0 )";
335
336 $dao = CRM_Core_DAO::executeQuery($query);
337
338 if ($count) {
339 $dao->fetch();
340 return $dao->cnt;
341 }
342
343 while ($dao->fetch()) {
bff5783c 344 $contactTags[$dao->id] = $dao->name;
6a488035
TO
345 }
346
347 return $contactTags;
348 }
349
350 /**
fe482240 351 * Get child contact tags given parentId.
ea3ddccf 352 *
353 * @param int $parentId
354 * @param int $entityId
355 * @param string $entityTable
356 *
357 * @return array
6a488035 358 */
00be9182 359 public static function getChildEntityTags($parentId, $entityId, $entityTable = 'civicrm_contact') {
be2fb01f 360 $entityTags = [];
6a488035
TO
361 $query = "SELECT ct.id as tag_id, name FROM civicrm_tag ct
362 INNER JOIN civicrm_entity_tag et ON ( et.entity_id = {$entityId} AND
363 et.entity_table = '{$entityTable}' AND et.tag_id = ct.id)
364 WHERE ct.parent_id = {$parentId}";
365
366 $dao = CRM_Core_DAO::executeQuery($query);
367
368 while ($dao->fetch()) {
be2fb01f 369 $entityTags[$dao->tag_id] = [
6a488035
TO
370 'id' => $dao->tag_id,
371 'name' => $dao->name,
be2fb01f 372 ];
6a488035
TO
373 }
374
375 return $entityTags;
376 }
377
378 /**
edfb12c4
CW
379 * Merge two tags
380 *
381 * Tag A will inherit all of tag B's properties.
382 * Tag B will be deleted.
ea3ddccf 383 *
384 * @param int $tagAId
385 * @param int $tagBId
386 *
387 * @return array
6a488035 388 */
00be9182 389 public function mergeTags($tagAId, $tagBId) {
be2fb01f
CW
390 $queryParams = [
391 1 => [$tagAId, 'Integer'],
392 2 => [$tagBId, 'Integer'],
393 ];
6a488035
TO
394
395 // re-compute used_for field
396 $query = "SELECT id, name, used_for FROM civicrm_tag WHERE id IN (%1, %2)";
353ffa53 397 $dao = CRM_Core_DAO::executeQuery($query, $queryParams);
be2fb01f 398 $tags = [];
6a488035
TO
399 while ($dao->fetch()) {
400 $label = ($dao->id == $tagAId) ? 'tagA' : 'tagB';
401 $tags[$label] = $dao->name;
be2fb01f 402 $tags["{$label}_used_for"] = $dao->used_for ? explode(",", $dao->used_for) : [];
6a488035
TO
403 }
404 $usedFor = array_merge($tags["tagA_used_for"], $tags["tagB_used_for"]);
405 $usedFor = implode(',', array_unique($usedFor));
edfb12c4 406 $tags["used_for"] = explode(",", $usedFor);
6a488035
TO
407
408 // get all merge queries together
be2fb01f 409 $sqls = [
6a488035 410 // 1. update entity tag entries
c2105be3 411 "UPDATE IGNORE civicrm_entity_tag SET tag_id = %1 WHERE tag_id = %2",
edfb12c4
CW
412 // 2. move children
413 "UPDATE civicrm_tag SET parent_id = %1 WHERE parent_id = %2",
414 // 3. update used_for info for tag A & children
415 "UPDATE civicrm_tag SET used_for = '{$usedFor}' WHERE id = %1 OR parent_id = %1",
416 // 4. delete tag B
6a488035 417 "DELETE FROM civicrm_tag WHERE id = %2",
edfb12c4 418 // 5. remove duplicate entity tag records
6a488035 419 "DELETE et2.* from civicrm_entity_tag et1 INNER JOIN civicrm_entity_tag et2 ON et1.entity_table = et2.entity_table AND et1.entity_id = et2.entity_id AND et1.tag_id = et2.tag_id WHERE et1.id < et2.id",
edfb12c4 420 // 6. remove orphaned entity_tags
c2105be3 421 "DELETE FROM civicrm_entity_tag WHERE tag_id = %2",
be2fb01f
CW
422 ];
423 $tables = ['civicrm_entity_tag', 'civicrm_tag'];
6a488035
TO
424
425 // Allow hook_civicrm_merge() to add SQL statements for the merge operation AND / OR
426 // perform any other actions like logging
427 CRM_Utils_Hook::merge('sqls', $sqls, $tagAId, $tagBId, $tables);
428
429 // call the SQL queries in one transaction
430 $transaction = new CRM_Core_Transaction();
431 foreach ($sqls as $sql) {
432 CRM_Core_DAO::executeQuery($sql, $queryParams, TRUE, NULL, TRUE);
433 }
434 $transaction->commit();
435
436 $tags['status'] = TRUE;
437 return $tags;
438 }
76773c5a
CW
439
440 /**
441 * Get options for a given field.
8d7a9d07 442 *
76773c5a 443 * @see CRM_Core_DAO::buildOptions
8d7a9d07 444 * @see CRM_Core_DAO::buildOptionsContext
76773c5a 445 *
6a0b768e
TO
446 * @param string $fieldName
447 * @param string $context
8d7a9d07 448 * As per CRM_Core_DAO::buildOptionsContext.
6a0b768e 449 * @param array $props
16b10e64 450 * whatever is known about this dao object.
76773c5a 451 *
8d7a9d07 452 * @return array|bool
76773c5a 453 */
be2fb01f
CW
454 public static function buildOptions($fieldName, $context = NULL, $props = []) {
455 $params = [];
76773c5a 456
985f4890
CW
457 if ($fieldName == 'tag' || $fieldName == 'tag_id') {
458 if (!empty($props['entity_table'])) {
459 $entity = CRM_Utils_Type::escape($props['entity_table'], 'String');
460 $params[] = "used_for LIKE '%$entity%'";
461 }
76773c5a 462
985f4890
CW
463 // Output tag list as nested hierarchy
464 // TODO: This will only work when api.entity is "entity_tag". What about others?
465 if ($context == 'search' || $context == 'create') {
be2fb01f 466 $dummyArray = [];
8e74c5fa 467 return CRM_Core_BAO_Tag::getTags(CRM_Utils_Array::value('entity_table', $props, 'civicrm_contact'), $dummyArray, CRM_Utils_Array::value('parent_id', $params), '- ');
985f4890 468 }
76773c5a
CW
469 }
470
985f4890
CW
471 $options = CRM_Core_PseudoConstant::get(__CLASS__, $fieldName, $params, $context);
472
466fce54 473 // Special formatting for validate/match context
be2fb01f
CW
474 if ($fieldName == 'entity_table' && in_array($context, ['validate', 'match'])) {
475 $options = [];
466fce54
CW
476 foreach (self::buildOptions($fieldName) as $tableName => $label) {
477 $bao = CRM_Core_DAO_AllCoreTables::getClassForTable($tableName);
478 $apiName = CRM_Core_DAO_AllCoreTables::getBriefName($bao);
479 $options[$tableName] = $apiName;
480 }
481 }
482
76773c5a
CW
483 return $options;
484 }
96025800 485
9d4c4ffd 486 /**
487 * This function deletes entity tags when a related entity is called.
488 *
489 * It is registered as a listener in \Civi\Core\Container::createEventDispatcher
490 *
491 * @param \Civi\Core\DAO\Event\PreDelete $event
492 */
493 public static function preDeleteOtherEntity($event) {
494 if (
495 $event->object instanceof CRM_Core_DAO_EntityTag
496 // Activity can call the pre hook for delete with no ID - this seems to be isolated to activity....
497 // @todo - what is the correct way to standardise activity delete?
498 || ($event->object instanceof CRM_Activity_DAO_Activity && !$event->object->id)
499
500 ) {
501 return;
502 }
503 // This is probably fairly mild in terms of helping performance - a case could be made to check if tags
504 // exist before deleting (further down) as delete is a locking action.
505 $entity = CRM_Core_DAO_AllCoreTables::getBriefName(get_class($event->object));
506 if (!isset(Civi::$statics[__CLASS__]['tagged_entities'][$entity])) {
507 $tableName = CRM_Core_DAO_AllCoreTables::getTableForEntityName($entity);
508 $used_for = CRM_Core_OptionGroup::values('tag_used_for');
509 Civi::$statics[__CLASS__]['tagged_entities'][$entity] = !empty($used_for[$tableName]) ? $tableName : FALSE;
510 }
511
512 if (Civi::$statics[__CLASS__]['tagged_entities'][$entity]) {
513 CRM_Core_DAO::executeQuery('DELETE FROM civicrm_entity_tag WHERE entity_table = %1 AND entity_id = %2',
514 [1 => [Civi::$statics[__CLASS__]['tagged_entities'][$entity], 'String'], 2 => [$event->object->id, 'Integer']]
515 );
516 }
517
518 }
519
6a488035 520}