Merge pull request #11986 from eileenmcnaughton/test
[civicrm-core.git] / CRM / Contact / BAO / Contact / Permission.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
7e9e8871 4 | CiviCRM version 4.7 |
6a488035 5 +--------------------------------------------------------------------+
8c9251b3 6 | Copyright CiviCRM LLC (c) 2004-2018 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
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. |
13 | |
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. |
18 | |
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
8c9251b3 31 * @copyright CiviCRM LLC (c) 2004-2018
6a488035
TO
32 */
33class CRM_Contact_BAO_Contact_Permission {
34
dddf4bf6 35 /**
e4541c56 36 * Check which of the given contact IDs the logged in user
340be2e7 37 * has permissions for the operation type according to:
38 * - general permissions (e.g. 'edit all contacts')
39 * - deletion status (unless you have 'access deleted contacts')
40 * - ACL
41 * - permissions inherited through relationships (also second degree if enabled)
dddf4bf6 42 *
43 * @param array $contact_ids
44 * Contact IDs.
340be2e7 45 * @param int $type the type of operation (view|edit)
dddf4bf6 46 *
47 * @see CRM_Contact_BAO_Contact_Permission::allow
48 *
49 * @return array
50 * list of contact IDs the logged in user has the given permission for
51 */
52 public static function allowList($contact_ids, $type = CRM_Core_Permission::VIEW) {
53 $result_set = array();
54 if (empty($contact_ids)) {
55 // empty contact lists would cause trouble in the SQL. And be pointless.
56 return $result_set;
57 }
58
59 // make sure the the general permissions are given
e4541c56 60 if (CRM_Core_Permission::check('edit all contacts')
19f13a7c 61 || $type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view all contacts')
dddf4bf6 62 ) {
67df1408 63
dddf4bf6 64 // if the general permission is there, all good
67df1408 65 if (CRM_Core_Permission::check('access deleted contacts')) {
e8a0f9e0 66 // if user can access deleted contacts -> fine
67df1408 67 return $contact_ids;
e4541c56 68 }
69 else {
67df1408 70 // if the user CANNOT access deleted contacts, these need to be filtered
340be2e7 71 $contact_id_list = implode(',', $contact_ids);
67df1408 72 $filter_query = "SELECT DISTINCT(id) FROM civicrm_contact WHERE id IN ($contact_id_list) AND is_deleted = 0";
73 $query = CRM_Core_DAO::executeQuery($filter_query);
74 while ($query->fetch()) {
75 $result_set[(int) $query->id] = TRUE;
76 }
77 return array_keys($result_set);
78 }
dddf4bf6 79 }
80
81 // get logged in user
340be2e7 82 $contactID = CRM_Core_Session::getLoggedInContactID();
dddf4bf6 83 if (empty($contactID)) {
67df1408 84 return array();
dddf4bf6 85 }
86
87 // make sure the cache is filled
88 self::cache($contactID, $type);
89
90 // compile query
dddf4bf6 91 $operation = ($type == CRM_Core_Permission::VIEW) ? 'View' : 'Edit';
92
93 // add clause for deleted contacts, if the user doesn't have the permission to access them
163bfad3 94 $LEFT_JOIN_DELETED = $AND_CAN_ACCESS_DELETED = '';
dddf4bf6 95 if (!CRM_Core_Permission::check('access deleted contacts')) {
67df1408 96 $LEFT_JOIN_DELETED = "LEFT JOIN civicrm_contact ON civicrm_contact.id = contact_id";
e4541c56 97 $AND_CAN_ACCESS_DELETED = "AND civicrm_contact.is_deleted = 0";
dddf4bf6 98 }
99
100 // RUN the query
340be2e7 101 $contact_id_list = implode(',', $contact_ids);
dddf4bf6 102 $query = "
103SELECT contact_id
104 FROM civicrm_acl_contact_cache
105 {$LEFT_JOIN_DELETED}
9aea8e14 106WHERE contact_id IN ({$contact_id_list})
dddf4bf6 107 AND user_id = {$contactID}
108 AND operation = '{$operation}'
109 {$AND_CAN_ACCESS_DELETED}";
110 $result = CRM_Core_DAO::executeQuery($query);
111 while ($result->fetch()) {
67df1408 112 $result_set[(int) $result->contact_id] = TRUE;
dddf4bf6 113 }
114
e4541c56 115 // if some have been rejected, double check for permissions inherited by relationship
dddf4bf6 116 if (count($result_set) < count($contact_ids)) {
67df1408 117 $rejected_contacts = array_diff_key($contact_ids, $result_set);
98445ac5 118 // @todo consider storing these to the acl cache for next time, since we have fetched.
dddf4bf6 119 $allowed_by_relationship = self::relationshipList($rejected_contacts);
67df1408 120 foreach ($allowed_by_relationship as $contact_id) {
121 $result_set[(int) $contact_id] = TRUE;
122 }
dddf4bf6 123 }
124
67df1408 125 return array_keys($result_set);
dddf4bf6 126 }
127
6a488035 128 /**
fe482240 129 * Check if the logged in user has permissions for the operation type.
6a488035 130 *
77c5b619
TO
131 * @param int $id
132 * Contact id.
77b97be7 133 * @param int|string $type the type of operation (view|edit)
6a488035 134 *
acb1052e 135 * @return bool
a6c01b45 136 * true if the user has permission, false otherwise
6a488035 137 */
00be9182 138 public static function allow($id, $type = CRM_Core_Permission::VIEW) {
9a41e168 139 // get logged in user
340be2e7 140 $contactID = CRM_Core_Session::getLoggedInContactID();
9a41e168 141
2b8d25f0 142 // first: check if contact is trying to view own contact
135367a6 143 if ($contactID == $id && ($type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view my contact')
144 || $type == CRM_Core_Permission::EDIT && CRM_Core_Permission::check('edit my contact'))
2b8d25f0 145 ) {
146 return TRUE;
147 }
6a488035
TO
148
149 # FIXME: push this somewhere below, to not give this permission so many rights
150 $isDeleted = (bool) CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $id, 'is_deleted');
151 if (CRM_Core_Permission::check('access deleted contacts') && $isDeleted) {
152 return TRUE;
153 }
154
155 // short circuit for admin rights here so we avoid unneeeded queries
156 // some duplication of code, but we skip 3-5 queries
157 if (CRM_Core_Permission::check('edit all contacts') ||
158 ($type == CRM_ACL_API::VIEW && CRM_Core_Permission::check('view all contacts'))
159 ) {
160 return TRUE;
161 }
162
9a41e168 163 // check permission based on relationship, CRM-2963
680a52de 164 if (self::relationshipList(array($id))) {
6a488035
TO
165 return TRUE;
166 }
167
8210399a 168 // We should probably do a cheap check whether it's in the cache first.
9a41e168 169 // check permission based on ACL
170 $tables = array();
171 $whereTables = array();
6a488035 172
8d0a0fd0 173 $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables, NULL, FALSE, FALSE, TRUE);
6a488035
TO
174 $from = CRM_Contact_BAO_Query::fromClause($whereTables);
175
176 $query = "
680a52de 177SELECT contact_a.id
6a488035 178 $from
680a52de 179WHERE contact_a.id = %1 AND $permission
180 LIMIT 1
181";
6a488035 182
680a52de 183 if (CRM_Core_DAO::singleValueQuery($query, array(1 => array($id, 'Integer')))) {
184 return TRUE;
185 }
5f652ac7 186 return FALSE;
6a488035
TO
187 }
188
189 /**
fe482240 190 * Fill the acl contact cache for this contact id if empty.
6a488035 191 *
c490a46a 192 * @param int $userID
dd244018 193 * @param int|string $type the type of operation (view|edit)
77c5b619
TO
194 * @param bool $force
195 * Should we force a recompute.
6a488035 196 */
00be9182 197 public static function cache($userID, $type = CRM_Core_Permission::VIEW, $force = FALSE) {
340be2e7 198 // FIXME: maybe find a better way of keeping track of this. @eileen pointed out
199 // that somebody might flush the cache away from under our feet,
5af77f03 200 // but the alternative would be a SQL call every time this is called,
340be2e7 201 // and a complete rebuild if the result was an empty set...
e4541c56 202 static $_processed = array(
203 CRM_Core_Permission::VIEW => array(),
204 CRM_Core_Permission::EDIT => array());
6a488035 205
c0e87307 206 if ($type == CRM_Core_Permission::VIEW) {
6a488035
TO
207 $operationClause = " operation IN ( 'Edit', 'View' ) ";
208 $operation = 'View';
209 }
210 else {
211 $operationClause = " operation = 'Edit' ";
212 $operation = 'Edit';
213 }
9aea8e14 214 $queryParams = array(1 => array($userID, 'Integer'));
6a488035
TO
215
216 if (!$force) {
c0e87307 217 // skip if already calculated
218 if (!empty($_processed[$type][$userID])) {
6a488035
TO
219 return;
220 }
221
222 // run a query to see if the cache is filled
223 $sql = "
21ca2cb6 224SELECT count(*)
6a488035
TO
225FROM civicrm_acl_contact_cache
226WHERE user_id = %1
227AND $operationClause
228";
9aea8e14 229 $count = CRM_Core_DAO::singleValueQuery($sql, $queryParams);
6a488035 230 if ($count > 0) {
c0e87307 231 $_processed[$type][$userID] = 1;
6a488035
TO
232 return;
233 }
234 }
235
236 $tables = array();
237 $whereTables = array();
238
9aea8e14 239 $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables, $userID, FALSE, FALSE, TRUE);
6a488035
TO
240
241 $from = CRM_Contact_BAO_Query::fromClause($whereTables);
6a488035
TO
242 CRM_Core_DAO::executeQuery("
243INSERT INTO civicrm_acl_contact_cache ( user_id, contact_id, operation )
0f765440 244SELECT DISTINCT $userID as user_id, contact_a.id as contact_id, '{$operation}' as operation
6a488035 245 $from
0f765440 246 LEFT JOIN civicrm_acl_contact_cache ac ON ac.user_id = $userID AND ac.contact_id = contact_a.id AND ac.operation = '{$operation}'
6a488035 247WHERE $permission
0f765440 248AND ac.user_id IS NULL
249");
9aea8e14 250
251 // Add in a row for the logged in contact. Do not try to combine with the above query or an ugly OR will appear in
252 // the permission clause.
253 if (CRM_Core_Permission::check('edit my contact') ||
254 ($type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view my contact'))) {
d0f057f4
EM
255 if (!CRM_Core_DAO::singleValueQuery("
256 SELECT count(*) FROM civicrm_acl_contact_cache WHERE user_id = %1 AND contact_id = %1 AND operation = '{$operation}' LIMIT 1", $queryParams)) {
251cca6a 257 CRM_Core_DAO::executeQuery("INSERT INTO civicrm_acl_contact_cache ( user_id, contact_id, operation ) VALUES(%1, %1, '{$operation}')", $queryParams);
9aea8e14 258 }
259 }
c0e87307 260 $_processed[$type][$userID] = 1;
6a488035
TO
261 }
262
86538308
EM
263 /**
264 * @param string $contactAlias
86538308
EM
265 *
266 * @return array
267 */
188fd46b 268 public static function cacheClause($contactAlias = 'contact_a') {
6a488035
TO
269 if (CRM_Core_Permission::check('view all contacts') ||
270 CRM_Core_Permission::check('edit all contacts')
271 ) {
272 if (is_array($contactAlias)) {
273 $wheres = array();
274 foreach ($contactAlias as $alias) {
275 // CRM-6181
276 $wheres[] = "$alias.is_deleted = 0";
277 }
278 return array(NULL, '(' . implode(' AND ', $wheres) . ')');
279 }
280 else {
281 // CRM-6181
282 return array(NULL, "$contactAlias.is_deleted = 0");
283 }
284 }
285
188fd46b 286 $contactID = (int) CRM_Core_Session::getLoggedInContactID();
6a488035
TO
287 self::cache($contactID);
288
289 if (is_array($contactAlias) && !empty($contactAlias)) {
290 //More than one contact alias
291 $clauses = array();
292 foreach ($contactAlias as $k => $alias) {
293 $clauses[] = " INNER JOIN civicrm_acl_contact_cache aclContactCache_{$k} ON {$alias}.id = aclContactCache_{$k}.contact_id AND aclContactCache_{$k}.user_id = $contactID ";
294 }
295
296 $fromClause = implode(" ", $clauses);
297 $whereClase = NULL;
298 }
299 else {
300 $fromClause = " INNER JOIN civicrm_acl_contact_cache aclContactCache ON {$contactAlias}.id = aclContactCache.contact_id ";
b49db103 301 $whereClase = " aclContactCache.user_id = $contactID AND $contactAlias.is_deleted = 0";
6a488035
TO
302 }
303
304 return array($fromClause, $whereClase);
305 }
306
188fd46b 307 /**
5af77f03 308 * Generate acl subquery that can be placed in the WHERE clause of a query or the ON clause of a JOIN.
309 *
310 * This is specifically for VIEW operations.
188fd46b 311 *
0b80f0b4 312 * @return string|null
188fd46b 313 */
b53bcc5d 314 public static function cacheSubquery() {
188fd46b
CW
315 if (!CRM_Core_Permission::check(array(array('view all contacts', 'edit all contacts')))) {
316 $contactID = (int) CRM_Core_Session::getLoggedInContactID();
317 self::cache($contactID);
0b80f0b4 318 return "IN (SELECT contact_id FROM civicrm_acl_contact_cache WHERE user_id = $contactID)";
188fd46b 319 }
0b80f0b4 320 return NULL;
188fd46b
CW
321 }
322
dddf4bf6 323 /**
324 * Filter a list of contact_ids by the ones that the
325 * currently active user as a permissioned relationship with
326 *
327 * @param array $contact_ids
328 * List of contact IDs to be filtered
329 *
330 * @return array
331 * List of contact IDs that the user has permissions for
332 */
333 public static function relationshipList($contact_ids) {
334 $result_set = array();
e4541c56 335
dddf4bf6 336 // no processing empty lists (avoid SQL errors as well)
337 if (empty($contact_ids)) {
67df1408 338 return array();
dddf4bf6 339 }
340
341 // get the currently logged in user
340be2e7 342 $contactID = CRM_Core_Session::getLoggedInContactID();
dddf4bf6 343 if (empty($contactID)) {
67df1408 344 return array();
dddf4bf6 345 }
346
347 // compile a list of queries (later to UNION)
348 $queries = array();
349 $contact_id_list = implode(',', $contact_ids);
350
67df1408 351 // add a select statement for each direection
dddf4bf6 352 $directions = array(array('from' => 'a', 'to' => 'b'), array('from' => 'b', 'to' => 'a'));
67df1408 353
354 // NORMAL/SINGLE DEGREE RELATIONSHIPS
dddf4bf6 355 foreach ($directions as $direction) {
356 $user_id_column = "contact_id_{$direction['from']}";
357 $contact_id_column = "contact_id_{$direction['to']}";
358
359 // add clause for deleted contacts, if the user doesn't have the permission to access them
67df1408 360 $LEFT_JOIN_DELETED = $AND_CAN_ACCESS_DELETED = '';
dddf4bf6 361 if (!CRM_Core_Permission::check('access deleted contacts')) {
67df1408 362 $LEFT_JOIN_DELETED = "LEFT JOIN civicrm_contact ON civicrm_contact.id = {$contact_id_column} ";
e4541c56 363 $AND_CAN_ACCESS_DELETED = "AND civicrm_contact.is_deleted = 0";
dddf4bf6 364 }
365
366 $queries[] = "
340be2e7 367SELECT civicrm_relationship.{$contact_id_column} AS contact_id
9aea8e14 368 FROM civicrm_relationship
dddf4bf6 369 {$LEFT_JOIN_DELETED}
370 WHERE civicrm_relationship.{$user_id_column} = {$contactID}
371 AND civicrm_relationship.{$contact_id_column} IN ({$contact_id_list})
372 AND civicrm_relationship.is_active = 1
373 AND civicrm_relationship.is_permission_{$direction['from']}_{$direction['to']} = 1
374 $AND_CAN_ACCESS_DELETED";
e4541c56 375 }
dddf4bf6 376
340be2e7 377 // FIXME: secondDegRelPermissions should be a setting
378 $config = CRM_Core_Config::singleton();
dddf4bf6 379 if ($config->secondDegRelPermissions) {
340be2e7 380 foreach ($directions as $first_direction) {
381 foreach ($directions as $second_direction) {
382 // add clause for deleted contacts, if the user doesn't have the permission to access them
383 $LEFT_JOIN_DELETED = $AND_CAN_ACCESS_DELETED = '';
384 if (!CRM_Core_Permission::check('access deleted contacts')) {
385 $LEFT_JOIN_DELETED = "LEFT JOIN civicrm_contact first_degree_contact ON first_degree_contact.id = second_degree_relationship.contact_id_{$second_direction['from']}\n";
386 $LEFT_JOIN_DELETED .= "LEFT JOIN civicrm_contact second_degree_contact ON second_degree_contact.id = second_degree_relationship.contact_id_{$second_direction['to']} ";
387 $AND_CAN_ACCESS_DELETED = "AND first_degree_contact.is_deleted = 0\n";
388 $AND_CAN_ACCESS_DELETED .= "AND second_degree_contact.is_deleted = 0 ";
dddf4bf6 389 }
340be2e7 390
391 $queries[] = "
392SELECT second_degree_relationship.contact_id_{$second_direction['to']} AS contact_id
393 FROM civicrm_relationship first_degree_relationship
394 LEFT JOIN civicrm_relationship second_degree_relationship ON first_degree_relationship.contact_id_{$first_direction['to']} = second_degree_relationship.contact_id_{$first_direction['from']}
395 {$LEFT_JOIN_DELETED}
396 WHERE first_degree_relationship.contact_id_{$first_direction['from']} = {$contactID}
397 AND second_degree_relationship.contact_id_{$second_direction['to']} IN ({$contact_id_list})
398 AND first_degree_relationship.is_active = 1
399 AND first_degree_relationship.is_permission_{$first_direction['from']}_{$first_direction['to']} = 1
400 AND second_degree_relationship.is_active = 1
401 AND second_degree_relationship.is_permission_{$second_direction['from']}_{$second_direction['to']} = 1
402 $AND_CAN_ACCESS_DELETED";
dddf4bf6 403 }
404 }
405 }
406
407 // finally UNION the queries and call
340be2e7 408 $query = "(" . implode(")\nUNION DISTINCT (", $queries) . ")";
dddf4bf6 409 $result = CRM_Core_DAO::executeQuery($query);
410 while ($result->fetch()) {
67df1408 411 $result_set[(int) $result->contact_id] = TRUE;
dddf4bf6 412 }
67df1408 413 return array_keys($result_set);
dddf4bf6 414 }
415
416
86538308 417 /**
100fef9d 418 * @param int $contactID
c490a46a 419 * @param CRM_Core_Form $form
86538308
EM
420 * @param bool $redirect
421 *
422 * @return bool
423 */
00be9182 424 public static function validateOnlyChecksum($contactID, &$form, $redirect = TRUE) {
6a488035
TO
425 // check if this is of the format cs=XXX
426 if (!CRM_Contact_BAO_Contact_Utils::validChecksum($contactID,
353ffa53
TO
427 CRM_Utils_Request::retrieve('cs', 'String', $form, FALSE)
428 )
429 ) {
6a488035
TO
430 if ($redirect) {
431 // also set a message in the UF framework
432 $message = ts('You do not have permission to edit this contact record. Contact the site administrator if you need assistance.');
433 CRM_Utils_System::setUFMessage($message);
434
435 $config = CRM_Core_Config::singleton();
436 CRM_Core_Error::statusBounce($message,
437 $config->userFrameworkBaseURL
438 );
439 // does not come here, we redirect in the above statement
440 }
441 return FALSE;
442 }
443
a9a1ea2c 444 // set appropriate AUTH source
e8f14831 445 self::initChecksumAuthSrc(TRUE, $form);
a9a1ea2c 446
6a488035
TO
447 // so here the contact is posing as $contactID, lets set the logging contact ID variable
448 // CRM-8965
449 CRM_Core_DAO::executeQuery('SET @civicrm_user_id = %1',
450 array(1 => array($contactID, 'Integer'))
451 );
77b97be7 452
6a488035
TO
453 return TRUE;
454 }
455
86538308
EM
456 /**
457 * @param bool $checkSumValidationResult
458 * @param null $form
459 */
00be9182 460 public static function initChecksumAuthSrc($checkSumValidationResult = FALSE, $form = NULL) {
a9a1ea2c 461 $session = CRM_Core_Session::singleton();
e8f14831 462 if ($checkSumValidationResult && $form && CRM_Utils_Request::retrieve('cs', 'String', $form, FALSE)) {
a9a1ea2c
DS
463 // if result is already validated, and url has cs, set the flag.
464 $session->set('authSrc', CRM_Core_Permission::AUTH_SRC_CHECKSUM);
0db6c3e1 465 }
4c9b6178 466 elseif (($session->get('authSrc') & CRM_Core_Permission::AUTH_SRC_CHECKSUM) == CRM_Core_Permission::AUTH_SRC_CHECKSUM) {
77b97be7 467 // if checksum wasn't present in REQUEST OR checksum result validated as FALSE,
a9a1ea2c
DS
468 // and flag was already set exactly as AUTH_SRC_CHECKSUM, unset it.
469 $session->set('authSrc', CRM_Core_Permission::AUTH_SRC_UNKNOWN);
470 }
471 }
472
86538308 473 /**
100fef9d 474 * @param int $contactID
c490a46a 475 * @param CRM_Core_Form $form
86538308
EM
476 * @param bool $redirect
477 *
478 * @return bool
479 */
00be9182 480 public static function validateChecksumContact($contactID, &$form, $redirect = TRUE) {
6a488035
TO
481 if (!self::allow($contactID, CRM_Core_Permission::EDIT)) {
482 // check if this is of the format cs=XXX
483 return self::validateOnlyChecksum($contactID, $form, $redirect);
484 }
485 return TRUE;
486 }
96025800 487
6a488035 488}