Test Failed
Push — master ( 8c47c2...3acf9f )
by Steve
12:37
created

engine/classes/Elgg/Database/AccessCollections.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Elgg\Database;
4
5
use Elgg\Config as Conf;
6
use Elgg\Database;
7
use Elgg\Database\EntityTable\UserFetchFailureException;
8
use Elgg\I18n\Translator;
9
use Elgg\PluginHooksService;
10
use ElggEntity;
11
use ElggSession;
12
use ElggStaticVariableCache;
13
use ElggUser;
14
15
/**
16
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
17
 *
18
 * @access private
19
 *
20
 * @package    Elgg.Core
21
 * @subpackage Database
22
 * @since      1.10.0
23
 */
24
class AccessCollections {
25
26
	/**
27
	 * @var Conf
28
	 */
29
	protected $config;
30
31
	/**
32
	 * @var Database
33
	 */
34
	protected $db;
35
36
	/**
37
	 * @vars \ElggStateVariableCache
38
	 */
39
	protected $access_cache;
40
41
	/**
42
	 * @var PluginHooksService
43
	 */
44
	protected $hooks;
45
46
	/**
47
	 * @var ElggSession
48
	 */
49
	protected $session;
50
51
	/**
52
	 * @var EntityTable
53
	 */
54
	protected $entities;
55
56
	/**
57
	 * @var Translator
58
	 */
59
	protected $translator;
60
61
	/**
62
	 * @var string
63
	 */
64
	protected $table;
65
66
	/**
67
	 * @var string
68
	 */
69
	protected $membership_table;
70
71
	/**
72
	 * @var bool
73
	 */
74
	protected $init_complete = false;
75
76
	/**
77
	 * Constructor
78
	 *
79
	 * @param Conf                    $config     Config
80
	 * @param Database                $db         Database
81
	 * @param EntityTable             $entities   Entity table
82 196
	 * @param ElggStaticVariableCache $cache      Access cache
83
	 * @param PluginHooksService      $hooks      Hooks
84
	 * @param ElggSession             $session    Session
85
	 * @param Translator              $translator Translator
86
	 */
87
	public function __construct(
88
			Conf $config,
89
			Database $db,
90 196
			EntityTable $entities,
91 196
			ElggStaticVariableCache $cache,
92 196
			PluginHooksService $hooks,
93 196
			ElggSession $session,
94 196
			Translator $translator) {
95 196
		$this->config = $config;
96 196
		$this->db = $db;
97
		$this->entities = $entities;
98 196
		$this->access_cache = $cache;
99 196
		$this->hooks = $hooks;
100 196
		$this->session = $session;
101
		$this->translator = $translator;
102
103
		$this->table = "{$this->db->prefix}access_collections";
104
		$this->membership_table = "{$this->db->prefix}access_collection_membership";
105
	}
106
107
	/**
108
	 * Mark the access system as initialized
109
	 *
110
	 * @return void
111
	 */
112
	public function markInitComplete() {
113
		$this->init_complete = true;
114 261
	}
115 261
116 261
	/**
117 261
	 * Returns a string of access_ids for $user_guid appropriate for inserting into an SQL IN clause.
118
	 *
119
	 * @see get_access_array()
120 261
	 *
121 261
	 * @param int  $user_guid User ID; defaults to currently logged in user
122
	 * @param bool $flush     If set to true, will refresh the access list from the
123 261
	 *                        database rather than using this function's cache.
124
	 *
125
	 * @return string A concatenated string of access collections suitable for using in an SQL IN clause
126
	 * @access private
127
	 */
128
	public function getAccessList($user_guid = 0, $flush = false) {
129
		$access_array = $this->getAccessArray($user_guid, $flush);
130
		$access_ids = implode(',', $access_array);
131
		$list = "($access_ids)";
132
133
		// for BC, populate the cache
134
		$hash = $user_guid . 'get_access_list';
135
		$this->access_cache->add($hash, $list);
136
137
		return $list;
138
	}
139
140
	/**
141
	 * Returns an array of access IDs a user is permitted to see.
142
	 *
143
	 * Can be overridden with the 'access:collections:read', 'user' plugin hook.
144
	 * @warning A callback for that plugin hook needs to either not retrieve data
145
	 * from the database that would use the access system (triggering the plugin again)
146
	 * or ignore the second call. Otherwise, an infinite loop will be created.
147
	 *
148
	 * This returns a list of all the collection ids a user owns or belongs
149
	 * to plus public and logged in access levels. If the user is an admin, it includes
150 261
	 * the private access level.
151 261
	 *
152
	 * @internal this is only used in core for creating the SQL where clause when
153 261
	 * retrieving content from the database. The friends access level is handled by
154
	 * _elgg_get_access_where_sql().
155 261
	 *
156
	 * @see get_write_access_array() for the access levels that a user can write to.
157
	 *
158
	 * @param int  $user_guid User ID; defaults to currently logged in user
159 261
	 * @param bool $flush     If set to true, will refresh the access ids from the
160 261
	 *                        database rather than using this function's cache.
161
	 *
162
	 * @return array An array of access collections ids
163 261
	 */
164
	public function getAccessArray($user_guid = 0, $flush = false) {
165 261
		$cache = $this->access_cache;
166
167 261
		if ($flush) {
168
			$cache->clear();
169
		}
170
171 261
		if ($user_guid == 0) {
172
			$user_guid = $this->session->getLoggedInUserGuid();
173
		}
174 261
175 261
		$user_guid = (int) $user_guid;
176
177
		$hash = $user_guid . 'get_access_array';
178
179
		if ($cache[$hash]) {
180 261
			$access_array = $cache[$hash];
181
		} else {
182
			// Public access is always visible
183 261
			$access_array = [ACCESS_PUBLIC];
184
185
			// The following can only return sensible data for a known user.
186
			if ($user_guid) {
187
				$access_array[] = ACCESS_LOGGED_IN;
188 261
189 261
				// Get ACLs that user owns or is a member of
190
				$query = "
191
					SELECT ac.id
192 261
					FROM {$this->table} ac
193
					WHERE ac.owner_guid = :user_guid
194
					OR EXISTS (SELECT 1
195
							   FROM {$this->membership_table}
196
							   WHERE access_collection_id = ac.id
197
							   AND user_guid = :user_guid)
198 261
				";
199
200 261
				$collections = $this->db->getData($query, null, [
201 17
					':user_guid' => $user_guid,
202
				]);
203
204
				if ($collections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $collections of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
205 261
					foreach ($collections as $collection) {
206
						$access_array[] = (int) $collection->id;
207
					}
208
				}
209
210
				$ignore_access = elgg_check_access_overrides($user_guid);
211 261
212
				if ($ignore_access == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
213
					$access_array[] = ACCESS_PRIVATE;
214
				}
215 261
			}
216
217
			if ($this->init_complete) {
218
				$cache[$hash] = $access_array;
219
			}
220
		}
221
222
		$options = [
223
			'user_id' => $user_guid,
224
		];
225
226
		// see the warning in the docs for this function about infinite loop potential
227
		return $this->hooks->trigger('access:collections:read', 'user', $options, $access_array);
228
	}
229
230
	/**
231
	 * Returns the SQL where clause for enforcing read access to data.
232
	 *
233
	 * Note that if this code is executed in privileged mode it will return (1=1).
234
	 *
235
	 * Otherwise it returns a where clause to retrieve the data that a user has
236
	 * permission to read.
237
	 *
238
	 * Plugin authors can hook into the 'get_sql', 'access' plugin hook to modify,
239
	 * remove, or add to the where clauses. The plugin hook will pass an array with the current
240
	 * ors and ands to the function in the form:
241
	 *  array(
242
	 *      'ors' => array(),
243
	 *      'ands' => array()
244
	 *  )
245
	 *
246
	 * The results will be combined into an SQL where clause in the form:
247
	 *  ((or1 OR or2 OR orN) AND (and1 AND and2 AND andN))
248
	 *
249
	 * @param array $options Array in format:
250
	 *
251
	 * 	table_alias => STR Optional table alias. This is based on the select and join clauses.
252
	 *                     Default is 'e'.
253
	 *
254
	 *  user_guid => INT Optional GUID for the user that we are retrieving data for.
255
	 *                   Defaults to the logged in user if null.
256
	 *                   Passing 0 will build a query for a logged out user (even if there is a logged in user)
257
	 *
258 261
	 *  use_enabled_clause => BOOL Optional. Should we append the enabled clause? The default
259
	 *                             is set by access_show_hidden_entities().
260
	 *
261 261
	 *  access_column => STR Optional access column name. Default is 'access_id'.
262 261
	 *
263 261
	 *  owner_guid_column => STR Optional owner_guid column. Default is 'owner_guid'.
264 261
	 *
265 261
	 *  guid_column => STR Optional guid_column. Default is 'guid'.
266 261
	 *
267
	 * @return string
268
	 * @access private
269 261
	 */
270 261
	public function getWhereSql(array $options = []) {
271
272 261
		$defaults = [
273
			'table_alias' => 'e',
274
			'user_guid' => $this->session->getLoggedInUserGuid(),
275
			'use_enabled_clause' => !access_get_show_hidden_status(),
276 261
			'access_column' => 'access_id',
277
			'owner_guid_column' => 'owner_guid',
278
			'guid_column' => 'guid',
279 261
		];
280
281 261
		foreach ($options as $key => $value) {
282 261
			if (is_null($value)) {
283
				// remove null values so we don't loose defaults in array_merge
284 261
				unset($options[$key]);
285
			}
286
		}
287 261
288
		$options = array_merge($defaults, $options);
289 261
290 110
		// just in case someone passes a . at the end
291
		$options['table_alias'] = rtrim($options['table_alias'], '.');
292
293
		foreach (['table_alias', 'access_column', 'owner_guid_column', 'guid_column'] as $key) {
294 261
			$options[$key] = sanitize_string($options[$key]);
295
		}
296
		$options['user_guid'] = sanitize_int($options['user_guid'], false);
297
298 261
		// only add dot if we have an alias or table name
299
		$table_alias = $options['table_alias'] ? $options['table_alias'] . '.' : '';
300 261
301 261
		if (!isset($options['ignore_access'])) {
302 261
			$options['ignore_access'] = elgg_check_access_overrides($options['user_guid']);
303
		}
304 261
305 261
		$clauses = [
306 261
			'ors' => [],
307 261
			'ands' => []
308
		];
309
310
		$prefix = $this->db->prefix;
311 261
312
		if ($options['ignore_access']) {
313
			$clauses['ors']['ignore_access'] = '1 = 1';
314
		} else if ($options['user_guid']) {
315 261
			// include content of user's friends
316 261
			$clauses['ors']['friends_access'] = "$table_alias{$options['access_column']} = " . ACCESS_FRIENDS . "
317 261
				AND $table_alias{$options['owner_guid_column']} IN (
318
					SELECT guid_one FROM {$prefix}entity_relationships
319
					WHERE relationship = 'friend' AND guid_two = {$options['user_guid']}
320 261
				)";
321 261
322
			// include user's content
323
			$clauses['ors']['owner_access'] = "$table_alias{$options['owner_guid_column']} = {$options['user_guid']}";
324 261
		}
325
326 261
		// include standard accesses (public, logged in, access collections)
327 261
		if (!$options['ignore_access']) {
328 261
			$access_list = $this->getAccessList($options['user_guid']);
329
			$clauses['ors']['acl_access'] = "$table_alias{$options['access_column']} IN {$access_list}";
330
		}
331 261
332 261
		if ($options['use_enabled_clause']) {
333 261
			$clauses['ands']['use_enabled'] = "{$table_alias}enabled = 'yes'";
334
		}
335 261
336
		$clauses = $this->hooks->trigger('get_sql', 'access', $options, $clauses);
337
338 261
		$clauses_str = '';
339
		if (is_array($clauses['ors']) && $clauses['ors']) {
340
			$clauses_str = '(' . implode(' OR ', $clauses['ors']) . ')';
341
		}
342
343
		if (is_array($clauses['ands']) && $clauses['ands']) {
344
			if ($clauses_str) {
345
				$clauses_str .= ' AND ';
346
			}
347
			$clauses_str .= '(' . implode(' AND ', $clauses['ands']) . ')';
348
		}
349
350
		return "($clauses_str)";
351
	}
352
353
	/**
354
	 * Can a user access an entity.
355
	 *
356
	 * @warning If a logged in user doesn't have access to an entity, the
357
	 * core engine will not load that entity.
358
	 *
359
	 * @tip This is mostly useful for checking if a user other than the logged in
360 9
	 * user has access to an entity that is currently loaded.
361 9
	 *
362
	 * @todo This function would be much more useful if we could pass the guid of the
363
	 * entity to test access for. We need to be able to tell whether the entity exists
364
	 * and whether the user has access to the entity.
365 9
	 *
366
	 * @param ElggEntity $entity The entity to check access for.
367 3
	 * @param ElggUser   $user   Optionally user to check access for. Defaults to
368
	 *                           logged in user (which is a useless default).
369
	 *
370 6
	 * @return bool
371
	 */
372 6
	public function hasAccessToEntity($entity, $user = null) {
373
		if (!$entity instanceof \ElggEntity) {
374
			return false;
375
		}
376
377 6
		if ($entity->access_id == ACCESS_PUBLIC) {
378
			// Public entities are always accessible
379 6
			return true;
380
		}
381
382
		$user_guid = isset($user) ? (int) $user->guid : elgg_get_logged_in_user_guid();
383
384
		if ($user_guid && $user_guid == $entity->owner_guid) {
385
			// Owners have access to their own content
386
			return true;
387
		}
388
389
		if ($user_guid && $entity->access_id == ACCESS_LOGGED_IN) {
390
			// Existing users have access to entities with logged in access
391
			return true;
392
		}
393
394
		// See #7159. Must not allow ignore access to affect query
395
		$ia = elgg_set_ignore_access(false);
396
		
397
		$row = $this->entities->getRow($entity->guid, $user_guid);
398
399
		elgg_set_ignore_access($ia);
400
401
		return !empty($row);
402
	}
403
404
	/**
405
	 * Returns an array of access permissions that the user is allowed to save content with.
406
	 * Permissions returned are of the form (id => 'name').
407
	 *
408
	 * Example return value in English:
409
	 * array(
410
	 *     0 => 'Private',
411
	 *    -2 => 'Friends',
412
	 *     1 => 'Logged in users',
413
	 *     2 => 'Public',
414
	 *    34 => 'My favorite friends',
415
	 * );
416
	 *
417
	 * Plugin hook of 'access:collections:write', 'user'
418
	 *
419
	 * @warning this only returns access collections that the user owns plus the
420
	 * standard access levels. It does not return access collections that the user
421
	 * belongs to such as the access collection for a group.
422
	 *
423
	 * @param int   $user_guid    The user's GUID.
424
	 * @param bool  $flush        If this is set to true, this will ignore a cached access array
425
	 * @param array $input_params Some parameters passed into an input/access view
426
	 *
427
	 * @return array List of access permissions
428
	 */
429
	public function getWriteAccessArray($user_guid = 0, $flush = false, array $input_params = []) {
430
		$cache = $this->access_cache;
431
432
		if ($flush) {
433
			$cache->clear();
434
		}
435
436
		if ($user_guid == 0) {
437
			$user_guid = $this->session->getLoggedInUserGuid();
438
		}
439
440
		$user_guid = (int) $user_guid;
441
442
		$hash = $user_guid . 'get_write_access_array';
443
444
		if ($cache[$hash]) {
445
			$access_array = $cache[$hash];
446
		} else {
447
			// @todo is there such a thing as public write access?
448
			$access_array = [
449
				ACCESS_PRIVATE => $this->getReadableAccessLevel(ACCESS_PRIVATE),
450
				ACCESS_LOGGED_IN => $this->getReadableAccessLevel(ACCESS_LOGGED_IN),
451
				ACCESS_PUBLIC => $this->getReadableAccessLevel(ACCESS_PUBLIC)
452
			];
453
454
			$collections = $this->getEntityCollections($user_guid);
455
			if ($collections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $collections of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
456
				foreach ($collections as $collection) {
457
					$access_array[$collection->id] = $collection->name;
458
				}
459
			}
460
461
			if ($this->init_complete) {
462
				$cache[$hash] = $access_array;
463
			}
464
		}
465
466
		$options = [
467
			'user_id' => $user_guid,
468
			'input_params' => $input_params,
469
		];
470
		return $this->hooks->trigger('access:collections:write', 'user', $options, $access_array);
471
	}
472
473
	/**
474
	 * Can the user change this access collection?
475
	 *
476
	 * Use the plugin hook of 'access:collections:write', 'user' to change this.
477
	 * @see get_write_access_array() for details on the hook.
478
	 *
479
	 * Respects access control disabling for admin users and {@link elgg_set_ignore_access()}
480
	 *
481
	 * @see get_write_access_array()
482
	 *
483
	 * @param int   $collection_id The collection id
484
	 * @param mixed $user_guid     The user GUID to check for. Defaults to logged in user.
485
	 * @return bool
486
	 */
487
	public function canEdit($collection_id, $user_guid = null) {
488
		try {
489
			$user = $this->entities->getUserForPermissionsCheck($user_guid);
490
		} catch (UserFetchFailureException $e) {
491
			return false;
492
		}
493
494
		$collection = $this->get($collection_id);
495
496
		if (!$user || !$collection) {
497
			return false;
498
		}
499
500
		if (elgg_check_access_overrides($user->guid)) {
501
			return true;
502
		}
503
504
		$write_access = $this->getWriteAccessArray($user->guid, true);
505
		return array_key_exists($collection_id, $write_access);
506
	}
507
508
	/**
509
	 * Creates a new access collection.
510
	 *
511
	 * Access colletions allow plugins and users to create granular access
512
	 * for entities.
513
	 *
514
	 * Triggers plugin hook 'access:collections:addcollection', 'collection'
515
	 *
516
	 * @internal Access collections are stored in the access_collections table.
517
	 * Memberships to collections are in access_collections_membership.
518
	 *
519
	 * @param string $name       The name of the collection.
520
	 * @param int    $owner_guid The GUID of the owner (default: currently logged in user).
521
	 *
522
	 * @return int|false The collection ID if successful and false on failure.
523
	 */
524
	public function create($name, $owner_guid = 0) {
525
		$name = trim($name);
526
		if (empty($name)) {
527
			return false;
528
		}
529
530
		if ($owner_guid == 0) {
531
			$owner_guid = $this->session->getLoggedInUserGuid();
532
		}
533
534
		$query = "
535
			INSERT INTO {$this->table}
536
			SET name = :name,
537
				owner_guid = :owner_guid
538
		";
539
540
		$params = [
541
			':name' => $name,
542
			':owner_guid' => (int) $owner_guid,
543
		];
544
545
		$id = $this->db->insertData($query, $params);
546
		if (!$id) {
547
			return false;
548
		}
549
550
		$this->access_cache->clear();
551
552
		$hook_params = [
553
			'collection_id' => $id,
554
			'name' => $name,
555
			'owner_guid' => $owner_guid,
556
		];
557
558
		if (!$this->hooks->trigger('access:collections:addcollection', 'collection', $hook_params, true)) {
559
			$this->delete($id);
560
			return false;
561
		}
562
563
		return $id;
564
	}
565
566
	/**
567
	 * Renames an access collection
568
	 *
569
	 * @param int    $collection_id ID of the collection
570
	 * @param string $name          The name of the collection
571
	 * @return bool
572
	 */
573
	public function rename($collection_id, $name) {
574
575
		$query = "
576
			UPDATE {$this->table}
577
			SET name = :name
578
			WHERE id = :id
579
		";
580
581
		$params = [
582
			':name' => $name,
583
			':id' => (int) $collection_id,
584
		];
585
586
		if ($this->db->insertData($query, $params)) {
587
			$this->access_cache->clear();
588
			return (int) $collection_id;
589
		}
590
591
		return false;
592
	}
593
594
595
	/**
596
	 * Updates the membership in an access collection.
597
	 *
598
	 * @warning Expects a full list of all members that should
599
	 * be part of the access collection
600
	 *
601
	 * @note This will run all hooks associated with adding or removing
602
	 * members to access collections.
603
	 *
604
	 * @param int   $collection_id ID of the collection.
605
	 * @param array $new_members   Array of member entities or GUIDs
606
	 * @return bool
607
	 */
608
	public function update($collection_id, array $new_members = []) {
609
		$acl = $this->get($collection_id);
610
611
		if (!$acl) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $acl of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
612
			return false;
613
		}
614
		
615
		$to_guid = function($elem) {
616
			if (empty($elem)) {
617
				return 0;
618
			}
619
			if (is_object($elem)) {
620
				return (int) $elem->guid;
621
			}
622
			return (int) $elem;
623
		};
624
		
625
		$current_members = [];
626
		$new_members = array_map($to_guid, $new_members);
627
628
		$current_members_batch = $this->getMembers($collection_id, [
629
			'batch' => true,
630
			'limit' => 0,
631
			'callback' => false,
632
		]);
633
634
		foreach ($current_members_batch as $row) {
635
			$current_members[] = $to_guid($row);
636
		}
637
638
		$remove_members = array_diff($current_members, $new_members);
639
		$add_members = array_diff($new_members, $current_members);
640
641
		$result = true;
642
643
		foreach ($add_members as $guid) {
644
			$result = $result && $this->addUser($guid, $collection_id);
645
		}
646
647
		foreach ($remove_members as $guid) {
648
			$result = $result && $this->removeUser($guid, $collection_id);
649
		}
650
651
		$this->access_cache->clear();
652
653
		return $result;
654
	}
655
656
	/**
657
	 * Deletes a collection and its membership information
658
	 *
659
	 * @param int $collection_id ID of the collection
660
	 * @return bool
661
	 */
662
	public function delete($collection_id) {
663
		$collection_id = (int) $collection_id;
664
665
		$params = [
666
			'collection_id' => $collection_id,
667
		];
668
669
		if (!$this->hooks->trigger('access:collections:deletecollection', 'collection', $params, true)) {
670
			return false;
671
		}
672
673
		// Deleting membership doesn't affect result of deleting ACL.
674
		$query = "
675
			DELETE FROM {$this->membership_table}
676
			WHERE access_collection_id = :access_collection_id
677
		";
678
		$this->db->deleteData($query, [
679
			':access_collection_id' => $collection_id,
680
		]);
681
682
		$query = "
683
			DELETE FROM {$this->table}
684
			WHERE id = :id
685
		";
686
		$result = $this->db->deleteData($query, [
687
			':id' => $collection_id,
688
		]);
689
690
		$this->access_cache->clear();
691
		
692
		return (bool) $result;
693
	}
694
695
	/**
696
	 * Transforms a database row to an instance of ElggAccessCollection
697
	 *
698
	 * @param \stdClass $row Database row
699
	 * @return \ElggAccessCollection
700
	 */
701
	public function rowToElggAccessCollection(\stdClass $row) {
702
		return new \ElggAccessCollection($row);
703
	}
704
705
	/**
706
	 * Get a specified access collection
707
	 *
708
	 * @note This doesn't return the members of an access collection,
709
	 * just the database row of the actual collection.
710
	 *
711
	 * @see get_members_of_access_collection()
712
	 *
713
	 * @param int $collection_id The collection ID
714
	 * @return \ElggAccessCollection|false
715
	 */
716
	public function get($collection_id) {
717
718
		$callback = [$this, 'rowToElggAccessCollection'];
719
720
		$query = "
721
			SELECT * FROM {$this->table}
722
			WHERE id = :id
723
		";
724
725
		return $this->db->getDataRow($query, $callback, [
726
			':id' => (int) $collection_id,
727
		]);
728
	}
729
730
	/**
731
	 * Check if user is already in the collection
732
	 *
733
	 * @param int $user_guid     GUID of the user
734
	 * @param int $collection_id ID of the collection
735
	 * @return bool
736
	 */
737
	public function hasUser($user_guid, $collection_id) {
738
		$options = [
739
			'guids' => (int) $user_guid,
740
			'count' => true,
741
		];
742
		return (bool) $this->getMembers($collection_id, $options);
743
	}
744
745
	/**
746
	 * Adds a user to an access collection.
747
	 *
748
	 * Triggers the 'access:collections:add_user', 'collection' plugin hook.
749
	 *
750
	 * @param int $user_guid     GUID of the user to add
751
	 * @param int $collection_id ID of the collection to add them to
752
	 * @return bool
753
	 */
754
	public function addUser($user_guid, $collection_id) {
755
756
		$collection = $this->get($collection_id);
757
758
		if (!$collection) {
759
			return false;
760
		}
761
762
		if (!$this->entities->exists($user_guid)) {
763
			return false;
764
		}
765
766
		$hook_params = [
767
			'collection_id' => $collection->id,
768
			'user_guid' => (int) $user_guid
769
		];
770
771
		$result = $this->hooks->trigger('access:collections:add_user', 'collection', $hook_params, true);
772
		if ($result == false) {
773
			return false;
774
		}
775
776
		// if someone tries to insert the same data twice, we do a no-op on duplicate key
777
		$query = "
778
			INSERT INTO {$this->membership_table}
779
				SET access_collection_id = :access_collection_id,
780
				    user_guid = :user_guid
781
				ON DUPLICATE KEY UPDATE user_guid = user_guid
782
		";
783
784
		$result = $this->db->insertData($query, [
785
			':access_collection_id' => (int) $collection->id,
786
			':user_guid' => (int) $user_guid,
787
		]);
788
789
		$this->access_cache->clear();
790
		
791
		return $result !== false;
792
	}
793
794
	/**
795
	 * Removes a user from an access collection.
796
	 *
797
	 * Triggers the 'access:collections:remove_user', 'collection' plugin hook.
798
	 *
799
	 * @param int $user_guid     GUID of the user
800
	 * @param int $collection_id ID of the collection
801
	 * @return bool
802
	 */
803
	public function removeUser($user_guid, $collection_id) {
804
805
		$params = [
806
			'collection_id' => (int) $collection_id,
807
			'user_guid' => (int) $user_guid,
808
		];
809
810
		if (!$this->hooks->trigger('access:collections:remove_user', 'collection', $params, true)) {
811
			return false;
812
		}
813
814
		$query = "
815
			DELETE FROM {$this->membership_table}
816
			WHERE access_collection_id = :access_collection_id
817
				AND user_guid = :user_guid
818
		";
819
820
		$this->access_cache->clear();
821
822
		return (bool) $this->db->deleteData($query, [
823 1
			':access_collection_id' => (int) $collection_id,
824
			':user_guid' => (int) $user_guid,
825 1
		]);
826
	}
827
828 1
	/**
829
	 * Returns access collections owned by the user
830
	 *
831
	 * @param int $owner_guid GUID of the owner
832
	 * @return ElggAccessCollection[]|false
833
	 */
834 1 View Code Duplication
	public function getEntityCollections($owner_guid) {
835
836
		$callback = [$this, 'rowToElggAccessCollection'];
837 1
838
		$query = "
839
			SELECT * FROM {$this->table}
840
				WHERE owner_guid = :owner_guid
841
				ORDER BY name ASC
842
		";
843
844
		$params = [
845
			':owner_guid' => (int) $owner_guid,
846
		];
847
848
		return $this->db->getData($query, $callback, $params);
849
	}
850
851
	/**
852
	 * Get members of an access collection
853
	 *
854
	 * @param int   $collection_id The collection's ID
855
	 * @param array $options       Ege* options
856
	 * @return ElggEntity[]|false
857
	 */
858
	public function getMembers($collection_id, array $options = []) {
859
860
		$options['joins'][] = "JOIN {$this->membership_table} acm";
861
862
		$collection_id = (int) $collection_id;
863
		$options['wheres'][] = "e.guid = acm.user_guid AND acm.access_collection_id = {$collection_id}";
864
865
		return $this->entities->getEntities($options);
866
	}
867
868
	/**
869
	 * Return an array of collections that the entity is member of
870
	 *
871
	 * @param int $member_guid GUID of th member
872
	 *
873
	 * @return ElggAccessCollection[]|false
874
	 */
875 View Code Duplication
	public function getCollectionsByMember($member_guid) {
876
877
		$callback = [$this, 'rowToElggAccessCollection'];
878
879
		$query = "
880
			SELECT ac.* FROM {$this->table} ac
881
				JOIN {$this->membership_table} acm
882
					ON ac.id = acm.access_collection_id
883
				WHERE acm.user_guid = :member_guid
884
				ORDER BY name ASC
885
		";
886
887
		return $this->db->getData($query, $callback, [
888
			':member_guid' => (int) $member_guid,
889
		]);
890
	}
891
892
	/**
893
	 * Return the name of an ACCESS_* constant or an access collection,
894
	 * but only if the logged in user owns the access collection or is an admin.
895
	 * Ownership requirement prevents us from exposing names of access collections
896
	 * that current user has been added to by other members and may contain
897
	 * sensitive classification of the current user (e.g. close friends vs acquaintances).
898
	 *
899
	 * Returns a string in the language of the user for global access levels, e.g.'Public, 'Friends', 'Logged in', 'Private';
900
	 * or a name of the owned access collection, e.g. 'My work colleagues';
901
	 * or a name of the group or other access collection, e.g. 'Group: Elgg technical support';
902
	 * or 'Limited' if the user access is restricted to read-only, e.g. a friends collection the user was added to
903
	 *
904
	 * @param int $entity_access_id The entity's access id
905
	 *
906
	 * @return string
907
	 * @since 1.11
908
	 */
909
	public function getReadableAccessLevel($entity_access_id) {
910
		$access = (int) $entity_access_id;
911
912
		$translator = $this->translator;
913
914
		// Check if entity access id is a defined global constant
915
		$access_array = [
916
			ACCESS_PRIVATE => $translator->translate("PRIVATE"),
917
			ACCESS_FRIENDS => $translator->translate("access:friends:label"),
918
			ACCESS_LOGGED_IN => $translator->translate("LOGGED_IN"),
919
			ACCESS_PUBLIC => $translator->translate("PUBLIC"),
920
		];
921
922
		if (array_key_exists($access, $access_array)) {
923
			return $access_array[$access];
924
		}
925
926
		// Entity access id is probably a custom access collection
927
		// Check if the user has write access to it and can see it's label
928
		// Admins should always be able to see the readable version
929
		$collection = $this->get($access);
930
931
		$user_guid = $this->session->getLoggedInUserGuid();
932
		
933
		if (!$collection || !$user_guid) {
934
			// return 'Limited' if there is no logged in user or collection can not be loaded
935
			return $translator->translate('access:limited:label');
936
		}
937
938
		return $collection->getDisplayName();
939
	}
940
941
}
942