package richtext import ( "context" "encoding/json" "fmt" "io" "net/url" "strings" "time" ) type PublicSessionResult struct { SessionResult DocumentURL string `json:"documentUrl,omitempty"` SaveURL string `json:"saveUrl,omitempty"` } func (s *Service) CreatePublicSession(ctx context.Context, token, filePath, mode, password, guestID, guestName, displayName string) (*PublicSessionResult, error) { if !s.Cfg.Enabled { return nil, fmt.Errorf("rich text editor disabled") } filePath = normalizePath(filePath) canonical, source, importRequired, err := s.resolvePublicCanonicalPath(ctx, token, filePath, password, displayName) if err != nil { return nil, err } ownerID, ownerPath, err := s.ownerSidecarPathForPublic(ctx, token, password, canonical, displayName) if err != nil { return nil, err } roomID, err := s.resolveCollabRoomID(ctx, ownerID, ownerPath) if err != nil { return nil, err } tokenJWT, err := signRoomToken(roomTokenPayload{ Room: roomID, Path: canonical, User: "public:" + token, Sub: guestID, Name: guestName, Mode: mode, Expires: time.Now().Add(8 * time.Hour).Unix(), }, s.Cfg.HocuspocusSecret) if err != nil { return nil, err } apiBase := strings.TrimRight(s.Cfg.APIInternalURL, "/") sig, _ := signPublicDocAccess(token, canonical, password, s.Cfg.HocuspocusSecret) docURL := fmt.Sprintf("%s/api/v1/drive/public/shares/%s/richtext/document?path=%s&password=%s&sig=%s", apiBase, url.PathEscape(token), url.QueryEscape(canonical), url.QueryEscape(password), url.QueryEscape(sig)) saveURL := docURL wsURL := strings.TrimSpace(s.Cfg.HocuspocusPublicURL) collab := wsURL != "" && s.Cfg.HocuspocusSecret != "" pageSetup, _ := s.loadPublicPageSetupFromSidecar(ctx, token, password, canonical, displayName) return &PublicSessionResult{ SessionResult: SessionResult{ RoomID: roomID, CanonicalPath: canonical, SourcePath: source, WsURL: wsURL, Token: tokenJWT, Mode: mode, ImportRequired: importRequired, Collaboration: collab, PageSetup: pageSetup, }, DocumentURL: docURL, SaveURL: saveURL, }, nil } func (s *Service) resolvePublicCanonicalPath(ctx context.Context, token, filePath, password, displayName string) (canonical, source string, importRequired bool, err error) { filePath = normalizePath(filePath) source = filePath if source == "/" { source = s.publicClientSourcePath(ctx, token, password, filePath, displayName) } if isUltidocPath(filePath) { return filePath, "", false, nil } if isUltidocPath(source) { return source, "", false, nil } sidecar := sidecarPathForSource(source) if s.publicSidecarExists(ctx, token, password, sidecar, displayName) { needsImport := s.publicSidecarImportRequired(ctx, token, password, sidecar, source, displayName) return sidecar, source, needsImport, nil } if _, err := s.publicFileExists(ctx, token, source, password); err != nil { return "", "", false, fmt.Errorf("file not found") } return sidecar, source, true, nil } func (s *Service) publicSidecarImportRequired(ctx context.Context, token, password, sidecarPath, sourcePath, displayName string) bool { body, err := s.LoadPublicDocument(ctx, token, sidecarPath, password, displayName) if err != nil { return true } doc, err := ParseUltiDoc(body) if err != nil { return true } if !isEmptyDocContent(doc.Content) { return false } candidates := []string{sourcePath} if doc.Source != nil && doc.Source.Path != "" && doc.Source.Path != sourcePath { candidates = append(candidates, doc.Source.Path) } for _, src := range candidates { if src == "" { continue } exists, err := s.publicFileExists(ctx, token, src, password) if err == nil && exists { return true } } return false } func (s *Service) publicClientSourcePath(ctx context.Context, token, password, clientPath, displayName string) string { binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password) if err == nil { return binding.ClientSourcePath(clientPath, displayName) } if name := strings.TrimSpace(displayName); name != "" { return normalizePath("/" + name) } return clientPath } func (s *Service) publicSidecarExists(ctx context.Context, token, password, clientSidecar, displayName string) bool { if _, err := s.publicFileExists(ctx, token, clientSidecar, password); err == nil { return true } binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password) if err != nil { return false } ownerPath := binding.OwnerPathForClient(clientSidecar, displayName) if _, err := s.nc.FileRevision(ctx, binding.OwnerID, ownerPath); err == nil { return true } return false } func (s *Service) ImportPublicDocument(ctx context.Context, token, password, displayName string, req ImportRequest) (string, error) { source := normalizePath(req.SourcePath) canonical, _, importRequired, err := s.resolvePublicCanonicalPath(ctx, token, source, password, displayName) if err != nil { return "", err } if !importRequired { return canonical, nil } var content json.RawMessage if len(req.Content) > 0 { content = req.Content } else { body, err := s.LoadPublicDocument(ctx, token, source, password, displayName) if err != nil { return "", err } name := fileNameFromPath(source) content, err = ImportBytes(name, "", body) if err != nil { return "", err } } doc := NewUltiDoc(content, &UltiDocSource{ Path: source, ImportedAt: time.Now().UTC().Format(time.RFC3339), }) if len(req.PageSetup) > 0 { var pageSetup UltiDocPageSetup if err := json.Unmarshal(req.PageSetup, &pageSetup); err == nil { doc.PageSetup = &pageSetup } } payload, err := doc.Marshal() if err != nil { return "", err } if err := s.SavePublicDocument(ctx, token, canonical, password, displayName, payload); err != nil { return "", err } return canonical, nil } func (s *Service) loadPublicPageSetupFromSidecar(ctx context.Context, token, password, canonicalPath, displayName string) (json.RawMessage, error) { body, err := s.LoadPublicDocument(ctx, token, canonicalPath, password, displayName) if err != nil { return nil, err } doc, err := ParseUltiDoc(body) if err != nil || doc.PageSetup == nil { return nil, err } return json.Marshal(doc.PageSetup) } func (s *Service) publicFileExists(ctx context.Context, token, path, password string) (bool, error) { _, err := s.nc.PublicShareFileRevision(ctx, token, path, password) if err != nil { return false, err } return true, nil } func (s *Service) LoadPublicDocument(ctx context.Context, token, clientPath, password, displayName string) ([]byte, error) { if binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password); err == nil { ownerPath := binding.OwnerPathForClient(clientPath, displayName) body, _, err := s.nc.Download(ctx, binding.OwnerID, ownerPath) if err == nil { defer body.Close() return io.ReadAll(body) } } body, _, err := s.nc.DownloadPublicShare(ctx, token, clientPath, password) if err != nil { return nil, err } defer body.Close() return io.ReadAll(body) } func (s *Service) SavePublicDocument(ctx context.Context, token, clientPath, password, displayName string, raw []byte) error { if binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password); err == nil { ownerPath := binding.OwnerPathForClient(clientPath, displayName) reader := strings.NewReader(string(raw)) if err := s.nc.Upload(ctx, binding.OwnerID, ownerPath, reader, "application/json"); err == nil { return nil } } reader := strings.NewReader(string(raw)) return s.nc.UploadPublicShare(ctx, token, clientPath, password, reader, "application/json") } func (s *Service) LoadPublicDocumentLegacy(ctx context.Context, token, path, password string) ([]byte, error) { return s.LoadPublicDocument(ctx, token, path, password, "") } func (s *Service) SavePublicDocumentLegacy(ctx context.Context, token, path, password string, raw []byte) error { return s.SavePublicDocument(ctx, token, path, password, "", raw) }