DocumentationController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Http\Controllers;
  4. use App\Http\Requests\RenameDocumentationItemRequest;
  5. use App\Http\Requests\StoreDocumentationFileRequest;
  6. use App\Http\Requests\StoreDocumentationFolderRequest;
  7. use App\Http\Requests\UpdateDocumentationPermissionsRequest;
  8. use App\Models\DocumentationDocument;
  9. use App\Models\DocumentationDocumentVersion;
  10. use App\Models\DocumentationFolder;
  11. use App\Models\DocumentationFolderPermission;
  12. use App\Models\Role;
  13. use App\Models\User;
  14. use App\Services\DocumentationAccessService;
  15. use App\Services\DocumentationService;
  16. use Illuminate\Contracts\View\View;
  17. use Illuminate\Http\RedirectResponse;
  18. use Illuminate\Http\Request;
  19. use Illuminate\Http\UploadedFile;
  20. use Illuminate\Support\Collection as SupportCollection;
  21. use Illuminate\Support\Facades\Storage;
  22. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  23. use Symfony\Component\HttpFoundation\StreamedResponse;
  24. class DocumentationController extends Controller
  25. {
  26. public function __construct(
  27. private readonly DocumentationAccessService $accessService,
  28. private readonly DocumentationService $documentationService,
  29. ) {}
  30. public function index(Request $request): View
  31. {
  32. $user = $this->user($request);
  33. $folders = $this->accessService->navigationFolders($user);
  34. $selectedFolder = $request->integer('folder')
  35. ? DocumentationFolder::query()->with('permissionRules')->findOrFail($request->integer('folder'))
  36. : $folders->first(fn (DocumentationFolder $folder): bool => (bool) $folder->getAttribute('can_read'));
  37. if ($selectedFolder) {
  38. abort_unless($this->accessService->canRead($user, $selectedFolder), 403);
  39. }
  40. $search = trim((string) $request->query('search', ''));
  41. $documents = $selectedFolder
  42. ? DocumentationDocument::query()
  43. ->where('folder_id', $selectedFolder->id)
  44. ->when($search !== '', fn ($query) => $query->where('name', 'like', "%{$search}%"))
  45. ->with(['currentVersion.file', 'currentVersion.creator', 'versions.file', 'versions.creator'])
  46. ->orderBy('name')
  47. ->paginate(20)
  48. ->withQueryString()
  49. : null;
  50. $canWrite = $selectedFolder && $this->accessService->canWrite($user, $selectedFolder);
  51. $canManagePermissions = $selectedFolder
  52. && $this->accessService->canManagePermissions($user, $selectedFolder);
  53. $selectedFolder?->loadMissing('permissionRules');
  54. $rules = $selectedFolder?->permissionRules ?? collect();
  55. return view('documents.index', [
  56. 'active' => 'documents',
  57. 'folders' => $folders,
  58. 'foldersByParent' => $folders->groupBy(fn (DocumentationFolder $folder): int => $folder->parent_id ?? 0),
  59. 'selectedFolder' => $selectedFolder,
  60. 'documents' => $documents,
  61. 'search' => $search,
  62. 'canCreateRoot' => $this->accessService->isAdministrator($user),
  63. 'canWrite' => (bool) $canWrite,
  64. 'canManagePermissions' => (bool) $canManagePermissions,
  65. 'roles' => $canManagePermissions ? Role::query()->where('is_active', true)->orderBy('name')->get() : collect(),
  66. 'users' => $canManagePermissions ? User::query()->with('roleModel')->orderBy('name')->get() : collect(),
  67. 'permissionValues' => $this->permissionValues($rules),
  68. ]);
  69. }
  70. public function storeFolder(StoreDocumentationFolderRequest $request): RedirectResponse
  71. {
  72. $user = $this->user($request);
  73. $parentId = $request->validated('parent_id');
  74. $parent = $parentId ? DocumentationFolder::query()->findOrFail((int) $parentId) : null;
  75. if ($parent) {
  76. abort_unless($this->accessService->canWrite($user, $parent), 403);
  77. } else {
  78. abort_unless($this->accessService->isAdministrator($user), 403);
  79. }
  80. $folder = $this->documentationService->createFolder($parent, (string) $request->validated('name'), $user);
  81. return redirect()
  82. ->route('documents.index', ['folder' => $folder->id])
  83. ->with('success', 'Папка создана.');
  84. }
  85. public function updateFolder(
  86. RenameDocumentationItemRequest $request,
  87. DocumentationFolder $folder,
  88. ): RedirectResponse {
  89. abort_unless($this->accessService->canWrite($this->user($request), $folder), 403);
  90. $this->documentationService->renameFolder($folder, (string) $request->validated('name'));
  91. return $this->folderRedirect($folder, 'Папка переименована.');
  92. }
  93. public function destroyFolder(Request $request, DocumentationFolder $folder): RedirectResponse
  94. {
  95. abort_unless($this->accessService->canWrite($this->user($request), $folder), 403);
  96. $parentId = $folder->parent_id;
  97. $this->documentationService->deleteFolder($folder);
  98. return redirect()
  99. ->route('documents.index', $parentId ? ['folder' => $parentId] : [])
  100. ->with('success', 'Папка и всё её содержимое удалены.');
  101. }
  102. public function updatePermissions(
  103. UpdateDocumentationPermissionsRequest $request,
  104. DocumentationFolder $folder,
  105. ): RedirectResponse {
  106. abort_unless($this->accessService->canManagePermissions($this->user($request), $folder), 403);
  107. $this->documentationService->updatePermissions($folder, $request->validated());
  108. return $this->folderRedirect($folder, 'Права папки обновлены.');
  109. }
  110. public function storeDocument(
  111. StoreDocumentationFileRequest $request,
  112. DocumentationFolder $folder,
  113. ): RedirectResponse {
  114. $user = $this->user($request);
  115. abort_unless($this->accessService->canWrite($user, $folder), 403);
  116. /** @var UploadedFile $file */
  117. $file = $request->file('file');
  118. $this->documentationService->createDocument(
  119. $folder,
  120. $request->validated('name'),
  121. $file,
  122. $user,
  123. );
  124. return $this->folderRedirect($folder, 'Документ загружен.');
  125. }
  126. public function updateDocument(
  127. RenameDocumentationItemRequest $request,
  128. DocumentationDocument $document,
  129. ): RedirectResponse {
  130. $document->loadMissing('folder');
  131. abort_unless($this->accessService->canWrite($this->user($request), $document->folder), 403);
  132. $this->documentationService->renameDocument($document, (string) $request->validated('name'));
  133. return $this->folderRedirect($document->folder, 'Документ переименован.');
  134. }
  135. public function destroyDocument(Request $request, DocumentationDocument $document): RedirectResponse
  136. {
  137. $document->loadMissing('folder');
  138. abort_unless($this->accessService->canWrite($this->user($request), $document->folder), 403);
  139. $folder = $document->folder;
  140. $this->documentationService->deleteDocument($document);
  141. return $this->folderRedirect($folder, 'Документ и все его версии удалены.');
  142. }
  143. public function storeVersion(
  144. StoreDocumentationFileRequest $request,
  145. DocumentationDocument $document,
  146. ): RedirectResponse {
  147. $document->loadMissing('folder');
  148. $user = $this->user($request);
  149. abort_unless($this->accessService->canWrite($user, $document->folder), 403);
  150. /** @var UploadedFile $file */
  151. $file = $request->file('file');
  152. $this->documentationService->addVersion($document, $file, $user);
  153. return $this->folderRedirect($document->folder, 'Новая версия загружена.');
  154. }
  155. public function downloadVersion(Request $request, DocumentationDocumentVersion $version): StreamedResponse
  156. {
  157. $version->loadMissing(['document.folder', 'file']);
  158. abort_unless($this->accessService->canRead($this->user($request), $version->document->folder), 403);
  159. abort_unless($version->file && Storage::disk('local')->exists($version->file->path), 404);
  160. return Storage::disk('local')->download($version->file->path, $version->file->original_name);
  161. }
  162. public function previewVersion(Request $request, DocumentationDocumentVersion $version): BinaryFileResponse
  163. {
  164. $version->loadMissing(['document.folder', 'file']);
  165. abort_unless($this->accessService->canRead($this->user($request), $version->document->folder), 403);
  166. abort_unless($version->file && Storage::disk('local')->exists($version->file->path), 404);
  167. $mimeType = Storage::disk('local')->mimeType($version->file->path);
  168. abort_unless(is_string($mimeType) && str_starts_with(strtolower($mimeType), 'image/'), 404);
  169. return response()->file(Storage::disk('local')->path($version->file->path), [
  170. 'Content-Type' => $mimeType,
  171. 'Content-Security-Policy' => "sandbox; default-src 'none'; img-src 'self' data:",
  172. 'X-Content-Type-Options' => 'nosniff',
  173. ]);
  174. }
  175. public function destroyVersion(Request $request, DocumentationDocumentVersion $version): RedirectResponse
  176. {
  177. $version->loadMissing('document.folder');
  178. $folder = $version->document->folder;
  179. abort_unless($this->accessService->canWrite($this->user($request), $folder), 403);
  180. $this->documentationService->deleteVersion($version);
  181. return $this->folderRedirect($folder, 'Версия полностью удалена.');
  182. }
  183. private function user(Request $request): User
  184. {
  185. /** @var User $user */
  186. $user = $request->user();
  187. return $user;
  188. }
  189. private function folderRedirect(DocumentationFolder $folder, string $message): RedirectResponse
  190. {
  191. return redirect()
  192. ->route('documents.index', ['folder' => $folder->id])
  193. ->with('success', $message);
  194. }
  195. /**
  196. * @param SupportCollection<int, DocumentationFolderPermission> $rules
  197. * @return array<string, mixed>
  198. */
  199. private function permissionValues(SupportCollection $rules): array
  200. {
  201. $all = $rules->firstWhere('subject_type', DocumentationFolderPermission::SUBJECT_ALL);
  202. return [
  203. 'all_read' => (bool) ($all?->can_read || $all?->can_write),
  204. 'all_write' => (bool) $all?->can_write,
  205. 'role_read_ids' => $rules
  206. ->where('subject_type', DocumentationFolderPermission::SUBJECT_ROLE)
  207. ->filter(fn (DocumentationFolderPermission $rule): bool => $rule->can_read || $rule->can_write)
  208. ->pluck('role_id')
  209. ->all(),
  210. 'role_write_ids' => $rules
  211. ->where('subject_type', DocumentationFolderPermission::SUBJECT_ROLE)
  212. ->where('can_write', true)
  213. ->pluck('role_id')
  214. ->all(),
  215. 'user_read_ids' => $rules
  216. ->where('subject_type', DocumentationFolderPermission::SUBJECT_USER)
  217. ->filter(fn (DocumentationFolderPermission $rule): bool => $rule->can_read || $rule->can_write)
  218. ->pluck('user_id')
  219. ->all(),
  220. 'user_write_ids' => $rules
  221. ->where('subject_type', DocumentationFolderPermission::SUBJECT_USER)
  222. ->where('can_write', true)
  223. ->pluck('user_id')
  224. ->all(),
  225. ];
  226. }
  227. }