diff --git a/README.md b/README.md
index 2917d8af..c52460ec 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/client/helpers/mimetype.js b/client/helpers/mimetype.js
index cb70a010..902473a0 100644
--- a/client/helpers/mimetype.js
+++ b/client/helpers/mimetype.js
@@ -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];
}
}
diff --git a/client/index.js b/client/index.js
index 47516f3a..b1aeb3d3 100644
--- a/client/index.js
+++ b/client/index.js
@@ -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()
+ });
+ });
+}
diff --git a/client/pages/viewerpage.js b/client/pages/viewerpage.js
index be0e53c7..da7b495c 100644
--- a/client/pages/viewerpage.js
+++ b/client/pages/viewerpage.js
@@ -9,17 +9,22 @@ import { debounce, opener, notify } from '../helpers/';
import { FileDownloader, ImageViewer, PDFViewer, FormViewer } from './viewerpage/';
const VideoPlayer = (props) => (
-
+
{(Comp) => }
);
const IDE = (props) => (
-
+
{(Comp) => }
);
const AudioPlayer = (props) => (
-
+
+ {(Comp) => }
+
+);
+const Appframe = (props) => (
+
{(Comp) => }
);
@@ -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 {
+
+
+
diff --git a/client/pages/viewerpage/appframe.js b/client/pages/viewerpage/appframe.js
new file mode 100644
index 00000000..2254bfe5
--- /dev/null
+++ b/client/pages/viewerpage/appframe.js
@@ -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 (
+
+ );
+ return (
+
+
+
+ );
+ }
+}
diff --git a/client/pages/viewerpage/appframe.scss b/client/pages/viewerpage/appframe.scss
new file mode 100644
index 00000000..a40b9e27
--- /dev/null
+++ b/client/pages/viewerpage/appframe.scss
@@ -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;
+ }
+}
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 312637f2..aefd2c83 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -9,5 +9,11 @@ services:
- GDRIVE_CLIENT_ID=
- GDRIVE_CLIENT_SECRET=
- DROPBOX_CLIENT_ID=
+ - ONLYOFFICE_URL=http://onlyoffice
ports:
- "8334:8334"
+
+ onlyoffice:
+ container_name: filestash_oods
+ image: onlyoffice/documentserver
+ restart: always
diff --git a/server/common/crypto.go b/server/common/crypto.go
index 8f95cbb7..a18d9b92 100644
--- a/server/common/crypto.go
+++ b/server/common/crypto.go
@@ -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
}
diff --git a/server/common/plugin.go b/server/common/plugin.go
index 1f507caa..7603a234 100644
--- a/server/common/plugin.go
+++ b/server/common/plugin.go
@@ -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)
diff --git a/server/ctrl/export.go b/server/ctrl/export.go
index a4f46ea5..87be5084 100644
--- a/server/ctrl/export.go
+++ b/server/ctrl/export.go
@@ -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
diff --git a/server/ctrl/files.go b/server/ctrl/files.go
index 2cbce9d0..21c5bef0 100644
--- a/server/ctrl/files.go
+++ b/server/ctrl/files.go
@@ -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)
}
diff --git a/server/ctrl/search.go b/server/ctrl/search.go
index d732caea..26995eb8 100644
--- a/server/ctrl/search.go
+++ b/server/ctrl/search.go
@@ -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 = "/"
}
diff --git a/server/ctrl/share.go b/server/ctrl/share.go
index 11af696b..9cb3bc98 100644
--- a/server/ctrl/share.go
+++ b/server/ctrl/share.go
@@ -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
diff --git a/server/main.go b/server/main.go
index 70ba6257..85393958 100644
--- a/server/main.go
+++ b/server/main.go
@@ -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("
"))
})
}
+
+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", 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{}
diff --git a/server/plugin/index.go b/server/plugin/index.go
index 131db3d2..1b99f929 100644
--- a/server/plugin/index.go
+++ b/server/plugin/index.go
@@ -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"
diff --git a/server/plugin/plg_editor_onlyoffice/index.go b/server/plugin/plg_editor_onlyoffice/index.go
new file mode 100644
index 00000000..0db1788e
--- /dev/null
+++ b/server/plugin/plg_editor_onlyoffice/index.go
@@ -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("The Onlyoffice server hasn't been configured
"))
+ res.Write([]byte(""))
+ 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(`
+
+
+
+
+
+
+
+
+
+
+
+
+`,
+ 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}`))
+}