added new list permission functions
[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
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')
60 ) {
61 // if the general permission is there, all good
62 // TODO: deleted
63 return $contact_ids;
64 }
65
66 // get logged in user
67 $session = CRM_Core_Session::singleton();
68 $contactID = (int) $session->get('userID');
69 if (empty($contactID)) {
70 return $result_set;
71 }
72
73 // make sure the cache is filled
74 self::cache($contactID, $type);
75
76 // compile query
77 $contact_id_list = implode(',', $contact_ids);
78 $operation = ($type == CRM_Core_Permission::VIEW) ? 'View' : 'Edit';
79
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';
85 }
86
87 // RUN the query
88 $query = "
89 SELECT contact_id
90 FROM civicrm_acl_contact_cache
91 {$LEFT_JOIN_DELETED}
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;
99 }
100
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);
106 }
107
108 return $result_set;
109 }
110
111 /**
112 * Check if the logged in user has permissions for the operation type.
113 *
114 * @param int $id
115 * Contact id.
116 * @param int|string $type the type of operation (view|edit)
117 *
118 * @return bool
119 * true if the user has permission, false otherwise
120 */
121 public static function allow($id, $type = CRM_Core_Permission::VIEW) {
122 $tables = array();
123 $whereTables = array();
124
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) {
128 return TRUE;
129 }
130
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'))
135 ) {
136 return TRUE;
137 }
138
139 //check permission based on relationship, CRM-2963
140 if (self::relationship($id)) {
141 return TRUE;
142 }
143
144 $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables);
145
146 $from = CRM_Contact_BAO_Query::fromClause($whereTables);
147
148 $query = "
149 SELECT count(DISTINCT contact_a.id)
150 $from
151 WHERE contact_a.id = %1 AND $permission";
152 $params = array(1 => array($id, 'Integer'));
153
154 return (CRM_Core_DAO::singleValueQuery($query, $params) > 0) ? TRUE : FALSE;
155 }
156
157 /**
158 * Fill the acl contact cache for this contact id if empty.
159 *
160 * @param int $userID
161 * @param int|string $type the type of operation (view|edit)
162 * @param bool $force
163 * Should we force a recompute.
164 */
165 public static function cache($userID, $type = CRM_Core_Permission::VIEW, $force = FALSE) {
166 static $_processed = array();
167
168 if ($type = CRM_Core_Permission::VIEW) {
169 $operationClause = " operation IN ( 'Edit', 'View' ) ";
170 $operation = 'View';
171 }
172 else {
173 $operationClause = " operation = 'Edit' ";
174 $operation = 'Edit';
175 }
176
177 if (!$force) {
178 if (!empty($_processed[$userID])) {
179 return;
180 }
181
182 // run a query to see if the cache is filled
183 $sql = "
184 SELECT count(id)
185 FROM civicrm_acl_contact_cache
186 WHERE user_id = %1
187 AND $operationClause
188 ";
189 $params = array(1 => array($userID, 'Integer'));
190 $count = CRM_Core_DAO::singleValueQuery($sql, $params);
191 if ($count > 0) {
192 $_processed[$userID] = 1;
193 return;
194 }
195 }
196
197 $tables = array();
198 $whereTables = array();
199
200 $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables, $userID);
201
202 $from = CRM_Contact_BAO_Query::fromClause($whereTables);
203
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
207 $from
208 WHERE $permission
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)"
214 );
215
216 $_processed[$userID] = 1;
217 }
218
219 /**
220 * Check if there are any contacts in cache table.
221 *
222 * @param int|string $type the type of operation (view|edit)
223 * @param int $contactID
224 * Contact id.
225 *
226 * @return bool
227 */
228 public static function hasContactsInCache(
229 $type = CRM_Core_Permission::VIEW,
230 $contactID = NULL
231 ) {
232 if (!$contactID) {
233 $session = CRM_Core_Session::singleton();
234 $contactID = $session->get('userID');
235 }
236
237 if ($type = CRM_Core_Permission::VIEW) {
238 $operationClause = " operation IN ( 'Edit', 'View' ) ";
239 $operation = 'View';
240 }
241 else {
242 $operationClause = " operation = 'Edit' ";
243 $operation = 'Edit';
244 }
245
246 // fill cache
247 self::cache($contactID);
248
249 $sql = "
250 SELECT id
251 FROM civicrm_acl_contact_cache
252 WHERE user_id = %1
253 AND $operationClause LIMIT 1";
254
255 $params = array(1 => array($contactID, 'Integer'));
256 return (bool) CRM_Core_DAO::singleValueQuery($sql, $params);
257 }
258
259 /**
260 * @param string $contactAlias
261 *
262 * @return array
263 */
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')
267 ) {
268 if (is_array($contactAlias)) {
269 $wheres = array();
270 foreach ($contactAlias as $alias) {
271 // CRM-6181
272 $wheres[] = "$alias.is_deleted = 0";
273 }
274 return array(NULL, '(' . implode(' AND ', $wheres) . ')');
275 }
276 else {
277 // CRM-6181
278 return array(NULL, "$contactAlias.is_deleted = 0");
279 }
280 }
281
282 $contactID = (int) CRM_Core_Session::getLoggedInContactID();
283 self::cache($contactID);
284
285 if (is_array($contactAlias) && !empty($contactAlias)) {
286 //More than one contact alias
287 $clauses = array();
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 ";
290 }
291
292 $fromClause = implode(" ", $clauses);
293 $whereClase = NULL;
294 }
295 else {
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";
298 }
299
300 return array($fromClause, $whereClase);
301 }
302
303 /**
304 * Generate acl subquery that can be placed in the WHERE clause of a query or the ON clause of a JOIN
305 *
306 * @return string|null
307 */
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)";
313 }
314 return NULL;
315 }
316
317 /**
318 * Get the permission base on its relationship.
319 *
320 * @param int $selectedContactID
321 * Contact id of selected contact.
322 * @param int $contactID
323 * Contact id of the current contact.
324 *
325 * @return bool
326 * true if logged in user has permission to view
327 * selected contact record else false
328 */
329 public static function relationship($selectedContactID, $contactID = NULL) {
330 $session = CRM_Core_Session::singleton();
331 $config = CRM_Core_Config::singleton();
332 if (!$contactID) {
333 $contactID = $session->get('userID');
334 if (!$contactID) {
335 return FALSE;
336 }
337 }
338 if ($contactID == $selectedContactID &&
339 (CRM_Core_Permission::check('edit my contact'))
340 ) {
341 return TRUE;
342 }
343 else {
344 if ($config->secondDegRelPermissions) {
345 $query = "
346 SELECT firstdeg.id
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
368 WHERE
369 (
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 )
372 OR (
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))
375 )
376 OR (
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))
379 )
380 OR (
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))
383 )
384 OR (
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))
386 )
387 )
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)
391 ";
392 }
393 else {
394 $query = "
395 SELECT id
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 )
402 ";
403 }
404 $params = array(
405 1 => array($contactID, 'Integer'),
406 2 => array($selectedContactID, 'Integer'),
407 );
408 return CRM_Core_DAO::singleValueQuery($query, $params);
409 }
410 }
411
412
413
414 /**
415 * Filter a list of contact_ids by the ones that the
416 * currently active user as a permissioned relationship with
417 *
418 * @param array $contact_ids
419 * List of contact IDs to be filtered
420 *
421 * @return array
422 * List of contact IDs that the user has permissions for
423 */
424 public static function relationshipList($contact_ids) {
425 $result_set = array();
426
427 // no processing empty lists (avoid SQL errors as well)
428 if (empty($contact_ids)) {
429 return $result_set;
430 }
431
432 // get the currently logged in user
433 $session = CRM_Core_Session::singleton();
434 $contactID = (int) $session->get('userID');
435 if (empty($contactID)) {
436 return $result_set;
437 }
438
439 // compile a list of queries (later to UNION)
440 $queries = array();
441 $contact_id_list = implode(',', $contact_ids);
442
443
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']}";
449
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';
455 }
456
457 $queries[] = "
458 SELECT DISTINCT(civicrm_relationship.{$contact_id_column}) AS contact_id
459 FROM civicrm_relationship
460 {$LEFT_JOIN_DELETED}
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";
466 }
467
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';
477 }
478
479 $queries[] = "
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']}
483 {$LEFT_JOIN_DELETED}
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";
491 }
492 }
493 }
494
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;
500 }
501
502 return $result_set;
503 }
504
505
506
507
508 /**
509 * @param int $contactID
510 * @param CRM_Core_Form $form
511 * @param bool $redirect
512 *
513 * @return bool
514 */
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)
519 )
520 ) {
521 if ($redirect) {
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);
525
526 $config = CRM_Core_Config::singleton();
527 CRM_Core_Error::statusBounce($message,
528 $config->userFrameworkBaseURL
529 );
530 // does not come here, we redirect in the above statement
531 }
532 return FALSE;
533 }
534
535 // set appropriate AUTH source
536 self::initChecksumAuthSrc(TRUE, $form);
537
538 // so here the contact is posing as $contactID, lets set the logging contact ID variable
539 // CRM-8965
540 CRM_Core_DAO::executeQuery('SET @civicrm_user_id = %1',
541 array(1 => array($contactID, 'Integer'))
542 );
543
544 return TRUE;
545 }
546
547 /**
548 * @param bool $checkSumValidationResult
549 * @param null $form
550 */
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);
556 }
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);
561 }
562 }
563
564 /**
565 * @param int $contactID
566 * @param CRM_Core_Form $form
567 * @param bool $redirect
568 *
569 * @return bool
570 */
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);
575 }
576 return TRUE;
577 }
578
579 }