Elgg  Version master
AccessCollections.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Database;
4 
7 use Elgg\Config;
8 use Elgg\Database;
16 
24 
25  use Loggable;
26 
30  public const TABLE_NAME = 'access_collections';
31 
35  public const MEMBERSHIP_TABLE_NAME = 'access_collection_membership';
36 
37  protected bool $init_complete = false;
38 
51  public function __construct(
52  protected Config $config,
53  protected Database $db,
54  protected EntityTable $entities,
55  protected UserCapabilities $capabilities,
56  protected AccessCache $access_cache,
57  protected EventsService $events,
58  protected SessionManagerService $session_manager,
59  protected Translator $translator) {
60  }
61 
67  public function markInitComplete(): void {
68  $this->init_complete = true;
69  }
70 
95  public function getAccessArray(int $user_guid = 0, bool $flush = false): array {
96  $cache = $this->access_cache;
97 
98  if ($flush) {
99  $cache->clear();
100  }
101 
102  if ($user_guid === 0) {
103  $user_guid = $this->session_manager->getLoggedInUserGuid();
104  }
105 
106  $hash = $user_guid . 'get_access_array';
107 
108  $access_array = $cache->load($hash);
109  if ($access_array === null) {
110  // Public access is always visible
111  $access_array = [ACCESS_PUBLIC];
112 
113  // The following can only return sensible data for a known user.
114  if ($user_guid) {
115  $access_array[] = ACCESS_LOGGED_IN;
116 
117  // Get ACLs that user owns or is a member of
118  $select = Select::fromTable(self::TABLE_NAME);
119 
120  $membership_query = $select->subquery(self::MEMBERSHIP_TABLE_NAME);
121  $membership_query->select('access_collection_id')
122  ->where($select->compare('user_guid', '=', $user_guid, ELGG_VALUE_GUID));
123 
124  $select->select('id')
125  ->where($select->compare('owner_guid', '=', $user_guid, ELGG_VALUE_GUID))
126  ->orWhere($select->compare('id', 'in', $membership_query->getSQL()));
127 
128  $collections = $this->db->getData($select);
129  if (!empty($collections)) {
130  foreach ($collections as $collection) {
131  $access_array[] = (int) $collection->id;
132  }
133  }
134 
135  $ignore_access = $this->capabilities->canBypassPermissionsCheck($user_guid);
136 
137  if ($ignore_access === true) {
138  $access_array[] = ACCESS_PRIVATE;
139  }
140  }
141 
142  if ($this->init_complete) {
143  $cache->save($hash, $access_array);
144  }
145  }
146 
147  $options = [
148  'user_id' => $user_guid,
149  ];
150 
151  // see the warning in the docs for this function about infinite loop potential
152  return $this->events->triggerResults('access:collections:read', 'user', $options, $access_array);
153  }
154 
169  public function hasAccessToEntity(\ElggEntity $entity, int $user_guid = 0): bool {
170  if ($entity->access_id === ACCESS_PUBLIC) {
171  // Public entities are always accessible
172  return true;
173  }
174 
175  try {
176  $user = $this->entities->getUserForPermissionsCheck($user_guid);
177  $user_guid = $user ? $user->guid : 0; // No GUID given and not logged in
178  } catch (UserFetchFailureException $e) {
179  // Not a user GUID
180  $user_guid = 0;
181  }
182 
183  if ($user_guid === $entity->owner_guid) {
184  // Owners have access to their own content
185  return true;
186  }
187 
188  if (!empty($user_guid) && $entity->access_id === ACCESS_LOGGED_IN) {
189  // Existing users have access to entities with logged in access
190  return true;
191  }
192 
193  // See #7159. Must not allow ignore access to affect query
194  $row = elgg_call(ELGG_ENFORCE_ACCESS, function() use ($entity, $user_guid) {
195  return $this->entities->getRow($entity->guid, $user_guid);
196  });
197 
198  return !empty($row);
199  }
200 
224  public function getWriteAccessArray(int $user_guid = 0, bool $flush = false, array $input_params = []): array {
225  $cache = $this->access_cache;
226 
227  if ($flush) {
228  $cache->clear();
229  }
230 
231  if ($user_guid === 0) {
232  $user_guid = $this->session_manager->getLoggedInUserGuid();
233  }
234 
235  $hash = $user_guid . 'get_write_access_array';
236 
237  $access_array = $cache->load($hash);
238  if ($access_array === null) {
239  $access_array = [
243  ];
244 
245  $access_array += $this->getCollectionsForWriteAccess($user_guid);
246 
247  if ($this->init_complete) {
248  $cache->save($hash, $access_array);
249  }
250  }
251 
252  $options = [
253  'user_id' => $user_guid,
254  'input_params' => $input_params,
255  ];
256 
257  $access_array = $this->events->triggerResults('access:collections:write', 'user', $options, $access_array);
258 
259  // move logged in and public to the end of the array
260  foreach ([ACCESS_LOGGED_IN, ACCESS_PUBLIC] as $access) {
261  if (!isset($access_array[$access])) {
262  continue;
263  }
264 
265  $temp = $access_array[$access];
266  unset($access_array[$access]);
267  $access_array[$access] = $temp;
268  }
269 
270 
271  return $access_array;
272  }
273 
282  protected function getCollectionsForWriteAccess(int $owner_guid): array {
283  $subtypes = $this->events->triggerResults('access:collections:write:subtypes', 'user', ['owner_guid' => $owner_guid], []);
284 
285  $select = Select::fromTable(self::TABLE_NAME);
286 
287  $ors = [
288  $select->compare('subtype', 'is null'),
289  ];
290  if (!empty($subtypes)) {
291  $ors[] = $select->compare('subtype', 'in', $subtypes, ELGG_VALUE_STRING);
292  }
293 
294  $select->select('*')
295  ->where($select->compare('owner_guid', '=', $owner_guid, ELGG_VALUE_GUID))
296  ->andWhere($select->merge($ors, 'OR'))
297  ->orderBy('name', 'ASC');
298 
299  $collections = $this->db->getData($select, [$this, 'rowToElggAccessCollection']);
300  if (empty($collections)) {
301  return [];
302  }
303 
304  $result = [];
305  foreach ($collections as $collection) {
306  $result[$collection->id] = $collection->getDisplayName();
307  }
308 
309  return $result;
310  }
311 
325  public function canEdit(int $collection_id, int $user_guid = null): bool {
326  try {
327  $user = $this->entities->getUserForPermissionsCheck($user_guid);
328  } catch (UserFetchFailureException $e) {
329  return false;
330  }
331 
332  if (!$user instanceof \ElggUser) {
333  return false;
334  }
335 
336  $collection = $this->get($collection_id);
337  if (!$collection instanceof \ElggAccessCollection) {
338  return false;
339  }
340 
341  if ($this->capabilities->canBypassPermissionsCheck($user->guid)) {
342  return true;
343  }
344 
345  $write_access = $this->getWriteAccessArray($user->guid, true);
346  return array_key_exists($collection_id, $write_access);
347  }
348 
364  public function create(\ElggAccessCollection $acl): bool {
365  if (!empty($acl->id)) {
366  return $this->update($acl);
367  }
368 
369  return $this->events->triggerSequence('create', $acl->getType(), $acl, function (\ElggAccessCollection $acl) {
370  $insert = Insert::intoTable(self::TABLE_NAME);
371  $insert->values([
372  'name' => $insert->param($acl->name, ELGG_VALUE_STRING),
373  'subtype' => $insert->param($acl->subtype, ELGG_VALUE_STRING),
374  'owner_guid' => $insert->param($acl->owner_guid, ELGG_VALUE_GUID),
375  ]);
376 
377  $id = $this->db->insertData($insert);
378  if (empty($id)) {
379  return false;
380  }
381 
382  $acl->id = $id;
383 
384  $this->access_cache->clear();
385 
386  return true;
387  });
388  }
389 
397  public function update(\ElggAccessCollection $acl): bool {
398  if (empty($acl->id)) {
399  return $this->create($acl);
400  }
401 
402  return $this->events->triggerSequence('update', $acl->getType(), $acl, function (\ElggAccessCollection $acl) {
403  $update = Update::table(self::TABLE_NAME);
404  $update->set('name', $update->param($acl->name, ELGG_VALUE_STRING))
405  ->set('subtype', $update->param($acl->subtype, ELGG_VALUE_STRING))
406  ->set('owner_guid', $update->param($acl->owner_guid, ELGG_VALUE_GUID))
407  ->where($update->compare('id', '=', $acl->id, ELGG_VALUE_ID));
408 
409  if (!$this->db->updateData($update)) {
410  return false;
411  }
412 
413  $this->access_cache->clear();
414 
415  return true;
416  });
417  }
418 
426  public function delete(\ElggAccessCollection $acl): bool {
427  if (empty($acl->id)) {
428  return false;
429  }
430 
431  return $this->events->triggerSequence('delete', $acl->getType(), $acl, function (\ElggAccessCollection $acl) {
432  $delete = Delete::fromTable(self::TABLE_NAME);
433  $delete->where($delete->compare('id', '=', $acl->id, ELGG_VALUE_ID));
434 
435  if (!$this->db->deleteData($delete)) {
436  return false;
437  }
438 
439  // cleanup access collection membership (doesn't affect the result of deleting the ACL)
440  $delete_membership = Delete::fromTable(self::MEMBERSHIP_TABLE_NAME);
441  $delete_membership->where($delete_membership->compare('access_collection_id', '=', $acl->id, ELGG_VALUE_ID));
442 
443  $this->db->deleteData($delete_membership);
444 
445  // clear cache
446  $this->access_cache->clear();
447 
448  return true;
449  });
450  }
451 
459  public function rowToElggAccessCollection(\stdClass $row): \ElggAccessCollection {
460  return new \ElggAccessCollection($row);
461  }
462 
470  public function get(int $collection_id): ?\ElggAccessCollection {
471  $query = Select::fromTable(self::TABLE_NAME);
472  $query->select('*')
473  ->where($query->compare('id', '=', $collection_id, ELGG_VALUE_ID));
474 
475  return $this->db->getDataRow($query, [$this, 'rowToElggAccessCollection']) ?: null;
476  }
477 
486  public function hasUser(int $user_guid, int $collection_id): bool {
487  $options = [
488  'guids' => $user_guid,
489  'count' => true,
490  ];
491  return (bool) $this->getMembers($collection_id, $options);
492  }
493 
504  public function addUser(int $user_guid, int $collection_id): bool {
505  $collection = $this->get($collection_id);
506  if (!$collection instanceof \ElggAccessCollection) {
507  return false;
508  }
509 
510  $user = $this->entities->get($user_guid);
511  if (!$user instanceof \ElggUser) {
512  return false;
513  }
514 
515  $event_params = [
516  'collection_id' => $collection->id,
517  'user_guid' => $user_guid
518  ];
519 
520  // @todo https://github.com/Elgg/Elgg/issues/10823
521  $result = $this->events->triggerResults('access:collections:add_user', 'collection', $event_params, true);
522  if ($result === false) {
523  return false;
524  }
525 
526  // if someone tries to insert the same data twice, we catch the exception and return true
527  $insert = Insert::intoTable(self::MEMBERSHIP_TABLE_NAME);
528  $insert->values([
529  'access_collection_id' => $insert->param($collection_id, ELGG_VALUE_ID),
530  'user_guid' => $insert->param($user_guid, ELGG_VALUE_GUID),
531  ]);
532 
533  try {
534  $result = $this->db->insertData($insert);
535  } catch (DatabaseException $e) {
536  $prev = $e->getPrevious();
537  if ($prev instanceof UniqueConstraintViolationException) {
538  // duplicate key exception, catched for performance reasons
539  return true;
540  }
541 
542  throw $e;
543  }
544 
545  $this->access_cache->clear();
546 
547  return $result !== false;
548  }
549 
560  public function removeUser(int $user_guid, int $collection_id): bool {
561 
562  $params = [
563  'collection_id' => $collection_id,
564  'user_guid' => $user_guid,
565  ];
566 
567  // @todo https://github.com/Elgg/Elgg/issues/10823
568  if (!$this->events->triggerResults('access:collections:remove_user', 'collection', $params, true)) {
569  return false;
570  }
571 
572  $delete = Delete::fromTable(self::MEMBERSHIP_TABLE_NAME);
573  $delete->where($delete->compare('access_collection_id', '=', $collection_id, ELGG_VALUE_ID))
574  ->andWhere($delete->compare('user_guid', '=', $user_guid, ELGG_VALUE_GUID));
575 
576  $this->access_cache->clear();
577 
578  return (bool) $this->db->deleteData($delete);
579  }
580 
589  public function getEntityCollections(array $options = []): array {
590  $supported_options = ['owner_guid', 'subtype'];
591 
592  $select = Select::fromTable(self::TABLE_NAME);
593  $select->select('*')
594  ->orderBy('name', 'ASC');
595 
596  foreach ($supported_options as $option) {
597  $option_value = elgg_extract($option, $options);
598  if (!isset($option_value)) {
599  continue;
600  }
601 
602  switch ($option) {
603  case 'owner_guid':
604  $select->andWhere($select->compare($option, '=', $option_value, ELGG_VALUE_GUID));
605  break;
606  case 'subtype':
607  $select->andWhere($select->compare($option, '=', $option_value, ELGG_VALUE_STRING));
608  break;
609  }
610  }
611 
612  return $this->db->getData($select, [$this, 'rowToElggAccessCollection']);
613  }
614 
623  public function getMembers(int $collection_id, array $options = []) {
624  $options['wheres'][] = function(QueryBuilder $qb, $table_alias) use ($collection_id) {
625  $qb->join($table_alias, self::MEMBERSHIP_TABLE_NAME, 'acm', $qb->compare('acm.user_guid', '=', "{$table_alias}.guid"));
626 
627  return $qb->compare('acm.access_collection_id', '=', $collection_id, ELGG_VALUE_INTEGER);
628  };
629 
630  return Entities::find($options);
631  }
632 
640  public function getCollectionsByMember(int $member_guid): array {
641  $select = Select::fromTable(self::TABLE_NAME, 'ac');
642  $select->join($select->getTableAlias(), self::MEMBERSHIP_TABLE_NAME, 'acm', $select->compare("{$select->getTableAlias()}.id", '=', 'acm.access_collection_id'));
643 
644  $select->select("{$select->getTableAlias()}.*")
645  ->where($select->compare('acm.user_guid', '=', $member_guid, ELGG_VALUE_GUID))
646  ->orderBy('name', 'ASC');
647 
648  return $this->db->getData($select, [$this, 'rowToElggAccessCollection']);
649  }
650 
668  public function getReadableAccessLevel(int $entity_access_id): string {
669  $translator = $this->translator;
670 
671  // Check if entity access id is a defined global constant
672  $access_array = [
673  ACCESS_PRIVATE => $translator->translate('access:label:private'),
674  ACCESS_FRIENDS => $translator->translate('access:label:friends'),
675  ACCESS_LOGGED_IN => $translator->translate('access:label:logged_in'),
676  ACCESS_PUBLIC => $translator->translate('access:label:public'),
677  ];
678 
679  if (array_key_exists($entity_access_id, $access_array)) {
680  return $access_array[$entity_access_id];
681  }
682 
683  // Entity access id is probably a custom access collection
684  // Check if the user has write access to it and can see it's label
685  // Admins should always be able to see the readable version
686  $collection = $this->get($entity_access_id);
687 
688  if (!$collection instanceof \ElggAccessCollection || !$collection->canEdit()) {
689  // return 'Limited' if the collection can not be loaded or it can not be edited
690  return $translator->translate('access:limited:label');
691  }
692 
693  return $collection->getDisplayName();
694  }
695 }
getReadableAccessLevel(int $entity_access_id)
Return the name of an ACCESS_* constant or an access collection, but only if the logged in user owns ...
canEdit(int $user_guid=null)
Check if user can edit this collection.
elgg_call(int $flags, Closure $closure)
Calls a callable autowiring the arguments using public DI services and applying logic based on flags...
Definition: elgglib.php:304
$event_params
Definition: save.php:31
static table(string $table)
Returns a QueryBuilder for updating data in a given table.
Definition: Update.php:17
removeUser(int $user_guid, int $collection_id)
Removes a user from an access collection.
$user_guid
Definition: login_as.php:10
hasAccessToEntity(\ElggEntity $entity, int $user_guid=0)
Can a user access an entity.
$params
Saves global plugin settings.
Definition: save.php:13
__construct(protected Config $config, protected Database $db, protected EntityTable $entities, protected UserCapabilities $capabilities, protected AccessCache $access_cache, protected EventsService $events, protected SessionManagerService $session_manager, protected Translator $translator)
Constructor.
const ELGG_ENFORCE_ACCESS
Definition: constants.php:122
static find(array $options=[])
Build and execute a new query from an array of legacy options.
Definition: Repository.php:110
The Elgg database.
Definition: Database.php:26
const ACCESS_FRIENDS
Definition: constants.php:13
if(elgg_view_exists("widgets/{$widget->handler}/edit")) $access
Definition: save.php:19
markInitComplete()
Mark the access system as initialized.
const ELGG_VALUE_INTEGER
Value types.
Definition: constants.php:111
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is void
Definition: LICENSE.txt:215
canEdit(int $collection_id, int $user_guid=null)
Can the user change this access collection?
const ELGG_VALUE_GUID
Definition: constants.php:113
Events service.
Database abstraction query builder.
$input_params
Definition: livesearch.php:15
$delete
const ELGG_VALUE_ID
Definition: constants.php:114
static intoTable(string $table)
Returns a QueryBuilder for inserting data in a given table.
Definition: Insert.php:17
getCollectionsForWriteAccess(int $owner_guid)
Returns an array of access collections to be used in the write access array.
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:254
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
if($who_can_change_language=== 'nobody') elseif($who_can_change_language=== 'admin_only'&&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
getWriteAccessArray(int $user_guid=0, bool $flush=false, array $input_params=[])
Returns an array of access permissions that the user is allowed to save content with.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
$owner_guid
const ACCESS_PRIVATE
Definition: constants.php:10
$entity
Definition: reset.php:8
const ACCESS_LOGGED_IN
Definition: constants.php:11
getCollectionsByMember(int $member_guid)
Return an array of collections that the entity is member of.
foreach($recommendedExtensions as $extension) if(empty(ini_get('session.gc_probability'))||empty(ini_get('session.gc_divisor'))) $db
Exception indicating a user could not be looked up for a permissions check.
rowToElggAccessCollection(\stdClass $row)
Transforms a database row to an instance of ElggAccessCollection.
compare(string $x, string $comparison, $y=null, string $type=null, bool $case_sensitive=null)
Build value comparison clause.
A generic parent class for database exceptions.
$user
Definition: ban.php:7
static fromTable(string $table)
Returns a QueryBuilder for deleting data from a given table.
Definition: Delete.php:17
getEntityCollections(array $options=[])
Returns access collections.
create(\ElggAccessCollection $acl)
Creates a new access collection.
addUser(int $user_guid, int $collection_id)
Adds a user to an access collection.
const ELGG_VALUE_STRING
Definition: constants.php:112
$query
$subtypes
getAccessArray(int $user_guid=0, bool $flush=false)
Returns an array of access IDs a user is permitted to see.
update(\ElggAccessCollection $acl)
Update an existing access collection.
static fromTable(string $table, string $alias=null)
Returns a QueryBuilder for selecting data from a given table.
Definition: Select.php:18
const ACCESS_PUBLIC
Definition: constants.php:12
join(string $fromAlias, string $join, string $alias,?string $condition=null)
{}
User capabilities service.
$id
Generic annotation delete action.
Definition: delete.php:6
$qb
Definition: queue.php:12
Entity table database service.
Definition: EntityTable.php:24
hasUser(int $user_guid, int $collection_id)
Check if user is already in the collection.
getMembers(int $collection_id, array $options=[])
Get members of an access collection.