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