*/ class FileManagerApi { private $basePath = null; private $translate; public function __construct($basePath = null, $lang = 'en', $muteErrors = true) { if ($muteErrors) { ini_set('display_errors', 0); } $this->basePath = $basePath ?: dirname(__DIR__); $this->translate = new Translate($lang); } public function postHandler($query, $request, $files) { $t = $this->translate; // Probably file upload if (!isset($request['action']) && (isset($_SERVER["CONTENT_TYPE"]) && strpos($_SERVER["CONTENT_TYPE"], 'multipart/form-data') !== false) ) { $uploaded = $this->uploadAction($request['destination'], $files); if ($uploaded === true) { $response = $this->simpleSuccessResponse(); } else { $response = $this->simpleErrorResponse($t->upload_failed); } return $response; } switch ($request['action']) { case 'list': $list = $this->listAction($request['path']); if (!is_array($list)) { $response = $this->simpleErrorResponse($t->listing_failed); } else { $response = new Response(); $response->setData([ 'result' => $list ]); } break; case 'rename': $renamed = $this->renameAction($request['item'], $request['newItemPath']); if ($renamed === true) { $response = $this->simpleSuccessResponse(); } elseif ($renamed === 'notfound') { $response = $this->simpleErrorResponse($t->file_not_found); } else { $response = $this->simpleErrorResponse($t->renaming_failed); } break; case 'move': $moved = $this->moveAction($request['items'], $request['newPath']); if ($moved === true) { $response = $this->simpleSuccessResponse(); } else { $response = $this->simpleErrorResponse($t->moving_failed); } break; case 'copy': $copied = $this->copyAction($request['items'], $request['newPath']); if ($copied === true) { $response = $this->simpleSuccessResponse(); } else { $response = $this->simpleErrorResponse($t->copying_failed); } break; case 'remove': $removed = $this->removeAction($request['items']); if ($removed === true) { $response = $this->simpleSuccessResponse(); } elseif ($removed === 'notempty') { $response = $this->simpleErrorResponse($t->removing_failed_directory_not_empty); } else { $response = $this->simpleErrorResponse($t->removing_failed); } break; case 'edit': $edited = $this->editAction($request['item'], $request['content']); if ($edited !== false) { $response = $this->simpleSuccessResponse(); } else { $response = $this->simpleErrorResponse($t->saving_failed); } break; case 'getContent': $content = $this->getContentAction($request['item']); if ($content !== false) { $response = new Response(); $response->setData([ 'result' => $content ]); } else { $response = $this->simpleErrorResponse($t->file_not_found); } break; case 'createFolder': $created = $this->createFolderAction($request['newPath']); if ($created === true) { $response = $this->simpleSuccessResponse(); } elseif ($created === 'exists') { $response = $this->simpleErrorResponse($t->folder_already_exists); } else { $response = $this->simpleErrorResponse($t->folder_creation_failed); } break; case 'changePermissions': $changed = $this->changePermissionsAction($request['items'], $request['perms'], $request['recursive']); if ($changed === true) { $response = $this->simpleSuccessResponse(); } elseif ($changed === 'missing') { $response = $this->simpleErrorResponse($t->file_not_found); } else { $response = $this->simpleErrorResponse($t->permissions_change_failed); } break; case 'compress': $compressed = $this->compressAction( $request['items'], $request['destination'], $request['compressedFilename'] ); if ($compressed === true) { $response = $this->simpleSuccessResponse(); } else { $response = $this->simpleErrorResponse($t->compression_failed); } break; case 'extract': $extracted = $this->extractAction($request['destination'], $request['item'], $request['folderName']); if ($extracted === true) { $response = $this->simpleSuccessResponse(); } elseif ($extracted === 'unsupported') { $response = $this->simpleErrorResponse($t->archive_opening_failed); } else { $response = $this->simpleErrorResponse($t->extraction_failed); } break; default: $response = $this->simpleErrorResponse($t->function_not_implemented); break; } return $response; } public function getHandler($queries) { $t = $this->translate; switch ($queries['action']) { case 'download': $downloaded = $this->downloadAction($queries['path']); if ($downloaded === true) { exit; } else { $response = $this->simpleErrorResponse($t->file_not_found); } break; case 'downloadMultiple': $downloaded = $this->downloadMultipleAction($queries['items'], $queries['toFilename']); if ($downloaded === true) { exit; } else { $response = $this->simpleErrorResponse($t->file_not_found); } break; default: $response = $this->simpleErrorResponse($t->function_not_implemented); break; } return $response; } private function downloadAction($path) { $file_name = basename($path); $path = $this->canonicalizePath($this->basePath . $path); if (!file_exists($path)) { return false; } $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $path); finfo_close($finfo); if (ob_get_level()) { ob_end_clean(); } header("Content-Disposition: attachment; filename=\"$file_name\""); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header("Content-Type: $mime_type"); header('Pragma: public'); header('Content-Length: ' . filesize($path)); readfile($path); return true; } private function downloadMultipleAction($items, $archiveName) { $archivePath = tempnam('../', 'archive'); $zip = new \ZipArchive(); if ($zip->open($archivePath, \ZipArchive::CREATE) !== true) { unlink($archivePath); return false; } foreach ($items as $path) { $zip->addFile($this->basePath . $path, basename($path)); } $zip->close(); header("Content-Disposition: attachment; filename=\"$archiveName\""); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header("Content-Type: application/zip"); header('Pragma: public'); header('Content-Length: ' . filesize($archivePath)); readfile($archivePath); unlink($archivePath); return true; } private function uploadAction($path, $files) { $path = $this->canonicalizePath($this->basePath . $path); foreach ($_FILES as $file) { $fileInfo = pathinfo($file['name']); $fileName = $this->normalizeName($fileInfo['filename']) . '.' . $fileInfo['extension']; $uploaded = move_uploaded_file( $file['tmp_name'], $path . DIRECTORY_SEPARATOR . $fileName ); if ($uploaded === false) { return false; } } return true; } private function listAction($path) { $files = array_values(array_filter( scandir($this->basePath . $path), function ($path) { return !($path === '.' || $path === '..'); } )); $files = array_map(function ($file) use ($path) { $file = $this->canonicalizePath( $this->basePath . $path . DIRECTORY_SEPARATOR . $file ); $date = new \DateTime('@' . filemtime($file)); return [ 'name' => basename($file), 'rights' => $this->parsePerms(fileperms($file)), 'size' => filesize($file), 'date' => $date->format('Y-m-d H:i:s'), 'type' => is_dir($file) ? 'dir' : 'file' ]; }, $files); return $files; } public function listActionData($path) { $files = array_values(array_filter( scandir($this->basePath . $path), function ($path) { return !($path === '.' || $path === '..'); } )); $files = array_map(function ($file) use ($path) { $file = $this->canonicalizePath( $this->basePath . $path . DIRECTORY_SEPARATOR . $file ); $date = new \DateTime('@' . filemtime($file)); return [ 'name' => basename($file), 'rights' => $this->parsePerms(fileperms($file)), 'size' => filesize($file), 'date' => $date->format('Y-m-d H:i:s'), 'type' => is_dir($file) ? 'dir' : 'file' ]; }, $files); return $files; } private function renameAction($oldPath, $newPath) { $oldPath = $this->basePath . $oldPath; $newPath = $this->basePath . $newPath; if (! file_exists($oldPath)) { return 'notfound'; } return rename($oldPath, $newPath); } private function moveAction($oldPaths, $newPath) { $newPath = $this->basePath . $this->canonicalizePath($newPath) . DIRECTORY_SEPARATOR; foreach ($oldPaths as $oldPath) { if (!file_exists($this->basePath . $oldPath)) { return false; } $renamed = rename($this->basePath . $oldPath, $newPath . basename($oldPath)); if ($renamed === false) { return false; } } return true; } private function copyAction($oldPaths, $newPath) { $newPath = $this->basePath . $this->canonicalizePath($newPath) . DIRECTORY_SEPARATOR; foreach ($oldPaths as $oldPath) { if (!file_exists($this->basePath . $oldPath)) { return false; } $copied = copy( $this->basePath . $oldPath, $newPath . basename($oldPath) ); if ($copied === false) { return false; } } return true; } private function removeAction($paths) { foreach ($paths as $path) { $path = $this->canonicalizePath($this->basePath . $path); if (is_dir($path)) { $dirEmpty = (new \FilesystemIterator($path))->valid(); if ($dirEmpty) { return 'notempty'; } else { $removed = rmdir($path); } } else { $removed = unlink($path); } if ($removed === false) { return false; } } return true; } private function editAction($path, $content) { $path = $this->basePath . $path; return file_put_contents($path, $content); } private function getContentAction($path) { $path = $this->basePath . $path; if (! file_exists($path)) { return false; } return file_get_contents($path); } private function createFolderAction($path) { $path = $this->basePath . $path; if (file_exists($path) && is_dir($path)) { return 'exists'; } return mkdir($path); } private function changePermissionsAction($paths, $permissions, $recursive) { foreach ($paths as $path) { if (!file_exists($this->basePath . $path)) { return 'missing'; } if (is_dir($path) && $recursive === true) { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $item) { $changed = chmod($this->basePath . $item, octdec($permissions)); if ($changed === false) { return false; } } } return chmod($this->basePath . $path, octdec($permissions)); } } private function compressAction($paths, $destination, $archiveName) { $archivePath = $this->basePath . $destination . $archiveName; $zip = new \ZipArchive(); if ($zip->open($archivePath, \ZipArchive::CREATE) !== true) { return false; } foreach ($paths as $path) { $fullPath = $this->basePath . $path; if (is_dir($fullPath)) { $dirs = [ [ 'dir' => basename($path), 'path' => $this->canonicalizePath($this->basePath . $path), ] ]; while (count($dirs)) { $dir = current($dirs); $zip->addEmptyDir($dir['dir']); $dh = opendir($dir['path']); while ($file = readdir($dh)) { if ($file != '.' && $file != '..') { $filePath = $dir['path'] . DIRECTORY_SEPARATOR . $file; if (is_file($filePath)) { $zip->addFile( $dir['path'] . DIRECTORY_SEPARATOR . $file, $dir['dir'] . '/' . basename($file) ); } elseif (is_dir($filePath)) { $dirs[] = [ 'dir' => $dir['dir'] . '/' . $file, 'path' => $dir['path'] . DIRECTORY_SEPARATOR . $file ]; } } } closedir($dh); array_shift($dirs); } } else { $zip->addFile($path, basename($path)); } } return $zip->close(); } private function extractAction($destination, $archivePath, $folderName) { $archivePath = $this->basePath . $archivePath; $folderPath = $this->basePath . $this->canonicalizePath($destination) . DIRECTORY_SEPARATOR . $folderName; $zip = new \ZipArchive; if ($zip->open($archivePath) === false) { return 'unsupported'; } mkdir($folderPath); $zip->extractTo($folderPath); return $zip->close(); } private function simpleSuccessResponse() { $response = new Response(); $response->setData([ 'result' => [ 'success' => true ] ]); return $response; } private function simpleErrorResponse($message) { $response = new Response(); $response ->setStatus(500, 'Internal Server Error') ->setData([ 'result' => [ 'success' => false, 'error' => $message ] ]); return $response; } private function parsePerms($perms) { if (($perms & 0xC000) == 0xC000) { // Socket $info = 's'; } elseif (($perms & 0xA000) == 0xA000) { // Symbolic Link $info = 'l'; } elseif (($perms & 0x8000) == 0x8000) { // Regular $info = '-'; } elseif (($perms & 0x6000) == 0x6000) { // Block special $info = 'b'; } elseif (($perms & 0x4000) == 0x4000) { // Directory $info = 'd'; } elseif (($perms & 0x2000) == 0x2000) { // Character special $info = 'c'; } elseif (($perms & 0x1000) == 0x1000) { // FIFO pipe $info = 'p'; } else { // Unknown $info = 'u'; } // Owner $info .= (($perms & 0x0100) ? 'r' : '-'); $info .= (($perms & 0x0080) ? 'w' : '-'); $info .= (($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x' ) : (($perms & 0x0800) ? 'S' : '-')); // Group $info .= (($perms & 0x0020) ? 'r' : '-'); $info .= (($perms & 0x0010) ? 'w' : '-'); $info .= (($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x' ) : (($perms & 0x0400) ? 'S' : '-')); // World $info .= (($perms & 0x0004) ? 'r' : '-'); $info .= (($perms & 0x0002) ? 'w' : '-'); $info .= (($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x' ) : (($perms & 0x0200) ? 'T' : '-')); return $info; } private function canonicalizePath($path) { $dirSep = DIRECTORY_SEPARATOR; $wrongDirSep = DIRECTORY_SEPARATOR === '/' ? '\\' : '/'; // Replace incorrect dir separators $path = str_replace($wrongDirSep, $dirSep, $path); $path = explode($dirSep, $path); $stack = array(); foreach ($path as $seg) { if ($seg == '..') { // Ignore this segment, remove last segment from stack array_pop($stack); continue; } if ($seg == '.') { // Ignore this segment continue; } $stack[] = $seg; } // Remove last / if (empty($stack[count($stack) - 1])) { array_pop($stack); } return implode($dirSep, $stack); } /** * Creates ASCII name * * @param string name encoded in UTF-8 * @return string name containing only numbers, chars without diacritics, underscore and dash * @copyright Jakub Vrána, https://php.vrana.cz/ */ private function normalizeName($name) { $name = preg_replace('~[^\\pL0-9_]+~u', '-', $name); $name = trim($name, "-"); //$name = iconv("utf-8", "us-ascii//TRANSLIT", $name); $name = preg_replace('~[^-a-z0-9_]+~', '', $name); return $name; } }