init push

This commit is contained in:
2026-07-16 22:16:45 +07:00
commit 8b068bdb10
1021 changed files with 332816 additions and 0 deletions

View File

@@ -0,0 +1,511 @@
package handlers
import (
"archive/zip"
"bytes"
"context"
"errors"
"io"
"log/slog"
"path/filepath"
"regexp"
"strconv"
"strings"
filemanager "wucher/internal/domain/file_manager"
)
var (
xlsxVMLStyleAttrRE = regexp.MustCompile(`(?i)(style\s*=\s*["'])([^"']*)(["'])`)
xlsxVMLLengthTokenRE = regexp.MustCompile(`^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:pt|px|in|cm|mm|pc|em|ex|%)$`)
xlsxVMLShapeTagRE = regexp.MustCompile(`(?is)<v:shape\b[^>]*>`)
xlsxVMLShapeIDAttrRE = regexp.MustCompile(`(?i)(\bid\s*=\s*["'])(_x0000_s\d+)(["'])`)
xlsxVMLShapeIDNumRE = regexp.MustCompile(`(?i)^_x0000_s(\d+)$`)
xlsxVMLShapeTypeRE = regexp.MustCompile(`(?i)<v:shapetype\b`)
xlsxVMLNoteTypeRefRE = regexp.MustCompile(`(?i)type\s*=\s*["']#_x0000_t202["']`)
xlsxVMLRootTagRE = regexp.MustCompile(`(?is)<xml\b[^>]*>`)
)
// xlsxVMLNoteShapeTypeHeader is the standard <o:shapelayout>/<v:shapetype>
// preamble that Excel writes at the top of every comment/note VML drawing.
// Some producers (and files that have been round-tripped through other tools)
// omit it while still emitting shapes that reference type="#_x0000_t202";
// LibreOffice/Collabora then flags the package as corrupt and offers a repair.
// Injecting the missing definition makes the shape reference resolve and
// removes the repair prompt.
const xlsxVMLNoteShapeTypeHeader = `<o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout>` +
`<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe">` +
`<v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>`
type xlsxSanitizerStorage interface {
GetObject(context.Context, string) (io.ReadCloser, filemanager.ObjectMetadata, error)
PutObject(context.Context, filemanager.PutObjectInput) (filemanager.ObjectMetadata, error)
}
type xlsxSanitizerFileUpdater interface {
UpdateFile(context.Context, *filemanager.File) error
}
func sanitizeXLSXPayload(payload []byte) ([]byte, int, bool, error) {
reader, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
if err != nil {
return nil, 0, false, errors.New("file content is not a valid office zip document")
}
var buf bytes.Buffer
writer := zip.NewWriter(&buf)
if reader.Comment != "" {
if err := writer.SetComment(reader.Comment); err != nil {
_ = writer.Close()
return nil, 0, false, err
}
}
totalStyleFixes := 0
changed := false
for _, file := range reader.File {
header := file.FileHeader
header.Name = file.Name
header.Comment = file.Comment
header.NonUTF8 = file.NonUTF8
if file.FileInfo().IsDir() {
entry, err := writer.CreateHeader(&header)
if err != nil {
_ = writer.Close()
return nil, 0, false, err
}
if _, err := entry.Write(nil); err != nil {
_ = writer.Close()
return nil, 0, false, err
}
continue
}
if !isVMLDrawingEntry(file.Name) {
raw, err := file.OpenRaw()
if err != nil {
_ = writer.Close()
return nil, 0, false, err
}
entry, err := writer.CreateRaw(&header)
if err != nil {
_ = writer.Close()
return nil, 0, false, err
}
if _, err := io.Copy(entry, raw); err != nil {
_ = writer.Close()
return nil, 0, false, err
}
continue
}
rc, err := file.Open()
if err != nil {
_ = writer.Close()
return nil, 0, false, err
}
filePayload, readErr := io.ReadAll(rc)
_ = rc.Close()
if readErr != nil {
_ = writer.Close()
return nil, 0, false, readErr
}
sanitizedPayload, styleFixes, fileChanged, err := sanitizeVMLDrawingPayload(filePayload)
if err != nil {
_ = writer.Close()
return nil, 0, false, err
}
if dedupedPayload, idFixes, idChanged := dedupeVMLShapeIDs(sanitizedPayload); idChanged {
sanitizedPayload = dedupedPayload
fileChanged = true
totalStyleFixes += idFixes
}
if shapeTypePayload, shapeTypeFixes, shapeTypeChanged := ensureVMLNoteShapeType(sanitizedPayload); shapeTypeChanged {
sanitizedPayload = shapeTypePayload
fileChanged = true
totalStyleFixes += shapeTypeFixes
}
if fileChanged {
changed = true
totalStyleFixes += styleFixes
}
if !fileChanged {
raw, err := file.OpenRaw()
if err != nil {
_ = writer.Close()
return nil, 0, false, err
}
entry, err := writer.CreateRaw(&header)
if err != nil {
_ = writer.Close()
return nil, 0, false, err
}
if _, err := io.Copy(entry, raw); err != nil {
_ = writer.Close()
return nil, 0, false, err
}
continue
}
entry, err := writer.CreateHeader(&header)
if err != nil {
_ = writer.Close()
return nil, 0, false, err
}
if _, err := entry.Write(sanitizedPayload); err != nil {
_ = writer.Close()
return nil, 0, false, err
}
}
if err := writer.Close(); err != nil {
return nil, 0, false, err
}
if !changed {
return payload, 0, false, nil
}
return buf.Bytes(), totalStyleFixes, true, nil
}
func sanitizeVMLDrawingPayload(payload []byte) ([]byte, int, bool, error) {
totalFixes := 0
changed := false
rewritten := xlsxVMLStyleAttrRE.ReplaceAllFunc(payload, func(match []byte) []byte {
sub := xlsxVMLStyleAttrRE.FindSubmatch(match)
if len(sub) != 4 {
return match
}
normalized, styleFixes, fileChanged := sanitizeVMLStyleValue(string(sub[2]))
if !fileChanged {
return match
}
totalFixes += styleFixes
changed = true
out := make([]byte, 0, len(sub[1])+len(normalized)+len(sub[3]))
out = append(out, sub[1]...)
out = append(out, normalized...)
out = append(out, sub[3]...)
return out
})
if !changed {
return payload, 0, false, nil
}
return rewritten, totalFixes, true, nil
}
func dedupeVMLShapeIDs(payload []byte) ([]byte, int, bool) {
if len(payload) == 0 {
return payload, 0, false
}
source := string(payload)
tags := xlsxVMLShapeTagRE.FindAllStringIndex(source, -1)
if len(tags) == 0 {
return payload, 0, false
}
maxID := 0
for _, match := range xlsxVMLShapeIDAttrRE.FindAllStringSubmatch(source, -1) {
if len(match) < 3 {
continue
}
if sub := xlsxVMLShapeIDNumRE.FindStringSubmatch(match[2]); len(sub) == 2 {
if n, err := strconv.Atoi(sub[1]); err == nil && n > maxID {
maxID = n
}
}
}
if maxID == 0 {
return payload, 0, false
}
seen := make(map[string]struct{}, len(tags))
var buf strings.Builder
buf.Grow(len(source) + len(tags)*8)
last := 0
nextID := maxID + 1
fixCount := 0
for _, loc := range tags {
tag := source[loc[0]:loc[1]]
newTag, changed := dedupeVMLShapeIDTag(tag, seen, &nextID)
if changed {
fixCount++
}
buf.WriteString(source[last:loc[0]])
buf.WriteString(newTag)
last = loc[1]
}
buf.WriteString(source[last:])
if fixCount == 0 {
return payload, 0, false
}
return []byte(buf.String()), fixCount, true
}
func dedupeVMLShapeIDTag(tag string, seen map[string]struct{}, nextID *int) (string, bool) {
match := xlsxVMLShapeIDAttrRE.FindStringSubmatchIndex(tag)
if len(match) < 8 {
return tag, false
}
currentID := tag[match[4]:match[5]]
if _, ok := seen[currentID]; !ok {
seen[currentID] = struct{}{}
return tag, false
}
if nextID == nil {
return tag, false
}
newID := "_x0000_s" + strconv.Itoa(*nextID)
*nextID = *nextID + 1
return tag[:match[4]] + newID + tag[match[5]:], true
}
// ensureVMLNoteShapeType injects the standard note <v:shapetype> definition
// (and its <o:shapelayout> preamble) when the drawing references
// type="#_x0000_t202" but never defines it. Returns the payload unchanged when
// a shapetype already exists, when no note shape references it, or when the
// root <xml> element lacks the o:/v: namespaces the injected markup needs.
func ensureVMLNoteShapeType(payload []byte) ([]byte, int, bool) {
if len(payload) == 0 {
return payload, 0, false
}
if xlsxVMLShapeTypeRE.Match(payload) {
return payload, 0, false
}
if !xlsxVMLNoteTypeRefRE.Match(payload) {
return payload, 0, false
}
root := xlsxVMLRootTagRE.FindIndex(payload)
if root == nil {
return payload, 0, false
}
rootTag := bytes.ToLower(payload[root[0]:root[1]])
if !bytes.Contains(rootTag, []byte("xmlns:o")) || !bytes.Contains(rootTag, []byte("xmlns:v")) {
return payload, 0, false
}
out := make([]byte, 0, len(payload)+len(xlsxVMLNoteShapeTypeHeader))
out = append(out, payload[:root[1]]...)
out = append(out, xlsxVMLNoteShapeTypeHeader...)
out = append(out, payload[root[1]:]...)
return out, 1, true
}
func sanitizeVMLStyleValue(raw string) (string, int, bool) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return raw, 0, false
}
tokens := strings.Split(trimmed, ";")
if len(tokens) == 0 {
return raw, 0, false
}
positionIndex := -1
for i := range tokens {
if isVMLAbsolutePositionToken(tokens[i]) {
positionIndex = i
break
}
}
if positionIndex < 0 {
return raw, 0, false
}
leadingLengths := make([]string, 0, 2)
nextIndex := positionIndex + 1
for nextIndex < len(tokens) && len(leadingLengths) < 2 {
token := strings.TrimSpace(tokens[nextIndex])
if token == "" {
nextIndex++
continue
}
if !isVMLLengthToken(token) {
break
}
leadingLengths = append(leadingLengths, token)
nextIndex++
}
if len(leadingLengths) == 0 {
return raw, 0, false
}
remaining := make([]string, 0, len(tokens)-nextIndex+3)
for ; nextIndex < len(tokens); nextIndex++ {
token := strings.TrimSpace(tokens[nextIndex])
if token == "" {
continue
}
remaining = append(remaining, token)
}
styleParts := make([]string, 0, len(tokens)+2)
for i := 0; i < positionIndex; i++ {
token := strings.TrimSpace(tokens[i])
if token == "" {
continue
}
styleParts = append(styleParts, token)
}
styleParts = append(styleParts, "position:absolute")
styleParts = append(styleParts, "margin-left:"+leadingLengths[0])
if len(leadingLengths) >= 2 {
styleParts = append(styleParts, "margin-top:"+leadingLengths[1])
} else {
styleParts = append(styleParts, "margin-top:0pt")
}
styleParts = append(styleParts, remaining...)
return strings.Join(styleParts, ";"), len(leadingLengths), true
}
func isVMLDrawingEntry(name string) bool {
name = strings.ToLower(strings.TrimSpace(strings.ReplaceAll(name, "\\", "/")))
if name == "" {
return false
}
return strings.HasPrefix(name, "xl/drawings/vmldrawing") && strings.HasSuffix(name, ".vml")
}
func isVMLAbsolutePositionToken(token string) bool {
token = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(token), " ", ""))
return token == "position:absolute"
}
func isVMLLengthToken(token string) bool {
return xlsxVMLLengthTokenRE.MatchString(strings.ToLower(strings.TrimSpace(token)))
}
func (h *FileManagerFileHandler) sanitizeStoredXLSXObject(ctx context.Context, row *filemanager.File, objectKey, nameHint, mimeHint, operation string) (bool, int64, error) {
if h == nil || h.storage == nil {
return false, 0, nil
}
storage, ok := any(h.storage).(xlsxSanitizerStorage)
if !ok {
return false, 0, errors.New("storage does not support office sanitization")
}
return sanitizeStoredXLSXObject(ctx, storage, h.svc, row, objectKey, nameHint, mimeHint, operation)
}
func sanitizeStoredXLSXObject(
ctx context.Context,
storage xlsxSanitizerStorage,
updater xlsxSanitizerFileUpdater,
row *filemanager.File,
objectKey,
nameHint,
mimeHint,
operation string,
) (bool, int64, error) {
if storage == nil {
return false, 0, nil
}
if ctx == nil {
ctx = context.Background()
}
objectKey = strings.TrimSpace(objectKey)
if objectKey == "" && row != nil {
objectKey = strings.TrimSpace(row.ObjectKey)
}
if objectKey == "" {
return false, 0, nil
}
name := strings.TrimSpace(nameHint)
if name == "" && row != nil {
name = strings.TrimSpace(row.Name)
}
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), "."))
if ext == "" {
ext = strings.ToLower(strings.TrimPrefix(filepath.Ext(objectKey), "."))
}
if ext != "xlsx" {
return false, 0, nil
}
body, meta, err := storage.GetObject(ctx, objectKey)
if err != nil {
return false, 0, err
}
defer body.Close()
payload, err := io.ReadAll(body)
if err != nil {
return false, 0, err
}
sanitizedPayload, styleFixes, changed, err := sanitizeXLSXPayload(payload)
if err != nil {
return false, 0, err
}
if !changed {
return false, int64(len(payload)), nil
}
contentType := strings.TrimSpace(firstNonEmpty(mimeHint, meta.ContentType, canonicalOfficeContentType(name)))
if contentType == "" {
contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
}
newMeta, err := storage.PutObject(ctx, filemanager.PutObjectInput{
ObjectKey: objectKey,
ContentType: contentType,
SizeBytes: int64(len(sanitizedPayload)),
Body: bytes.NewReader(sanitizedPayload),
})
if err != nil {
return true, int64(len(sanitizedPayload)), err
}
logArgs := []any{
slog.String("operation", strings.TrimSpace(operation)),
slog.String("object_key", objectKey),
slog.String("mime_type", contentType),
slog.Int("style_fix_count", styleFixes),
slog.Int("before_bytes", len(payload)),
slog.Int("after_bytes", len(sanitizedPayload)),
}
if row != nil {
logArgs = append(logArgs, slog.String("file_id", idString(row.ID)))
logArgs = append(logArgs, slog.String("file_name", strings.TrimSpace(row.Name)))
}
slog.WarnContext(ctx, "sanitized malformed xlsx VML drawing", logArgs...)
if row != nil {
row.SizeBytes = int64(len(sanitizedPayload))
row.MimeType = contentType
if strings.TrimSpace(newMeta.Bucket) != "" {
row.Bucket = newMeta.Bucket
}
if strings.TrimSpace(newMeta.ObjectKey) != "" {
row.ObjectKey = newMeta.ObjectKey
}
if strings.TrimSpace(newMeta.ETag) != "" {
row.ETag = newMeta.ETag
}
if strings.TrimSpace(newMeta.VersionID) != "" {
row.ObjectVersion = newMeta.VersionID
}
if updater != nil {
if err := updater.UpdateFile(ctx, row); err != nil {
return true, int64(len(sanitizedPayload)), err
}
}
}
return true, int64(len(sanitizedPayload)), nil
}