fixed: EDIT implies VIEW
[civicrm-core.git] / CRM / Contact / BAO / Contact / Permission.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
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 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2016
32 */
33 class CRM_Contact_BAO_Contact_Permission {
34
35 /**
36 * Check which of the given contact IDs the logged in user
37 * has permissions for the operation type according
38 *
39 * Caution: general permissions (like 'edit all contacts')
40 *
41 * @param array $contact_ids
42 * Contact IDs.
43 * @param int|string $type the type of operation (view|edit)
44 *
45 * @see CRM_Contact_BAO_Contact_Permission::allow
46 *
47 * @return array
48 * list of contact IDs the logged in user has the given permission for
49 */
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.
54 return $result_set;
55 }
56 $contact_id_list = implode(',', $contact_ids);
57
58 // make sure the the general permissions are given
59 if ( CRM_Core_Permission::check('edit all contacts')
60 || $type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view all contacts')
61 ) {
62
63 // if the general permission is there, all good
64 if (CRM_Core_Permission::check('access deleted contacts')) {
65 // if user can access delted contacts -> fine
66 return $contact_ids;
67
68 } else {
69 // if the user CANNOT access deleted contacts, these need to be filtered
70 $filter_query = "SELECT DISTINCT(id) FROM civicrm_contact WHERE id IN ($contact_id_list) AND is_deleted = 0";
71 $query = CRM_Core_DAO::executeQuery($filter_query);
72 while ($query->fetch()) {
73 $result_set[(int) $query->id] = TRUE;
74 }
75 return array_keys($result_set);
76 }
77 }
78
79 // get logged in user
80 $session = CRM_Core_Session::singleton();
81 $contactID = (int) $session->get('userID');
82 if (empty($contactID)) {
83 return array();
84 }
85
86 // make sure the cache is filled
87 self::cache($contactID, $type);
88
89 // compile query
90 $operation = ($type == CRM_Core_Permission::VIEW) ? 'View' : 'Edit';
91
92 // add clause for deleted contacts, if the user doesn't have the permission to access them
93 $LEFT_JOIN_DELETED = $CAN_ACCESS_DELETED = '';
94 if (!CRM_Core_Permission::check('access deleted contacts')) {
95 $LEFT_JOIN_DELETED = "LEFT JOIN civicrm_contact ON civicrm_contact.id = contact_id";
96 $AND_CAN_ACCESS_DELETED = "AND civicrm_contact.is_deleted = 0";
97 }
98
99 // RUN the query
100 $query = "
101 SELECT contact_id
102 FROM civicrm_acl_contact_cache
103 {$LEFT_JOIN_DELETED}
104 WHERE contact_id IN ({$contact_id_list})
105 AND user_id = {$contactID}
106 AND operation = '{$operation}'
107 {$AND_CAN_ACCESS_DELETED}";
108 $result = CRM_Core_DAO::executeQuery($query);
109 while ($result->fetch()) {
110 $result_set[(int) $result->contact_id] = TRUE;
111 }
112
113 // if some have been rejected, double check for permissions inherited by relationship
114 if (count($result_set) < count($contact_ids)) {
115 $rejected_contacts = array_diff_key($contact_ids, $result_set);
116 $allowed_by_relationship = self::relationshipList($rejected_contacts);
117 foreach ($allowed_by_relationship as $contact_id) {
118 $result_set[(int) $contact_id] = TRUE;
119 }
120 }
121
122 return array_keys($result_set);
123 }
124
125 /**
126 * Check if the logged in user has permissions for the operation type.
127 *
128 * @param int $id
129 * Contact id.
130 * @param int|string $type the type of operation (view|edit)
131 *
132 * @return bool
133 * true if the user has permission, false otherwise
134 */
135 public static function allow($id, $type = CRM_Core_Permission::VIEW) {
136 // get logged in user
137 $session = CRM_Core_Session::singleton();
138 $contactID = (int) $session->get('userID');
139
140 // first: check if contact is trying to view own contact
141 if ( $type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view my contact')
142 || $type == CRM_Core_Permission::EDIT && CRM_Core_Permission::check('edit my contact')
143 ) {
144 return TRUE;
145 }
146
147 # FIXME: push this somewhere below, to not give this permission so many rights
148 $isDeleted = (bool) CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $id, 'is_deleted');
149 if (CRM_Core_Permission::check('access deleted contacts') && $isDeleted) {
150 return TRUE;
151 }
152
153 // short circuit for admin rights here so we avoid unneeeded queries
154 // some duplication of code, but we skip 3-5 queries
155 if (CRM_Core_Permission::check('edit all contacts') ||
156 ($type == CRM_ACL_API::VIEW && CRM_Core_Permission::check('view all contacts'))
157 ) {
158 return TRUE;
159 }
160
161 // check permission based on relationship, CRM-2963
162 if (self::relationship($id)) {
163 return TRUE;
164 }
165
166 // check permission based on ACL
167 $tables = array();
168 $whereTables = array();
169
170 $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables);
171 $from = CRM_Contact_BAO_Query::fromClause($whereTables);
172
173 $query = "
174 SELECT count(DISTINCT contact_a.id)
175 $from
176 WHERE contact_a.id = %1 AND $permission";
177 $params = array(1 => array($id, 'Integer'));
178
179 return (CRM_Core_DAO::singleValueQuery($query, $params) > 0) ? TRUE : FALSE;
180 }
181
182 /**
183 * Fill the acl contact cache for this contact id if empty.
184 *
185 * @param int $userID
186 * @param int|string $type the type of operation (view|edit)
187 * @param bool $force
188 * Should we force a recompute.
189 */
190 public static function cache($userID, $type = CRM_Core_Permission::VIEW, $force = FALSE) {
191 static $_processed = array();
192
193 if ($type = CRM_Core_Permission::VIEW) {
194 $operationClause = " operation IN ( 'Edit', 'View' ) ";
195 $operation = 'View';
196 }
197 else {
198 $operationClause = " operation = 'Edit' ";
199 $operation = 'Edit';
200 }
201
202 if (!$force) {
203 if (!empty($_processed[$userID])) {
204 return;
205 }
206
207 // run a query to see if the cache is filled
208 $sql = "
209 SELECT count(id)
210 FROM civicrm_acl_contact_cache
211 WHERE user_id = %1
212 AND $operationClause
213 ";
214 $params = array(1 => array($userID, 'Integer'));
215 $count = CRM_Core_DAO::singleValueQuery($sql, $params);
216 if ($count > 0) {
217 $_processed[$userID] = 1;
218 return;
219 }
220 }
221
222 $tables = array();
223 $whereTables = array();
224
225 $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables, $userID);
226
227 $from = CRM_Contact_BAO_Query::fromClause($whereTables);
228
229 // FIXME: don't use 'ON DUPLICATE KEY UPDATE'
230 CRM_Core_DAO::executeQuery("
231 INSERT INTO civicrm_acl_contact_cache ( user_id, contact_id, operation )
232 SELECT $userID as user_id, contact_a.id as contact_id, '$operation' as operation
233 $from
234 WHERE $permission
235 GROUP BY contact_a.id
236 ON DUPLICATE KEY UPDATE
237 user_id=VALUES(user_id),
238 contact_id=VALUES(contact_id),
239 operation=VALUES(operation)"
240 );
241
242 $_processed[$userID] = 1;
243 }
244
245 /**
246 * Check if there are any contacts in cache table.
247 *
248 * @param int|string $type the type of operation (view|edit)
249 * @param int $contactID
250 * Contact id.
251 *
252 * @return bool
253 */
254 public static function hasContactsInCache(
255 $type = CRM_Core_Permission::VIEW,
256 $contactID = NULL
257 ) {
258 if (!$contactID) {
259 $session = CRM_Core_Session::singleton();
260 $contactID = $session->get('userID');
261 }
262
263 if ($type = CRM_Core_Permission::VIEW) {
264 $operationClause = " operation IN ( 'Edit', 'View' ) ";
265 $operation = 'View';
266 }
267 else {
268 $operationClause = " operation = 'Edit' ";
269 $operation = 'Edit';
270 }
271
272 // fill cache
273 self::cache($contactID);
274
275 $sql = "
276 SELECT id
277 FROM civicrm_acl_contact_cache
278 WHERE user_id = %1
279 AND $operationClause LIMIT 1";
280
281 $params = array(1 => array($contactID, 'Integer'));
282 return (bool) CRM_Core_DAO::singleValueQuery($sql, $params);
283 }
284
285 /**
286 * @param string $contactAlias
287 *
288 * @return array
289 */
290 public static function cacheClause($contactAlias = 'contact_a') {
291 if (CRM_Core_Permission::check('view all contacts') ||
292 CRM_Core_Permission::check('edit all contacts')
293 ) {
294 if (is_array($contactAlias)) {
295 $wheres = array();
296 foreach ($contactAlias as $alias) {
297 // CRM-6181
298 $wheres[] = "$alias.is_deleted = 0";
299 }
300 return array(NULL, '(' . implode(' AND ', $wheres) . ')');
301 }
302 else {
303 // CRM-6181
304 return array(NULL, "$contactAlias.is_deleted = 0");
305 }
306 }
307
308 $contactID = (int) CRM_Core_Session::getLoggedInContactID();
309 self::cache($contactID);
310
311 if (is_array($contactAlias) && !empty($contactAlias)) {
312 //More than one contact alias
313 $clauses = array();
314 foreach ($contactAlias as $k => $alias) {
315 $clauses[] = " INNER JOIN civicrm_acl_contact_cache aclContactCache_{$k} ON {$alias}.id = aclContactCache_{$k}.contact_id AND aclContactCache_{$k}.user_id = $contactID ";
316 }
317
318 $fromClause = implode(" ", $clauses);
319 $whereClase = NULL;
320 }
321 else {
322 $fromClause = " INNER JOIN civicrm_acl_contact_cache aclContactCache ON {$contactAlias}.id = aclContactCache.contact_id ";
323 $whereClase = " aclContactCache.user_id = $contactID AND $contactAlias.is_deleted = 0";
324 }
325
326 return array($fromClause, $whereClase);
327 }
328
329 /**
330 * Generate acl subquery that can be placed in the WHERE clause of a query or the ON clause of a JOIN
331 *
332 * @return string|null
333 */
334 public static function cacheSubquery() {
335 if (!CRM_Core_Permission::check(array(array('view all contacts', 'edit all contacts')))) {
336 $contactID = (int) CRM_Core_Session::getLoggedInContactID();
337 self::cache($contactID);
338 return "IN (SELECT contact_id FROM civicrm_acl_contact_cache WHERE user_id = $contactID)";
339 }
340 return NULL;
341 }
342
343 /**
344 * Get the permission base on its relationship.
345 *
346 * @param int $selectedContactID
347 * Contact id of selected contact.
348 * @param int $contactID
349 * Contact id of the current contact.
350 *
351 * @return bool
352 * true if logged in user has permission to view
353 * selected contact record else false
354 */
355 public static function relationship($selectedContactID, $contactID = NULL) {
356 $session = CRM_Core_Session::singleton();
357 $config = CRM_Core_Config::singleton();
358 if (!$contactID) {
359 $contactID = $session->get('userID');
360 if (!$contactID) {
361 return FALSE;
362 }
363 }
364 if ($contactID == $selectedContactID &&
365 (CRM_Core_Permission::check('edit my contact'))
366 ) {
367 return TRUE;
368 }
369 else {
370 if ($config->secondDegRelPermissions) {
371 $query = "
372 SELECT firstdeg.id
373 FROM civicrm_relationship firstdeg
374 LEFT JOIN civicrm_relationship seconddegaa
375 on firstdeg.contact_id_a = seconddegaa.contact_id_b
376 and seconddegaa.is_permission_b_a = 1
377 and firstdeg.is_permission_b_a = 1
378 and seconddegaa.is_active = 1
379 LEFT JOIN civicrm_relationship seconddegab
380 on firstdeg.contact_id_a = seconddegab.contact_id_a
381 and seconddegab.is_permission_a_b = 1
382 and firstdeg.is_permission_b_a = 1
383 and seconddegab.is_active = 1
384 LEFT JOIN civicrm_relationship seconddegba
385 on firstdeg.contact_id_b = seconddegba.contact_id_b
386 and seconddegba.is_permission_b_a = 1
387 and firstdeg.is_permission_a_b = 1
388 and seconddegba.is_active = 1
389 LEFT JOIN civicrm_relationship seconddegbb
390 on firstdeg.contact_id_b = seconddegbb.contact_id_a
391 and seconddegbb.is_permission_a_b = 1
392 and firstdeg.is_permission_a_b = 1
393 and seconddegbb.is_active = 1
394 WHERE
395 (
396 ( firstdeg.contact_id_a = %1 AND firstdeg.contact_id_b = %2 AND firstdeg.is_permission_a_b = 1 )
397 OR ( firstdeg.contact_id_a = %2 AND firstdeg.contact_id_b = %1 AND firstdeg.is_permission_b_a = 1 )
398 OR (
399 firstdeg.contact_id_a = %1 AND seconddegba.contact_id_a = %2
400 AND (seconddegba.contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
401 )
402 OR (
403 firstdeg.contact_id_a = %1 AND seconddegbb.contact_id_b = %2
404 AND (seconddegbb.contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
405 )
406 OR (
407 firstdeg.contact_id_b = %1 AND seconddegab.contact_id_b = %2
408 AND (seconddegab.contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
409 )
410 OR (
411 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))
412 )
413 )
414 AND (firstdeg.contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
415 AND (firstdeg.contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
416 AND ( firstdeg.is_active = 1)
417 ";
418 }
419 else {
420 $query = "
421 SELECT id
422 FROM civicrm_relationship
423 WHERE (( contact_id_a = %1 AND contact_id_b = %2 AND is_permission_a_b = 1 ) OR
424 ( contact_id_a = %2 AND contact_id_b = %1 AND is_permission_b_a = 1 )) AND
425 (contact_id_a NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)) AND
426 (contact_id_b NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1))
427 AND ( civicrm_relationship.is_active = 1 )
428 ";
429 }
430 $params = array(
431 1 => array($contactID, 'Integer'),
432 2 => array($selectedContactID, 'Integer'),
433 );
434 return CRM_Core_DAO::singleValueQuery($query, $params);
435 }
436 }
437
438
439
440 /**
441 * Filter a list of contact_ids by the ones that the
442 * currently active user as a permissioned relationship with
443 *
444 * @param array $contact_ids
445 * List of contact IDs to be filtered
446 *
447 * @return array
448 * List of contact IDs that the user has permissions for
449 */
450 public static function relationshipList($contact_ids) {
451 $result_set = array();
452
453 // no processing empty lists (avoid SQL errors as well)
454 if (empty($contact_ids)) {
455 return array();
456 }
457
458 // get the currently logged in user
459 $config = CRM_Core_Config::singleton();
460 $session = CRM_Core_Session::singleton();
461 $contactID = (int) $session->get('userID');
462 if (empty($contactID)) {
463 return array();
464 }
465
466 // compile a list of queries (later to UNION)
467 $queries = array();
468 $contact_id_list = implode(',', $contact_ids);
469
470 // add a select statement for each direection
471 $directions = array(array('from' => 'a', 'to' => 'b'), array('from' => 'b', 'to' => 'a'));
472
473 // NORMAL/SINGLE DEGREE RELATIONSHIPS
474 foreach ($directions as $direction) {
475 $user_id_column = "contact_id_{$direction['from']}";
476 $contact_id_column = "contact_id_{$direction['to']}";
477
478 // add clause for deleted contacts, if the user doesn't have the permission to access them
479 $LEFT_JOIN_DELETED = $AND_CAN_ACCESS_DELETED = '';
480 if (!CRM_Core_Permission::check('access deleted contacts')) {
481 $LEFT_JOIN_DELETED = "LEFT JOIN civicrm_contact ON civicrm_contact.id = {$contact_id_column} ";
482 $AND_CAN_ACCESS_DELETED = "AND civicrm_contact.is_deleted = 0";
483 }
484
485 $queries[] = "
486 SELECT DISTINCT(civicrm_relationship.{$contact_id_column}) AS contact_id
487 FROM civicrm_relationship
488 {$LEFT_JOIN_DELETED}
489 WHERE civicrm_relationship.{$user_id_column} = {$contactID}
490 AND civicrm_relationship.{$contact_id_column} IN ({$contact_id_list})
491 AND civicrm_relationship.is_active = 1
492 AND civicrm_relationship.is_permission_{$direction['from']}_{$direction['to']} = 1
493 $AND_CAN_ACCESS_DELETED";
494 }
495
496
497 if ($config->secondDegRelPermissions) {
498 // ADD SECOND DEGREE RELATIONSHIPS
499 if ($config->secondDegRelPermissions) {
500 foreach ($directions as $first_direction) {
501 foreach ($directions as $second_direction) {
502 // add clause for deleted contacts, if the user doesn't have the permission to access them
503 $LEFT_JOIN_DELETED = $AND_CAN_ACCESS_DELETED = '';
504 if (!CRM_Core_Permission::check('access deleted contacts')) {
505 $LEFT_JOIN_DELETED = "LEFT JOIN civicrm_contact first_degree_contact ON first_degree_contact.id = second_degree_relationship.contact_id_{$second_direction['from']}\n";
506 $LEFT_JOIN_DELETED .= "LEFT JOIN civicrm_contact second_degree_contact ON second_degree_contact.id = second_degree_relationship.contact_id_{$second_direction['to']} ";
507 $AND_CAN_ACCESS_DELETED = "AND first_degree_contact.is_deleted = 0\n";
508 $AND_CAN_ACCESS_DELETED .= "AND second_degree_contact.is_deleted = 0 ";
509 }
510
511 $queries[] = "
512 SELECT DISTINCT(second_degree_relationship.contact_id_{$second_direction['to']}) AS contact_id
513 FROM civicrm_relationship first_degree_relationship
514 LEFT JOIN civicrm_relationship second_degree_relationship ON first_degree_relationship.contact_id_{$first_direction['to']} = second_degree_relationship.contact_id_{$first_direction['from']}
515 {$LEFT_JOIN_DELETED}
516 WHERE first_degree_relationship.contact_id_{$first_direction['from']} = {$contactID}
517 AND second_degree_relationship.contact_id_{$second_direction['to']} IN ({$contact_id_list})
518 AND first_degree_relationship.is_active = 1
519 AND first_degree_relationship.is_permission_{$first_direction['from']}_{$first_direction['to']} = 1
520 AND second_degree_relationship.is_active = 1
521 AND second_degree_relationship.is_permission_{$second_direction['from']}_{$second_direction['to']} = 1
522 $AND_CAN_ACCESS_DELETED";
523 }
524 }
525 }
526 }
527
528 // finally UNION the queries and call
529 $query = "(" . implode(")\nUNION (", $queries) . ")";
530 $result = CRM_Core_DAO::executeQuery($query);
531 while ($result->fetch()) {
532 $result_set[(int) $result->contact_id] = TRUE;
533 }
534 $keys = array_keys($result_set);
535 return array_keys($result_set);
536 }
537
538
539
540
541 /**
542 * @param int $contactID
543 * @param CRM_Core_Form $form
544 * @param bool $redirect
545 *
546 * @return bool
547 */
548 public static function validateOnlyChecksum($contactID, &$form, $redirect = TRUE) {
549 // check if this is of the format cs=XXX
550 if (!CRM_Contact_BAO_Contact_Utils::validChecksum($contactID,
551 CRM_Utils_Request::retrieve('cs', 'String', $form, FALSE)
552 )
553 ) {
554 if ($redirect) {
555 // also set a message in the UF framework
556 $message = ts('You do not have permission to edit this contact record. Contact the site administrator if you need assistance.');
557 CRM_Utils_System::setUFMessage($message);
558
559 $config = CRM_Core_Config::singleton();
560 CRM_Core_Error::statusBounce($message,
561 $config->userFrameworkBaseURL
562 );
563 // does not come here, we redirect in the above statement
564 }
565 return FALSE;
566 }
567
568 // set appropriate AUTH source
569 self::initChecksumAuthSrc(TRUE, $form);
570
571 // so here the contact is posing as $contactID, lets set the logging contact ID variable
572 // CRM-8965
573 CRM_Core_DAO::executeQuery('SET @civicrm_user_id = %1',
574 array(1 => array($contactID, 'Integer'))
575 );
576
577 return TRUE;
578 }
579
580 /**
581 * @param bool $checkSumValidationResult
582 * @param null $form
583 */
584 public static function initChecksumAuthSrc($checkSumValidationResult = FALSE, $form = NULL) {
585 $session = CRM_Core_Session::singleton();
586 if ($checkSumValidationResult && $form && CRM_Utils_Request::retrieve('cs', 'String', $form, FALSE)) {
587 // if result is already validated, and url has cs, set the flag.
588 $session->set('authSrc', CRM_Core_Permission::AUTH_SRC_CHECKSUM);
589 }
590 elseif (($session->get('authSrc') & CRM_Core_Permission::AUTH_SRC_CHECKSUM) == CRM_Core_Permission::AUTH_SRC_CHECKSUM) {
591 // if checksum wasn't present in REQUEST OR checksum result validated as FALSE,
592 // and flag was already set exactly as AUTH_SRC_CHECKSUM, unset it.
593 $session->set('authSrc', CRM_Core_Permission::AUTH_SRC_UNKNOWN);
594 }
595 }
596
597 /**
598 * @param int $contactID
599 * @param CRM_Core_Form $form
600 * @param bool $redirect
601 *
602 * @return bool
603 */
604 public static function validateChecksumContact($contactID, &$form, $redirect = TRUE) {
605 if (!self::allow($contactID, CRM_Core_Permission::EDIT)) {
606 // check if this is of the format cs=XXX
607 return self::validateOnlyChecksum($contactID, $form, $redirect);
608 }
609 return TRUE;
610 }
611
612 }