* @param array $payload * @return array */
(McpOpsContext $ctx, array $payload)
| 1022 | * @return array<string,mixed> |
| 1023 | */ |
| 1024 | private static function opMoveFiles(McpOpsContext $ctx, array $payload): array |
| 1025 | { |
| 1026 | self::assertWritableAccount($ctx, true); |
| 1027 | |
| 1028 | $sourceFolder = self::normalizeFolder((string)($payload['source'] ?? 'root')); |
| 1029 | $destinationFolder = self::normalizeFolder((string)($payload['destination'] ?? 'root')); |
| 1030 | $files = self::normalizeFileList($payload['files'] ?? []); |
| 1031 | |
| 1032 | $sourcePair = self::resolveSourcePair($payload); |
| 1033 | $sourceId = $sourcePair['sourceId']; |
| 1034 | $destSourceId = $sourcePair['destSourceId']; |
| 1035 | $crossSource = $sourcePair['crossSource']; |
| 1036 | |
| 1037 | if ($crossSource && (!class_exists(SourceContext::class) || !SourceContext::sourcesEnabled())) { |
| 1038 | throw new RuntimeException('Cross-source operations require sources to be enabled.', 400); |
| 1039 | } |
| 1040 | |
| 1041 | if ($crossSource) { |
| 1042 | return self::opMoveFilesCrossSource( |
| 1043 | $ctx, |
| 1044 | $sourceFolder, |
| 1045 | $destinationFolder, |
| 1046 | $files, |
| 1047 | $sourceId, |
| 1048 | $destSourceId |
| 1049 | ); |
| 1050 | } |
| 1051 | |
| 1052 | $singlePayload = $payload; |
| 1053 | if ($sourceId !== '') { |
| 1054 | $singlePayload['sourceId'] = $sourceId; |
| 1055 | } |
| 1056 | |
| 1057 | return self::withSourceContext( |
| 1058 | $ctx, |
| 1059 | $singlePayload, |
| 1060 | true, |
| 1061 | function () use ($ctx, $sourceFolder, $destinationFolder, $files): array { |
| 1062 | $username = $ctx->username(); |
| 1063 | $perms = $ctx->permissions(); |
| 1064 | |
| 1065 | $hasSourceView = ACL::canReadOwn($username, $perms, $sourceFolder) |
| 1066 | || ACL::ownsFolderOrAncestor($username, $perms, $sourceFolder); |
| 1067 | if (!$hasSourceView) { |
| 1068 | throw new RuntimeException('Forbidden: no read access to source.', 403); |
| 1069 | } |
| 1070 | |
| 1071 | $hasSourceDelete = ACL::canDelete($username, $perms, $sourceFolder) |
| 1072 | || ACL::ownsFolderOrAncestor($username, $perms, $sourceFolder); |
| 1073 | if (!$hasSourceDelete) { |
| 1074 | throw new RuntimeException('Forbidden: no delete permission on source.', 403); |
| 1075 | } |
| 1076 | |
| 1077 | $hasDestMove = ACL::canMove($username, $perms, $destinationFolder) |
| 1078 | || ACL::ownsFolderOrAncestor($username, $perms, $destinationFolder); |
| 1079 | if (!$hasDestMove) { |
| 1080 | throw new RuntimeException('Forbidden: no move permission on destination.', 403); |
| 1081 | } |
no test coverage detected