*/ private array $folders = []; /** @var array> */ private array $effectiveRules = []; public function isAdministrator(User $user): bool { return $user->resolvedRoleSlug() === Role::ADMIN; } public function canRead(User $user, DocumentationFolder $folder): bool { return $this->can($user, $folder, false); } public function canWrite(User $user, DocumentationFolder $folder): bool { return $this->can($user, $folder, true); } public function canManagePermissions(User $user, DocumentationFolder $folder): bool { return $this->isAdministrator($user) || ($user->hasPermission('documents.update') && $this->canWrite($user, $folder)); } /** * Returns readable folders plus their ancestors required to render the tree. * Ancestors without read access are navigation-only nodes. * * @return Collection */ public function navigationFolders(User $user): Collection { $folders = DocumentationFolder::query() ->with(['permissionRules.role', 'creator']) ->orderBy('name') ->get(); foreach ($folders as $folder) { $this->folders[$folder->id] = $folder; } $included = []; foreach ($folders as $folder) { $canRead = $this->canRead($user, $folder); $folder->setAttribute('can_read', $canRead); $folder->setAttribute('can_write', $this->canWrite($user, $folder)); if (! $canRead) { continue; } $current = $folder; while ($current) { $included[$current->id] = true; $current = $current->parent_id ? ($this->folders[$current->parent_id] ?? null) : null; } } return $folders ->filter(fn (DocumentationFolder $folder): bool => isset($included[$folder->id])) ->values(); } public function forgetCachedRules(): void { $this->effectiveRules = []; $this->folders = []; } private function can(User $user, DocumentationFolder $folder, bool $write): bool { if ($this->isAdministrator($user)) { return true; } $this->folders[$folder->id] = $folder; foreach ($this->rulesFor($folder) as $rule) { if (! $this->matches($rule, $user)) { continue; } if ($write && $rule->can_write) { return true; } if (! $write && ($rule->can_read || $rule->can_write)) { return true; } } return false; } /** @return Collection */ private function rulesFor(DocumentationFolder $folder): Collection { if (isset($this->effectiveRules[$folder->id])) { return $this->effectiveRules[$folder->id]; } if ($folder->inherits_permissions && $folder->parent_id) { $parent = $this->folders[$folder->parent_id] ?? DocumentationFolder::query() ->with('permissionRules.role') ->find($folder->parent_id); if ($parent) { $this->folders[$parent->id] = $parent; return $this->effectiveRules[$folder->id] = $this->rulesFor($parent); } } $rules = $folder->relationLoaded('permissionRules') ? $folder->permissionRules : $folder->permissionRules()->with('role')->get(); return $this->effectiveRules[$folder->id] = $rules; } private function matches(DocumentationFolderPermission $rule, User $user): bool { return match ($rule->subject_type) { DocumentationFolderPermission::SUBJECT_ALL => true, DocumentationFolderPermission::SUBJECT_USER => $rule->user_id === $user->id, DocumentationFolderPermission::SUBJECT_ROLE => $rule->role_id === $user->role_id || $rule->role?->slug === $user->resolvedRoleSlug(), default => false, }; } }