Merge in 5.15
[civicrm-core.git] / CRM / Core / BAO / EntityTag.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
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 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * This class contains functions for managing Tag(tag) for a contact
30 *
31 * @package CRM
32 * @copyright CiviCRM LLC (c) 2004-2019
33 */
34 class CRM_Core_BAO_EntityTag extends CRM_Core_DAO_EntityTag {
35
36 /**
37 * Given a contact id, it returns an array of tag id's the contact belongs to.
38 *
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'.
43 *
44 * @return array
45 * reference $tag array of category id's the contact belongs to.
46 */
47 public static function getTag($entityID, $entityTable = 'civicrm_contact') {
48 $tags = [];
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 /**
62 * Takes an associative array and creates a entityTag object.
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 *
68 * @param array $params
69 * (reference ) an assoc array of name/value pairs.
70 *
71 * @return CRM_Core_BAO_EntityTag
72 */
73 public static function add(&$params) {
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)) {
84 //invoke pre hook
85 CRM_Utils_Hook::pre('create', 'EntityTag', $params['tag_id'], $params);
86
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
92 $object = [0 => [0 => $params['entity_id']], 1 => $params['entity_table']];
93 CRM_Utils_Hook::post('create', 'EntityTag', $params['tag_id'], $object);
94 }
95 return $entityTag;
96 }
97
98 /**
99 * Check if there is data to create the object.
100 *
101 * @param array $params
102 * An assoc array of name/value pairs.
103 *
104 * @return bool
105 */
106 public static function dataExists($params) {
107 return !($params['tag_id'] == 0);
108 }
109
110 /**
111 * Delete the tag for a contact.
112 *
113 * @param array $params
114 * (reference ) an assoc array of name/value pairs.
115 */
116 public static function del(&$params) {
117 //invoke pre hook
118 if (!empty($params['tag_id'])) {
119 CRM_Utils_Hook::pre('delete', 'EntityTag', $params['tag_id'], $params);
120 }
121
122 $entityTag = new CRM_Core_BAO_EntityTag();
123 $entityTag->copyValues($params);
124 $entityTag->delete();
125
126 //invoke post hook on entityTag
127 if (!empty($params['tag_id'])) {
128 $object = [0 => [0 => $params['entity_id']], 1 => $params['entity_table']];
129 CRM_Utils_Hook::post('delete', 'EntityTag', $params['tag_id'], $object);
130 }
131 }
132
133 /**
134 * Given an array of entity ids and entity table, add all the entity to the tags.
135 *
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.
142 * @param bool $applyPermissions
143 * Should permissions be applied in this function.
144 *
145 * @return array
146 * (total, added, notAdded) count of entities added to tag
147 */
148 public static function addEntitiesToTag(&$entityIds, $tagId, $entityTable, $applyPermissions) {
149 $numEntitiesAdded = 0;
150 $numEntitiesNotAdded = 0;
151 $entityIdsAdded = [];
152
153 //invoke pre hook for entityTag
154 $preObject = [$entityIds, $entityTable];
155 CRM_Utils_Hook::pre('create', 'EntityTag', $tagId, $preObject);
156
157 foreach ($entityIds as $entityId) {
158 // CRM-17350 - check if we have permission to edit the contact
159 // that this tag belongs to.
160 if ($applyPermissions && !self::checkPermissionOnEntityTag($entityId, $entityTable)) {
161 $numEntitiesNotAdded++;
162 continue;
163 }
164 $tag = new CRM_Core_DAO_EntityTag();
165
166 $tag->entity_id = $entityId;
167 $tag->tag_id = $tagId;
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
180 $object = [$entityIdsAdded, $entityTable];
181 CRM_Utils_Hook::post('create', 'EntityTag', $tagId, $object);
182
183 CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
184
185 return [count($entityIds), $numEntitiesAdded, $numEntitiesNotAdded];
186 }
187
188 /**
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.
213 *
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.
220 * @param bool $applyPermissions
221 * Should permissions be applied in this function.
222 *
223 * @return array
224 * (total, removed, notRemoved) count of entities removed from tags
225 */
226 public static function removeEntitiesFromTag(&$entityIds, $tagId, $entityTable, $applyPermissions) {
227 $numEntitiesRemoved = 0;
228 $numEntitiesNotRemoved = 0;
229 $entityIdsRemoved = [];
230
231 //invoke pre hook for entityTag
232 $preObject = [$entityIds, $entityTable];
233 CRM_Utils_Hook::pre('delete', 'EntityTag', $tagId, $preObject);
234
235 foreach ($entityIds as $entityId) {
236 // CRM-17350 - check if we have permission to edit the contact
237 // that this tag belongs to.
238 if ($applyPermissions && !self::checkPermissionOnEntityTag($entityId, $entityTable)) {
239 $numEntitiesNotRemoved++;
240 continue;
241 }
242 $tag = new CRM_Core_DAO_EntityTag();
243
244 $tag->entity_id = $entityId;
245 $tag->tag_id = $tagId;
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
258 $object = [$entityIdsRemoved, $entityTable];
259 CRM_Utils_Hook::post('delete', 'EntityTag', $tagId, $object);
260
261 CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
262
263 return [count($entityIds), $numEntitiesRemoved, $numEntitiesNotRemoved];
264 }
265
266 /**
267 * Takes an associative array and creates tag entity record for all tag entities.
268 *
269 * @param array $params
270 * (reference) an assoc array of name/value pairs.
271 * @param string $entityTable
272 * @param int $entityID
273 */
274 public static function create(&$params, $entityTable, $entityID) {
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)) {
283 $params = [];
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)) {
288 $entityTag = [];
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 /**
309 * This function returns all entities assigned to a specific tag.
310 *
311 * @param object $tag
312 * An object of a tag.
313 *
314 * @return array
315 * array of entity ids
316 */
317 public function getEntitiesByTag($tag) {
318 $entityIds = [];
319 $entityTagDAO = new CRM_Core_DAO_EntityTag();
320 $entityTagDAO->tag_id = $tag->id;
321 $entityTagDAO->find();
322 while ($entityTagDAO->fetch()) {
323 $entityIds[] = $entityTagDAO->entity_id;
324 }
325 return $entityIds;
326 }
327
328 /**
329 * Get contact tags.
330 *
331 * @param int $contactID
332 * @param bool $count
333 *
334 * @return array
335 */
336 public static function getContactTags($contactID, $count = FALSE) {
337 $contactTags = [];
338 if (!$count) {
339 $select = "SELECT ct.id, ct.name ";
340 }
341 else {
342 $select = "SELECT count(*) as cnt";
343 }
344
345 $query = "{$select}
346 FROM civicrm_tag ct
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()) {
360 $contactTags[$dao->id] = $dao->name;
361 }
362
363 return $contactTags;
364 }
365
366 /**
367 * Get child contact tags given parentId.
368 *
369 * @param int $parentId
370 * @param int $entityId
371 * @param string $entityTable
372 *
373 * @return array
374 */
375 public static function getChildEntityTags($parentId, $entityId, $entityTable = 'civicrm_contact') {
376 $entityTags = [];
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()) {
385 $entityTags[$dao->tag_id] = [
386 'id' => $dao->tag_id,
387 'name' => $dao->name,
388 ];
389 }
390
391 return $entityTags;
392 }
393
394 /**
395 * Merge two tags
396 *
397 * Tag A will inherit all of tag B's properties.
398 * Tag B will be deleted.
399 *
400 * @param int $tagAId
401 * @param int $tagBId
402 *
403 * @return array
404 */
405 public function mergeTags($tagAId, $tagBId) {
406 $queryParams = [
407 1 => [$tagAId, 'Integer'],
408 2 => [$tagBId, 'Integer'],
409 ];
410
411 // re-compute used_for field
412 $query = "SELECT id, name, used_for FROM civicrm_tag WHERE id IN (%1, %2)";
413 $dao = CRM_Core_DAO::executeQuery($query, $queryParams);
414 $tags = [];
415 while ($dao->fetch()) {
416 $label = ($dao->id == $tagAId) ? 'tagA' : 'tagB';
417 $tags[$label] = $dao->name;
418 $tags["{$label}_used_for"] = $dao->used_for ? explode(",", $dao->used_for) : [];
419 }
420 $usedFor = array_merge($tags["tagA_used_for"], $tags["tagB_used_for"]);
421 $usedFor = implode(',', array_unique($usedFor));
422 $tags["used_for"] = explode(",", $usedFor);
423
424 // get all merge queries together
425 $sqls = [
426 // 1. update entity tag entries
427 "UPDATE IGNORE civicrm_entity_tag SET tag_id = %1 WHERE tag_id = %2",
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
433 "DELETE FROM civicrm_tag WHERE id = %2",
434 // 5. remove duplicate entity tag records
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",
436 // 6. remove orphaned entity_tags
437 "DELETE FROM civicrm_entity_tag WHERE tag_id = %2",
438 ];
439 $tables = ['civicrm_entity_tag', 'civicrm_tag'];
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 }
455
456 /**
457 * Get options for a given field.
458 *
459 * @see CRM_Core_DAO::buildOptions
460 * @see CRM_Core_DAO::buildOptionsContext
461 *
462 * @param string $fieldName
463 * @param string $context
464 * As per CRM_Core_DAO::buildOptionsContext.
465 * @param array $props
466 * whatever is known about this dao object.
467 *
468 * @return array|bool
469 */
470 public static function buildOptions($fieldName, $context = NULL, $props = []) {
471 $params = [];
472
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 }
478
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') {
482 $dummyArray = [];
483 return CRM_Core_BAO_Tag::getTags(CRM_Utils_Array::value('entity_table', $props, 'civicrm_contact'), $dummyArray, CRM_Utils_Array::value('parent_id', $params), '- ');
484 }
485 }
486
487 $options = CRM_Core_PseudoConstant::get(__CLASS__, $fieldName, $params, $context);
488
489 // Special formatting for validate/match context
490 if ($fieldName == 'entity_table' && in_array($context, ['validate', 'match'])) {
491 $options = [];
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
499 return $options;
500 }
501
502 }