Merge pull request #12486 from eileenmcnaughton/savedSearch
[civicrm-core.git] / CRM / Contact / BAO / Contact / Permission.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
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-2018
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 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)
42 *
43 * @param array $contact_ids
44 * Contact IDs.
45 * @param int $type the type of operation (view|edit)
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
60 if (CRM_Core_Permission::check('edit all contacts')
61 || $type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view all contacts')
62 ) {
63
64 // if the general permission is there, all good
65 if (CRM_Core_Permission::check('access deleted contacts')) {
66 // if user can access deleted contacts -> fine
67 return $contact_ids;
68 }
69 else {
70 // if the user CANNOT access deleted contacts, these need to be filtered
71 $contact_id_list = implode(',', $contact_ids);
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 }
79 }
80
81 // get logged in user
82 $contactID = CRM_Core_Session::getLoggedInContactID();
83 if (empty($contactID)) {
84 return array();
85 }
86
87 // make sure the cache is filled
88 self::cache($contactID, $type);
89
90 // compile query
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
94 $LEFT_JOIN_DELETED = $AND_CAN_ACCESS_DELETED = '';
95 if (!CRM_Core_Permission::check('access deleted contacts')) {
96 $LEFT_JOIN_DELETED = "LEFT JOIN civicrm_contact ON civicrm_contact.id = contact_id";
97 $AND_CAN_ACCESS_DELETED = "AND civicrm_contact.is_deleted = 0";
98 }
99
100 // RUN the query
101 $contact_id_list = implode(',', $contact_ids);
102 $query = "
103 SELECT contact_id
104 FROM civicrm_acl_contact_cache
105 {$LEFT_JOIN_DELETED}
106 WHERE contact_id IN ({$contact_id_list})
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()) {
112 $result_set[(int) $result->contact_id] = TRUE;
113 }
114
115 // if some have been rejected, double check for permissions inherited by relationship
116 if (count($result_set) < count($contact_ids)) {
117 $rejected_contacts = array_diff_key($contact_ids, $result_set);
118 // @todo consider storing these to the acl cache for next time, since we have fetched.
119 $allowed_by_relationship = self::relationshipList($rejected_contacts, $type);
120 foreach ($allowed_by_relationship as $contact_id) {
121 $result_set[(int) $contact_id] = TRUE;
122 }
123 }
124
125 return array_keys($result_set);
126 }
127
128 /**
129 * Check if the logged in user has permissions for the operation type.
130 *
131 * @param int $id
132 * Contact id.
133 * @param int|string $type the type of operation (view|edit)
134 *
135 * @return bool
136 * true if the user has permission, false otherwise
137 */
138 public static function allow($id, $type = CRM_Core_Permission::VIEW) {
139 // get logged in user
140 $contactID = CRM_Core_Session::getLoggedInContactID();
141
142 // first: check if contact is trying to view own contact
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'))
145 ) {
146 return TRUE;
147 }
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
163 // check permission based on relationship, CRM-2963
164 if (self::relationshipList(array($id), $type)) {
165 return TRUE;
166 }
167
168 // We should probably do a cheap check whether it's in the cache first.
169 // check permission based on ACL
170 $tables = array();
171 $whereTables = array();
172
173 $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables, NULL, FALSE, FALSE, TRUE);
174 $from = CRM_Contact_BAO_Query::fromClause($whereTables);
175
176 $query = "
177 SELECT contact_a.id
178 $from
179 WHERE contact_a.id = %1 AND $permission
180 LIMIT 1
181 ";
182
183 if (CRM_Core_DAO::singleValueQuery($query, array(1 => array($id, 'Integer')))) {
184 return TRUE;
185 }
186 return FALSE;
187 }
188
189 /**
190 * Fill the acl contact cache for this contact id if empty.
191 *
192 * @param int $userID
193 * @param int|string $type the type of operation (view|edit)
194 * @param bool $force
195 * Should we force a recompute.
196 */
197 public static function cache($userID, $type = CRM_Core_Permission::VIEW, $force = FALSE) {
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,
200 // but the alternative would be a SQL call every time this is called,
201 // and a complete rebuild if the result was an empty set...
202 if (!isset(Civi::$statics[__CLASS__]['processed'])) {
203 Civi::$statics[__CLASS__]['processed'] = [
204 CRM_Core_Permission::VIEW => [],
205 CRM_Core_Permission::EDIT => [],
206 ];
207 }
208
209 if ($type == CRM_Core_Permission::VIEW) {
210 $operationClause = " operation IN ( 'Edit', 'View' ) ";
211 $operation = 'View';
212 }
213 else {
214 $operationClause = " operation = 'Edit' ";
215 $operation = 'Edit';
216 }
217 $queryParams = array(1 => array($userID, 'Integer'));
218
219 if (!$force) {
220 // skip if already calculated
221 if (!empty(Civi::$statics[__CLASS__]['processed'][$type][$userID])) {
222 return;
223 }
224
225 // run a query to see if the cache is filled
226 $sql = "
227 SELECT count(*)
228 FROM civicrm_acl_contact_cache
229 WHERE user_id = %1
230 AND $operationClause
231 ";
232 $count = CRM_Core_DAO::singleValueQuery($sql, $queryParams);
233 if ($count > 0) {
234 Civi::$statics[__CLASS__]['processed'][$type][$userID] = 1;
235 return;
236 }
237 }
238
239 $tables = array();
240 $whereTables = array();
241
242 $permission = CRM_ACL_API::whereClause($type, $tables, $whereTables, $userID, FALSE, FALSE, TRUE);
243
244 $from = CRM_Contact_BAO_Query::fromClause($whereTables);
245 CRM_Core_DAO::executeQuery("
246 INSERT INTO civicrm_acl_contact_cache ( user_id, contact_id, operation )
247 SELECT DISTINCT $userID as user_id, contact_a.id as contact_id, '{$operation}' as operation
248 $from
249 LEFT JOIN civicrm_acl_contact_cache ac ON ac.user_id = $userID AND ac.contact_id = contact_a.id AND ac.operation = '{$operation}'
250 WHERE $permission
251 AND ac.user_id IS NULL
252 ");
253
254 // 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
255 // the permission clause.
256 if (CRM_Core_Permission::check('edit my contact') ||
257 ($type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view my contact'))) {
258 if (!CRM_Core_DAO::singleValueQuery("
259 SELECT count(*) FROM civicrm_acl_contact_cache WHERE user_id = %1 AND contact_id = %1 AND operation = '{$operation}' LIMIT 1", $queryParams)) {
260 CRM_Core_DAO::executeQuery("INSERT INTO civicrm_acl_contact_cache ( user_id, contact_id, operation ) VALUES(%1, %1, '{$operation}')", $queryParams);
261 }
262 }
263 Civi::$statics[__CLASS__]['processed'][$type][$userID] = 1;
264 }
265
266 /**
267 * @param string $contactAlias
268 *
269 * @return array
270 */
271 public static function cacheClause($contactAlias = 'contact_a') {
272 if (CRM_Core_Permission::check('view all contacts') ||
273 CRM_Core_Permission::check('edit all contacts')
274 ) {
275 if (is_array($contactAlias)) {
276 $wheres = array();
277 foreach ($contactAlias as $alias) {
278 // CRM-6181
279 $wheres[] = "$alias.is_deleted = 0";
280 }
281 return array(NULL, '(' . implode(' AND ', $wheres) . ')');
282 }
283 else {
284 // CRM-6181
285 return array(NULL, "$contactAlias.is_deleted = 0");
286 }
287 }
288
289 $contactID = (int) CRM_Core_Session::getLoggedInContactID();
290 self::cache($contactID);
291
292 if (is_array($contactAlias) && !empty($contactAlias)) {
293 //More than one contact alias
294 $clauses = array();
295 foreach ($contactAlias as $k => $alias) {
296 $clauses[] = " INNER JOIN civicrm_acl_contact_cache aclContactCache_{$k} ON {$alias}.id = aclContactCache_{$k}.contact_id AND aclContactCache_{$k}.user_id = $contactID ";
297 }
298
299 $fromClause = implode(" ", $clauses);
300 $whereClase = NULL;
301 }
302 else {
303 $fromClause = " INNER JOIN civicrm_acl_contact_cache aclContactCache ON {$contactAlias}.id = aclContactCache.contact_id ";
304 $whereClase = " aclContactCache.user_id = $contactID AND $contactAlias.is_deleted = 0";
305 }
306
307 return array($fromClause, $whereClase);
308 }
309
310 /**
311 * Generate acl subquery that can be placed in the WHERE clause of a query or the ON clause of a JOIN.
312 *
313 * This is specifically for VIEW operations.
314 *
315 * @return string|null
316 */
317 public static function cacheSubquery() {
318 if (!CRM_Core_Permission::check(array(array('view all contacts', 'edit all contacts')))) {
319 $contactID = (int) CRM_Core_Session::getLoggedInContactID();
320 self::cache($contactID);
321 return "IN (SELECT contact_id FROM civicrm_acl_contact_cache WHERE user_id = $contactID)";
322 }
323 return NULL;
324 }
325
326 /**
327 * Filter a list of contact_ids by the ones that the
328 * currently active user as a permissioned relationship with
329 *
330 * @param array $contact_ids
331 * List of contact IDs to be filtered
332 *
333 * @param int $type
334 * access type CRM_Core_Permission::VIEW or CRM_Core_Permission::EDIT
335 *
336 * @return array
337 * List of contact IDs that the user has permissions for
338 */
339 public static function relationshipList($contact_ids, $type) {
340 $result_set = array();
341
342 // no processing empty lists (avoid SQL errors as well)
343 if (empty($contact_ids)) {
344 return array();
345 }
346
347 // get the currently logged in user
348 $contactID = CRM_Core_Session::getLoggedInContactID();
349 if (empty($contactID)) {
350 return array();
351 }
352
353 // compile a list of queries (later to UNION)
354 $queries = array();
355 $contact_id_list = implode(',', $contact_ids);
356
357 // add a select statement for each direction
358 $directions = array(array('from' => 'a', 'to' => 'b'), array('from' => 'b', 'to' => 'a'));
359
360 // CRM_Core_Permission::VIEW is satisfied by either CRM_Contact_BAO_Relationship::VIEW or CRM_Contact_BAO_Relationship::EDIT
361 if ($type == CRM_Core_Permission::VIEW) {
362 $is_perm_condition = ' IN ( ' . CRM_Contact_BAO_Relationship::EDIT . ' , ' . CRM_Contact_BAO_Relationship::VIEW . ' ) ';
363 }
364 else {
365 $is_perm_condition = ' = ' . CRM_Contact_BAO_Relationship::EDIT;
366 }
367
368 // NORMAL/SINGLE DEGREE RELATIONSHIPS
369 foreach ($directions as $direction) {
370 $user_id_column = "contact_id_{$direction['from']}";
371 $contact_id_column = "contact_id_{$direction['to']}";
372
373 // add clause for deleted contacts, if the user doesn't have the permission to access them
374 $LEFT_JOIN_DELETED = $AND_CAN_ACCESS_DELETED = '';
375 if (!CRM_Core_Permission::check('access deleted contacts')) {
376 $LEFT_JOIN_DELETED = "LEFT JOIN civicrm_contact ON civicrm_contact.id = {$contact_id_column} ";
377 $AND_CAN_ACCESS_DELETED = "AND civicrm_contact.is_deleted = 0";
378 }
379
380 $queries[] = "
381 SELECT civicrm_relationship.{$contact_id_column} AS contact_id
382 FROM civicrm_relationship
383 {$LEFT_JOIN_DELETED}
384 WHERE civicrm_relationship.{$user_id_column} = {$contactID}
385 AND civicrm_relationship.{$contact_id_column} IN ({$contact_id_list})
386 AND civicrm_relationship.is_active = 1
387 AND civicrm_relationship.is_permission_{$direction['from']}_{$direction['to']} {$is_perm_condition}
388 $AND_CAN_ACCESS_DELETED";
389 }
390
391 // FIXME: secondDegRelPermissions should be a setting
392 $config = CRM_Core_Config::singleton();
393 if ($config->secondDegRelPermissions) {
394 foreach ($directions as $first_direction) {
395 foreach ($directions as $second_direction) {
396 // add clause for deleted contacts, if the user doesn't have the permission to access them
397 $LEFT_JOIN_DELETED = $AND_CAN_ACCESS_DELETED = '';
398 if (!CRM_Core_Permission::check('access deleted contacts')) {
399 $LEFT_JOIN_DELETED = "LEFT JOIN civicrm_contact first_degree_contact ON first_degree_contact.id = second_degree_relationship.contact_id_{$second_direction['from']}\n";
400 $LEFT_JOIN_DELETED .= "LEFT JOIN civicrm_contact second_degree_contact ON second_degree_contact.id = second_degree_relationship.contact_id_{$second_direction['to']} ";
401 $AND_CAN_ACCESS_DELETED = "AND first_degree_contact.is_deleted = 0\n";
402 $AND_CAN_ACCESS_DELETED .= "AND second_degree_contact.is_deleted = 0 ";
403 }
404
405 $queries[] = "
406 SELECT second_degree_relationship.contact_id_{$second_direction['to']} AS contact_id
407 FROM civicrm_relationship first_degree_relationship
408 LEFT JOIN civicrm_relationship second_degree_relationship ON first_degree_relationship.contact_id_{$first_direction['to']} = second_degree_relationship.contact_id_{$second_direction['from']}
409 {$LEFT_JOIN_DELETED}
410 WHERE first_degree_relationship.contact_id_{$first_direction['from']} = {$contactID}
411 AND second_degree_relationship.contact_id_{$second_direction['to']} IN ({$contact_id_list})
412 AND first_degree_relationship.is_active = 1
413 AND first_degree_relationship.is_permission_{$first_direction['from']}_{$first_direction['to']} {$is_perm_condition}
414 AND second_degree_relationship.is_active = 1
415 AND second_degree_relationship.is_permission_{$second_direction['from']}_{$second_direction['to']} {$is_perm_condition}
416 $AND_CAN_ACCESS_DELETED";
417 }
418 }
419 }
420
421 // finally UNION the queries and call
422 $query = "(" . implode(")\nUNION DISTINCT (", $queries) . ")";
423 $result = CRM_Core_DAO::executeQuery($query);
424 while ($result->fetch()) {
425 $result_set[(int) $result->contact_id] = TRUE;
426 }
427 return array_keys($result_set);
428 }
429
430
431 /**
432 * @param int $contactID
433 * @param CRM_Core_Form $form
434 * @param bool $redirect
435 *
436 * @return bool
437 */
438 public static function validateOnlyChecksum($contactID, &$form, $redirect = TRUE) {
439 // check if this is of the format cs=XXX
440 if (!CRM_Contact_BAO_Contact_Utils::validChecksum($contactID,
441 CRM_Utils_Request::retrieve('cs', 'String', $form, FALSE)
442 )
443 ) {
444 if ($redirect) {
445 // also set a message in the UF framework
446 $message = ts('You do not have permission to edit this contact record. Contact the site administrator if you need assistance.');
447 CRM_Utils_System::setUFMessage($message);
448
449 $config = CRM_Core_Config::singleton();
450 CRM_Core_Error::statusBounce($message,
451 $config->userFrameworkBaseURL
452 );
453 // does not come here, we redirect in the above statement
454 }
455 return FALSE;
456 }
457
458 // set appropriate AUTH source
459 self::initChecksumAuthSrc(TRUE, $form);
460
461 // so here the contact is posing as $contactID, lets set the logging contact ID variable
462 // CRM-8965
463 CRM_Core_DAO::executeQuery('SET @civicrm_user_id = %1',
464 array(1 => array($contactID, 'Integer'))
465 );
466
467 return TRUE;
468 }
469
470 /**
471 * @param bool $checkSumValidationResult
472 * @param null $form
473 */
474 public static function initChecksumAuthSrc($checkSumValidationResult = FALSE, $form = NULL) {
475 $session = CRM_Core_Session::singleton();
476 if ($checkSumValidationResult && $form && CRM_Utils_Request::retrieve('cs', 'String', $form, FALSE)) {
477 // if result is already validated, and url has cs, set the flag.
478 $session->set('authSrc', CRM_Core_Permission::AUTH_SRC_CHECKSUM);
479 }
480 elseif (($session->get('authSrc') & CRM_Core_Permission::AUTH_SRC_CHECKSUM) == CRM_Core_Permission::AUTH_SRC_CHECKSUM) {
481 // if checksum wasn't present in REQUEST OR checksum result validated as FALSE,
482 // and flag was already set exactly as AUTH_SRC_CHECKSUM, unset it.
483 $session->set('authSrc', CRM_Core_Permission::AUTH_SRC_UNKNOWN);
484 }
485 }
486
487 /**
488 * @param int $contactID
489 * @param CRM_Core_Form $form
490 * @param bool $redirect
491 *
492 * @return bool
493 */
494 public static function validateChecksumContact($contactID, &$form, $redirect = TRUE) {
495 if (!self::allow($contactID, CRM_Core_Permission::EDIT)) {
496 // check if this is of the format cs=XXX
497 return self::validateOnlyChecksum($contactID, $form, $redirect);
498 }
499 return TRUE;
500 }
501
502 }