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