3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
13 * This class contains functions for managing Tag(tag) for a contact
16 * @copyright CiviCRM LLC https://civicrm.org/licensing
18 class CRM_Core_BAO_EntityTag
extends CRM_Core_DAO_EntityTag
{
19 use CRM_Core_DynamicFKAccessTrait
;
22 * Given a contact id, it returns an array of tag id's the contact belongs 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'.
30 * reference $tag array of category id's the contact belongs to.
32 public static function getTag($entityID, $entityTable = 'civicrm_contact') {
35 $entityTag = new CRM_Core_BAO_EntityTag();
36 $entityTag->entity_id
= $entityID;
37 $entityTag->entity_table
= $entityTable;
40 while ($entityTag->fetch()) {
41 $tags[$entityTag->tag_id
] = $entityTag->tag_id
;
47 * Takes an associative array and creates a entityTag object.
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
53 * @param array $params
54 * (reference ) an assoc array of name/value pairs.
56 * @return CRM_Core_BAO_EntityTag
58 public static function add(&$params) {
59 $dataExists = self
::dataExists($params);
64 $entityTag = new CRM_Core_BAO_EntityTag();
65 $entityTag->copyValues($params);
67 // dont save the object if it already exists, CRM-1276
68 if (!$entityTag->find(TRUE)) {
70 CRM_Utils_Hook
::pre('create', 'EntityTag', $params['tag_id'], $params);
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
77 $object = [0 => [0 => $params['entity_id']], 1 => $params['entity_table']];
78 CRM_Utils_Hook
::post('create', 'EntityTag', $params['tag_id'], $object);
84 * Check if there is data to create the object.
86 * @param array $params
87 * An assoc array of name/value pairs.
91 public static function dataExists($params) {
92 return !($params['tag_id'] == 0);
96 * Delete the tag for a contact.
98 * @param array $params
100 * WARNING: Nonstandard params searches by tag_id rather than id!
102 public static function del(&$params) {
104 if (!empty($params['tag_id'])) {
105 CRM_Utils_Hook
::pre('delete', 'EntityTag', $params['tag_id'], $params);
108 $entityTag = new CRM_Core_BAO_EntityTag();
109 $entityTag->copyValues($params);
110 $entityTag->delete();
112 //invoke post hook on entityTag
113 if (!empty($params['tag_id'])) {
114 $object = [0 => [0 => $params['entity_id']], 1 => $params['entity_table']];
115 CRM_Utils_Hook
::post('delete', 'EntityTag', $params['tag_id'], $object);
120 * Given an array of entity ids and entity table, add all the entity to the tags.
122 * @param array $entityIds
123 * (reference ) the array of entity ids to be added.
126 * @param string $entityTable
127 * Name of entity table default:civicrm_contact.
128 * @param bool $applyPermissions
129 * Should permissions be applied in this function.
132 * (total, added, notAdded) count of entities added to tag
134 public static function addEntitiesToTag(&$entityIds, $tagId, $entityTable, $applyPermissions) {
135 $numEntitiesAdded = 0;
136 $numEntitiesNotAdded = 0;
137 $entityIdsAdded = [];
139 //invoke pre hook for entityTag
140 $preObject = [$entityIds, $entityTable];
141 CRM_Utils_Hook
::pre('create', 'EntityTag', $tagId, $preObject);
143 foreach ($entityIds as $entityId) {
144 // CRM-17350 - check if we have permission to edit the contact
145 // that this tag belongs to.
146 if ($applyPermissions && !self
::checkPermissionOnEntityTag($entityId, $entityTable)) {
147 $numEntitiesNotAdded++
;
150 $tag = new CRM_Core_DAO_EntityTag();
152 $tag->entity_id
= $entityId;
153 $tag->tag_id
= $tagId;
154 $tag->entity_table
= $entityTable;
157 $entityIdsAdded[] = $entityId;
161 $numEntitiesNotAdded++
;
165 //invoke post hook on entityTag
166 $object = [$entityIdsAdded, $entityTable];
167 CRM_Utils_Hook
::post('create', 'EntityTag', $tagId, $object);
169 CRM_Contact_BAO_GroupContactCache
::opportunisticCacheFlush();
171 return [count($entityIds), $numEntitiesAdded, $numEntitiesNotAdded];
175 * Basic check for ACL permission on editing/creating/removing a tag.
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.
181 * Current possible entities are attachments, activities, cases & contacts.
183 * @param int $entityID
184 * @param string $entityTable
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
);
193 return CRM_Core_Permission
::check('edit all contacts');
198 * Given an array of entity ids and entity table, remove entity(s)tags.
200 * @param array $entityIds
201 * (reference ) the array of entity ids to be removed.
204 * @param string $entityTable
205 * Name of entity table default:civicrm_contact.
206 * @param bool $applyPermissions
207 * Should permissions be applied in this function.
210 * (total, removed, notRemoved) count of entities removed from tags
212 public static function removeEntitiesFromTag(&$entityIds, $tagId, $entityTable, $applyPermissions) {
213 $numEntitiesRemoved = 0;
214 $numEntitiesNotRemoved = 0;
215 $entityIdsRemoved = [];
217 //invoke pre hook for entityTag
218 $preObject = [$entityIds, $entityTable];
219 CRM_Utils_Hook
::pre('delete', 'EntityTag', $tagId, $preObject);
221 foreach ($entityIds as $entityId) {
222 // CRM-17350 - check if we have permission to edit the contact
223 // that this tag belongs to.
224 if ($applyPermissions && !self
::checkPermissionOnEntityTag($entityId, $entityTable)) {
225 $numEntitiesNotRemoved++
;
228 $tag = new CRM_Core_DAO_EntityTag();
230 $tag->entity_id
= $entityId;
231 $tag->tag_id
= $tagId;
232 $tag->entity_table
= $entityTable;
235 $entityIdsRemoved[] = $entityId;
236 $numEntitiesRemoved++
;
239 $numEntitiesNotRemoved++
;
243 //invoke post hook on entityTag
244 $object = [$entityIdsRemoved, $entityTable];
245 CRM_Utils_Hook
::post('delete', 'EntityTag', $tagId, $object);
247 CRM_Contact_BAO_GroupContactCache
::opportunisticCacheFlush();
249 return [count($entityIds), $numEntitiesRemoved, $numEntitiesNotRemoved];
253 * Takes an associative array and creates tag entity record for all tag entities.
255 * @param array $params
256 * (reference) an assoc array of name/value pairs.
257 * @param string $entityTable
258 * @param int $entityID
260 public static function create(&$params, $entityTable, $entityID) {
261 // get categories for the entity id
262 $entityTag = CRM_Core_BAO_EntityTag
::getTag($entityID, $entityTable);
264 // get the list of all the categories
265 $allTag = CRM_Core_BAO_Tag
::getTags($entityTable);
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)) {
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)) {
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;
283 if (array_key_exists($key, $params) && !array_key_exists($key, $entityTag)) {
284 // insert a new record
285 CRM_Core_BAO_EntityTag
::add($tagParams);
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);
297 * @param int $contactID
301 * Dependant on $count
303 public static function getContactTags($contactID, $count = FALSE) {
306 $select = "SELECT ct.id, ct.name ";
309 $select = "SELECT count(*) as cnt";
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
319 $dao = CRM_Core_DAO
::executeQuery($query);
323 return (int) $dao->cnt
;
326 while ($dao->fetch()) {
327 $contactTags[$dao->id
] = $dao->name
;
334 * Get child contact tags given parentId.
336 * @param int $parentId
337 * @param int $entityId
338 * @param string $entityTable
342 public static function getChildEntityTags($parentId, $entityId, $entityTable = 'civicrm_contact') {
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}";
349 $dao = CRM_Core_DAO
::executeQuery($query);
351 while ($dao->fetch()) {
352 $entityTags[$dao->tag_id
] = [
353 'id' => $dao->tag_id
,
354 'name' => $dao->name
,
364 * Tag A will inherit all of tag B's properties.
365 * Tag B will be deleted.
372 public static function mergeTags($tagAId, $tagBId) {
374 1 => [$tagAId, 'Integer'],
375 2 => [$tagBId, 'Integer'],
378 // re-compute used_for field
379 $query = "SELECT id, name, used_for FROM civicrm_tag WHERE id IN (%1, %2)";
380 $dao = CRM_Core_DAO
::executeQuery($query, $queryParams);
382 while ($dao->fetch()) {
383 $label = ($dao->id
== $tagAId) ?
'tagA' : 'tagB';
384 $tags[$label] = $dao->name
;
385 $tags["{$label}_used_for"] = $dao->used_for ?
explode(",", $dao->used_for
) : [];
387 $usedFor = array_merge($tags["tagA_used_for"], $tags["tagB_used_for"]);
388 $usedFor = implode(',', array_unique($usedFor));
389 $tags["used_for"] = explode(",", $usedFor);
391 // get all merge queries together
393 // 1. update entity tag entries
394 "UPDATE IGNORE civicrm_entity_tag SET tag_id = %1 WHERE tag_id = %2",
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",
400 "DELETE FROM civicrm_tag WHERE id = %2",
401 // 5. remove duplicate entity tag records
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",
403 // 6. remove orphaned entity_tags
404 "DELETE FROM civicrm_entity_tag WHERE tag_id = %2",
406 $tables = ['civicrm_entity_tag', 'civicrm_tag'];
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);
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);
417 $transaction->commit();
419 $tags['status'] = TRUE;
424 * Get options for a given field.
426 * @see CRM_Core_DAO::buildOptions
427 * @see CRM_Core_DAO::buildOptionsContext
429 * @param string $fieldName
430 * @param string $context
431 * As per CRM_Core_DAO::buildOptionsContext.
432 * @param array $props
433 * whatever is known about this dao object.
437 public static function buildOptions($fieldName, $context = NULL, $props = []) {
440 if ($fieldName == 'tag' ||
$fieldName == 'tag_id') {
441 $table = 'civicrm_contact';
442 if (!empty($props['entity_table'])) {
443 $table = CRM_Utils_Type
::escape($props['entity_table'], 'String');
444 $params['condition'][] = "used_for LIKE '%$table%'";
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') {
451 return CRM_Core_BAO_Tag
::getTags($table, $dummyArray, NULL, '- ');
455 $options = CRM_Core_PseudoConstant
::get(__CLASS__
, $fieldName, $params, $context);
457 // Special formatting for validate/match context
458 if ($fieldName == 'entity_table' && in_array($context, ['validate', 'match'])) {
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;
471 * This function deletes entity tags when a related entity is called.
473 * It is registered as a listener in \Civi\Core\Container::createEventDispatcher
475 * @param \Civi\Core\DAO\Event\PreDelete $event
477 public static function preDeleteOtherEntity($event) {
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
)
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));
490 if ($entity && !isset(Civi
::$statics[__CLASS__
]['tagged_entities'][$entity])) {
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;
496 if (!empty(Civi
::$statics[__CLASS__
]['tagged_entities'][$entity])) {
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']]