* Save a file’s contents *and* record its metadata, including who uploaded it. * * @param string $folder Folder key (e.g. "root" or "invoices/2025") * @param string $fileName Basename of the file * @param resource|string $content File contents (stream or string) * @param string|null $uploader Username of uploader (if nul
(string $folder, string $fileName, $content, ?string $uploader = null)
| 1211 | * @return array ["success"=>"…"] or ["error"=>"…"] |
| 1212 | */ |
| 1213 | public static function saveFile(string $folder, string $fileName, $content, ?string $uploader = null): array |
| 1214 | { |
| 1215 | $folder = trim($folder) ?: 'root'; |
| 1216 | $fileName = basename(trim($fileName)); |
| 1217 | |
| 1218 | if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { |
| 1219 | return ["error" => "Invalid folder name"]; |
| 1220 | } |
| 1221 | if (!UploadNamePolicy::isAllowedForWrite($fileName)) { |
| 1222 | return ["error" => "Invalid file name"]; |
| 1223 | } |
| 1224 | |
| 1225 | $storage = self::storage(); |
| 1226 | $isLocal = $storage->isLocal(); |
| 1227 | $root = self::uploadRoot(); |
| 1228 | $baseDirReal = $isLocal ? realpath($root) : rtrim($root, '/\\'); |
| 1229 | if ($baseDirReal === false || $baseDirReal === '') { |
| 1230 | return ["error" => "Server misconfiguration"]; |
| 1231 | } |
| 1232 | |
| 1233 | $root = self::uploadRoot(); |
| 1234 | |
| 1235 | if (!$isLocal) { |
| 1236 | try { |
| 1237 | if (FolderCrypto::isEncryptedOrAncestor($folder)) { |
| 1238 | return ["error" => "Encrypted folders are not supported for remote storage."]; |
| 1239 | } |
| 1240 | } catch (\Throwable $e) { |
| 1241 | /* ignore */ |
| 1242 | } |
| 1243 | } |
| 1244 | |
| 1245 | $targetDir = (strtolower($folder) === 'root') |
| 1246 | ? rtrim($root, '/\\') . DIRECTORY_SEPARATOR |
| 1247 | : rtrim($root, '/\\') . DIRECTORY_SEPARATOR . trim($folder, "/\\ ") . DIRECTORY_SEPARATOR; |
| 1248 | |
| 1249 | // Ensure directory exists *before* realpath + containment check |
| 1250 | $dirStat = $storage->stat($targetDir); |
| 1251 | if ($dirStat === null || $dirStat['type'] !== 'dir') { |
| 1252 | if (!$storage->mkdir($targetDir, 0775, true)) { |
| 1253 | return ["error" => "Failed to create destination folder"]; |
| 1254 | } |
| 1255 | } |
| 1256 | |
| 1257 | if ($isLocal) { |
| 1258 | $targetDirReal = realpath($targetDir); |
| 1259 | if ($targetDirReal === false || strpos($targetDirReal, $baseDirReal) !== 0) { |
| 1260 | return ["error" => "Invalid folder path"]; |
| 1261 | } |
| 1262 | $filePath = $targetDirReal . DIRECTORY_SEPARATOR . $fileName; |
| 1263 | } else { |
| 1264 | $filePath = rtrim($targetDir, '/\\') . DIRECTORY_SEPARATOR . $fileName; |
| 1265 | } |
| 1266 | |
| 1267 | if (is_resource($content)) { |
| 1268 | if ($isLocal) { |
| 1269 | $out = fopen($filePath, 'wb'); |
| 1270 | if ($out === false) { |
no test coverage detected