getPublicPath($uploadType, $addPath); $storagePath = $this->getStoragePath($uploadType, $addPath); // 해당 경로가 없다면 생성 해주고 index.php 파일 생성 if (is_dir($storagePath) === false) { mkdir($storagePath, 0707, true); touch($storagePath . '/index.php'); chmod($storagePath . '/index.php', 0644); } return $uploadPath; } /* * Public 저장 경로 조회 */ public function getPublicPath(string $uploadType, string|null $addPath = null): string { return (join(DIRECTORY_SEPARATOR, [ UPLOAD_PATH_PUBLIC, $uploadType, date('Y'), date('m'), date('d'), $addPath ]) . DIRECTORY_SEPARATOR); } /* * Storage 저장 경로 조회 */ public function getStoragePath(string $uploadType, string|null $addPath = null): string { return storage_path(UPLOAD_PATH_APP . DIRECTORY_SEPARATOR . $this->getPublicPath($uploadType, $addPath)); } /* * 경로 완전 삭제 */ public function removePath(string $uploadType, string|null $addPath = null): void { $storagePath = $this->getStoragePath($uploadType, $addPath); if(is_dir($storagePath)) { foreach (scandir($storagePath) as $file) { if (in_array($file, ['.', '..'])) { continue; } $path = ($storagePath . DIRECTORY_SEPARATOR . $file); if(is_dir($path)) { $this->removePath($uploadType, $addPath . DIRECTORY_SEPARATOR . $file); }else{ unlink($path); } } rmdir($storagePath); } } /* * 본문 내용중 외부 이미지 주소를 서버로 가져온 후에 내부 주소로 변경합니다. */ public function saveAsImage(string|null $contents, string $uploadType, string|null $addPath = null): string|null { return $this->saveImageFromBlob( $this->saveImageFromUrl($contents, $uploadType, $addPath), $uploadType, $addPath); } /* * CURL를 이용한 외부 이미지 다운로드 */ public function saveImageFromUrl(string|null $contents, string $uploadType, string|null $addPath = null): string|null { $dom = new DOMDocument('1.0', 'UTF-8'); libxml_use_internal_errors(true); $dom->loadHTML('' . $contents); libxml_clear_errors(); $images = []; foreach ($dom->getElementsByTagName('img') as $img) { $src = $img->attributes->getNamedItem("src")->value; $path = canonicalizePath($src); if(file_exists(public_path($path))) { $images[] = $path; continue; } if(filter_var($src, FILTER_VALIDATE_URL)) { $url = parse_url($src); if (!empty($url['host']) && $url['host'] !== request()->getHttpHost()) { $ch = curl_init($src); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $err = curl_error($ch); $rawData = []; if (empty($err)) { $rawData = curl_exec($ch); } curl_close($ch); if ($rawData) { // 파일 경로 설정 $uploadPath = $this->setPath($uploadType, $addPath); $ext = $this->getExtension($src); [$unix, $sec] = explode(' ', microtime()); $fileName = (md5(uniqid(mt_rand())) . '_' . str_replace('.', '', $sec . $unix) . '.' . $ext); $savePath = Storage::path($uploadPath . $fileName); $saveURL = Storage::url($uploadPath . $fileName); $fp = fopen($savePath, 'w+'); fwrite($fp, $rawData); fclose($fp); if (file_exists($savePath)) { $images[] = $saveURL; $contents = str_replace($src, $saveURL, $contents); } } } } } // 기존 이미지 삭제 $this->clear($images, $uploadType, $addPath); unset($dom, $images, $uploadPath, $addPath); return $contents; } /* * blob 종류의 이미지 저장 */ public function saveImageFromBlob(string|null $contents, string $uploadType, string|null $addPath = null): string|null { $dom = new DOMDocument('1.0', 'UTF-8'); libxml_use_internal_errors(true); $dom->loadHTML('' . $contents); libxml_clear_errors(); $images = []; foreach ($dom->getElementsByTagName('img') as $img) { $src = $img->attributes->getNamedItem("src")?->value; $alt = $img->attributes->getNamedItem("alt")?->value; $target = $img->attributes->getNamedItem("data-target")?->value; $path = canonicalizePath($src); if(file_exists(public_path($path)) || !$target || !strpos($src, 'base64')) { $images[] = $path; continue; } try { $mime = mime_content_type($src); $ext = $this->getMimeExtension($mime); if (!$ext) { $ext = $this->getExtension($alt); } } catch (Exception) { return null; } // 파일 경로 설정 $uploadPath = $this->setPath($uploadType, $addPath); $link = str_replace('data:' . $mime . ';base64,', '', $src); $link = str_replace(' ', '+', $link); $isBase64 = isBase64($link); $link = base64_decode($link); [$unix, $sec] = explode(' ', microtime()); $fileName = (md5(uniqid(mt_rand())) . '_' . str_replace('.', '', $sec . $unix) . '.' . $ext); $savePath = Storage::path($uploadPath . $fileName); $saveUrl = Storage::url($uploadPath . $fileName); // 파일저장 if ($isBase64) { file_put_contents($savePath, $link, FILE_APPEND | LOCK_EX); } if (file_exists($savePath)) { $images[] = $saveUrl; $contents = str_replace($src, $saveUrl, $contents); } } // 기존 이미지 삭제 $this->clear($images, $uploadType, $addPath); unset($dom, $images, $uploadPath, $addPath); return $contents; } public function isBase64($s): bool { // Check if there are valid base64 characters if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $s)) { return false; } // Decode the string in strict mode and check the results $decoded = base64_decode($s, true); if (false === $decoded) { return false; } // if string returned contains not printable chars if (0 < preg_match('/((?![[:graph:]])(?!\s)(?!\p{L}))./', $decoded, $matched)) { return false; } // Encode the string again if (base64_encode($decoded) != $s) { return false; } return true; } public function isBase64Encoded(string $s): bool { if ((bool)preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $s) === false) { return false; } $decoded = base64_decode($s, true); if ($decoded === false) { return false; } $encoding = mb_detect_encoding($decoded); if (!in_array($encoding, ['UTF-8', 'ASCII'], true)) { return false; } return ($decoded !== false && base64_encode($decoded) === $s); } /* * 파일 확장자 조회 */ public function getExtension(string $path): string { return pathinfo($path, PATHINFO_EXTENSION) ?? 'unknown'; } /* * 이미지 파일 여부 */ public function isImage(string $path): bool { if(!file_exists($path)) { return false; } $mime = mime_content_type($path); if($mime) { return in_array($mime, [ image_type_to_mime_type(IMAGETYPE_GIF), image_type_to_mime_type(IMAGETYPE_JPEG), image_type_to_mime_type(IMAGETYPE_PNG), image_type_to_mime_type(IMAGETYPE_BMP)] ); } return false; } /* * 첨부파일 정보 조회 */ public function upload(UploadedFile $file, string $uploadType, string|null $addPath = null): object { $publicPath = $this->getPublicPath($uploadType, $addPath); // 파일 경로 조회 $filePath = $file->store($publicPath); // 파일 저장 $fullPath = Storage::path($filePath); $fullURL = Storage::url($filePath); $imageInfo = $this->getImageInfo($fullPath); return (object)[ 'name' => $file->hashName(), 'path' => $fullPath, 'url' => $fullURL, 'originName' => $file->getClientOriginalName(), 'extension' => $file->getClientOriginalExtension(), 'size' => $file->getSize(), 'isImage' => $imageInfo->answer, 'imageWidth' => $imageInfo->width, 'imageHeight' => $imageInfo->height, 'imageType' => $imageInfo->type ]; } /* * 이미지 크기 조정 */ public function resize(string $filePath, int $width, int $height): void { [$w, $h, $ext] = getimagesize($filePath); $ratio = max($width / $w, $height / $h); $h = ceil($height / $ratio); $x = ($w - $width / $ratio) / 2; $w = ceil($width / $ratio); $imgString = file_get_contents($filePath); $image = imagecreatefromstring($imgString); $tmp = imagecreatetruecolor($width, $height); imagecopyresampled($tmp, $image, 0, 0, $x, 0, $width, $height, $w, $h); switch ($ext) { case IMAGETYPE_JPEG: imagejpeg($tmp, $filePath, 100); break; case IMAGETYPE_PNG: imagepng($tmp, $filePath, 0); break; case IMAGETYPE_GIF: imagegif($tmp, $filePath); break; default: break; } imagedestroy($image); imagedestroy($tmp); } /* * mime_content_type 값의 확장자 반환 */ public function getMimeExtension(string $imageType): string { return ([ 'image/bmp' => 'bmp', 'image/cis-cod' => 'cod', 'image/gif' => 'gif', 'image/ief' => 'ief', 'image/jpeg' => 'jpeg', 'image/pipeg' => 'jfif', 'image/tiff' => 'tif', 'image/x-cmu-raster' => 'ras', 'image/x-cmx' => 'cmx', 'image/x-icon' => 'ico', 'image/x-portable-anymap' => 'pnm', 'image/x-portable-bitmap' => 'pbm', 'image/x-portable-graymap' => 'pgm', 'image/x-portable-pixmap' => 'ppm', 'image/x-rgb' => 'rgb', 'image/x-xbitmap' => 'xbm', 'image/x-xpixmap' => 'xpm', 'image/x-xwindowdump' => 'xwd', 'image/png' => 'png', 'image/x-jps' => 'jps', 'image/x-freehand' => 'fh' ][$imageType] ?? ''); } /* * Image type 반환 */ public function getImageType(int $imageType): string { return ([ IMAGETYPE_UNKNOWN => 'UNKNOWN', IMAGETYPE_GIF => 'GIF', IMAGETYPE_JPEG => 'JPEG', IMAGETYPE_PNG => 'PNG', IMAGETYPE_SWF => 'SWF', IMAGETYPE_PSD => 'PSD', IMAGETYPE_BMP => 'BMP', IMAGETYPE_TIFF_II => 'TIFF_II', IMAGETYPE_TIFF_MM => 'TIFF_MM', IMAGETYPE_JPC => 'JPC', IMAGETYPE_JP2 => 'JP2', IMAGETYPE_JPX => 'JPX', IMAGETYPE_JB2 => 'JB2', IMAGETYPE_SWC => 'SWC', IMAGETYPE_IFF => 'IFF', IMAGETYPE_WBMP => 'WBMP', IMAGETYPE_XBM => 'XBM', IMAGETYPE_ICO => 'ICO', IMAGETYPE_WEBP => 'WEBP', IMAGETYPE_AVIF => 'AVIF', IMAGETYPE_COUNT => 'COUNT' ][$imageType] ?? ''); } /* * Image Info 반환 */ public function getImageInfo(string $path): object { $ret = (object)[ 'answer' => $this->isImage($path), 'name' => basename($path), 'size' => filesize($path), 'type' => null, 'width' => 0, 'height' => 0, 'ext' => null ]; if(!file_exists($path)) { return $ret; } if($ret->answer) { [$ret->width, $ret->height, $ret->ext] = getimagesize($path); $ret->type = $this->getImageType($ret->ext); } return $ret; } /* * 파일 삭제 */ public function remove(string $path): bool { if (file_exists($path)) { return unlink($path); } $path = public_path($path); if (file_exists($path)) { return unlink($path); } return false; } /* * 본문 내용에 없는 이미지를 삭제한다. */ public function clear(array $images, string $uploadType, string|null $addPath = null): void { $images = array_map(function ($src) { return last(explode(DIRECTORY_SEPARATOR, $src)); }, $images); $storagePath = $this->getStoragePath($uploadType, $addPath); if (is_dir($storagePath)) { foreach (scandir($storagePath) as $file) { if (in_array($file, ['.', '..', 'index.php'])) { continue; } if (!in_array($file, $images)) { // 에디터에 없는 이미지면 삭제 $filePath = ($storagePath . $file); if (is_dir($filePath)) { continue; } unlink($filePath); } } // 이미지가 없으면 디렉토리 삭제 if (count($images) <= 0) { if(unlink($storagePath . 'index.php')) { rmdir($storagePath); } } } unset($images, $uploadType, $addPath, $storagePath); } /* * 이미지 정보 반환 */ public function getImageFromContent(?string $content): array { $dom = new DOMDocument('1.0', 'UTF-8'); libxml_use_internal_errors(true); $dom->loadHTML('' . $content); libxml_clear_errors(); $images = []; foreach($dom->getElementsByTagName('img') as $img) { $src = canonicalizePath($img->attributes->getNamedItem("src")?->value); $origin = $img->attributes->getNamedItem("alt")?->value; $path = public_path($src); if(!file_exists($path)) { continue; } $imageInfo = $this->getImageInfo($path); $images[] = (object)[ 'origin' => $origin, 'name' => $imageInfo->name, 'path' => $path, 'url' => $src, 'size' => $imageInfo->size, 'type' => $imageInfo->type, 'width' => $imageInfo->width, 'height' => $imageInfo->height ]; } return $images; } }