3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2016
33 class CRM_Contact_BAO_Contact_Permission
{
36 * Check which of the given contact IDs the logged in user
37 * has permissions for the operation type according
39 * Caution: general permissions (like 'edit all contacts')
41 * @param array $contact_ids
43 * @param int|string $type the type of operation (view|edit)
45 * @see CRM_Contact_BAO_Contact_Permission::allow
48 * list of contact IDs the logged in user has the given permission for
50 public static function allowList($contact_ids, $type = CRM_Core_Permission
::VIEW
) {
51 $result_set = array();
52 if (empty($contact_ids)) {
53 // empty contact lists would cause trouble in the SQL. And be pointless.
57 // make sure the the general permissions are given
58 if ( $type == CRM_Core_Permission
::VIEW
&& CRM_Core_Permission
::check('view all contacts')
59 ||
$type == CRM_Core_Permission
::EDIT
&& CRM_Core_Permission
::check('edit all contacts')
61 // if the general permission is there, all good
67 $session = CRM_Core_Session
::singleton();
68 $contactID = (int) $session->get('userID');
69 if (empty($contactID)) {
73 // make sure the cache is filled
74 self
::cache($contactID, $type);
77 $contact_id_list = implode(',', $contact_ids);
78 $operation = ($type == CRM_Core_Permission
::VIEW
) ?
'View' : 'Edit';
80 // add clause for deleted contacts, if the user doesn't have the permission to access them
81 $LEFT_JOIN_DELETED = $CAN_ACCESS_DELETED = '';
82 if (!CRM_Core_Permission
::check('access deleted contacts')) {
83 $LEFT_JOIN_DELETED = 'LEFT JOIN civicrm_contact ON civicrm_contact.id = contact_id';
84 $AND_CAN_ACCESS_DELETED = 'AND civicrm_contact.is_deleted = 0';
90 FROM civicrm_acl_contact_cache
92 WHERE contact_id IN ({$contact_id_list})
93 AND user_id = {$contactID}
94 AND operation = '{$operation}'
95 {$AND_CAN_ACCESS_DELETED}";
96 $result = CRM_Core_DAO
::executeQuery($query);
97 while ($result->fetch()) {
98 $result_set[] = (int) $result->contact_id
;
101 // if some have been rejected, double check for permissions inherited by relationship
102 if (count($result_set) < count($contact_ids)) {
103 $rejected_contacts = array_diff($contact_ids, $result_set);
104 $allowed_by_relationship = self
::relationshipList($rejected_contacts);
105 $result_set = array_merge($result_set, $allowed_by_relationship);
112 * Check if the logged in user has permissions for the operation type.
116 * @param int|string $type the type of operation (view|edit)
119 * true if the user has permission, false otherwise
121 public static function allow($id, $type = CRM_Core_Permission
::VIEW
) {
123 $whereTables = array();
125 # FIXME: push this somewhere below, to not give this permission so many rights
126 $isDeleted = (bool) CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $id, 'is_deleted');
127 if (CRM_Core_Permission
::check('access deleted contacts') && $isDeleted) {
131 // short circuit for admin rights here so we avoid unneeeded queries
132 // some duplication of code, but we skip 3-5 queries
133 if (CRM_Core_Permission
::check('edit all contacts') ||
134 ($type == CRM_ACL_API
::VIEW
&& CRM_Core_Permission
::check('view all contacts'))
139 //check permission based on relationship, CRM-2963
140 if (self
::relationship($id)) {
144 $permission = CRM_ACL_API
::whereClause($type, $tables, $whereTables);
146 $from = CRM_Contact_BAO_Query
::fromClause($whereTables);
149 SELECT count(DISTINCT contact_a.id)
151 WHERE contact_a.id = %1 AND $permission";
152 $params = array(1 => array($id, 'Integer'));
154 return (CRM_Core_DAO
::singleValueQuery($query, $params) > 0) ?
TRUE : FALSE;
158 * Fill the acl contact cache for this contact id if empty.
161 * @param int|string $type the type of operation (view|edit)
163 * Should we force a recompute.
165 public static function cache($userID, $type = CRM_Core_Permission
::VIEW
, $force = FALSE) {
166 static $_processed = array();
168 if ($type = CRM_Core_Permission
::VIEW
) {
169 $operationClause = " operation IN ( 'Edit', 'View' ) ";
173 $operationClause = " operation = 'Edit' ";
178 if (!empty($_processed[$userID])) {
182 // run a query to see if the cache is filled
185 FROM civicrm_acl_contact_cache
189 $params = array(1 => array($userID, 'Integer'));
190 $count = CRM_Core_DAO
::singleValueQuery($sql, $params);
192 $_processed[$userID] = 1;
198 $whereTables = array();
200 $permission = CRM_ACL_API
::whereClause($type, $tables, $whereTables, $userID);
202 $from = CRM_Contact_BAO_Query
::fromClause($whereTables);
204 CRM_Core_DAO
::executeQuery("
205 INSERT INTO civicrm_acl_contact_cache ( user_id, contact_id, operation )
206 SELECT $userID as user_id, contact_a.id as contact_id, '$operation' as operation
209 GROUP BY contact_a.id
210 ON DUPLICATE KEY UPDATE
211 user_id=VALUES(user_id),
212 contact_id=VALUES(contact_id),
213 operation=VALUES(operation)"
216 $_processed[$userID] = 1;
220 * Check if there are any contacts in cache table.
222 * @param int|string $type the type of operation (view|edit)
223 * @param int $contactID
228 public static function hasContactsInCache(
229 $type = CRM_Core_Permission
::VIEW
,
233 $session = CRM_Core_Session
::singleton();
234 $contactID = $session->get('userID');
237 if ($type = CRM_Core_Permission
::VIEW
) {
238 $operationClause = " operation IN ( 'Edit', 'View' ) ";
242 $operationClause = " operation = 'Edit' ";
247 self
::cache($contactID);
251 FROM civicrm_acl_contact_cache
253 AND $operationClause LIMIT 1";
255 $params = array(1 => array($contactID, 'Integer'));
256 return (bool) CRM_Core_DAO
::singleValueQuery($sql, $params);
260 * @param string $contactAlias
264 public static function cacheClause($contactAlias = 'contact_a') {
265 if (CRM_Core_Permission
::check('view all contacts') ||
266 CRM_Core_Permission
::check('edit all contacts')
268 if (is_array($contactAlias)) {
270 foreach ($contactAlias as $alias) {
272 $wheres[] = "$alias.is_deleted = 0";
274 return array(NULL, '(' . implode(' AND ', $wheres) . ')');
278 return array(NULL, "$contactAlias.is_deleted = 0");
282 $contactID = (int) CRM_Core_Session
::getLoggedInContactID();
283 self
::cache($contactID);
285 if (is_array($contactAlias) && !empty($contactAlias)) {
286 //More than one contact alias
288 foreach ($contactAlias as $k => $alias) {
289 $clauses[] = " INNER JOIN civicrm_acl_contact_cache aclContactCache_{$k} ON {$alias}.id = aclContactCache_{$k}.contact_id AND aclContactCache_{$k}.user_id = $contactID ";
292 $fromClause = implode(" ", $clauses);
296 $fromClause = " INNER JOIN civicrm_acl_contact_cache aclContactCache ON {$contactAlias}.id = aclContactCache.contact_id ";
297 $whereClase = " aclContactCache.user_id = $contactID AND $contactAlias.is_deleted = 0";
300 return array($fromClause, $whereClase);
304 * Generate acl subquery that can be placed in the WHERE clause of a query or the ON clause of a JOIN
306 * @return string|null
308 public static function cacheSubquery() {
309 if (!CRM_Core_Permission
::check(array(array('view all contacts', 'edit all contacts')))) {
310 $contactID = (int) CRM_Core_Session
::getLoggedInContactID();
311 self
::cache($contactID);
312 return "IN (SELECT contact_id FROM civicrm_acl_contact_cache WHERE user_id = $contactID)";
318 * Get the permission base on its relationship.
320 * @param int $selectedContactID
321 * Contact id of selected contact.
322 * @param int $contactID
323 * Contact id of the current contact.
326 * true if logged in user has permission to view
327 * selected contact record else false
329 public static function relationship($selectedContactID, $contactID = NULL) {
330 $session = CRM_Core_Session
::singleton();
331 $config = CRM_Core_Config
::singleton();
333 $contactID = $session->get('userID');
338 if ($contactID == $selectedContactID &&
339 (CRM_Core_Permission
::check('edit my contact'))
344 if ($config->secondDegRelPermissions
) {
347 FROM civicrm_relationship firstdeg
348 LEFT JOIN civicrm_relationship seconddegaa
349 on firstdeg.contact_id_a = seconddegaa.contact_id_b
350 and seconddegaa.is_permission_b_a = 1
351 and firstdeg.is_permission_b_a = 1
352 and seconddegaa.is_active = 1
353 LEFT JOIN civicrm_relationship seconddegab
354 on firstdeg.contact_id_a = seconddegab.contact_id_a
355 and seconddegab.is_permission_a_b = 1
356 and firstdeg.is_permission_b_a = 1
357 and seconddegab.is_active = 1
358 LEFT JOIN civicrm_relationship seconddegba
359 on firstdeg.contact_id_b = seconddegba.contact_id_b
360 and seconddegba.is_permission_b_a = 1
361 and firstdeg.is_permission_a_b = 1
362 and seconddegba.is_active = 1
363 LEFT JOIN civicrm_relationship seconddegbb
364 on firstdeg.contact_id_b = seconddegbb.contact_id_a
365 and seconddegbb.is_permission_a_b = 1
366 and firstdeg.is_permission_a_b = 1
367 and seconddegbb.is_active = 1
370 ( firstdeg.contact_id_a = %1 AND firstdeg.contact_id_b = %2 AND firstdeg.is_permission_a_b = 1 )
371 OR ( firstdeg.contact_id_a = %2 AND firstdeg.contact_id_b = %1 AND firstdeg.is_permission_b_a = 1 )
373 firstdeg.contact_id_a = %1 AND seconddegba.contact_id_a = %2
374 AND (seconddegba.contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
377 firstdeg.contact_id_a = %1 AND seconddegbb.contact_id_b = %2
378 AND (seconddegbb.contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
381 firstdeg.contact_id_b = %1 AND seconddegab.contact_id_b = %2
382 AND (seconddegab.contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
385 firstdeg.contact_id_b = %1 AND seconddegaa.contact_id_a = %2 AND (seconddegaa.contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
388 AND (firstdeg.contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
389 AND (firstdeg.contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
390 AND ( firstdeg.is_active = 1)
396 FROM civicrm_relationship
397 WHERE (( contact_id_a = %1 AND contact_id_b = %2 AND is_permission_a_b = 1 ) OR
398 ( contact_id_a = %2 AND contact_id_b = %1 AND is_permission_b_a = 1 )) AND
399 (contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)) AND
400 (contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
401 AND ( civicrm_relationship.is_active = 1 )
405 1 => array($contactID, 'Integer'),
406 2 => array($selectedContactID, 'Integer'),
408 return CRM_Core_DAO
::singleValueQuery($query, $params);
415 * Filter a list of contact_ids by the ones that the
416 * currently active user as a permissioned relationship with
418 * @param array $contact_ids
419 * List of contact IDs to be filtered
422 * List of contact IDs that the user has permissions for
424 public static function relationshipList($contact_ids) {
425 $result_set = array();
427 // no processing empty lists (avoid SQL errors as well)
428 if (empty($contact_ids)) {
432 // get the currently logged in user
433 $session = CRM_Core_Session
::singleton();
434 $contactID = (int) $session->get('userID');
435 if (empty($contactID)) {
439 // compile a list of queries (later to UNION)
441 $contact_id_list = implode(',', $contact_ids);
444 // add a select for each direection
445 $directions = array(array('from' => 'a', 'to' => 'b'), array('from' => 'b', 'to' => 'a'));
446 foreach ($directions as $direction) {
447 $user_id_column = "contact_id_{$direction['from']}";
448 $contact_id_column = "contact_id_{$direction['to']}";
450 // add clause for deleted contacts, if the user doesn't have the permission to access them
451 $LEFT_JOIN_DELETED = $CAN_ACCESS_DELETED = '';
452 if (!CRM_Core_Permission
::check('access deleted contacts')) {
453 $LEFT_JOIN_DELETED = 'LEFT JOIN civicrm_contact ON civicrm_contact.id = {$contact_id_column}';
454 $AND_CAN_ACCESS_DELETED = 'AND civicrm_contact.is_deleted = 0';
458 SELECT DISTINCT(civicrm_relationship.{$contact_id_column}) AS contact_id
459 FROM civicrm_relationship
461 WHERE civicrm_relationship.{$user_id_column} = {$contactID}
462 AND civicrm_relationship.{$contact_id_column} IN ({$contact_id_list})
463 AND civicrm_relationship.is_active = 1
464 AND civicrm_relationship.is_permission_{$direction['from']}_{$direction['to']} = 1
465 $AND_CAN_ACCESS_DELETED";
468 // add second degree relationship support
469 if ($config->secondDegRelPermissions
) {
470 foreach ($directions as $first_direction) {
471 foreach ($directions as $second_direction) {
472 // add clause for deleted contacts, if the user doesn't have the permission to access them
473 $LEFT_JOIN_DELETED = $CAN_ACCESS_DELETED = '';
474 if (!CRM_Core_Permission
::check('access deleted contacts')) {
475 $LEFT_JOIN_DELETED = 'LEFT JOIN civicrm_contact ON civicrm_contact.id = {$contact_id_column}';
476 $AND_CAN_ACCESS_DELETED = 'AND civicrm_contact.is_deleted = 0';
480 SELECT DISTINCT(civicrm_relationship.{$contact_id_column}) AS contact_id
481 FROM civicrm_relationship first_degree_relationship
482 LEFT JOIN civicrm_relationship second_degree_relationship ON first_degree_relationship.contact_id_{$first_direction['to']} = second_degree_relationship.contact_id_{$first_direction['from']}
484 WHERE first_degree_relationship.contact_id_{$first_direction['from']} = {$contactID}
485 AND second_degree_relationship.contact_id_{$second_direction['to']} IN ({$contact_id_list})
486 AND first_degree_relationship.is_active = 1
487 AND first_degree_relationship.is_permission_{$first_direction['from']}_{$first_direction['to']} = 1
488 AND second_degree_relationship.is_active = 1
489 AND second_degree_relationship.is_permission_{$second_direction['from']}_{$second_direction['to']} = 1
490 $AND_CAN_ACCESS_DELETED";
495 // finally UNION the queries and call
496 $query = "(" . implode(")\nUNION (", $queries) . ")";
497 $result = CRM_Core_DAO
::executeQuery($query);
498 while ($result->fetch()) {
499 $result_set[] = (int) $result->contact_id
;
509 * @param int $contactID
510 * @param CRM_Core_Form $form
511 * @param bool $redirect
515 public static function validateOnlyChecksum($contactID, &$form, $redirect = TRUE) {
516 // check if this is of the format cs=XXX
517 if (!CRM_Contact_BAO_Contact_Utils
::validChecksum($contactID,
518 CRM_Utils_Request
::retrieve('cs', 'String', $form, FALSE)
522 // also set a message in the UF framework
523 $message = ts('You do not have permission to edit this contact record. Contact the site administrator if you need assistance.');
524 CRM_Utils_System
::setUFMessage($message);
526 $config = CRM_Core_Config
::singleton();
527 CRM_Core_Error
::statusBounce($message,
528 $config->userFrameworkBaseURL
530 // does not come here, we redirect in the above statement
535 // set appropriate AUTH source
536 self
::initChecksumAuthSrc(TRUE, $form);
538 // so here the contact is posing as $contactID, lets set the logging contact ID variable
540 CRM_Core_DAO
::executeQuery('SET @civicrm_user_id = %1',
541 array(1 => array($contactID, 'Integer'))
548 * @param bool $checkSumValidationResult
551 public static function initChecksumAuthSrc($checkSumValidationResult = FALSE, $form = NULL) {
552 $session = CRM_Core_Session
::singleton();
553 if ($checkSumValidationResult && $form && CRM_Utils_Request
::retrieve('cs', 'String', $form, FALSE)) {
554 // if result is already validated, and url has cs, set the flag.
555 $session->set('authSrc', CRM_Core_Permission
::AUTH_SRC_CHECKSUM
);
557 elseif (($session->get('authSrc') & CRM_Core_Permission
::AUTH_SRC_CHECKSUM
) == CRM_Core_Permission
::AUTH_SRC_CHECKSUM
) {
558 // if checksum wasn't present in REQUEST OR checksum result validated as FALSE,
559 // and flag was already set exactly as AUTH_SRC_CHECKSUM, unset it.
560 $session->set('authSrc', CRM_Core_Permission
::AUTH_SRC_UNKNOWN
);
565 * @param int $contactID
566 * @param CRM_Core_Form $form
567 * @param bool $redirect
571 public static function validateChecksumContact($contactID, &$form, $redirect = TRUE) {
572 if (!self
::allow($contactID, CRM_Core_Permission
::EDIT
)) {
573 // check if this is of the format cs=XXX
574 return self
::validateOnlyChecksum($contactID, $form, $redirect);