* @param array $payload * @return array */
(McpOpsContext $ctx, array $payload)
| 1276 | * @return array<string,mixed> |
| 1277 | */ |
| 1278 | private static function opMoveFolder(McpOpsContext $ctx, array $payload): array |
| 1279 | { |
| 1280 | self::assertWritableAccount($ctx, false); |
| 1281 | |
| 1282 | $source = self::normalizeFolder((string)($payload['source'] ?? '')); |
| 1283 | if ($source === '' || $source === 'root') { |
| 1284 | throw new RuntimeException('Invalid source folder.', 400); |
| 1285 | } |
| 1286 | |
| 1287 | $destinationRaw = trim((string)($payload['destination'] ?? 'root')); |
| 1288 | $destination = self::normalizeFolder($destinationRaw === '' ? 'root' : $destinationRaw); |
| 1289 | |
| 1290 | $mode = strtolower(trim((string)($payload['mode'] ?? 'move'))); |
| 1291 | if ($mode !== '' && $mode !== 'move') { |
| 1292 | throw new RuntimeException('Unsupported mode for MCP folder move.', 400); |
| 1293 | } |
| 1294 | |
| 1295 | $sourcePair = self::resolveSourcePair($payload); |
| 1296 | $sourceId = $sourcePair['sourceId']; |
| 1297 | $destSourceId = $sourcePair['destSourceId']; |
| 1298 | $crossSource = $sourcePair['crossSource']; |
| 1299 | |
| 1300 | if ($crossSource && (!class_exists(SourceContext::class) || !SourceContext::sourcesEnabled())) { |
| 1301 | throw new RuntimeException('Cross-source operations require sources to be enabled.', 400); |
| 1302 | } |
| 1303 | |
| 1304 | $srcNorm = trim($source, "/\\ "); |
| 1305 | $dstNorm = $destination === 'root' ? '' : trim($destination, "/\\ "); |
| 1306 | if ( |
| 1307 | !$crossSource |
| 1308 | && $dstNorm !== '' |
| 1309 | && (strcasecmp($dstNorm, $srcNorm) === 0 || strpos($dstNorm . '/', $srcNorm . '/') === 0) |
| 1310 | ) { |
| 1311 | throw new RuntimeException('Destination cannot be the source or its descendant.', 400); |
| 1312 | } |
| 1313 | |
| 1314 | $baseName = basename(str_replace('\\', '/', $srcNorm)); |
| 1315 | if ($baseName === '' || !preg_match((string)REGEX_FOLDER_NAME, $baseName)) { |
| 1316 | throw new RuntimeException('Invalid source folder.', 400); |
| 1317 | } |
| 1318 | $target = $destination === 'root' ? $baseName : rtrim($destination, "/\\ ") . '/' . $baseName; |
| 1319 | $target = self::normalizeFolder($target); |
| 1320 | |
| 1321 | $username = $ctx->username(); |
| 1322 | $perms = $ctx->permissions(); |
| 1323 | |
| 1324 | if ($crossSource) { |
| 1325 | self::assertWritableAccount($ctx, true); |
| 1326 | if ($sourceId === '' || $destSourceId === '') { |
| 1327 | throw new RuntimeException('Cross-source move requires both source ids.', 400); |
| 1328 | } |
| 1329 | |
| 1330 | self::withSourceContext( |
| 1331 | $ctx, |
| 1332 | ['sourceId' => $sourceId], |
| 1333 | true, |
| 1334 | function () use ($ctx, $source, $username, $perms): array { |
| 1335 | $canManageSource = ACL::canManage($username, $perms, $source) |
no test coverage detected