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