Completed
Push — master ( 0030e4...e4893b )
by Jeroen
13:26
created

AccessCollections::getMembers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 2
dl 0
loc 9
ccs 0
cts 5
cp 0
crap 2
rs 9.6666
c 0
b 0
f 0
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
	 * Constructor
73
	 *
74
	 * @param Config                  $config     Config
75
	 * @param Database                $db         Database
76
	 * @param EntityTable             $entities   Entity table
77
	 * @param ElggStaticVariableCache $cache      Access cache
78
	 * @param PluginHooksService      $hooks      Hooks
79
	 * @param ElggSession             $session    Session
80
	 * @param Translator              $translator Translator
81
	 */
82 186
	public function __construct(
83
			Conf $config,
84
			Database $db,
85
			EntityTable $entities,
86
			ElggStaticVariableCache $cache,
87
			PluginHooksService $hooks,
88
			ElggSession $session,
89
			Translator $translator) {
90 186
		$this->config = $config;
91 186
		$this->db = $db;
92 186
		$this->entities = $entities;
93 186
		$this->access_cache = $cache;
94 186
		$this->hooks = $hooks;
95 186
		$this->session = $session;
96 186
		$this->translator = $translator;
97
98 186
		$this->table = "{$this->db->prefix}access_collections";
99 186
		$this->membership_table = "{$this->db->prefix}access_collection_membership";
100 186
	}
101
102
	/**
103
	 * Returns a string of access_ids for $user_guid appropriate for inserting into an SQL IN clause.
104
	 *
105
	 * @see get_access_array()
106
	 *
107
	 * @param int  $user_guid User ID; defaults to currently logged in user
108
	 * @param bool $flush     If set to true, will refresh the access list from the
109
	 *                        database rather than using this function's cache.
110
	 *
111
	 * @return string A concatenated string of access collections suitable for using in an SQL IN clause
112
	 * @access private
113
	 */
114 239
	public function getAccessList($user_guid = 0, $flush = false) {
115 239
		$access_array = $this->getAccessArray($user_guid, $flush);
116 239
		$access_ids = implode(',', $access_array);
117 239
		$list = "($access_ids)";
118
119
		// for BC, populate the cache
120 239
		$hash = $user_guid . 'get_access_list';
121 239
		$this->access_cache->add($hash, $list);
122
123 239
		return $list;
124
	}
125
126
	/**
127
	 * Returns an array of access IDs a user is permitted to see.
128
	 *
129
	 * Can be overridden with the 'access:collections:read', 'user' plugin hook.
130
	 * @warning A callback for that plugin hook needs to either not retrieve data
131
	 * from the database that would use the access system (triggering the plugin again)
132
	 * or ignore the second call. Otherwise, an infinite loop will be created.
133
	 *
134
	 * This returns a list of all the collection ids a user owns or belongs
135
	 * to plus public and logged in access levels. If the user is an admin, it includes
136
	 * the private access level.
137
	 *
138
	 * @internal this is only used in core for creating the SQL where clause when
139
	 * retrieving content from the database. The friends access level is handled by
140
	 * _elgg_get_access_where_sql().
141
	 *
142
	 * @see get_write_access_array() for the access levels that a user can write to.
143
	 *
144
	 * @param int  $user_guid User ID; defaults to currently logged in user
145
	 * @param bool $flush     If set to true, will refresh the access ids from the
146
	 *                        database rather than using this function's cache.
147
	 *
148
	 * @return array An array of access collections ids
149
	 */
150 239
	public function getAccessArray($user_guid = 0, $flush = false) {
151 239
		global $init_finished;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
152
153 239
		$cache = $this->access_cache;
154
155 239
		if ($flush) {
156
			$cache->clear();
157
		}
158
159 239
		if ($user_guid == 0) {
160 239
			$user_guid = $this->session->getLoggedInUserGuid();
161 239
		}
162
163 239
		$user_guid = (int) $user_guid;
164
165 239
		$hash = $user_guid . 'get_access_array';
166
167 239
		if ($cache[$hash]) {
168
			$access_array = $cache[$hash];
169
		} else {
170
			// Public access is always visible
171 239
			$access_array = [ACCESS_PUBLIC];
172
173
			// The following can only return sensible data for a known user.
174 239
			if ($user_guid) {
175 239
				$access_array[] = ACCESS_LOGGED_IN;
176
177
				// Get ACLs that user owns or is a member of
178
				$query = "
179
					SELECT ac.id
180 239
					FROM {$this->table} ac
181
					WHERE ac.owner_guid = :user_guid
182
					OR EXISTS (SELECT 1
183 239
							   FROM {$this->membership_table}
184
							   WHERE access_collection_id = ac.id
185
							   AND user_guid = :user_guid)
186 239
				";
187
188 239
				$collections = $this->db->getData($query, null, [
189 239
					':user_guid' => $user_guid,
190 239
				]);
191
192 239
				if ($collections) {
193
					foreach ($collections as $collection) {
194
						$access_array[] = (int) $collection->id;
195
					}
196
				}
197
198 239
				$ignore_access = elgg_check_access_overrides($user_guid);
199
200 239
				if ($ignore_access == true) {
201 17
					$access_array[] = ACCESS_PRIVATE;
202 17
				}
203 239
			}
204
205 239
			if ($init_finished) {
206
				$cache[$hash] = $access_array;
207
			}
208
		}
209
210
		$options = array(
211 239
			'user_id' => $user_guid,
212 239
		);
213
214
		// see the warning in the docs for this function about infinite loop potential
215 239
		return $this->hooks->trigger('access:collections:read', 'user', $options, $access_array);
216
	}
217
218
	/**
219
	 * Returns the SQL where clause for enforcing read access to data.
220
	 *
221
	 * Note that if this code is executed in privileged mode it will return (1=1).
222
	 *
223
	 * Otherwise it returns a where clause to retrieve the data that a user has
224
	 * permission to read.
225
	 *
226
	 * Plugin authors can hook into the 'get_sql', 'access' plugin hook to modify,
227
	 * remove, or add to the where clauses. The plugin hook will pass an array with the current
228
	 * ors and ands to the function in the form:
229
	 *  array(
230
	 *      'ors' => array(),
231
	 *      'ands' => array()
232
	 *  )
233
	 *
234
	 * The results will be combined into an SQL where clause in the form:
235
	 *  ((or1 OR or2 OR orN) AND (and1 AND and2 AND andN))
236
	 *
237
	 * @param array $options Array in format:
238
	 *
239
	 * 	table_alias => STR Optional table alias. This is based on the select and join clauses.
240
	 *                     Default is 'e'.
241
	 *
242
	 *  user_guid => INT Optional GUID for the user that we are retrieving data for.
243
	 *                   Defaults to the logged in user if null.
244
	 *                   Passing 0 will build a query for a logged out user (even if there is a logged in user)
245
	 *
246
	 *  use_enabled_clause => BOOL Optional. Should we append the enabled clause? The default
247
	 *                             is set by access_show_hidden_entities().
248
	 *
249
	 *  access_column => STR Optional access column name. Default is 'access_id'.
250
	 *
251
	 *  owner_guid_column => STR Optional owner_guid column. Default is 'owner_guid'.
252
	 *
253
	 *  guid_column => STR Optional guid_column. Default is 'guid'.
254
	 *
255
	 * @return string
256
	 * @access private
257
	 */
258 239
	public function getWhereSql(array $options = array()) {
259
260
		$defaults = array(
261 239
			'table_alias' => 'e',
262 239
			'user_guid' => $this->session->getLoggedInUserGuid(),
263 239
			'use_enabled_clause' => !access_get_show_hidden_status(),
264 239
			'access_column' => 'access_id',
265 239
			'owner_guid_column' => 'owner_guid',
266 239
			'guid_column' => 'guid',
267 239
		);
268
269 239
		foreach ($options as $key => $value) {
270 239
			if (is_null($value)) {
271
				// remove null values so we don't loose defaults in array_merge
272 56
				unset($options[$key]);
273 56
			}
274 239
		}
275
276 239
		$options = array_merge($defaults, $options);
277
278
		// just in case someone passes a . at the end
279 239
		$options['table_alias'] = rtrim($options['table_alias'], '.');
280
281 239
		foreach (array('table_alias', 'access_column', 'owner_guid_column', 'guid_column') as $key) {
282 239
			$options[$key] = sanitize_string($options[$key]);
0 ignored issues
show
Deprecated Code introduced by
The function sanitize_string() has been deprecated with message: Use query parameters where possible

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
283 239
		}
284 239
		$options['user_guid'] = sanitize_int($options['user_guid'], false);
0 ignored issues
show
Deprecated Code introduced by
The function sanitize_int() has been deprecated with message: Use query parameters where possible

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
285
286
		// only add dot if we have an alias or table name
287 239
		$table_alias = $options['table_alias'] ? $options['table_alias'] . '.' : '';
288
289 239
		if (!isset($options['ignore_access'])) {
290 155
			$options['ignore_access'] = elgg_check_access_overrides($options['user_guid']);
291 155
		}
292
293
		$clauses = array(
294 239
			'ors' => array(),
295 239
			'ands' => array()
296 239
		);
297
298 239
		$prefix = $this->db->prefix;
299
300 239
		if ($options['ignore_access']) {
301 239
			$clauses['ors']['ignore_access'] = '1 = 1';
302 239
		} else if ($options['user_guid']) {
303
			// include content of user's friends
304 239
			$clauses['ors']['friends_access'] = "$table_alias{$options['access_column']} = " . ACCESS_FRIENDS . "
305 239
				AND $table_alias{$options['owner_guid_column']} IN (
306 239
					SELECT guid_one FROM {$prefix}entity_relationships
307 239
					WHERE relationship = 'friend' AND guid_two = {$options['user_guid']}
308 239
				)";
309
310
			// include user's content
311 239
			$clauses['ors']['owner_access'] = "$table_alias{$options['owner_guid_column']} = {$options['user_guid']}";
312 239
		}
313
314
		// include standard accesses (public, logged in, access collections)
315 239
		if (!$options['ignore_access']) {
316 239
			$access_list = $this->getAccessList($options['user_guid']);
317 239
			$clauses['ors']['acl_access'] = "$table_alias{$options['access_column']} IN {$access_list}";
318 239
		}
319
320 239
		if ($options['use_enabled_clause']) {
321 239
			$clauses['ands']['use_enabled'] = "{$table_alias}enabled = 'yes'";
322 239
		}
323
324 239
		$clauses = $this->hooks->trigger('get_sql', 'access', $options, $clauses);
325
326 239
		$clauses_str = '';
327 239
		if (is_array($clauses['ors']) && $clauses['ors']) {
328 239
			$clauses_str = '(' . implode(' OR ', $clauses['ors']) . ')';
329 239
		}
330
331 239
		if (is_array($clauses['ands']) && $clauses['ands']) {
332 239
			if ($clauses_str) {
333 239
				$clauses_str .= ' AND ';
334 239
			}
335 239
			$clauses_str .= '(' . implode(' AND ', $clauses['ands']) . ')';
336 239
		}
337
338 239
		return "($clauses_str)";
339
	}
340
341
	/**
342
	 * Can a user access an entity.
343
	 *
344
	 * @warning If a logged in user doesn't have access to an entity, the
345
	 * core engine will not load that entity.
346
	 *
347
	 * @tip This is mostly useful for checking if a user other than the logged in
348
	 * user has access to an entity that is currently loaded.
349
	 *
350
	 * @todo This function would be much more useful if we could pass the guid of the
351
	 * entity to test access for. We need to be able to tell whether the entity exists
352
	 * and whether the user has access to the entity.
353
	 *
354
	 * @param ElggEntity $entity The entity to check access for.
355
	 * @param ElggUser   $user   Optionally user to check access for. Defaults to
356
	 *                           logged in user (which is a useless default).
357
	 *
358
	 * @return bool
359
	 */
360 9
	public function hasAccessToEntity($entity, $user = null) {
361 9
		if (!$entity instanceof \ElggEntity) {
362
			return false;
363
		}
364
365 9
		if ($entity->access_id == ACCESS_PUBLIC) {
366
			// Public entities are always accessible
367 3
			return true;
368
		}
369
370
		// See #7159. Must not allow ignore access to affect query
371 6
		$ia = elgg_set_ignore_access(false);
372
373 6
		$user_guid = isset($user) ? (int) $user->guid : elgg_get_logged_in_user_guid();
374
375 6
		if ($user_guid && $user_guid == $entity->owner_guid) {
376
			// Owners have access to their own content
377
			return true;
378
		}
379
380 6
		if ($user_guid && $entity->access_id == ACCESS_LOGGED_IN) {
381
			// Existing users have access to entities with logged in access
382 6
			return true;
383
		}
384
385
		$row = $this->entities->getRow($entity->guid, $user_guid);
386
387
		elgg_set_ignore_access($ia);
388
389
		return !empty($row);
390
	}
391
392
	/**
393
	 * Returns an array of access permissions that the user is allowed to save content with.
394
	 * Permissions returned are of the form (id => 'name').
395
	 *
396
	 * Example return value in English:
397
	 * array(
398
	 *     0 => 'Private',
399
	 *    -2 => 'Friends',
400
	 *     1 => 'Logged in users',
401
	 *     2 => 'Public',
402
	 *    34 => 'My favorite friends',
403
	 * );
404
	 *
405
	 * Plugin hook of 'access:collections:write', 'user'
406
	 *
407
	 * @warning this only returns access collections that the user owns plus the
408
	 * standard access levels. It does not return access collections that the user
409
	 * belongs to such as the access collection for a group.
410
	 *
411
	 * @param int   $user_guid    The user's GUID.
412
	 * @param bool  $flush        If this is set to true, this will ignore a cached access array
413
	 * @param array $input_params Some parameters passed into an input/access view
414
	 *
415
	 * @return array List of access permissions
416
	 */
417
	public function getWriteAccessArray($user_guid = 0, $flush = false, array $input_params = array()) {
418
		global $init_finished;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
419
		$cache = $this->access_cache;
420
421
		if ($flush) {
422
			$cache->clear();
423
		}
424
425
		if ($user_guid == 0) {
426
			$user_guid = $this->session->getLoggedInUserGuid();
427
		}
428
429
		$user_guid = (int) $user_guid;
430
431
		$hash = $user_guid . 'get_write_access_array';
432
433
		if ($cache[$hash]) {
434
			$access_array = $cache[$hash];
435
		} else {
436
			// @todo is there such a thing as public write access?
437
			$access_array = array(
438
				ACCESS_PRIVATE => $this->getReadableAccessLevel(ACCESS_PRIVATE),
439
				ACCESS_LOGGED_IN => $this->getReadableAccessLevel(ACCESS_LOGGED_IN),
440
				ACCESS_PUBLIC => $this->getReadableAccessLevel(ACCESS_PUBLIC)
441
			);
442
443
			$collections = $this->getEntityCollections($user_guid);
444
			if ($collections) {
445
				foreach ($collections as $collection) {
446
					$access_array[$collection->id] = $collection->name;
447
				}
448
			}
449
450
			if ($init_finished) {
451
				$cache[$hash] = $access_array;
452
			}
453
		}
454
455
		$options = array(
456
			'user_id' => $user_guid,
457
			'input_params' => $input_params,
458
		);
459
		return $this->hooks->trigger('access:collections:write', 'user', $options, $access_array);
460
	}
461
462
	/**
463
	 * Can the user change this access collection?
464
	 *
465
	 * Use the plugin hook of 'access:collections:write', 'user' to change this.
466
	 * @see get_write_access_array() for details on the hook.
467
	 *
468
	 * Respects access control disabling for admin users and {@link elgg_set_ignore_access()}
469
	 *
470
	 * @see get_write_access_array()
471
	 *
472
	 * @param int   $collection_id The collection id
473
	 * @param mixed $user_guid     The user GUID to check for. Defaults to logged in user.
474
	 * @return bool
475
	 */
476
	public function canEdit($collection_id, $user_guid = null) {
477
		try {
478
			$user = $this->entities->getUserForPermissionsCheck($user_guid);
479
		} catch (UserFetchFailureException $e) {
480
			return false;
481
		}
482
483
		$collection = $this->get($collection_id);
484
485
		if (!$user || !$collection) {
486
			return false;
487
		}
488
489
		if (elgg_check_access_overrides($user->guid)) {
490
			return true;
491
		}
492
493
		$write_access = $this->getWriteAccessArray($user->guid, true);
494
		return array_key_exists($collection_id, $write_access);
495
	}
496
497
	/**
498
	 * Creates a new access collection.
499
	 *
500
	 * Access colletions allow plugins and users to create granular access
501
	 * for entities.
502
	 *
503
	 * Triggers plugin hook 'access:collections:addcollection', 'collection'
504
	 *
505
	 * @internal Access collections are stored in the access_collections table.
506
	 * Memberships to collections are in access_collections_membership.
507
	 *
508
	 * @param string $name       The name of the collection.
509
	 * @param int    $owner_guid The GUID of the owner (default: currently logged in user).
510
	 *
511
	 * @return int|false The collection ID if successful and false on failure.
512
	 */
513
	public function create($name, $owner_guid = 0) {
514
		$name = trim($name);
515
		if (empty($name)) {
516
			return false;
517
		}
518
519
		if ($owner_guid == 0) {
520
			$owner_guid = $this->session->getLoggedInUserGuid();
521
		}
522
523
		$query = "
524
			INSERT INTO {$this->table}
525
			SET name = :name,
526
				owner_guid = :owner_guid
527
		";
528
529
		$params = [
530
			':name' => $name,
531
			':owner_guid' => (int) $owner_guid,
532
		];
533
534
		$id = $this->db->insertData($query, $params);
535
		if (!$id) {
536
			return false;
537
		}
538
539
		$this->access_cache->clear();
540
541
		$hook_params = array(
542
			'collection_id' => $id,
543
			'name' => $name,
544
			'owner_guid' => $owner_guid,
545
		);
546
547
		if (!$this->hooks->trigger('access:collections:addcollection', 'collection', $hook_params, true)) {
548
			$this->delete($id);
549
			return false;
550
		}
551
552
		return $id;
553
	}
554
555
	/**
556
	 * Renames an access collection
557
	 *
558
	 * @param int    $collection_id ID of the collection
559
	 * @param string $name          The name of the collection
560
	 * @return bool
561
	 */
562
	public function rename($collection_id, $name) {
563
564
		$query = "
565
			UPDATE {$this->table}
566
			SET name = :name
567
			WHERE id = :id
568
		";
569
570
		$params = [
571
			':name' => $name,
572
			':id' => (int) $collection_id,
573
		];
574
575
		if ($this->db->insertData($query, $params)) {
576
			$this->access_cache->clear();
577
			return (int) $collection_id;
578
		}
579
580
		return false;
581
	}
582
583
584
	/**
585
	 * Updates the membership in an access collection.
586
	 *
587
	 * @warning Expects a full list of all members that should
588
	 * be part of the access collection
589
	 *
590
	 * @note This will run all hooks associated with adding or removing
591
	 * members to access collections.
592
	 *
593
	 * @param int   $collection_id ID of the collection.
594
	 * @param array $new_members   Array of member entities or GUIDs
595
	 * @return bool
596
	 */
597
	public function update($collection_id, array $new_members = []) {
598
		$acl = $this->get($collection_id);
599
600
		if (!$acl) {
601
			return false;
602
		}
603
		
604
		$to_guid = function($elem) {
605
			if (empty($elem)) {
606
				return 0;
607
			}
608
			if (is_object($elem)) {
609
				return (int) $elem->guid;
610
			}
611
			return (int) $elem;
612
		};
613
		
614
		$current_members = [];
615
		$new_members = array_map($to_guid, $new_members);
616
617
		$current_members_batch = $this->getMembers($collection_id, [
618
			'batch' => true,
619
			'limit' => 0,
620
			'callback' => false,
621
		]);
622
623
		foreach ($current_members_batch as $row) {
0 ignored issues
show
Bug introduced by
The expression $current_members_batch of type object<ElggBatch>|false|integer|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

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