Elgg  Version 6.2
AccessCollections.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Database;
4 
5 use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
7 use Elgg\Config;
8 use Elgg\Database;
14 use Elgg\Traits\Loggable;
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 {
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 = [
240  ACCESS_PRIVATE => $this->getReadableAccessLevel(ACCESS_PRIVATE),
241  ACCESS_LOGGED_IN => $this->getReadableAccessLevel(ACCESS_LOGGED_IN),
242  ACCESS_PUBLIC => $this->getReadableAccessLevel(ACCESS_PUBLIC),
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 }
$entity
Definition: reset.php:8
$id
Generic annotation delete action.
Definition: delete.php:6
$comment access_id
Definition: save.php:55
$params
Saves global plugin settings.
Definition: save.php:13
$event_params
Definition: save.php:31
return[ 'admin/delete_admin_notices'=>['access'=> 'admin'], 'admin/menu/save'=>['access'=> 'admin'], 'admin/plugins/activate'=>['access'=> 'admin'], 'admin/plugins/activate_all'=>['access'=> 'admin'], 'admin/plugins/deactivate'=>['access'=> 'admin'], 'admin/plugins/deactivate_all'=>['access'=> 'admin'], 'admin/plugins/set_priority'=>['access'=> 'admin'], 'admin/security/security_txt'=>['access'=> 'admin'], 'admin/security/settings'=>['access'=> 'admin'], 'admin/security/regenerate_site_secret'=>['access'=> 'admin'], 'admin/site/cache/invalidate'=>['access'=> 'admin'], 'admin/site/flush_cache'=>['access'=> 'admin'], 'admin/site/icons'=>['access'=> 'admin'], 'admin/site/set_maintenance_mode'=>['access'=> 'admin'], 'admin/site/set_robots'=>['access'=> 'admin'], 'admin/site/theme'=>['access'=> 'admin'], 'admin/site/unlock_upgrade'=>['access'=> 'admin'], 'admin/site/settings'=>['access'=> 'admin'], 'admin/upgrade'=>['access'=> 'admin'], 'admin/upgrade/reset'=>['access'=> 'admin'], 'admin/user/ban'=>['access'=> 'admin'], 'admin/user/bulk/ban'=>['access'=> 'admin'], 'admin/user/bulk/delete'=>['access'=> 'admin'], 'admin/user/bulk/unban'=>['access'=> 'admin'], 'admin/user/bulk/validate'=>['access'=> 'admin'], 'admin/user/change_email'=>['access'=> 'admin'], 'admin/user/delete'=>['access'=> 'admin'], 'admin/user/login_as'=>['access'=> 'admin'], 'admin/user/logout_as'=>[], 'admin/user/makeadmin'=>['access'=> 'admin'], 'admin/user/resetpassword'=>['access'=> 'admin'], 'admin/user/removeadmin'=>['access'=> 'admin'], 'admin/user/unban'=>['access'=> 'admin'], 'admin/user/validate'=>['access'=> 'admin'], 'annotation/delete'=>[], 'avatar/upload'=>[], 'comment/save'=>[], 'diagnostics/download'=>['access'=> 'admin'], 'entity/chooserestoredestination'=>[], 'entity/delete'=>[], 'entity/mute'=>[], 'entity/restore'=>[], 'entity/subscribe'=>[], 'entity/trash'=>[], 'entity/unmute'=>[], 'entity/unsubscribe'=>[], 'login'=>['access'=> 'logged_out'], 'logout'=>[], 'notifications/mute'=>['access'=> 'public'], 'plugins/settings/remove'=>['access'=> 'admin'], 'plugins/settings/save'=>['access'=> 'admin'], 'plugins/usersettings/save'=>[], 'register'=>['access'=> 'logged_out', 'middleware'=>[\Elgg\Router\Middleware\RegistrationAllowedGatekeeper::class,],], 'river/delete'=>[], 'settings/notifications'=>[], 'settings/notifications/subscriptions'=>[], 'user/changepassword'=>['access'=> 'public'], 'user/requestnewpassword'=>['access'=> 'public'], 'useradd'=>['access'=> 'admin'], 'usersettings/save'=>[], 'widgets/add'=>[], 'widgets/delete'=>[], 'widgets/move'=>[], 'widgets/save'=>[],]
Definition: actions.php:73
$delete
$user
Definition: ban.php:7
$query
getType()
{Return the type of the object - eg.object, group, user, relationship, metadata, annotation etcstring...
canEdit(?int $user_guid=null)
Check if user can edit this collection.
Access collections database service.
removeUser(int $user_guid, int $collection_id)
Removes a user from an access collection.
canEdit(int $collection_id, ?int $user_guid=null)
Can the user change this access collection?
getCollectionsByMember(int $member_guid)
Return an array of collections that the entity is member of.
getEntityCollections(array $options=[])
Returns access collections.
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.
__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.
create(\ElggAccessCollection $acl)
Creates a new access collection.
getCollectionsForWriteAccess(int $owner_guid)
Returns an array of access collections to be used in the write access array.
getMembers(int $collection_id, array $options=[])
Get members of an access collection.
hasUser(int $user_guid, int $collection_id)
Check if user is already in the collection.
addUser(int $user_guid, int $collection_id)
Adds a user to an access collection.
hasAccessToEntity(\ElggEntity $entity, int $user_guid=0)
Can a user access an entity.
rowToElggAccessCollection(\stdClass $row)
Transforms a database row to an instance of ElggAccessCollection.
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.
getReadableAccessLevel(int $entity_access_id)
Return the name of an ACCESS_* constant or an access collection, but only if the logged in user owns ...
markInitComplete()
Mark the access system as initialized.
Entity table database service.
Definition: EntityTable.php:24
Database abstraction query builder.
join(string $fromAlias, string $join, string $alias, ?string $condition=null)
{}
Query builder for fetching data from the database.
Definition: Select.php:8
static fromTable(string $table, ?string $alias=null)
Returns a QueryBuilder for selecting data from a given table.
Definition: Select.php:18
The Elgg database.
Definition: Database.php:26
Events service.
A generic parent class for database exceptions.
Exception indicating a user could not be looked up for a permissions check.
User capabilities service.
$owner_guid
$subtypes
const ELGG_VALUE_STRING
Definition: constants.php:112
const ELGG_VALUE_ID
Definition: constants.php:114
const ELGG_VALUE_GUID
Definition: constants.php:113
const ACCESS_PRIVATE
Definition: constants.php:10
const ACCESS_FRIENDS
Definition: constants.php:13
const ACCESS_LOGGED_IN
Definition: constants.php:11
const ELGG_VALUE_INTEGER
Value types.
Definition: constants.php:111
const ELGG_ENFORCE_ACCESS
Definition: constants.php:122
const ACCESS_PUBLIC
Definition: constants.php:12
if($who_can_change_language==='nobody') elseif($who_can_change_language==='admin_only' &&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
foreach($recommendedExtensions as $extension) if(empty(ini_get('session.gc_probability'))||empty(ini_get('session.gc_divisor'))) $db
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:306
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:256
$input_params
Definition: livesearch.php:15
$user_guid
Definition: login_as.php:10
try
Definition: login_as.php:33
$qb
Definition: queue.php:12
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10
if(elgg_view_exists("widgets/{$widget->handler}/edit")) $access
Definition: save.php:19