Merge pull request #15829 from eileenmcnaughton/dedupe3
[civicrm-core.git] / CRM / ACL / BAO / ACL.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * Access Control List
20 */
21 class CRM_ACL_BAO_ACL extends CRM_ACL_DAO_ACL {
22 /**
23 * @var string
24 */
25 public static $_entityTable = NULL;
26 public static $_objectTable = NULL;
27 public static $_operation = NULL;
28
29 public static $_fieldKeys = NULL;
30
31 /**
32 * Get ACL entity table.
33 *
34 * @return array|null
35 */
36 public static function entityTable() {
37 if (!self::$_entityTable) {
38 self::$_entityTable = [
39 'civicrm_contact' => ts('Contact'),
40 'civicrm_acl_role' => ts('ACL Role'),
41 ];
42 }
43 return self::$_entityTable;
44 }
45
46 /**
47 * @return array|null
48 */
49 public static function objectTable() {
50 if (!self::$_objectTable) {
51 self::$_objectTable = [
52 'civicrm_contact' => ts('Contact'),
53 'civicrm_group' => ts('Group'),
54 'civicrm_saved_search' => ts('Contact Group'),
55 'civicrm_admin' => ts('Import'),
56 ];
57 }
58 return self::$_objectTable;
59 }
60
61 /**
62 * @return array|null
63 */
64 public static function operation() {
65 if (!self::$_operation) {
66 self::$_operation = [
67 'View' => ts('View'),
68 'Edit' => ts('Edit'),
69 'Create' => ts('Create'),
70 'Delete' => ts('Delete'),
71 'Search' => ts('Search'),
72 'All' => ts('All'),
73 ];
74 }
75 return self::$_operation;
76 }
77
78 /**
79 * Given a table and id pair, return the filter clause
80 *
81 * @param string $table
82 * The table owning the object.
83 * @param int $id
84 * The ID of the object.
85 * @param array $tables
86 * Tables that will be needed in the FROM.
87 *
88 * @return string|null
89 * WHERE-style clause to filter results,
90 * or null if $table or $id is null
91 *
92 * @throws \CRM_Core_Exception
93 */
94 public static function getClause($table, $id, &$tables) {
95 $table = CRM_Utils_Type::escape($table, 'String');
96 $id = CRM_Utils_Type::escape($id, 'Integer');
97 $whereTables = [];
98
99 $ssTable = CRM_Contact_BAO_SavedSearch::getTableName();
100
101 if (empty($table)) {
102 return NULL;
103 }
104 elseif ($table == $ssTable) {
105 return CRM_Contact_BAO_SavedSearch::whereClause($id, $tables, $whereTables);
106 }
107 elseif (!empty($id)) {
108 $tables[$table] = TRUE;
109 return "$table.id = $id";
110 }
111 return NULL;
112 }
113
114 /**
115 * Construct an associative array of an ACL rule's properties
116 *
117 * @param string $format
118 * Sprintf format for array.
119 * @param bool $hideEmpty
120 * Only return elements that have a value set.
121 *
122 * @return array
123 * Assoc. array of the ACL rule's properties
124 */
125 public function toArray($format = '%s', $hideEmpty = FALSE) {
126 $result = [];
127
128 if (!self::$_fieldKeys) {
129 $fields = CRM_ACL_DAO_ACL::fields();
130 self::$_fieldKeys = array_keys($fields);
131 }
132
133 foreach (self::$_fieldKeys as $field) {
134 $result[$field] = $this->$field;
135 }
136 return $result;
137 }
138
139 /**
140 * Retrieve ACLs for a contact or group. Note that including a contact id
141 * without a group id will return those ACL rules which are granted
142 * directly to the contact, but not those granted to the contact through
143 * any/all of his group memberships.
144 *
145 * @param int $contact_id
146 * ID of a contact to search for.
147 *
148 * @return array
149 * Array of assoc. arrays of ACL rules
150 *
151 * @throws \CRM_Core_Exception
152 */
153 public static function getACLs($contact_id = NULL) {
154 $results = [];
155
156 if (empty($contact_id)) {
157 return $results;
158 }
159
160 $contact_id = CRM_Utils_Type::escape($contact_id, 'Integer');
161
162 $rule = new CRM_ACL_BAO_ACL();
163
164 $acl = self::getTableName();
165 $contact = CRM_Contact_BAO_Contact::getTableName();
166 $c2g = CRM_Contact_BAO_GroupContact::getTableName();
167 $group = CRM_Contact_BAO_Group::getTableName();
168
169 $query = " SELECT acl.*
170 FROM $acl acl";
171
172 if (!empty($contact_id)) {
173 $query .= " WHERE acl.entity_table = '$contact'
174 AND acl.entity_id = $contact_id";
175 }
176
177 $rule->query($query);
178
179 while ($rule->fetch()) {
180 $results[$rule->id] = $rule->toArray();
181 }
182
183 $results += self::getACLRoles($contact_id);
184
185 return $results;
186 }
187
188 /**
189 * Get all of the ACLs through ACL groups.
190 *
191 * @param int $contact_id
192 * ID of a contact to search for.
193 *
194 * @return array
195 * Array of assoc. arrays of ACL rules
196 *
197 * @throws \CRM_Core_Exception
198 */
199 public static function getACLRoles($contact_id = NULL) {
200 $contact_id = CRM_Utils_Type::escape($contact_id, 'Integer');
201
202 $rule = new CRM_ACL_BAO_ACL();
203
204 $contact = CRM_Contact_BAO_Contact::getTableName();
205
206 $query = 'SELECT acl.* FROM civicrm_acl acl';
207 $where = ['acl.entity_table = "civicrm_acl_role" AND acl.entity_id IN (' . implode(',', array_keys(CRM_Core_OptionGroup::values('acl_role'))) . ')'];
208
209 if (!empty($contact_id)) {
210 $where[] = " acl.entity_table = '$contact' AND acl.is_active = 1 AND acl.entity_id = $contact_id";
211 }
212
213 $results = [];
214
215 $rule->query($query . ' WHERE ' . implode(' AND ', $where));
216
217 while ($rule->fetch()) {
218 $results[$rule->id] = $rule->toArray();
219 }
220
221 return $results;
222 }
223
224 /**
225 * Get all ACLs granted to a contact through all group memberships.
226 *
227 * @param int $contact_id
228 * The contact's ID.
229 * @param bool $aclRoles
230 * Include ACL Roles?.
231 *
232 * @return array
233 * Assoc array of ACL rules
234 * @throws \CRM_Core_Exception
235 */
236 public static function getGroupACLs($contact_id, $aclRoles = FALSE) {
237 $contact_id = CRM_Utils_Type::escape($contact_id, 'Integer');
238
239 $rule = new CRM_ACL_BAO_ACL();
240
241 $acl = self::getTableName();
242 $c2g = CRM_Contact_BAO_GroupContact::getTableName();
243 $group = CRM_Contact_BAO_Group::getTableName();
244 $results = [];
245
246 if ($contact_id) {
247 $query = "
248 SELECT acl.*
249 FROM $acl acl
250 INNER JOIN $c2g group_contact
251 ON acl.entity_id = group_contact.group_id
252 WHERE acl.entity_table = '$group'
253 AND group_contact.contact_id = $contact_id
254 AND group_contact.status = 'Added'";
255
256 $rule->query($query);
257
258 while ($rule->fetch()) {
259 $results[$rule->id] = $rule->toArray();
260 }
261 }
262
263 if ($aclRoles) {
264 $results += self::getGroupACLRoles($contact_id);
265 }
266
267 return $results;
268 }
269
270 /**
271 * Get all of the ACLs for a contact through ACL groups owned by Contact.
272 * groups.
273 *
274 * @param int $contact_id
275 * ID of a contact to search for.
276 *
277 * @return array
278 * Array of assoc. arrays of ACL rules
279 * @throws \CRM_Core_Exception
280 */
281 public static function getGroupACLRoles($contact_id) {
282 $contact_id = CRM_Utils_Type::escape($contact_id, 'Integer');
283
284 $rule = new CRM_ACL_BAO_ACL();
285
286 $acl = self::getTableName();
287 $aclRole = 'civicrm_acl_role';
288
289 $aclER = CRM_ACL_DAO_EntityRole::getTableName();
290 $c2g = CRM_Contact_BAO_GroupContact::getTableName();
291 $group = CRM_Contact_BAO_Group::getTableName();
292
293 $query = " SELECT acl.*
294 FROM $acl acl
295 INNER JOIN civicrm_option_group og
296 ON og.name = 'acl_role'
297 INNER JOIN civicrm_option_value ov
298 ON acl.entity_table = '$aclRole'
299 AND ov.option_group_id = og.id
300 AND acl.entity_id = ov.value
301 AND ov.is_active = 1
302 INNER JOIN $aclER
303 ON $aclER.acl_role_id = acl.entity_id
304 AND $aclER.is_active = 1
305 INNER JOIN $c2g
306 ON $aclER.entity_id = $c2g.group_id
307 AND $aclER.entity_table = 'civicrm_group'
308 WHERE acl.entity_table = '$aclRole'
309 AND acl.is_active = 1
310 AND $c2g.contact_id = $contact_id
311 AND $c2g.status = 'Added'";
312
313 $results = [];
314
315 $rule->query($query);
316
317 while ($rule->fetch()) {
318 $results[$rule->id] = $rule->toArray();
319 }
320
321 // also get all acls for "Any Role" case
322 // and authenticated User Role if present
323 $roles = "0";
324 $session = CRM_Core_Session::singleton();
325 if ($session->get('ufID') > 0) {
326 $roles .= ",2";
327 }
328
329 $query = "
330 SELECT acl.*
331 FROM $acl acl
332 WHERE acl.entity_id IN ( $roles )
333 AND acl.entity_table = 'civicrm_acl_role'
334 ";
335
336 $rule->query($query);
337 while ($rule->fetch()) {
338 $results[$rule->id] = $rule->toArray();
339 }
340
341 return $results;
342 }
343
344 /**
345 * Get all ACLs owned by a given contact, including domain and group-level.
346 *
347 * @param int $contact_id
348 * The contact ID.
349 *
350 * @return array
351 * Assoc array of ACL rules
352 *
353 * @throws \CRM_Core_Exception
354 */
355 public static function getAllByContact($contact_id) {
356 $result = [];
357
358 /* First, the contact-specific ACLs, including ACL Roles */
359 $result += self::getACLs($contact_id);
360
361 /* Then, all ACLs granted through group membership */
362 $result += self::getGroupACLs($contact_id, TRUE);
363
364 return $result;
365 }
366
367 /**
368 * @param array $params
369 *
370 * @return CRM_ACL_DAO_ACL
371 */
372 public static function create($params) {
373 $dao = new CRM_ACL_DAO_ACL();
374 $dao->copyValues($params);
375 $dao->save();
376 return $dao;
377 }
378
379 /**
380 * @param array $params
381 * @param array $defaults
382 */
383 public static function retrieve(&$params, &$defaults) {
384 CRM_Core_DAO::commonRetrieve('CRM_ACL_DAO_ACL', $params, $defaults);
385 }
386
387 /**
388 * Update the is_active flag in the db.
389 *
390 * @param int $id
391 * Id of the database record.
392 * @param bool $is_active
393 * Value we want to set the is_active field.
394 *
395 * @return bool
396 * true if we found and updated the object, else false
397 */
398 public static function setIsActive($id, $is_active) {
399 Civi::cache('fields')->flush();
400 // reset ACL and system caches.
401 CRM_Core_BAO_Cache::resetCaches();
402
403 return CRM_Core_DAO::setFieldValue('CRM_ACL_DAO_ACL', $id, 'is_active', $is_active);
404 }
405
406 /**
407 * @param $str
408 * @param int $contactID
409 *
410 * @return bool
411 */
412 public static function check($str, $contactID) {
413
414 $acls = CRM_ACL_BAO_Cache::build($contactID);
415
416 $aclKeys = array_keys($acls);
417 $aclKeys = implode(',', $aclKeys);
418
419 if (empty($aclKeys)) {
420 return FALSE;
421 }
422
423 $query = "
424 SELECT count( a.id )
425 FROM civicrm_acl_cache c, civicrm_acl a
426 WHERE c.acl_id = a.id
427 AND a.is_active = 1
428 AND a.object_table = %1
429 AND a.id IN ( $aclKeys )
430 ";
431 $params = [1 => [$str, 'String']];
432
433 $count = CRM_Core_DAO::singleValueQuery($query, $params);
434 return ($count) ? TRUE : FALSE;
435 }
436
437 /**
438 * @param $type
439 * @param $tables
440 * @param $whereTables
441 * @param int $contactID
442 *
443 * @return null|string
444 */
445 public static function whereClause($type, &$tables, &$whereTables, $contactID = NULL) {
446 $acls = CRM_ACL_BAO_Cache::build($contactID);
447
448 $whereClause = NULL;
449 $clauses = [];
450
451 if (!empty($acls)) {
452 $aclKeys = array_keys($acls);
453 $aclKeys = implode(',', $aclKeys);
454
455 $query = "
456 SELECT a.operation, a.object_id
457 FROM civicrm_acl_cache c, civicrm_acl a
458 WHERE c.acl_id = a.id
459 AND a.is_active = 1
460 AND a.object_table = 'civicrm_saved_search'
461 AND a.id IN ( $aclKeys )
462 ORDER BY a.object_id
463 ";
464
465 $dao = CRM_Core_DAO::executeQuery($query);
466
467 // do an or of all the where clauses u see
468 $ids = [];
469 while ($dao->fetch()) {
470 // make sure operation matches the type TODO
471 if (self::matchType($type, $dao->operation)) {
472 if (!$dao->object_id) {
473 $ids = [];
474 $whereClause = ' ( 1 ) ';
475 break;
476 }
477 $ids[] = $dao->object_id;
478 }
479 }
480
481 if (!empty($ids)) {
482 $ids = implode(',', $ids);
483 $query = "
484 SELECT g.*
485 FROM civicrm_group g
486 WHERE g.id IN ( $ids )
487 AND g.is_active = 1
488 ";
489 $dao = CRM_Core_DAO::executeQuery($query);
490 $groupIDs = [];
491 $groupContactCacheClause = FALSE;
492 while ($dao->fetch()) {
493 $groupIDs[] = $dao->id;
494
495 if (($dao->saved_search_id || $dao->children || $dao->parents)) {
496 if ($dao->cache_date == NULL) {
497 CRM_Contact_BAO_GroupContactCache::load($dao);
498 }
499 $groupContactCacheClause = " UNION SELECT contact_id FROM civicrm_group_contact_cache WHERE group_id IN (" . implode(', ', $groupIDs) . ")";
500 }
501
502 }
503
504 if ($groupIDs) {
505 $clauses[] = "(
506 `contact_a`.id IN (
507 SELECT contact_id FROM civicrm_group_contact WHERE group_id IN (" . implode(', ', $groupIDs) . ") AND status = 'Added'
508 $groupContactCacheClause
509 )
510 )";
511 }
512 }
513 }
514
515 if (!empty($clauses)) {
516 $whereClause = ' ( ' . implode(' OR ', $clauses) . ' ) ';
517 }
518
519 // call the hook to get additional whereClauses
520 CRM_Utils_Hook::aclWhereClause($type, $tables, $whereTables, $contactID, $whereClause);
521
522 if (empty($whereClause)) {
523 $whereClause = ' ( 0 ) ';
524 }
525
526 return $whereClause;
527 }
528
529 /**
530 * @param int $type
531 * @param int $contactID
532 * @param string $tableName
533 * @param null $allGroups
534 * @param null $includedGroups
535 *
536 * @return array
537 */
538 public static function group(
539 $type,
540 $contactID = NULL,
541 $tableName = 'civicrm_saved_search',
542 $allGroups = NULL,
543 $includedGroups = NULL
544 ) {
545 $userCacheKey = "{$contactID}_{$type}_{$tableName}_" . CRM_Core_Config::domainID() . '_' . md5(implode(',', array_merge((array) $allGroups, (array) $includedGroups)));
546 if (empty(Civi::$statics[__CLASS__]['permissioned_groups'])) {
547 Civi::$statics[__CLASS__]['permissioned_groups'] = [];
548 }
549 if (!empty(Civi::$statics[__CLASS__]['permissioned_groups'][$userCacheKey])) {
550 return Civi::$statics[__CLASS__]['permissioned_groups'][$userCacheKey];
551 }
552
553 if ($allGroups == NULL) {
554 $allGroups = CRM_Contact_BAO_Contact::buildOptions('group_id', NULL, ['onlyActive' => FALSE]);
555 }
556
557 $acls = CRM_ACL_BAO_Cache::build($contactID);
558
559 $ids = [];
560 if (!empty($acls)) {
561 $aclKeys = array_keys($acls);
562 $aclKeys = implode(',', $aclKeys);
563
564 $cacheKey = CRM_Utils_Cache::cleanKey("$tableName-$aclKeys");
565 $cache = CRM_Utils_Cache::singleton();
566 $ids = $cache->get($cacheKey);
567 if (!$ids) {
568 $ids = [];
569 $query = "
570 SELECT a.operation, a.object_id
571 FROM civicrm_acl_cache c, civicrm_acl a
572 WHERE c.acl_id = a.id
573 AND a.is_active = 1
574 AND a.object_table = %1
575 AND a.id IN ( $aclKeys )
576 GROUP BY a.operation,a.object_id
577 ORDER BY a.object_id
578 ";
579 $params = [1 => [$tableName, 'String']];
580 $dao = CRM_Core_DAO::executeQuery($query, $params);
581 while ($dao->fetch()) {
582 if ($dao->object_id) {
583 if (self::matchType($type, $dao->operation)) {
584 $ids[] = $dao->object_id;
585 }
586 }
587 else {
588 // this user has got the permission for all objects of this type
589 // check if the type matches
590 if (self::matchType($type, $dao->operation)) {
591 foreach ($allGroups as $id => $dontCare) {
592 $ids[] = $id;
593 }
594 }
595 break;
596 }
597 }
598 $cache->set($cacheKey, $ids);
599 }
600 }
601
602 if (empty($ids) && !empty($includedGroups) &&
603 is_array($includedGroups)
604 ) {
605 $ids = $includedGroups;
606 }
607 if ($contactID) {
608 $groupWhere = '';
609 if (!empty($allGroups)) {
610 $groupWhere = " AND id IN (" . implode(',', array_keys($allGroups)) . ")";
611 }
612 // Contacts create hidden groups from search results. They should be able to retrieve their own.
613 $ownHiddenGroupsList = CRM_Core_DAO::singleValueQuery("
614 SELECT GROUP_CONCAT(id) FROM civicrm_group WHERE is_hidden =1 AND created_id = $contactID
615 $groupWhere
616 ");
617 if ($ownHiddenGroupsList) {
618 $ownHiddenGroups = explode(',', $ownHiddenGroupsList);
619 $ids = array_merge((array) $ids, $ownHiddenGroups);
620 }
621
622 }
623
624 CRM_Utils_Hook::aclGroup($type, $contactID, $tableName, $allGroups, $ids);
625 Civi::$statics[__CLASS__]['permissioned_groups'][$userCacheKey] = $ids;
626 return $ids;
627 }
628
629 /**
630 * @param int $type
631 * @param $operation
632 *
633 * @return bool
634 */
635 public static function matchType($type, $operation) {
636 $typeCheck = FALSE;
637 switch ($operation) {
638 case 'All':
639 $typeCheck = TRUE;
640 break;
641
642 case 'View':
643 if ($type == CRM_ACL_API::VIEW) {
644 $typeCheck = TRUE;
645 }
646 break;
647
648 case 'Edit':
649 if ($type == CRM_ACL_API::VIEW || $type == CRM_ACL_API::EDIT) {
650 $typeCheck = TRUE;
651 }
652 break;
653
654 case 'Create':
655 if ($type == CRM_ACL_API::CREATE) {
656 $typeCheck = TRUE;
657 }
658 break;
659
660 case 'Delete':
661 if ($type == CRM_ACL_API::DELETE) {
662 $typeCheck = TRUE;
663 }
664 break;
665
666 case 'Search':
667 if ($type == CRM_ACL_API::SEARCH) {
668 $typeCheck = TRUE;
669 }
670 break;
671 }
672 return $typeCheck;
673 }
674
675 /**
676 * Delete ACL records.
677 *
678 * @param int $aclId
679 * ID of the ACL record to be deleted.
680 *
681 */
682 public static function del($aclId) {
683 // delete all entries from the acl cache
684 CRM_ACL_BAO_Cache::resetCache();
685
686 $acl = new CRM_ACL_DAO_ACL();
687 $acl->id = $aclId;
688 $acl->delete();
689 }
690
691 }