request->getFiles($field); if (!is_array($file) || empty($file['tmp_name']) || (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE ) { return null; } if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { throw new LocalizedException(__('The %1 image could not be uploaded.', $field)); } $this->sanitizePngUpload((string) $file['tmp_name']); $relativeDirectory = self::MEDIA_DIRECTORY . '/' . $field . '/' . date('Y/m'); $media = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); $media->create($relativeDirectory); $uploader = $this->uploaderFactory->create(['fileId' => $field]); $uploader->setAllowedExtensions(['jpg', 'jpeg', 'png', 'gif', 'webp']); $uploader->setAllowRenameFiles(true); $uploader->setFilesDispersion(true); $saved = $uploader->save($media->getAbsolutePath($relativeDirectory)); if (!is_array($saved) || empty($saved['file'])) { throw new LocalizedException(__('Unable to save the %1 image.', $field)); } return trim($relativeDirectory . '/' . ltrim((string) $saved['file'], '/'), '/'); } /** * Strip optional PNG text chunks before Magento asks GD to validate the image. * * Some image exporters embed multi-megabyte iTXt payloads in tiny icons. * libpng versions with stricter chunk limits reject those files before GD can * inspect the actual pixels. Text chunks do not affect image rendering, so * rebuilding the PNG without them keeps the original image and CRC-protected * critical chunks intact while avoiding environment-dependent validation. */ public function sanitizePngUpload(string $path): void { if ($path === '' || !is_file($path)) { return; } $source = @fopen($path, 'rb'); if ($source === false) { return; } $signature = fread($source, 8); if ($signature !== self::PNG_SIGNATURE) { fclose($source); return; } $temporary = tempnam(dirname($path), 'esun-png-'); if ($temporary === false) { fclose($source); throw new LocalizedException(__('Unable to prepare the PNG image for validation.')); } $target = @fopen($temporary, 'wb'); if ($target === false) { fclose($source); @unlink($temporary); throw new LocalizedException(__('Unable to prepare the PNG image for validation.')); } $removedText = false; $hasHeader = false; $hasImageData = false; $hasEnd = false; try { $this->writeAll($target, self::PNG_SIGNATURE); while (!feof($source)) { $header = fread($source, 8); if ($header === '') { break; } if (strlen($header) !== 8) { throw new LocalizedException(__('The uploaded PNG image is incomplete.')); } $chunk = unpack('Nlength/a4type', $header); $length = (int) ($chunk['length'] ?? -1); $type = (string) ($chunk['type'] ?? ''); if ($length < 0 || !preg_match('/^[A-Za-z]{4}$/', $type)) { throw new LocalizedException(__('The uploaded PNG image has an invalid chunk.')); } $skip = in_array($type, self::PNG_TEXT_CHUNKS, true); if ($skip) { $this->copyBytes($source, null, $length + 4); $removedText = true; } else { $this->writeAll($target, $header); $this->copyBytes($source, $target, $length + 4); } $hasHeader = $hasHeader || $type === 'IHDR'; $hasImageData = $hasImageData || $type === 'IDAT'; if ($type === 'IEND') { $hasEnd = true; break; } } if (!$hasHeader || !$hasImageData || !$hasEnd) { throw new LocalizedException(__('The uploaded PNG image is incomplete.')); } fclose($source); $source = null; fflush($target); fclose($target); $target = null; if ($removedText && !@copy($temporary, $path)) { throw new LocalizedException(__('Unable to prepare the PNG image for validation.')); } } finally { if (is_resource($source)) { fclose($source); } if (is_resource($target)) { fclose($target); } @unlink($temporary); } } /** @param resource $source @param resource|null $target */ private function copyBytes($source, $target, int $remaining): void { while ($remaining > 0) { $data = fread($source, min(8192, $remaining)); if ($data === false || $data === '') { throw new LocalizedException(__('The uploaded PNG image is incomplete.')); } if ($target !== null) { $this->writeAll($target, $data); } $remaining -= strlen($data); } } /** @param resource $target */ private function writeAll($target, string $data): void { $offset = 0; $length = strlen($data); while ($offset < $length) { $written = fwrite($target, substr($data, $offset)); if ($written === false || $written === 0) { throw new LocalizedException(__('Unable to prepare the PNG image for validation.')); } $offset += $written; } } public function delete(string $path): void { $path = ltrim($path, '/'); if ($path === '' || !str_starts_with($path, self::MEDIA_DIRECTORY . '/')) { return; } $media = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); if ($media->isExist($path)) { $media->delete($path); } } }