* @param array $payload * @return array */
(McpOpsContext $ctx, array $payload)
| 687 | * @return array<string,mixed> |
| 688 | */ |
| 689 | private static function opReadFile(McpOpsContext $ctx, array $payload): array |
| 690 | { |
| 691 | return self::withSourceContext($ctx, $payload, false, function () use ($ctx, $payload): array { |
| 692 | $folder = self::normalizeFolder((string)($payload['folder'] ?? 'root')); |
| 693 | $file = self::normalizeFileName((string)($payload['file'] ?? ($payload['filename'] ?? ''))); |
| 694 | $maxBytes = self::boundedInt( |
| 695 | $payload['maxBytes'] ?? self::DEFAULT_READ_FILE_PREVIEW_BYTES, |
| 696 | 256, |
| 697 | self::MAX_READ_FILE_PREVIEW_BYTES, |
| 698 | self::DEFAULT_READ_FILE_PREVIEW_BYTES |
| 699 | ); |
| 700 | |
| 701 | $username = $ctx->username(); |
| 702 | $perms = $ctx->permissions(); |
| 703 | |
| 704 | $fullView = ACL::canRead($username, $perms, $folder) |
| 705 | || ACL::ownsFolderOrAncestor($username, $perms, $folder); |
| 706 | $ownOnlyGrant = ACL::hasGrant($username, $folder, 'read_own'); |
| 707 | if (!$fullView && !$ownOnlyGrant) { |
| 708 | throw new RuntimeException('Forbidden: no view access to this folder.', 403); |
| 709 | } |
| 710 | |
| 711 | self::assertFolderScope($ctx, $folder, $fullView ? 'read' : 'read_own'); |
| 712 | |
| 713 | if ( |
| 714 | !$ctx->canBypassOwnership() |
| 715 | && !$fullView |
| 716 | && $ownOnlyGrant |
| 717 | ) { |
| 718 | self::assertFilesOwnedByUser($folder, [$file], $username); |
| 719 | } |
| 720 | |
| 721 | $storage = StorageRegistry::getAdapter(); |
| 722 | $baseDir = rtrim(SourceContext::uploadRoot(), '/\\'); |
| 723 | $dir = ($folder === 'root') |
| 724 | ? $baseDir |
| 725 | : $baseDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folder); |
| 726 | $path = $dir . DIRECTORY_SEPARATOR . $file; |
| 727 | |
| 728 | $stat = $storage->stat($path); |
| 729 | if ($stat === null || ($stat['type'] ?? '') !== 'file') { |
| 730 | throw new RuntimeException('File not found.', 404); |
| 731 | } |
| 732 | |
| 733 | $size = max(0, (int)($stat['size'] ?? 0)); |
| 734 | if ($size > self::MAX_READ_FILE_SIZE_BYTES) { |
| 735 | throw new RuntimeException('File is too large for AI text preview.', 413); |
| 736 | } |
| 737 | |
| 738 | $raw = $storage->read($path, $maxBytes, 0); |
| 739 | if (!is_string($raw)) { |
| 740 | throw new RuntimeException('Failed to read file.', 500); |
| 741 | } |
| 742 | if ($raw !== '' && strpos($raw, "\0") !== false) { |
| 743 | throw new RuntimeException('File appears to be binary; text preview is unavailable.', 415); |
| 744 | } |
| 745 | |
| 746 | $content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $raw); |
no test coverage detected