All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m9s
639 lines
23 KiB
Go
639 lines
23 KiB
Go
package handlers
|
|
|
|
import (
|
|
"archive/zip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.elijahkuntz.com/Elijah/drive/config"
|
|
"git.elijahkuntz.com/Elijah/drive/database"
|
|
"git.elijahkuntz.com/Elijah/drive/middleware"
|
|
"git.elijahkuntz.com/Elijah/drive/workers"
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"sync"
|
|
"time"
|
|
"mime"
|
|
)
|
|
|
|
type OnlyOfficeHandler struct {
|
|
Config *config.Config
|
|
DB *database.DB
|
|
}
|
|
|
|
type CallbackRequest struct {
|
|
Status int `json:"status"`
|
|
Url string `json:"url"`
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
func (h *OnlyOfficeHandler) Callback(c *fiber.Ctx) error {
|
|
path := c.Query("path")
|
|
token := c.Query("token")
|
|
|
|
if path == "" {
|
|
return c.Status(400).SendString("path required")
|
|
}
|
|
|
|
// Validate the token to ensure the callback is authorized.
|
|
// First check if it's a long-lived edit token, fallback to short-lived download token.
|
|
err := middleware.ValidateEditToken(token, h.Config.JWTSecret, "", path)
|
|
if err != nil {
|
|
err = middleware.ValidateDownloadToken(token, h.Config.JWTSecret, "", path)
|
|
}
|
|
if err != nil {
|
|
return c.Status(401).SendString("unauthorized")
|
|
}
|
|
|
|
var req CallbackRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "invalid json"})
|
|
}
|
|
|
|
// Status 2: Document is ready for saving
|
|
// Status 6: Document is being edited, force save was triggered
|
|
if req.Status == 2 || req.Status == 6 {
|
|
if req.Url == "" {
|
|
return c.Status(400).JSON(fiber.Map{"error": "url required"})
|
|
}
|
|
|
|
// If the server cannot resolve the public URL due to hairpin NAT,
|
|
// we can rewrite it to use the internal Docker IP
|
|
downloadUrl := req.Url
|
|
|
|
trustedHosts := []string{h.Config.OnlyOfficeHost}
|
|
if err := validateCallbackURL(downloadUrl, trustedHosts); err != nil {
|
|
return c.Status(403).JSON(fiber.Map{"error": "blocked URL: " + err.Error()})
|
|
}
|
|
|
|
// Download modified file from Document Server
|
|
resp, err := SafeHTTPClient.Get(downloadUrl)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "failed to download file"})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
targetPath, err := resolveSafe(h.Config.StorageDir, path)
|
|
if err != nil {
|
|
return c.Status(403).JSON(fiber.Map{"error": "invalid path"})
|
|
}
|
|
|
|
// Save file
|
|
out, err := os.Create(targetPath)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "failed to open file for writing"})
|
|
}
|
|
defer out.Close()
|
|
|
|
if _, err := io.Copy(out, resp.Body); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "failed to write file"})
|
|
}
|
|
|
|
h.scheduleThumbnailUpdate(targetPath, path)
|
|
}
|
|
|
|
// Send successful response according to OnlyOffice API
|
|
return c.JSON(fiber.Map{"error": 0})
|
|
}
|
|
|
|
var (
|
|
saveDebounceMutex sync.Mutex
|
|
saveTimers = make(map[string]*time.Timer)
|
|
)
|
|
|
|
func (h *OnlyOfficeHandler) scheduleThumbnailUpdate(targetPath, relativePath string) {
|
|
saveDebounceMutex.Lock()
|
|
defer saveDebounceMutex.Unlock()
|
|
|
|
if timer, exists := saveTimers[targetPath]; exists {
|
|
timer.Stop()
|
|
}
|
|
|
|
saveTimers[targetPath] = time.AfterFunc(10*time.Second, func() {
|
|
saveDebounceMutex.Lock()
|
|
delete(saveTimers, targetPath)
|
|
saveDebounceMutex.Unlock()
|
|
|
|
checksum, err := generateChecksum(targetPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
info, err := os.Stat(targetPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
h.DB.Exec(`
|
|
INSERT INTO files (path, name, size, mime_type, checksum)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(path) DO UPDATE SET
|
|
checksum=excluded.checksum,
|
|
size=excluded.size,
|
|
updated_at=CURRENT_TIMESTAMP`,
|
|
filepath.ToSlash(relativePath), filepath.Base(relativePath), info.Size(), mime.TypeByExtension(filepath.Ext(relativePath)), checksum,
|
|
)
|
|
|
|
workers.Instance.Enqueue(workers.ThumbnailJob{
|
|
FilePath: relativePath,
|
|
Checksum: checksum,
|
|
MimeType: mime.TypeByExtension(filepath.Ext(relativePath)),
|
|
})
|
|
})
|
|
}
|
|
|
|
// DownloadForEditor serves a file to the OnlyOffice Document Server.
|
|
// This is a public endpoint that validates the download token manually,
|
|
// bypassing the auth middleware (which can fail when requests are proxied
|
|
// through Next.js rewrites).
|
|
func (h *OnlyOfficeHandler) DownloadForEditor(c *fiber.Ctx) error {
|
|
path := c.Query("path")
|
|
token := c.Query("token")
|
|
|
|
if path == "" {
|
|
return c.Status(400).JSON(fiber.Map{"error": "path required"})
|
|
}
|
|
|
|
// Validate the token.
|
|
// Allow both short-lived download tokens and long-lived edit tokens.
|
|
err := middleware.ValidateEditToken(token, h.Config.JWTSecret, "", path)
|
|
if err != nil {
|
|
err = middleware.ValidateDownloadToken(token, h.Config.JWTSecret, "", path)
|
|
}
|
|
if err != nil {
|
|
return c.Status(401).JSON(fiber.Map{"error": "unauthorized"})
|
|
}
|
|
|
|
// Jail the path to the storage directory
|
|
fullPath, err := resolveSafe(h.Config.StorageDir, path)
|
|
if err != nil {
|
|
return c.Status(403).JSON(fiber.Map{"error": "invalid path"})
|
|
}
|
|
|
|
info, err := os.Stat(fullPath)
|
|
if err != nil || info.IsDir() {
|
|
return c.Status(404).JSON(fiber.Map{"error": "file not found"})
|
|
}
|
|
|
|
return c.SendFile(fullPath)
|
|
}
|
|
|
|
// CreateBlank creates an empty document for Docs or Slides mode
|
|
func (h *OnlyOfficeHandler) CreateBlank(c *fiber.Ctx) error {
|
|
req := new(struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
})
|
|
|
|
if err := c.BodyParser(req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "invalid request"})
|
|
}
|
|
|
|
if req.Name == "" {
|
|
req.Name = "Untitled Document"
|
|
if req.Type == "slide" {
|
|
req.Name = "Untitled Presentation"
|
|
} else if req.Type == "cell" {
|
|
req.Name = "Untitled Spreadsheet"
|
|
}
|
|
}
|
|
|
|
ext := ".docx"
|
|
if req.Type == "slide" {
|
|
ext = ".pptx"
|
|
} else if req.Type == "cell" {
|
|
ext = ".xlsx"
|
|
}
|
|
|
|
// Add extension if missing
|
|
if filepath.Ext(req.Name) != ext {
|
|
req.Name += ext
|
|
}
|
|
|
|
// Create Documents directory if it doesn't exist
|
|
docsDir := filepath.Join(h.Config.StorageDir, "Documents")
|
|
os.MkdirAll(docsDir, 0755)
|
|
|
|
// Make sure the file name doesn't collide
|
|
basePath := filepath.Join(docsDir, req.Name)
|
|
finalPath := basePath
|
|
counter := 1
|
|
for {
|
|
if _, err := os.Stat(finalPath); os.IsNotExist(err) {
|
|
break
|
|
}
|
|
finalPath = filepath.Join(docsDir, fmt.Sprintf("%s (%d)%s", req.Name[:len(req.Name)-len(ext)], counter, ext))
|
|
counter++
|
|
}
|
|
|
|
// Create a valid Office Open XML file (docx/pptx are ZIP archives)
|
|
if err := createBlankOfficeFile(finalPath, ext); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "failed to create file"})
|
|
}
|
|
|
|
// Return the relative path
|
|
relativePath, _ := filepath.Rel(h.Config.StorageDir, finalPath)
|
|
|
|
// Enqueue for thumbnail generation (with 10s debounce)
|
|
h.scheduleThumbnailUpdate(finalPath, relativePath)
|
|
|
|
return c.JSON(fiber.Map{
|
|
"path": filepath.ToSlash(relativePath),
|
|
"name": filepath.Base(finalPath),
|
|
})
|
|
}
|
|
|
|
// POST /api/onlyoffice/template
|
|
func (h *OnlyOfficeHandler) CreateFromTemplate(c *fiber.Ctx) error {
|
|
var req struct {
|
|
TemplateName string `json:"template"` // e.g., "apa.docx"
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "invalid request"})
|
|
}
|
|
|
|
srcPath := filepath.Join("templates", req.TemplateName)
|
|
if _, err := os.Stat(srcPath); os.IsNotExist(err) {
|
|
return c.Status(404).JSON(fiber.Map{"error": "template not found"})
|
|
}
|
|
|
|
// Figure out a safe name in root directory
|
|
baseName := "Untitled Document"
|
|
ext := ".docx"
|
|
if strings.HasSuffix(req.TemplateName, ".pptx") {
|
|
baseName = "Untitled Presentation"
|
|
ext = ".pptx"
|
|
}
|
|
if req.TemplateName == "apa.docx" {
|
|
baseName = "APA Paper"
|
|
} else if req.TemplateName == "resume.docx" {
|
|
baseName = "Resume"
|
|
}
|
|
|
|
// Create Documents directory if it doesn't exist
|
|
docsDir := filepath.Join(h.Config.StorageDir, "Documents")
|
|
os.MkdirAll(docsDir, 0755)
|
|
|
|
finalPath := filepath.Join(docsDir, baseName+ext)
|
|
counter := 1
|
|
for {
|
|
if _, err := os.Stat(finalPath); os.IsNotExist(err) {
|
|
break
|
|
}
|
|
finalPath = filepath.Join(docsDir, fmt.Sprintf("%s (%d)%s", baseName, counter, ext))
|
|
counter++
|
|
}
|
|
|
|
// Copy the template file
|
|
srcFile, err := os.Open(srcPath)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "failed to open template"})
|
|
}
|
|
defer srcFile.Close()
|
|
|
|
destFile, err := os.Create(finalPath)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "failed to create destination file"})
|
|
}
|
|
defer destFile.Close()
|
|
|
|
if _, err := io.Copy(destFile, srcFile); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "failed to copy template data"})
|
|
}
|
|
|
|
relativePath, _ := filepath.Rel(h.Config.StorageDir, finalPath)
|
|
|
|
h.scheduleThumbnailUpdate(finalPath, relativePath)
|
|
|
|
return c.JSON(fiber.Map{
|
|
"path": filepath.ToSlash(relativePath),
|
|
"name": filepath.Base(finalPath),
|
|
})
|
|
}
|
|
|
|
// ListOfficeFiles returns all .docx, .pptx, or .xlsx files across the entire storage directory.
|
|
// GET /api/onlyoffice/files?type=docx|pptx|xlsx
|
|
func (h *OnlyOfficeHandler) ListOfficeFiles(c *fiber.Ctx) error {
|
|
fileType := c.Query("type", "docx") // "docx", "pptx", or "xlsx"
|
|
ext := ".docx"
|
|
if fileType == "pptx" {
|
|
ext = ".pptx"
|
|
} else if fileType == "xlsx" {
|
|
ext = ".xlsx"
|
|
}
|
|
|
|
type OfficeFile struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Size int64 `json:"size"`
|
|
ModTime string `json:"mod_time"`
|
|
Checksum string `json:"checksum,omitempty"`
|
|
}
|
|
|
|
var results []OfficeFile
|
|
|
|
checksums := make(map[string]string)
|
|
dbPaths := []string{}
|
|
rows, err := h.DB.Query("SELECT path, checksum FROM files WHERE checksum IS NOT NULL AND checksum != ''")
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var p, c string
|
|
if rows.Scan(&p, &c) == nil {
|
|
checksums[p] = c
|
|
dbPaths = append(dbPaths, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
walkPaths := []string{}
|
|
|
|
filepath.Walk(h.Config.StorageDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
if info.IsDir() {
|
|
base := filepath.Base(path)
|
|
if strings.HasPrefix(base, ".") && path != h.Config.StorageDir {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if strings.HasSuffix(strings.ToLower(info.Name()), ext) {
|
|
relPath, _ := filepath.Rel(h.Config.StorageDir, path)
|
|
relPathSlash := filepath.ToSlash(relPath)
|
|
|
|
chk := checksums[relPathSlash]
|
|
walkPaths = append(walkPaths, relPathSlash)
|
|
|
|
results = append(results, OfficeFile{
|
|
Name: info.Name(),
|
|
Path: relPathSlash,
|
|
Size: info.Size(),
|
|
ModTime: info.ModTime().UTC().Format("2006-01-02T15:04:05Z"),
|
|
Checksum: chk,
|
|
})
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if results == nil {
|
|
results = []OfficeFile{}
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"files": results,
|
|
"count": len(results),
|
|
"debug_db": dbPaths,
|
|
"debug_walk": walkPaths,
|
|
})
|
|
}
|
|
|
|
// createBlankOfficeFile creates a minimal valid .docx or .pptx file.
|
|
// These formats are ZIP archives containing XML files following the
|
|
// Office Open XML (OOXML) standard.
|
|
func createBlankOfficeFile(path string, ext string) error {
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
w := zip.NewWriter(f)
|
|
defer w.Close()
|
|
|
|
if ext == ".pptx" {
|
|
return writePptxContents(w)
|
|
} else if ext == ".xlsx" {
|
|
return writeXlsxContents(w)
|
|
}
|
|
return writeDocxContents(w)
|
|
}
|
|
|
|
func writeDocxContents(w *zip.Writer) error {
|
|
files := map[string]string{
|
|
"[Content_Types].xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
|
</Types>`,
|
|
"_rels/.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
|
</Relationships>`,
|
|
"word/document.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
|
<w:body>
|
|
<w:p><w:r><w:t></w:t></w:r></w:p>
|
|
</w:body>
|
|
</w:document>`,
|
|
}
|
|
for name, content := range files {
|
|
fw, err := w.Create(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := fw.Write([]byte(content)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writePptxContents(w *zip.Writer) error {
|
|
files := map[string]string{
|
|
"[Content_Types].xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
|
|
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
|
|
<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
|
|
<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
|
|
<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
|
|
</Types>`,
|
|
"_rels/.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
|
|
</Relationships>`,
|
|
"ppt/presentation.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
|
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
|
<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
|
|
<p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst>
|
|
<p:sldSz cx="12192000" cy="6858000"/>
|
|
<p:notesSz cx="6858000" cy="9144000"/>
|
|
</p:presentation>`,
|
|
"ppt/_rels/presentation.xml.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>
|
|
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
|
|
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
|
|
</Relationships>`,
|
|
"ppt/theme/theme1.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
|
|
<a:themeElements>
|
|
<a:clrScheme name="Office">
|
|
<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
|
|
<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
|
|
<a:dk2><a:srgbClr val="44546A"/></a:dk2>
|
|
<a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>
|
|
<a:accent1><a:srgbClr val="4472C4"/></a:accent1>
|
|
<a:accent2><a:srgbClr val="ED7D31"/></a:accent2>
|
|
<a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>
|
|
<a:accent4><a:srgbClr val="FFC000"/></a:accent4>
|
|
<a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>
|
|
<a:accent6><a:srgbClr val="70AD47"/></a:accent6>
|
|
<a:hlink><a:srgbClr val="0563C1"/></a:hlink>
|
|
<a:folHlink><a:srgbClr val="954F72"/></a:folHlink>
|
|
</a:clrScheme>
|
|
<a:fontScheme name="Office">
|
|
<a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>
|
|
<a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>
|
|
</a:fontScheme>
|
|
<a:fmtScheme name="Office">
|
|
<a:fillStyleLst>
|
|
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
</a:fillStyleLst>
|
|
<a:lnStyleLst>
|
|
<a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>
|
|
<a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>
|
|
<a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln>
|
|
</a:lnStyleLst>
|
|
<a:effectStyleLst>
|
|
<a:effectStyle><a:effectLst/></a:effectStyle>
|
|
<a:effectStyle><a:effectLst/></a:effectStyle>
|
|
<a:effectStyle><a:effectLst/></a:effectStyle>
|
|
</a:effectStyleLst>
|
|
<a:bgFillStyleLst>
|
|
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
|
|
</a:bgFillStyleLst>
|
|
</a:fmtScheme>
|
|
</a:themeElements>
|
|
</a:theme>`,
|
|
"ppt/slideMasters/slideMaster1.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<p:sldMaster xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
|
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
|
<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
|
|
<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>
|
|
</p:spTree></p:cSld>
|
|
<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
|
|
<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>
|
|
</p:sldMaster>`,
|
|
"ppt/slideMasters/_rels/slideMaster1.xml.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
|
|
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>
|
|
</Relationships>`,
|
|
"ppt/slideLayouts/slideLayout1.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<p:sldLayout xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
|
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" type="blank">
|
|
<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
|
|
<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>
|
|
</p:spTree></p:cSld>
|
|
</p:sldLayout>`,
|
|
"ppt/slideLayouts/_rels/slideLayout1.xml.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>
|
|
</Relationships>`,
|
|
"ppt/slides/slide1.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
|
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
|
<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
|
|
<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>
|
|
</p:spTree></p:cSld>
|
|
</p:sld>`,
|
|
"ppt/slides/_rels/slide1.xml.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
|
|
</Relationships>`,
|
|
}
|
|
for name, content := range files {
|
|
fw, err := w.Create(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := fw.Write([]byte(content)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeXlsxContents(w *zip.Writer) error {
|
|
files := map[string]string{
|
|
"[Content_Types].xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
|
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
|
</Types>`,
|
|
"_rels/.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
|
</Relationships>`,
|
|
"xl/workbook.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
<sheets>
|
|
<sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
|
</sheets>
|
|
</workbook>`,
|
|
"xl/_rels/workbook.xml.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
|
</Relationships>`,
|
|
"xl/worksheets/sheet1.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
<sheetData/>
|
|
</worksheet>`,
|
|
}
|
|
for name, content := range files {
|
|
fw, err := w.Create(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := fw.Write([]byte(content)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SignConfig takes the OnlyOffice configuration payload and signs it with the JWT secret
|
|
func (h *OnlyOfficeHandler) SignConfig(c *fiber.Ctx) error {
|
|
if h.Config.OnlyOfficeJWT == "" {
|
|
return c.Status(500).JSON(fiber.Map{"error": "JWT secret is not configured on the server"})
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := c.BodyParser(&payload); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "invalid json payload"})
|
|
}
|
|
|
|
// Create a new JWT token using the HS256 algorithm
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload))
|
|
|
|
// Sign the token with our secret
|
|
signedToken, err := token.SignedString([]byte(h.Config.OnlyOfficeJWT))
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "failed to sign token"})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"token": signedToken,
|
|
})
|
|
}
|