get('userID'); if (empty($contactID)) { return $result_set; } // make sure the cache is filled self::cache($contactID, $type); // compile query $contact_id_list = implode(',', $contact_ids); $operation = ($type == CRM_Core_Permission::VIEW) ? 'View' : 'Edit'; // add clause for deleted contacts, if the user doesn't have the permission to access them $LEFT_JOIN_DELETED = $CAN_ACCESS_DELETED = ''; if (!CRM_Core_Permission::check('access deleted contacts')) { $LEFT_JOIN_DELETED = 'LEFT JOIN civicrm_contact ON civicrm_contact.id = contact_id'; $AND_CAN_ACCESS_DELETED = 'AND civicrm_contact.is_deleted = 0'; } // RUN the query $query = " SELECT contact_id FROM civicrm_acl_contact_cache {$LEFT_JOIN_DELETED} WHERE contact_id IN ({$contact_id_list}) AND user_id = {$contactID} AND operation = '{$operation}' {$AND_CAN_ACCESS_DELETED}"; $result = CRM_Core_DAO::executeQuery($query); while ($result->fetch()) { $result_set[] = (int) $result->contact_id; } // if some have been rejected, double check for permissions inherited by relationship if (count($result_set) < count($contact_ids)) { $rejected_contacts = array_diff($contact_ids, $result_set); $allowed_by_relationship = self::relationshipList($rejected_contacts); $result_set = array_merge($result_set, $allowed_by_relationship); } return $result_set; } /** * Check if the logged in user has permissions for the operation type. * * @param int $id * Contact id. * @param int|string $type the type of operation (view|edit) * * @return bool * true if the user has permission, false otherwise */ public static function allow($id, $type = CRM_Core_Permission::VIEW) { $tables = array(); $whereTables = array(); # FIXME: push this somewhere below, to not give this permission so many rights $isDeleted = (bool) CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $id, 'is_deleted'); if (CRM_Core_Permission::check('access deleted contacts') && $isDeleted) { return TRUE; } // short circuit for admin rights here so we avoid unneeeded queries // some duplication of code, but we skip 3-5 queries if (CRM_Core_Permission::check('edit all contacts') || ($type == CRM_ACL_API::VIEW && CRM_Core_Permission::check('view all contacts')) ) { return TRUE; } //check permission based on relationship, CRM-2963 if (self::relationship($id)) { return TRUE; } $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables); $from = CRM_Contact_BAO_Query::fromClause($whereTables); $query = " SELECT count(DISTINCT contact_a.id) $from WHERE contact_a.id = %1 AND $permission"; $params = array(1 => array($id, 'Integer')); return (CRM_Core_DAO::singleValueQuery($query, $params) > 0) ? TRUE : FALSE; } /** * Fill the acl contact cache for this contact id if empty. * * @param int $userID * @param int|string $type the type of operation (view|edit) * @param bool $force * Should we force a recompute. */ public static function cache($userID, $type = CRM_Core_Permission::VIEW, $force = FALSE) { static $_processed = array(); if ($type = CRM_Core_Permission::VIEW) { $operationClause = " operation IN ( 'Edit', 'View' ) "; $operation = 'View'; } else { $operationClause = " operation = 'Edit' "; $operation = 'Edit'; } if (!$force) { if (!empty($_processed[$userID])) { return; } // run a query to see if the cache is filled $sql = " SELECT count(id) FROM civicrm_acl_contact_cache WHERE user_id = %1 AND $operationClause "; $params = array(1 => array($userID, 'Integer')); $count = CRM_Core_DAO::singleValueQuery($sql, $params); if ($count > 0) { $_processed[$userID] = 1; return; } } $tables = array(); $whereTables = array(); $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables, $userID); $from = CRM_Contact_BAO_Query::fromClause($whereTables); CRM_Core_DAO::executeQuery(" INSERT INTO civicrm_acl_contact_cache ( user_id, contact_id, operation ) SELECT $userID as user_id, contact_a.id as contact_id, '$operation' as operation $from WHERE $permission GROUP BY contact_a.id ON DUPLICATE KEY UPDATE user_id=VALUES(user_id), contact_id=VALUES(contact_id), operation=VALUES(operation)" ); $_processed[$userID] = 1; } /** * Check if there are any contacts in cache table. * * @param int|string $type the type of operation (view|edit) * @param int $contactID * Contact id. * * @return bool */ public static function hasContactsInCache( $type = CRM_Core_Permission::VIEW, $contactID = NULL ) { if (!$contactID) { $session = CRM_Core_Session::singleton(); $contactID = $session->get('userID'); } if ($type = CRM_Core_Permission::VIEW) { $operationClause = " operation IN ( 'Edit', 'View' ) "; $operation = 'View'; } else { $operationClause = " operation = 'Edit' "; $operation = 'Edit'; } // fill cache self::cache($contactID); $sql = " SELECT id FROM civicrm_acl_contact_cache WHERE user_id = %1 AND $operationClause LIMIT 1"; $params = array(1 => array($contactID, 'Integer')); return (bool) CRM_Core_DAO::singleValueQuery($sql, $params); } /** * @param string $contactAlias * * @return array */ public static function cacheClause($contactAlias = 'contact_a') { if (CRM_Core_Permission::check('view all contacts') || CRM_Core_Permission::check('edit all contacts') ) { if (is_array($contactAlias)) { $wheres = array(); foreach ($contactAlias as $alias) { // CRM-6181 $wheres[] = "$alias.is_deleted = 0"; } return array(NULL, '(' . implode(' AND ', $wheres) . ')'); } else { // CRM-6181 return array(NULL, "$contactAlias.is_deleted = 0"); } } $contactID = (int) CRM_Core_Session::getLoggedInContactID(); self::cache($contactID); if (is_array($contactAlias) && !empty($contactAlias)) { //More than one contact alias $clauses = array(); foreach ($contactAlias as $k => $alias) { $clauses[] = " INNER JOIN civicrm_acl_contact_cache aclContactCache_{$k} ON {$alias}.id = aclContactCache_{$k}.contact_id AND aclContactCache_{$k}.user_id = $contactID "; } $fromClause = implode(" ", $clauses); $whereClase = NULL; } else { $fromClause = " INNER JOIN civicrm_acl_contact_cache aclContactCache ON {$contactAlias}.id = aclContactCache.contact_id "; $whereClase = " aclContactCache.user_id = $contactID AND $contactAlias.is_deleted = 0"; } return array($fromClause, $whereClase); } /** * Generate acl subquery that can be placed in the WHERE clause of a query or the ON clause of a JOIN * * @return string|null */ public static function cacheSubquery() { if (!CRM_Core_Permission::check(array(array('view all contacts', 'edit all contacts')))) { $contactID = (int) CRM_Core_Session::getLoggedInContactID(); self::cache($contactID); return "IN (SELECT contact_id FROM civicrm_acl_contact_cache WHERE user_id = $contactID)"; } return NULL; } /** * Get the permission base on its relationship. * * @param int $selectedContactID * Contact id of selected contact. * @param int $contactID * Contact id of the current contact. * * @return bool * true if logged in user has permission to view * selected contact record else false */ public static function relationship($selectedContactID, $contactID = NULL) { $session = CRM_Core_Session::singleton(); $config = CRM_Core_Config::singleton(); if (!$contactID) { $contactID = $session->get('userID'); if (!$contactID) { return FALSE; } } if ($contactID == $selectedContactID && (CRM_Core_Permission::check('edit my contact')) ) { return TRUE; } else { if ($config->secondDegRelPermissions) { $query = " SELECT firstdeg.id FROM civicrm_relationship firstdeg LEFT JOIN civicrm_relationship seconddegaa on firstdeg.contact_id_a = seconddegaa.contact_id_b and seconddegaa.is_permission_b_a = 1 and firstdeg.is_permission_b_a = 1 and seconddegaa.is_active = 1 LEFT JOIN civicrm_relationship seconddegab on firstdeg.contact_id_a = seconddegab.contact_id_a and seconddegab.is_permission_a_b = 1 and firstdeg.is_permission_b_a = 1 and seconddegab.is_active = 1 LEFT JOIN civicrm_relationship seconddegba on firstdeg.contact_id_b = seconddegba.contact_id_b and seconddegba.is_permission_b_a = 1 and firstdeg.is_permission_a_b = 1 and seconddegba.is_active = 1 LEFT JOIN civicrm_relationship seconddegbb on firstdeg.contact_id_b = seconddegbb.contact_id_a and seconddegbb.is_permission_a_b = 1 and firstdeg.is_permission_a_b = 1 and seconddegbb.is_active = 1 WHERE ( ( firstdeg.contact_id_a = %1 AND firstdeg.contact_id_b = %2 AND firstdeg.is_permission_a_b = 1 ) OR ( firstdeg.contact_id_a = %2 AND firstdeg.contact_id_b = %1 AND firstdeg.is_permission_b_a = 1 ) OR ( firstdeg.contact_id_a = %1 AND seconddegba.contact_id_a = %2 AND (seconddegba.contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)) ) OR ( firstdeg.contact_id_a = %1 AND seconddegbb.contact_id_b = %2 AND (seconddegbb.contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)) ) OR ( firstdeg.contact_id_b = %1 AND seconddegab.contact_id_b = %2 AND (seconddegab.contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)) ) OR ( 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)) ) ) AND (firstdeg.contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)) AND (firstdeg.contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)) AND ( firstdeg.is_active = 1) "; } else { $query = " SELECT id FROM civicrm_relationship WHERE (( contact_id_a = %1 AND contact_id_b = %2 AND is_permission_a_b = 1 ) OR ( contact_id_a = %2 AND contact_id_b = %1 AND is_permission_b_a = 1 )) AND (contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)) AND (contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)) AND ( civicrm_relationship.is_active = 1 ) "; } $params = array( 1 => array($contactID, 'Integer'), 2 => array($selectedContactID, 'Integer'), ); return CRM_Core_DAO::singleValueQuery($query, $params); } } /** * Filter a list of contact_ids by the ones that the * currently active user as a permissioned relationship with * * @param array $contact_ids * List of contact IDs to be filtered * * @return array * List of contact IDs that the user has permissions for */ public static function relationshipList($contact_ids) { $result_set = array(); // no processing empty lists (avoid SQL errors as well) if (empty($contact_ids)) { return $result_set; } // get the currently logged in user $session = CRM_Core_Session::singleton(); $contactID = (int) $session->get('userID'); if (empty($contactID)) { return $result_set; } // compile a list of queries (later to UNION) $queries = array(); $contact_id_list = implode(',', $contact_ids); // add a select for each direection $directions = array(array('from' => 'a', 'to' => 'b'), array('from' => 'b', 'to' => 'a')); foreach ($directions as $direction) { $user_id_column = "contact_id_{$direction['from']}"; $contact_id_column = "contact_id_{$direction['to']}"; // add clause for deleted contacts, if the user doesn't have the permission to access them $LEFT_JOIN_DELETED = $CAN_ACCESS_DELETED = ''; if (!CRM_Core_Permission::check('access deleted contacts')) { $LEFT_JOIN_DELETED = 'LEFT JOIN civicrm_contact ON civicrm_contact.id = {$contact_id_column}'; $AND_CAN_ACCESS_DELETED = 'AND civicrm_contact.is_deleted = 0'; } $queries[] = " SELECT DISTINCT(civicrm_relationship.{$contact_id_column}) AS contact_id FROM civicrm_relationship {$LEFT_JOIN_DELETED} WHERE civicrm_relationship.{$user_id_column} = {$contactID} AND civicrm_relationship.{$contact_id_column} IN ({$contact_id_list}) AND civicrm_relationship.is_active = 1 AND civicrm_relationship.is_permission_{$direction['from']}_{$direction['to']} = 1 $AND_CAN_ACCESS_DELETED"; } // add second degree relationship support if ($config->secondDegRelPermissions) { foreach ($directions as $first_direction) { foreach ($directions as $second_direction) { // add clause for deleted contacts, if the user doesn't have the permission to access them $LEFT_JOIN_DELETED = $CAN_ACCESS_DELETED = ''; if (!CRM_Core_Permission::check('access deleted contacts')) { $LEFT_JOIN_DELETED = 'LEFT JOIN civicrm_contact ON civicrm_contact.id = {$contact_id_column}'; $AND_CAN_ACCESS_DELETED = 'AND civicrm_contact.is_deleted = 0'; } $queries[] = " SELECT DISTINCT(civicrm_relationship.{$contact_id_column}) AS contact_id FROM civicrm_relationship first_degree_relationship LEFT JOIN civicrm_relationship second_degree_relationship ON first_degree_relationship.contact_id_{$first_direction['to']} = second_degree_relationship.contact_id_{$first_direction['from']} {$LEFT_JOIN_DELETED} WHERE first_degree_relationship.contact_id_{$first_direction['from']} = {$contactID} AND second_degree_relationship.contact_id_{$second_direction['to']} IN ({$contact_id_list}) AND first_degree_relationship.is_active = 1 AND first_degree_relationship.is_permission_{$first_direction['from']}_{$first_direction['to']} = 1 AND second_degree_relationship.is_active = 1 AND second_degree_relationship.is_permission_{$second_direction['from']}_{$second_direction['to']} = 1 $AND_CAN_ACCESS_DELETED"; } } } // finally UNION the queries and call $query = "(" . implode(")\nUNION (", $queries) . ")"; $result = CRM_Core_DAO::executeQuery($query); while ($result->fetch()) { $result_set[] = (int) $result->contact_id; } return $result_set; } /** * @param int $contactID * @param CRM_Core_Form $form * @param bool $redirect * * @return bool */ public static function validateOnlyChecksum($contactID, &$form, $redirect = TRUE) { // check if this is of the format cs=XXX if (!CRM_Contact_BAO_Contact_Utils::validChecksum($contactID, CRM_Utils_Request::retrieve('cs', 'String', $form, FALSE) ) ) { if ($redirect) { // also set a message in the UF framework $message = ts('You do not have permission to edit this contact record. Contact the site administrator if you need assistance.'); CRM_Utils_System::setUFMessage($message); $config = CRM_Core_Config::singleton(); CRM_Core_Error::statusBounce($message, $config->userFrameworkBaseURL ); // does not come here, we redirect in the above statement } return FALSE; } // set appropriate AUTH source self::initChecksumAuthSrc(TRUE, $form); // so here the contact is posing as $contactID, lets set the logging contact ID variable // CRM-8965 CRM_Core_DAO::executeQuery('SET @civicrm_user_id = %1', array(1 => array($contactID, 'Integer')) ); return TRUE; } /** * @param bool $checkSumValidationResult * @param null $form */ public static function initChecksumAuthSrc($checkSumValidationResult = FALSE, $form = NULL) { $session = CRM_Core_Session::singleton(); if ($checkSumValidationResult && $form && CRM_Utils_Request::retrieve('cs', 'String', $form, FALSE)) { // if result is already validated, and url has cs, set the flag. $session->set('authSrc', CRM_Core_Permission::AUTH_SRC_CHECKSUM); } elseif (($session->get('authSrc') & CRM_Core_Permission::AUTH_SRC_CHECKSUM) == CRM_Core_Permission::AUTH_SRC_CHECKSUM) { // if checksum wasn't present in REQUEST OR checksum result validated as FALSE, // and flag was already set exactly as AUTH_SRC_CHECKSUM, unset it. $session->set('authSrc', CRM_Core_Permission::AUTH_SRC_UNKNOWN); } } /** * @param int $contactID * @param CRM_Core_Form $form * @param bool $redirect * * @return bool */ public static function validateChecksumContact($contactID, &$form, $redirect = TRUE) { if (!self::allow($contactID, CRM_Core_Permission::EDIT)) { // check if this is of the format cs=XXX return self::validateOnlyChecksum($contactID, $form, $redirect); } return TRUE; } }