Merge pull request #22654 from colemanw/optionGroupName
[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 implements \Civi\Test\HookInterface {
22
23 /**
24 * Available operations for pseudoconstant.
25 *
26 * @return array
27 */
28 public static function operation(): array {
29 return [
30 'View' => ts('View'),
31 'Edit' => ts('Edit'),
32 'Create' => ts('Create'),
33 'Delete' => ts('Delete'),
34 'Search' => ts('Search'),
35 'All' => ts('All'),
36 ];
37 }
38
39 /**
40 * Get all of the ACLs for a contact through ACL groups owned by Contact.
41 * groups.
42 *
43 * @param int $contact_id
44 * ID of a contact to search for.
45 *
46 * @return array
47 * Array of assoc. arrays of ACL rules
48 * @throws \CRM_Core_Exception
49 */
50 protected static function getGroupACLRoles(int $contact_id) {
51
52 $query = " SELECT acl.*
53 FROM civicrm_acl acl
54 INNER JOIN civicrm_option_group og
55 ON og.name = 'acl_role'
56 INNER JOIN civicrm_option_value ov
57 ON acl.entity_table = 'civicrm_acl_role'
58 AND ov.option_group_id = og.id
59 AND acl.entity_id = ov.value
60 AND ov.is_active = 1
61 INNER JOIN civicrm_acl_entity_role acl_entity_role
62 ON acl_entity_role.acl_role_id = acl.entity_id
63 AND acl_entity_role.is_active = 1
64 INNER JOIN civicrm_group_contact group_contact
65 ON acl_entity_role.entity_id = group_contact.group_id
66 AND acl_entity_role.entity_table = 'civicrm_group'
67 WHERE acl.entity_table = 'civicrm_acl_role'
68 AND acl.is_active = 1
69 AND group_contact.contact_id = $contact_id
70 AND group_contact.status = 'Added'";
71
72 $results = [];
73
74 $rule = CRM_Core_DAO::executeQuery($query);
75
76 while ($rule->fetch()) {
77 $results[$rule->id] = $rule->toArray();
78 }
79
80 return $results;
81 }
82
83 /**
84 * Get all ACLs owned by a given contact, including domain and group-level.
85 *
86 * @param int $contact_id
87 * The contact ID.
88 *
89 * @return array
90 * Assoc array of ACL rules
91 *
92 * @throws \CRM_Core_Exception
93 */
94 public static function getAllByContact(int $contact_id): array {
95 $result = [];
96
97 /* First, the contact-specific ACLs, including ACL Roles */
98 // 0 would be the anonymous contact.
99 if ($contact_id > 0) {
100 $query = " SELECT acl.*
101 FROM civicrm_acl acl
102 WHERE acl.entity_table = 'civicrm_contact'
103 AND acl.entity_id = $contact_id";
104
105 $rule = CRM_Core_DAO::executeQuery($query);
106
107 while ($rule->fetch()) {
108 $result[$rule->id] = $rule->toArray();
109 }
110 $query = "
111 SELECT acl.*
112 FROM civicrm_acl acl
113 INNER JOIN civicrm_group_contact group_contact
114 ON acl.entity_id = group_contact.group_id
115 WHERE acl.entity_table = 'civicrm_group'
116 AND group_contact.contact_id = $contact_id
117 AND group_contact.status = 'Added'";
118
119 $rule = CRM_Core_DAO::executeQuery($query);
120
121 while ($rule->fetch()) {
122 $result[$rule->id] = $rule->toArray();
123 }
124 $result += self::getGroupACLRoles($contact_id);
125 }
126 // also get all acls for "Any Role" case
127 // and authenticated User Role if present
128 $roles = '0';
129 $session = CRM_Core_Session::singleton();
130 if ($session->get('ufID') > 0) {
131 $roles .= ',2';
132 }
133
134 $query = "
135 SELECT acl.*
136 FROM civicrm_acl acl
137 WHERE acl.entity_id IN ( $roles )
138 AND acl.entity_table = 'civicrm_acl_role'
139 ";
140
141 $rule = CRM_Core_DAO::executeQuery($query);
142 while ($rule->fetch()) {
143 $result[$rule->id] = $rule->toArray();
144 }
145 return $result;
146 }
147
148 /**
149 * @param array $params
150 *
151 * @return CRM_ACL_DAO_ACL
152 */
153 public static function create($params) {
154 $dao = new CRM_ACL_DAO_ACL();
155 $dao->copyValues($params);
156 $dao->save();
157 return $dao;
158 }
159
160 /**
161 * @param array $params
162 * @param array $defaults
163 */
164 public static function retrieve(&$params, &$defaults) {
165 CRM_Core_DAO::commonRetrieve('CRM_ACL_DAO_ACL', $params, $defaults);
166 }
167
168 /**
169 * Update the is_active flag in the db.
170 *
171 * @param int $id
172 * Id of the database record.
173 * @param bool $is_active
174 * Value we want to set the is_active field.
175 *
176 * @return bool
177 * true if we found and updated the object, else false
178 */
179 public static function setIsActive($id, $is_active) {
180 Civi::cache('fields')->flush();
181 // reset ACL and system caches.
182 CRM_Core_BAO_Cache::resetCaches();
183
184 return CRM_Core_DAO::setFieldValue('CRM_ACL_DAO_ACL', $id, 'is_active', $is_active);
185 }
186
187 /**
188 * @param string $str
189 * @param int $contactID
190 *
191 * @return bool
192 *
193 * @deprecated
194 */
195 public static function check($str, $contactID) {
196 \CRM_Core_Error::deprecatedWarning(__CLASS__ . '::' . __FUNCTION__ . ' is deprecated.');
197
198 $acls = CRM_ACL_BAO_Cache::build($contactID);
199
200 $aclKeys = array_keys($acls);
201 $aclKeys = implode(',', $aclKeys);
202
203 if (empty($aclKeys)) {
204 return FALSE;
205 }
206
207 $query = "
208 SELECT count( a.id )
209 FROM civicrm_acl_cache c, civicrm_acl a
210 WHERE c.acl_id = a.id
211 AND a.is_active = 1
212 AND a.object_table = %1
213 AND a.id IN ( $aclKeys )
214 ";
215 $params = [1 => [$str, 'String']];
216
217 $count = CRM_Core_DAO::singleValueQuery($query, $params);
218 return (bool) $count;
219 }
220
221 /**
222 * @param int $type
223 * @param array $tables
224 * @param array $whereTables
225 * @param int $contactID
226 *
227 * @return null|string
228 */
229 public static function whereClause($type, &$tables, &$whereTables, $contactID = NULL) {
230 $acls = CRM_ACL_BAO_Cache::build($contactID);
231
232 $whereClause = NULL;
233 $clauses = [];
234
235 if (!empty($acls)) {
236 $aclKeys = array_keys($acls);
237 $aclKeys = implode(',', $aclKeys);
238
239 $query = "
240 SELECT a.operation, a.object_id
241 FROM civicrm_acl_cache c, civicrm_acl a
242 WHERE c.acl_id = a.id
243 AND a.is_active = 1
244 AND a.object_table = 'civicrm_saved_search'
245 AND a.id IN ( $aclKeys )
246 ORDER BY a.object_id
247 ";
248
249 $dao = CRM_Core_DAO::executeQuery($query);
250
251 // do an or of all the where clauses u see
252 $ids = [];
253 while ($dao->fetch()) {
254 // make sure operation matches the type TODO
255 if (self::matchType($type, $dao->operation)) {
256 if (!$dao->object_id) {
257 $ids = [];
258 $whereClause = ' ( 1 ) ';
259 break;
260 }
261 $ids[] = $dao->object_id;
262 }
263 }
264
265 if (!empty($ids)) {
266 $ids = implode(',', $ids);
267 $query = "
268 SELECT g.*
269 FROM civicrm_group g
270 WHERE g.id IN ( $ids )
271 AND g.is_active = 1
272 ";
273 $dao = CRM_Core_DAO::executeQuery($query);
274 $groupIDs = [];
275 $groupContactCacheClause = FALSE;
276 while ($dao->fetch()) {
277 $groupIDs[] = $dao->id;
278
279 if (($dao->saved_search_id || $dao->children || $dao->parents)) {
280 if ($dao->cache_date == NULL) {
281 CRM_Contact_BAO_GroupContactCache::load($dao);
282 }
283 $groupContactCacheClause = " UNION SELECT contact_id FROM civicrm_group_contact_cache WHERE group_id IN (" . implode(', ', $groupIDs) . ")";
284 }
285
286 }
287
288 if ($groupIDs) {
289 $clauses[] = "(
290 `contact_a`.id IN (
291 SELECT contact_id FROM civicrm_group_contact WHERE group_id IN (" . implode(', ', $groupIDs) . ") AND status = 'Added'
292 $groupContactCacheClause
293 )
294 )";
295 }
296 }
297 }
298
299 if (!empty($clauses)) {
300 $whereClause = ' ( ' . implode(' OR ', $clauses) . ' ) ';
301 }
302
303 // call the hook to get additional whereClauses
304 CRM_Utils_Hook::aclWhereClause($type, $tables, $whereTables, $contactID, $whereClause);
305
306 if (empty($whereClause)) {
307 $whereClause = ' ( 0 ) ';
308 }
309
310 return $whereClause;
311 }
312
313 /**
314 * @param int $type
315 * @param int $contactID
316 * @param string $tableName
317 * @param array|null $allGroups
318 * @param array|null $includedGroups
319 *
320 * @return array
321 */
322 public static function group(
323 $type,
324 $contactID = NULL,
325 $tableName = 'civicrm_saved_search',
326 $allGroups = NULL,
327 $includedGroups = NULL
328 ) {
329 $userCacheKey = "{$contactID}_{$type}_{$tableName}_" . CRM_Core_Config::domainID() . '_' . md5(implode(',', array_merge((array) $allGroups, (array) $includedGroups)));
330 if (empty(Civi::$statics[__CLASS__]['permissioned_groups'])) {
331 Civi::$statics[__CLASS__]['permissioned_groups'] = [];
332 }
333 if (!empty(Civi::$statics[__CLASS__]['permissioned_groups'][$userCacheKey])) {
334 return Civi::$statics[__CLASS__]['permissioned_groups'][$userCacheKey];
335 }
336
337 if ($allGroups == NULL) {
338 $allGroups = CRM_Contact_BAO_Contact::buildOptions('group_id', 'get');
339 }
340
341 $acls = CRM_ACL_BAO_Cache::build($contactID);
342
343 $ids = [];
344 if (!empty($acls)) {
345 $aclKeys = array_keys($acls);
346 $aclKeys = implode(',', $aclKeys);
347
348 $cacheKey = CRM_Utils_Cache::cleanKey("$type-$tableName-$aclKeys");
349 $cache = CRM_Utils_Cache::singleton();
350 $ids = $cache->get($cacheKey);
351 if (!is_array($ids)) {
352 $ids = self::loadPermittedIDs((int) $contactID, $tableName, $type, $allGroups);
353 $cache->set($cacheKey, $ids);
354 }
355 }
356
357 if (empty($ids) && !empty($includedGroups) &&
358 is_array($includedGroups)
359 ) {
360 // This is pretty alarming - we 'sometimes' include all included groups
361 // seems problematic per https://lab.civicrm.org/dev/core/-/issues/1879
362 $ids = $includedGroups;
363 }
364 if ($contactID) {
365 $groupWhere = '';
366 if (!empty($allGroups)) {
367 $groupWhere = ' AND id IN (' . implode(',', array_keys($allGroups)) . ")";
368 }
369 // Contacts create hidden groups from search results. They should be able to retrieve their own.
370 $ownHiddenGroupsList = CRM_Core_DAO::singleValueQuery("
371 SELECT GROUP_CONCAT(id) FROM civicrm_group WHERE is_hidden =1 AND created_id = $contactID
372 $groupWhere
373 ");
374 if ($ownHiddenGroupsList) {
375 $ownHiddenGroups = explode(',', $ownHiddenGroupsList);
376 $ids = array_merge((array) $ids, $ownHiddenGroups);
377 }
378
379 }
380
381 CRM_Utils_Hook::aclGroup($type, $contactID, $tableName, $allGroups, $ids);
382 Civi::$statics[__CLASS__]['permissioned_groups'][$userCacheKey] = $ids;
383 return $ids;
384 }
385
386 /**
387 * @param int $type
388 * @param string $operation
389 *
390 * @return bool
391 */
392 protected static function matchType($type, $operation) {
393 $typeCheck = FALSE;
394 switch ($operation) {
395 case 'All':
396 $typeCheck = TRUE;
397 break;
398
399 case 'View':
400 if ($type == CRM_ACL_API::VIEW) {
401 $typeCheck = TRUE;
402 }
403 break;
404
405 case 'Edit':
406 if ($type == CRM_ACL_API::VIEW || $type == CRM_ACL_API::EDIT) {
407 $typeCheck = TRUE;
408 }
409 break;
410
411 case 'Create':
412 if ($type == CRM_ACL_API::CREATE) {
413 $typeCheck = TRUE;
414 }
415 break;
416
417 case 'Delete':
418 if ($type == CRM_ACL_API::DELETE) {
419 $typeCheck = TRUE;
420 }
421 break;
422
423 case 'Search':
424 if ($type == CRM_ACL_API::SEARCH) {
425 $typeCheck = TRUE;
426 }
427 break;
428 }
429 return $typeCheck;
430 }
431
432 /**
433 * Delete ACL records.
434 *
435 * @param int $aclId
436 * @deprecated
437 */
438 public static function del($aclId) {
439 self::deleteRecord(['id' => $aclId]);
440 }
441
442 /**
443 * Event fired before an action is taken on an ACL record.
444 * @param \Civi\Core\Event\PreEvent $event
445 */
446 public static function self_hook_civicrm_pre(\Civi\Core\Event\PreEvent $event) {
447 // Reset cache when deleting an ACL record
448 if ($event->action === 'delete') {
449 CRM_ACL_BAO_Cache::resetCache();
450 }
451 }
452
453 /**
454 * Load permitted acl IDs.
455 *
456 * @param int $contactID
457 * @param string $tableName
458 * @param int $type
459 * @param array $allGroups
460 *
461 * @return array
462 */
463 protected static function loadPermittedIDs(int $contactID, string $tableName, int $type, $allGroups): array {
464 $ids = [];
465 $acls = CRM_ACL_BAO_Cache::build($contactID);
466 $aclKeys = array_keys($acls);
467 $aclKeys = implode(',', $aclKeys);
468 $query = "
469 SELECT a.operation, a.object_id
470 FROM civicrm_acl_cache c, civicrm_acl a
471 WHERE c.acl_id = a.id
472 AND a.is_active = 1
473 AND a.object_table = %1
474 AND a.id IN ( $aclKeys )
475 GROUP BY a.operation,a.object_id
476 ORDER BY a.object_id
477 ";
478 $params = [1 => [$tableName, 'String']];
479 $dao = CRM_Core_DAO::executeQuery($query, $params);
480 while ($dao->fetch()) {
481 if ($dao->object_id) {
482 if (self::matchType($type, $dao->operation)) {
483 $ids[] = $dao->object_id;
484 }
485 }
486 else {
487 // this user has got the permission for all objects of this type
488 // check if the type matches
489 if (self::matchType($type, $dao->operation)) {
490 foreach ($allGroups as $id => $dontCare) {
491 $ids[] = $id;
492 }
493 }
494 break;
495 }
496 }
497 return $ids;
498 }
499
500 }