feature (documents): handle office documents using onlyoffice

This commit is contained in:
Mickael Kerjean 2019-12-21 15:05:21 +11:00
parent 2572594930
commit f3e6c8c030
17 changed files with 574 additions and 55 deletions

View file

@ -46,6 +46,7 @@
- Audio player
- Full Text Search
- Shared links are full fledge network drive
- Office documents (docx, xlsx and more)
- User friendly
- Mobile friendly
- Customisable

View file

@ -1,34 +1,38 @@
import Path from 'path';
export function getMimeType(file){
let ext = Path.extname(file).replace(/^\./, '').toLowerCase();
let ext = Path.extname(file).replace(/^\./, "").toLowerCase();
let mime = CONFIG.mime[ext];
if(mime){
return mime;
}else{
return 'text/plain';
return "text/plain";
}
}
export function opener(file){
let mime = getMimeType(file);
if(mime.split('/')[0] === 'text'){
return 'editor';
}else if(mime === 'application/pdf'){
return 'pdf';
}else if(mime.split('/')[0] === 'image'){
return 'image';
}else if(['application/javascript', 'application/xml', 'application/json', 'application/x-perl'].indexOf(mime) !== -1){
return 'editor';
}else if(['audio/wav', 'audio/mp3', 'audio/flac', 'audio/ogg'].indexOf(mime) !== -1){
return 'audio';
let openerFromPlugin = window.overrides["xdg-open"](mime);
if(openerFromPlugin !== null){
return openerFromPlugin;
}else if(mime.split("/")[0] === "text"){
return ["editor", null];
}else if(mime === "application/pdf"){
return ["pdf", null];
}else if(mime.split("/")[0] === "image"){
return ["image", null];
}else if(["application/javascript", "application/xml", "application/json", "application/x-perl"].indexOf(mime) !== -1){
return ["editor", null];
}else if(["audio/wav", "audio/mp3", "audio/flac", "audio/ogg"].indexOf(mime) !== -1){
return ["audio", null];
}else if(mime === "application/x-form"){
return 'form';
}else if(mime.split('/')[0] === 'video' || mime === "application/ogg"){
return 'video';
}else if(mime.split('/')[0] === "application"){
return 'download';
return ["form", null];
}else if(mime.split("/")[0] === "video" || mime === "application/ogg"){
return ["video", null];
}else if(mime.split("/")[0] === "application"){
return ["download", null];
}else{
return 'editor';
return ["editor", null];
}
}

View file

@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import Router from './router';
import { Config, Log } from "./model/";
import load from "little-loader";
import './assets/css/reset.scss';
@ -35,7 +36,7 @@ window.addEventListener("DOMContentLoaded", () => {
return Promise.resolve();
}
Config.refresh().then(() => {
Promise.all([Config.refresh(), setup_xdg_open()]).then(() => {
const timeSinceBoot = new Date() - window.initTime;
if(timeSinceBoot >= 1500){
const timeoutToAvoidFlickering = timeSinceBoot > 2500 ? 0 : 500;
@ -55,8 +56,6 @@ window.onerror = function (msg, url, lineNo, colNo, error) {
Log.report(msg, url, lineNo, colNo, error)
}
window.overrides = {}; // server generated frontend overrides
if ("serviceWorker" in navigator) {
window.addEventListener("load", function() {
navigator.serviceWorker.register("/sw_cache.js").catch(function(err){
@ -64,3 +63,14 @@ if ("serviceWorker" in navigator) {
});
});
}
// server generated frontend overrides
window.overrides = {};
function setup_xdg_open(){
return new Promise((done, err) => {
load("/overrides/xdg-open.js", function(error) {
if(error) return err(error);
done()
});
});
}

View file

@ -9,17 +9,22 @@ import { debounce, opener, notify } from '../helpers/';
import { FileDownloader, ImageViewer, PDFViewer, FormViewer } from './viewerpage/';
const VideoPlayer = (props) => (
<Bundle loader={import(/* webpackChunkName: "video" */"../pages/viewerpage/videoplayer")} symbol="VideoPlayer" overrides={["/overrides/video-transcoder.js"]} >
<Bundle loader={import(/* webpackChunkName: "video" */"./viewerpage/videoplayer")} symbol="VideoPlayer" overrides={["/overrides/video-transcoder.js"]} >
{(Comp) => <Comp {...props}/>}
</Bundle>
);
const IDE = (props) => (
<Bundle loader={import(/* webpackChunkName: "ide" */"../pages/viewerpage/ide")} symbol="IDE">
<Bundle loader={import(/* webpackChunkName: "ide" */"./viewerpage/ide")} symbol="IDE">
{(Comp) => <Comp {...props}/>}
</Bundle>
);
const AudioPlayer = (props) => (
<Bundle loader={import(/* webpackChunkName: "audioplayer" */"../pages/viewerpage/audioplayer")} symbol="AudioPlayer">
<Bundle loader={import(/* webpackChunkName: "audioplayer" */"./viewerpage/audioplayer")} symbol="AudioPlayer">
{(Comp) => <Comp {...props}/>}
</Bundle>
);
const Appframe = (props) => (
<Bundle loader={import(/* webpackChunkName: "appframe" */"./viewerpage/appframe")} symbol="AppFrame">
{(Comp) => <Comp {...props}/>}
</Bundle>
);
@ -38,7 +43,8 @@ export class ViewerPage extends React.Component {
content: null,
needSaving: false,
isSaving: false,
loading: true
loading: true,
application_arguments: null
};
this.props.subscribe('file.select', this.onPathUpdate.bind(this));
}
@ -53,11 +59,12 @@ export class ViewerPage extends React.Component {
componentDidMount(){
const metadata = () => {
return new Promise((done, err) => {
let app_opener = opener(this.state.path);
let [app_opener, app_args] = opener(this.state.path);
Files.url(this.state.path).then((url) => {
this.setState({
url: url,
opener: app_opener
opener: app_opener,
application_arguments: app_args
}, () => done(app_opener));
}).catch(error => {
this.props.error(error);
@ -173,6 +180,9 @@ export class ViewerPage extends React.Component {
<NgIf cond={this.state.opener === 'download'}>
<FileDownloader data={this.state.url} filename={this.state.filename} />
</NgIf>
<NgIf cond={this.state.opener === 'appframe'}>
<Appframe data={this.state.path} filename={this.state.filename} args={this.state.application_arguments} />
</NgIf>
</NgIf>
<NgIf cond={this.state.loading === true}>
<Loader/>

View file

@ -0,0 +1,30 @@
import React from 'react';
import { MenuBar } from './menubar';
import { currentShare } from '../../helpers/';
import './appframe.scss';
export class AppFrame extends React.Component{
constructor(props){
super(props);
}
render(){
let error = null;
if(!this.props.args) {
error = "Missing configuration. Contact your administrator";
} else if(!this.props.args.endpoint) {
error = "Missing endpoint configuration. Contact your administrator";
}
if(error !== null) return (
<div className="component_appframe">
<div className="error">{error}</div>
</div>
);
return (
<div className="component_appframe">
<iframe src={this.props.args.endpoint + "?path=" + this.props.data + "&share=" + currentShare()} />
</div>
);
}
}

View file

@ -0,0 +1,17 @@
.component_appframe {
text-align: center;
background: #525659;
width: 100%;
iframe {
width: 100%;
height: 100%;
border: none;
}
.error {
color: white;
font-size: 17px;
margin-top: 10px;
font-family: monospace;
}
}

View file

@ -9,5 +9,11 @@ services:
- GDRIVE_CLIENT_ID=<gdrive_client>
- GDRIVE_CLIENT_SECRET=<gdrive_secret>
- DROPBOX_CLIENT_ID=<dropbox_key>
- ONLYOFFICE_URL=http://onlyoffice
ports:
- "8334:8334"
onlyoffice:
container_name: filestash_oods
image: onlyoffice/documentserver
restart: always

View file

@ -20,8 +20,8 @@ import (
var Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
func EncryptString(secret string, json string) (string, error) {
d, err := compress([]byte(json))
func EncryptString(secret string, data string) (string, error) {
d, err := compress([]byte(data))
if err != nil {
return "", err
}

View file

@ -65,6 +65,14 @@ func (this Get) FrontendOverrides() []string {
return overrides
}
var xdg_open []string
func (this Register) XDGOpen(jsString string) {
xdg_open = append(xdg_open, jsString)
}
func (this Get) XDGOpen() []string {
return xdg_open
}
const OverrideVideoSourceMapper = "/overrides/video-transcoder.js"
func init() {
Hooks.Register.FrontendOverrides(OverrideVideoSourceMapper)

View file

@ -25,7 +25,7 @@ func FileExport(ctx App, res http.ResponseWriter, req *http.Request) {
query := req.URL.Query()
p := mux.Vars(req)
mimeType := fmt.Sprintf("%s/%s", p["mtype0"], p["mtype1"])
path, err := pathBuilder(ctx, strings.Replace(req.URL.Path, fmt.Sprintf("/api/export/%s/%s/%s", p["share"], p["mtype0"], p["mtype1"]), "", 1))
path, err := PathBuilder(ctx, strings.Replace(req.URL.Path, fmt.Sprintf("/api/export/%s/%s/%s", p["share"], p["mtype0"], p["mtype1"]), "", 1))
if err != nil {
SendErrorResult(res, err)
return

View file

@ -41,7 +41,7 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
SendSuccessResults(res, make([]FileInfo, 0))
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
path, err := PathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil {
SendErrorResult(res, err)
return
@ -121,7 +121,7 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
SendErrorResult(res, ErrPermissionDenied)
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
path, err := PathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil {
SendErrorResult(res, err)
return
@ -280,7 +280,7 @@ func FileSave(ctx App, res http.ResponseWriter, req *http.Request) {
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
path, err := PathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil {
SendErrorResult(res, err)
return
@ -310,12 +310,12 @@ func FileMv(ctx App, res http.ResponseWriter, req *http.Request) {
return
}
from, err := pathBuilder(ctx, req.URL.Query().Get("from"))
from, err := PathBuilder(ctx, req.URL.Query().Get("from"))
if err != nil {
SendErrorResult(res, err)
return
}
to, err := pathBuilder(ctx, req.URL.Query().Get("to"))
to, err := PathBuilder(ctx, req.URL.Query().Get("to"))
if err != nil {
SendErrorResult(res, err)
return
@ -342,7 +342,7 @@ func FileRm(ctx App, res http.ResponseWriter, req *http.Request) {
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
path, err := PathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil {
SendErrorResult(res, err)
return
@ -362,7 +362,7 @@ func FileMkdir(ctx App, res http.ResponseWriter, req *http.Request) {
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
path, err := PathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil {
SendErrorResult(res, err)
return
@ -383,7 +383,7 @@ func FileTouch(ctx App, res http.ResponseWriter, req *http.Request) {
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
path, err := PathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil {
SendErrorResult(res, err)
return
@ -398,7 +398,7 @@ func FileTouch(ctx App, res http.ResponseWriter, req *http.Request) {
SendSuccessResult(res, nil)
}
func pathBuilder(ctx App, path string) (string, error) {
func PathBuilder(ctx App, path string) (string, error) {
if path == "" {
return "", NewError("No path available", 400)
}

View file

@ -13,7 +13,7 @@ func FileSearch(ctx App, res http.ResponseWriter, req *http.Request) {
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
path, err := PathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil {
path = "/"
}

View file

@ -11,7 +11,7 @@ import (
)
func ShareList(ctx App, res http.ResponseWriter, req *http.Request) {
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
path, err := PathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil {
SendErrorResult(res, err)
return

View file

@ -1,6 +1,7 @@
package main
import (
"fmt"
"github.com/gorilla/mux"
. "github.com/mickael-kerjean/filestash/server/common"
. "github.com/mickael-kerjean/filestash/server/ctrl"
@ -23,10 +24,6 @@ func Init(a *App) {
var middlewares []Middleware
r := mux.NewRouter()
if os.Getenv("DEBUG") == "true" {
initDebugRoutes(r)
}
// API for Session
session := r.PathPrefix("/api/session").Subrouter()
middlewares = []Middleware{ ApiHeaders, SecureHeaders, SecureAjax, SessionStart }
@ -100,18 +97,15 @@ func Init(a *App) {
r.HandleFunc("/report", NewMiddlewareChain(ReportHandler, middlewares, *a)).Methods("POST")
middlewares = []Middleware{ IndexHeaders }
r.HandleFunc("/about", NewMiddlewareChain(AboutHandler, middlewares, *a)).Methods("GET")
for _, obj := range Hooks.Get.HttpEndpoint() {
obj(r, a)
}
for _, obj := range Hooks.Get.FrontendOverrides() {
r.HandleFunc(obj, func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
res.Write([]byte("/* FRONTOFFICE OVERRIDES */"))
})
}
r.HandleFunc("/robots.txt", func(res http.ResponseWriter, req *http.Request) {
res.Write([]byte(""))
})
if os.Getenv("DEBUG") == "true" {
initDebugRoutes(r)
}
initPluginsRoutes(r, a)
r.PathPrefix("/").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, *a))).Methods("GET")
// Routes are served via plugins to avoid getting stuck with plain HTTP. The idea is to
@ -158,3 +152,26 @@ func initDebugRoutes(r *mux.Router) {
w.Write([]byte("</p>"))
})
}
func initPluginsRoutes(r *mux.Router, a *App) {
// Endpoints hanle by plugins
for _, obj := range Hooks.Get.HttpEndpoint() {
obj(r, a)
}
// frontoffice overrides: it is the mean by which plugin can interact with the frontoffice
for _, obj := range Hooks.Get.FrontendOverrides() {
r.HandleFunc(obj, func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
res.Write([]byte(fmt.Sprintf("/* Default '%s' */", obj)))
})
}
// map which file can be open with what application
r.HandleFunc("/overrides/xdg-open.js", func(res http.ResponseWriter, req *http.Request) {
res.Write([]byte(`window.overrides["xdg-open"] = function(mime){`))
openers := Hooks.Get.XDGOpen()
for i:=0; i<len(openers); i++ {
res.Write([]byte(openers[i]))
}
res.Write([]byte(`return null;}`))
})
}

View file

@ -48,7 +48,18 @@ func IndexHeaders(fn func(App, http.ResponseWriter, *http.Request)) func(ctx App
header.Set("X-Frame-Options", "DENY")
header.Set("X-Powered-By", fmt.Sprintf("Filestash/%s.%s <https://filestash.app>", APP_VERSION, BUILD_DATE))
cspHeader := "default-src 'none'; style-src 'unsafe-inline'; font-src 'self' data:; manifest-src 'self'; script-src 'self' 'sha256-JNAde5CZQqXtYRLUk8CGgyJXo6C7Zs1lXPPClLM1YM4=' 'sha256-9/gQeQaAmVkFStl6tfCbHXn8mr6PgtxlH+hEp685lzY='; img-src 'self' data: https://maps.wikimedia.org; connect-src 'self'; object-src 'self'; media-src 'self' blob:; worker-src 'self' blob:; form-action 'self'; base-uri 'self';"
cspHeader := "default-src 'none'; "
cspHeader += "style-src 'unsafe-inline'; "
cspHeader += "font-src 'self' data:; "
cspHeader += "manifest-src 'self'; "
cspHeader += "script-src 'self' 'sha256-JNAde5CZQqXtYRLUk8CGgyJXo6C7Zs1lXPPClLM1YM4=' 'sha256-9/gQeQaAmVkFStl6tfCbHXn8mr6PgtxlH+hEp685lzY='; "
cspHeader += "img-src 'self' data: https://maps.wikimedia.org; "
cspHeader += "connect-src 'self'; "
cspHeader += "object-src 'self'; "
cspHeader += "media-src 'self' blob:; "
cspHeader += "worker-src 'self' blob:; "
cspHeader += "form-action 'self'; base-uri 'self'; "
cspHeader += "frame-src 'self'; "
if allowedDomainsForIframe := Config.Get("features.protection.iframe").Schema(func(f *FormElement) *FormElement{
if f == nil {
f = &FormElement{}

View file

@ -4,6 +4,7 @@ import (
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_starter_tunnel"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_starter_tor"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_video_transcoder"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_editor_onlyoffice"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_light"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_backblaze"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_backend_dav"

View file

@ -0,0 +1,404 @@
package plg_editor_onlyoffice
// [ ] TODO shared link respect access right
// [X] TODO Proper config
// [ ] TODO Link to frontend:
// Hooks.Register.XDGOpen("mtype", "app", args)
// Hooks.Get.XDGOpen()
import (
"encoding/json"
"fmt"
"github.com/gorilla/mux"
. "github.com/mickael-kerjean/filestash/server/common"
"github.com/mickael-kerjean/filestash/server/ctrl"
. "github.com/mickael-kerjean/filestash/server/middleware"
"github.com/mickael-kerjean/filestash/server/model"
"github.com/patrickmn/go-cache"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
var (
SECRET_KEY_DERIVATE_FOR_ONLYOFFICE string
OnlyOfficeCache *cache.Cache
)
type OnlyOfficeCacheData struct {
Path string
Save func(path string, file io.Reader) error
Cat func(path string) (io.ReadCloser, error)
}
func init() {
plugin_enable := func() bool {
return Config.Get("features.office.enable").Schema(func(f *FormElement) *FormElement {
if f == nil {
f = &FormElement{}
}
f.Name = "enable"
f.Type = "enable"
f.Target = []string{"onlyoffice_server"}
f.Description = "Enable/Disable the office suite to manage word, excel and powerpoint documents. This setting requires a restart to comes into effect"
f.Default = false
if u := os.Getenv("ONLYOFFICE_URL"); u != "" {
f.Default = true
}
return f
}).Bool()
}()
Config.Get("features.office.onlyoffice_server").Schema(func(f *FormElement) *FormElement {
if f == nil {
f = &FormElement{}
}
f.Id = "onlyoffice_server"
f.Name = "onlyoffice_server"
f.Type = "text"
f.Description = "Location of your OnlyOffice server"
f.Default = ""
f.Placeholder = "Eg: http://127.0.0.1:8080"
if u := os.Getenv("ONLYOFFICE_URL"); u != "" {
f.Default = u
f.Placeholder = fmt.Sprintf("Default: '%s'", u)
}
return f
})
if plugin_enable == false {
return
}
SECRET_KEY_DERIVATE_FOR_ONLYOFFICE = Hash("ONLYOFFICE_" + SECRET_KEY, len(SECRET_KEY))
Hooks.Register.HttpEndpoint(func(r *mux.Router, app *App) error {
oods := r.PathPrefix("/onlyoffice").Subrouter()
oods.PathPrefix("/static/").HandlerFunc(StaticHandler).Methods("GET", "POST")
oods.HandleFunc("/event", OnlyOfficeEventHandler).Methods("POST")
oods.HandleFunc("/content", FetchContentHandler).Methods("GET")
r.HandleFunc(
COOKIE_PATH + "onlyoffice/iframe",
NewMiddlewareChain(
IframeContentHandler,
[]Middleware{ SessionStart, LoggedInOnly },
*app,
),
).Methods("GET")
return nil
})
Hooks.Register.XDGOpen(`
if(mime === "application/word" || mime === "application/msword" ||
mime === "application/vnd.oasis.opendocument.text" || mime === "application/vnd.oasis.opendocument.spreadsheet" ||
mime === "application/excel" || mime === "application/vnd.ms-excel" || mime === "application/powerpoint" ||
mime === "application/vnd.ms-powerpoint" || mime === "application/vnd.oasis.opendocument.presentation" ) {
return ["appframe", {"endpoint": "/api/onlyoffice/iframe"}];
}
`)
OnlyOfficeCache = cache.New(720*time.Minute, 720*time.Minute)
}
func StaticHandler(res http.ResponseWriter, req *http.Request) {
req.URL.Path = strings.TrimPrefix(req.URL.Path, "/onlyoffice/static/")
oodsLocation := Config.Get("features.office.onlyoffice_server").String()
u, err := url.Parse(oodsLocation)
if err != nil {
SendErrorResult(res, err)
return
}
req.Header.Set("X-Forwarded-Proto", "http")
req.Header.Set("X-Forwarded-Host", req.Host + "/onlyoffice/static/")
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.ErrorHandler = func(rw http.ResponseWriter, rq *http.Request, err error) {
Log.Warning("[onlyoffice] %s", err.Error())
SendErrorResult(rw, NewError(err.Error(), http.StatusBadGateway))
}
proxy.ServeHTTP(res, req)
}
func IframeContentHandler(ctx App, res http.ResponseWriter, req *http.Request) {
if model.CanRead(&ctx) == false {
SendErrorResult(res, ErrPermissionDenied)
return
} else if oodsLocation := Config.Get("features.office.onlyoffice_server").String(); oodsLocation == "" {
res.WriteHeader(http.StatusServiceUnavailable)
res.Write([]byte("<p>The Onlyoffice server hasn't been configured</p>"))
res.Write([]byte("<style>p {color: white; text-align: center; margin-top: 50px; font-size: 20px; opacity: 0.6; font-family: monospace; } </style>"))
return
}
var (
path string // path of the file we want to open via onlyoffice
filestashServerLocation string // location from which the oods server can reach filestash
userId string // as seen by onlyoffice to distinguish different users
username string // username as displayed by only office
key string // unique identifier for a file as seen be only office
contentType string // name of the application in onlyoffice
filetype string // extension of the document
filename string // filename of the document
oodsMode string // edit mode
oodsDevice string // mobile, desktop of embedded
localip string
)
query := req.URL.Query()
path, err := ctrl.PathBuilder(ctx, query.Get("path"))
if err != nil {
SendErrorResult(res, err)
return
}
userId = GenerateID(&ctx)
f, err := ctx.Backend.Cat(path)
if err != nil {
SendErrorResult(res, err)
return
}
key = HashStream(f, 20)
key = Hash(key + userId + path, 20)
filename = filepath.Base(path)
oodsMode = func() string {
if model.CanEdit(&ctx) == false {
return "view"
}
return "edit"
}()
oodsDevice = func() string {
ua := req.Header.Get("User-Agent")
if ua == "" {
return "desktop"
} else if strings.Contains(ua, "iPhone") {
return "mobile"
} else if strings.Contains(ua, "iPad") {
return "mobile"
} else if strings.Contains(ua, "Android") {
return "mobile"
} else if strings.Contains(ua, "Mobile") {
return "mobile"
}
if oodsMode == "view" {
return "embedded"
}
return "desktop"
}()
username = func() string {
if ctx.Session["username"] != "" {
return ctx.Session["username"]
}
return "Me"
}()
if ctx.Share.Id != "" {
username = "Anonymous"
userId = RandomString(10)
}
localip = func() string { // https://stackoverflow.com/questions/23558425/how-do-i-get-the-local-ip-address-in-go#23558495
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return ""
}()
filestashServerLocation = fmt.Sprintf("http://%s:%d", localip, Config.Get("general.port").Int())
contentType = func(p string) string {
var (
word string = "text"
excel string = "spreadsheet"
powerpoint string = "presentation"
)
switch GetMimeType(p) {
case "application/word": return word
case "application/msword": return word
case "application/vnd.oasis.opendocument.text": return word
case "application/vnd.oasis.opendocument.spreadsheet": return excel
case "application/excel": return excel
case "application/vnd.ms-excel": return excel
case "application/powerpoint": return powerpoint
case "application/vnd.ms-powerpoint": return powerpoint
case "application/vnd.oasis.opendocument.presentation": return powerpoint
}
return ""
}(path)
filetype = strings.TrimPrefix(filepath.Ext(filename), ".")
OnlyOfficeCache.Set(key, &OnlyOfficeCacheData{ path, ctx.Backend.Save, ctx.Backend.Cat }, cache.DefaultExpiration)
res.Write([]byte(fmt.Sprintf(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<style> body { margin: 0; } body, html{ height: 100%%; } iframe { width: 100%%; height: 100%%; } </style>
<div id="placeholder"></div>
<script type="text/javascript" src="/onlyoffice/static/web-apps/apps/api/documents/api.js"></script>
<script>
if("DocsAPI" in window) loadApplication();
else sendError("[error] Can't reach the onlyoffice server");
function loadApplication() {
new DocsAPI.DocEditor("placeholder", {
"token": "foobar",
"documentType": "%s",
"type": "%s",
"document": {
"title": "%s",
"url": "%s/onlyoffice/content?key=%s",
"fileType": "%s",
"key": "%s"
},
"editorConfig": {
"callbackUrl": "%s/onlyoffice/event",
"mode": "%s",
"customization": {
"autosave": false,
"forcesave": true,
"compactHeader": true
},
"user": {
"id": "%s",
"name": "%s"
}
}
});
}
function sendError(message){
let $el = document.createElement("p");
$el.innerHTML = message;
$el.setAttribute("style", "text-align: center; color: white; opacity: 0.8; font-size: 20px; font-family: monospace;");
document.body.appendChild($el);
}
</script>
</body>
</html>`,
contentType,
oodsDevice,
filename,
filestashServerLocation, key,
filetype,
key,
filestashServerLocation,
oodsMode,
userId,
username,
)))
}
func FetchContentHandler(res http.ResponseWriter, req *http.Request) {
var key string
if key = req.URL.Query().Get("key"); key == "" {
SendErrorResult(res, NewError("unspecified key", http.StatusBadRequest))
return
}
c, found := OnlyOfficeCache.Get(key)
if found == false {
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(`{"error": 1, "message": "missing data fetcher handler"}`))
return
}
cData, valid := c.(*OnlyOfficeCacheData)
if valid == false {
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(`{"error": 1, "message": "invalid cache"}`))
return
}
f, err := cData.Cat(cData.Path)
if err != nil {
res.WriteHeader(http.StatusNotFound)
res.Write([]byte(`{"error": 1, "message": "error while fetching data"}`))
return
}
io.Copy(res, f)
f.Close()
}
type OnlyOfficeEventObject struct {
Actions []struct {
Type int `json: "type"`
UserId string `json: "userid" `
} `json: "actions"`
ChangesURL string `json: "changesurl"`
Forcesavetype int `json: "forcesavetype"`
History struct {
ServerVersion string `json: "serverVersion"`
Changes []struct {
Created string `json: "created"`
User struct {
Id string `json: "id"`
Name string `json: "name"`
}
} `json: "changes"`
} `json: "history"`
Key string `json: "key"`
Status int `json: "status"`
Url string `json: "url"`
UserData string `json: "userdata"`
Lastsave string `json: "lastsave"`
Users []string `json: "users"`
}
func OnlyOfficeEventHandler(res http.ResponseWriter, req *http.Request) {
event := OnlyOfficeEventObject{}
if err := json.NewDecoder(req.Body).Decode(&event); err != nil {
SendErrorResult(res, err)
return
}
req.Body.Close()
switch event.Status {
case 0: Log.Warning("[onlyoffice] no document with the key identifier could be found. %+v", event)
case 1:
// document is being edited
case 2:
// document is ready for saving
case 3:
// document saving error has occurred
Log.Warning("[onlyoffice] document saving error has occurred. %+v", event)
case 4:
// document is closed with no changes
case 5:
Log.Warning("[onlyoffice] undocumented status. %+v", event)
case 6: // document is being edited, but the current document state is saved
saveObject, found := OnlyOfficeCache.Get(event.Key);
if found == false {
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(`{"error": 1, "message": "doens't know where to store the given data"}`))
return
}
cData, valid := saveObject.(*OnlyOfficeCacheData)
if valid == false {
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(`{"error": 1, "message": "[internal error] invalid save handler"}`))
return
}
r, err := http.NewRequest("GET", event.Url, nil)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(`{"error": 1, "message": "couldn't fetch the document on the oods server"}`))
return
}
f, err := HTTPClient.Do(r)
if err = cData.Save(cData.Path, f.Body); err != nil {
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(`{"error": 1, "message": "error while saving the document"}`))
return
}
f.Body.Close()
case 7: Log.Warning("[onlyoffice] error has occurred while force saving the document. %+v", event)
default: Log.Warning("[onlyoffice] undocumented status. %+v", event)
}
res.Write([]byte(`{"error": 0}`))
}