feat: onlyoffice editor

This commit is contained in:
DrosoCode 2021-05-31 13:36:58 +02:00 committed by Alan Castro
parent 434e49bf59
commit 08f37b90ce
32 changed files with 344 additions and 17 deletions

View File

@ -42,6 +42,10 @@ override the options.`,
Theme: mustGetString(flags, "branding.theme"),
Files: mustGetString(flags, "branding.files"),
},
OnlyOffice: settings.OnlyOffice{
URL: mustGetString(flags, "onlyoffice.url"),
JWTSecret: mustGetString(flags, "onlyoffice.jwtSecret"),
},
}
ser := &settings.Server{

View File

@ -63,6 +63,10 @@ you want to change. Other options will remain unchanged.`,
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
case "branding.files":
set.Branding.Files = mustGetString(flags, flag.Name)
case "onlyoffice.url":
set.OnlyOffice.URL = mustGetString(flags, flag.Name)
case "onlyoffice.jwtSecret":
set.OnlyOffice.JWTSecret = mustGetString(flags, flag.Name)
}
})

View File

@ -276,6 +276,9 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
i.Content = string(content)
}
return nil
case strings.HasPrefix(mimetype, "application/vnd.openxmlformats-officedocument"):
i.Type = "officedocument"
return nil
default:
i.Type = "blob"
}

View File

@ -195,6 +195,8 @@
"newPassword": "كلمة المرور الجديدة",
"newPasswordConfirm": "تأكيد كلمة المرور",
"newUser": "مستخدم جديد",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "كلمة المرور",
"passwordUpdated": "تم تغيير كلمة المرور!",
"path": "المسار",

View File

@ -182,6 +182,8 @@
"newPassword": "Ihr neues Passwort.",
"newPasswordConfirm": "Bestätigen Sie Ihr neues Passwort",
"newUser": "Neuer Benutzer",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Passwort",
"passwordUpdated": "Passwort aktualisiert!",
"path": "Pfad",
@ -247,4 +249,4 @@
"seconds": "Sekunden",
"unit": "Zeiteinheit"
}
}
}

View File

@ -195,6 +195,9 @@
"newPassword": "Your new password",
"newPasswordConfirm": "Confirm your new password",
"newUser": "New User",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"onlyOfficeJwtSecret": "Only Office JWT Secret (works only with https, leave blank to disable)",
"password": "Password",
"passwordUpdated": "Password updated!",
"path": "Path",
@ -261,4 +264,4 @@
"seconds": "Seconds",
"unit": "Time Unit"
}
}
}

View File

@ -182,6 +182,8 @@
"newPassword": "Tu nueva contraseña",
"newPasswordConfirm": "Confirma tu contraseña",
"newUser": "Nuevo usuario",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Contraseña",
"passwordUpdated": "¡Contraseña actualizada!",
"path": "Ruta",
@ -247,4 +249,4 @@
"seconds": "Segundos",
"unit": "Unidad"
}
}
}

View File

@ -181,6 +181,8 @@
"newPassword": "Votre nouveau mot de passe",
"newPasswordConfirm": "Confirmation du nouveau mot de passe",
"newUser": "Nouvel Utilisateur",
"onlyOffice": "Intégration Only Office",
"onlyOfficeUrl": "URL vers OnlyOffice (laisser vide pour désactiver)",
"password": "Mot de passe",
"passwordUpdated": "Mot de passe mis à jour !",
"path": "",
@ -246,4 +248,4 @@
"seconds": "Secondes",
"unit": "Unité de temps"
}
}
}

View File

@ -169,6 +169,8 @@
"newPassword": "Nýja lykilorðið þitt",
"newPasswordConfirm": "Staðfestu nýja lykilorðið",
"newUser": "Nýr notandi",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Lykilorð",
"passwordUpdated": "Lykilorð vistað!",
"path": "",
@ -232,4 +234,4 @@
"seconds": "Sekúndur",
"unit": "Tímastilling"
}
}
}

View File

@ -170,6 +170,8 @@
"newPassword": "La tua nuova password",
"newPasswordConfirm": "Conferma la password",
"newUser": "Nuovo utente",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Password",
"passwordUpdated": "Password aggiornata!",
"path": "Percorso",
@ -233,4 +235,4 @@
"seconds": "Secondi",
"unit": "Unità di tempo"
}
}
}

View File

@ -191,6 +191,8 @@
"newPassword": "新しいパスワード",
"newPasswordConfirm": "新しいパスワード(再入力)",
"newUser": "新規ユーザー作成",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "パスワード",
"passwordUpdated": "パスワードを更新しました!",
"path": "パス",
@ -256,4 +258,4 @@
"seconds": "秒",
"unit": "時間の単位"
}
}
}

View File

@ -169,6 +169,8 @@
"newPassword": "새로운 비밀번호",
"newPasswordConfirm": "새로운 비밀번호 확인",
"newUser": "새로운 사용자",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "비밀번호",
"passwordUpdated": "비밀번호 수정 완료!",
"path": "",
@ -232,4 +234,4 @@
"seconds": "초",
"unit": "Time Unit"
}
}
}

View File

@ -169,6 +169,8 @@
"newPassword": "Uw nieuw wachtwoord",
"newPasswordConfirm": "Bevestig uw nieuw wachtwoord",
"newUser": "Nieuwe gebruiker",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Wachtwoord",
"passwordUpdated": "Wachtwoord bijgewerkt!",
"path": "",
@ -232,4 +234,4 @@
"seconds": "Seconden",
"unit": "Tijdseenheid"
}
}
}

View File

@ -169,6 +169,8 @@
"newPassword": "Twoje nowe hasło",
"newPasswordConfirm": "Potwierdź swoje hasło",
"newUser": "Nowy Użytkownik",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Hasło",
"passwordUpdated": "Hasło zostało zapisane!",
"path": "Ścieżka",
@ -232,4 +234,4 @@
"seconds": "Sekundy",
"unit": "Jednostka czasu"
}
}
}

View File

@ -182,6 +182,8 @@
"newPassword": "Nova senha",
"newPasswordConfirm": "Confirme a nova senha",
"newUser": "Novo usuário",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Senha",
"passwordUpdated": "Senha atualizada!",
"path": "",
@ -247,4 +249,4 @@
"seconds": "Segundos",
"unit": "Unidades de Tempo"
}
}
}

View File

@ -170,6 +170,8 @@
"newPassword": "Nova palavra-passe",
"newPasswordConfirm": "Confirme a nova palavra-passe",
"newUser": "Novo utilizador",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Palavra-passe",
"passwordUpdated": "Palavra-passe atualizada!",
"path": "",
@ -233,4 +235,4 @@
"seconds": "Segundos",
"unit": "Unidades de tempo"
}
}
}

View File

@ -169,6 +169,8 @@
"newPassword": "Noua ta parolă",
"newPasswordConfirm": "Confirmă noua parolă",
"newUser": "Utilizator nou",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Parola",
"passwordUpdated": "Parola actualizată!",
"path": "",
@ -232,4 +234,4 @@
"seconds": "Secunde",
"unit": "Unitate de timp"
}
}
}

View File

@ -177,6 +177,8 @@
"newPassword": "Новый пароль",
"newPasswordConfirm": "Повтор нового пароля",
"newUser": "Новый пользователь",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Пароль",
"passwordUpdated": "Пароль обновлен!",
"path": "Путь",
@ -242,4 +244,4 @@
"seconds": "Секунды",
"unit": "Единица времени"
}
}
}

View File

@ -169,6 +169,8 @@
"newPassword": "Ditt nya lösenord",
"newPasswordConfirm": "Bekräfta ditt nya lösenord",
"newUser": "Ny användare",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "Lösenord",
"passwordUpdated": "Lösenord uppdaterat",
"path": "",
@ -232,4 +234,4 @@
"seconds": "Sekunder",
"unit": "Tidsenhet"
}
}
}

View File

@ -191,6 +191,8 @@
"newPassword": "你的新密码",
"newPasswordConfirm": "再次输入以确认你的新密码",
"newUser": "新建用户",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "密码",
"passwordUpdated": "密码已更新!",
"path": "路径",
@ -256,4 +258,4 @@
"seconds": "秒",
"unit": "时间单位"
}
}
}

View File

@ -169,6 +169,8 @@
"newPassword": "您的新密碼",
"newPasswordConfirm": "重輸一遍新密碼",
"newUser": "建立使用者",
"onlyOffice": "Only Office Integration",
"onlyOfficeUrl": "Only Office URL (leave blank to disable)",
"password": "密碼",
"passwordUpdated": "密碼已更新!",
"path": "",
@ -232,4 +234,4 @@
"seconds": "秒",
"unit": "時間單位"
}
}
}

View File

@ -18,6 +18,7 @@ const enableExec: boolean = window.FileBrowser.EnableExec;
const tusSettings = window.FileBrowser.TusSettings;
const origin = window.location.origin;
const tusEndpoint = `/api/tus`;
const onlyOffice = window.FileBrowser.OnlyOffice;
export {
name,
@ -39,4 +40,5 @@ export {
tusSettings,
origin,
tusEndpoint,
onlyOffice,
};

View File

@ -37,6 +37,7 @@ import { storeToRefs } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useUploadStore } from "@/stores/upload";
import { onlyOffice } from "@/utils/constants";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
@ -47,6 +48,7 @@ import FileListing from "@/views/files/FileListing.vue";
import { StatusError } from "@/api/utils";
const Editor = defineAsyncComponent(() => import("@/views/files/Editor.vue"));
const Preview = defineAsyncComponent(() => import("@/views/files/Preview.vue"));
const OnlyOfficeEditor = defineAsyncComponent(() => import("@/views/files/OnlyOfficeEditor.vue"));
const layoutStore = useLayoutStore();
const fileStore = useFileStore();
@ -77,6 +79,8 @@ const currentView = computed(() => {
fileStore.req.type === "textImmutable"
) {
return Editor;
} else if (fileStore.req.type === "officedocument" && onlyOffice !== "") {
return OnlyOfficeEditor;
} else {
return Preview;
}

View File

@ -0,0 +1,170 @@
<template>
<div id="editor-container">
<header-bar>
<action icon="close" :label="$t('buttons.close')" @action="close()" />
<title>{{ req.name }}</title>
</header-bar>
<breadcrumbs base="/files" noLink />
<div id="editor"></div>
</div>
</template>
<style scoped>
#editor-container {
height: 100vh;
width: 100vw;
}
</style>
<script>
import { mapState } from "vuex";
import url from "@/utils/url";
import { baseURL, onlyOffice } from "@/utils/constants";
import * as jose from "jose";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
export default {
name: "onlyofficeeditor",
components: {
HeaderBar,
Action,
Breadcrumbs,
},
data: function () {
return {};
},
computed: {
...mapState(["req", "user", "jwt"]),
breadcrumbs() {
let parts = this.$route.path.split("/");
if (parts[0] === "") {
parts.shift();
}
if (parts[parts.length - 1] === "") {
parts.pop();
}
let breadcrumbs = [];
for (let i = 0; i < parts.length; i++) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]) });
}
breadcrumbs.shift();
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift();
}
breadcrumbs[0].name = "...";
}
return breadcrumbs;
},
},
created() {
window.addEventListener("keydown", this.keyEvent);
},
beforeDestroy() {
window.removeEventListener("keydown", this.keyEvent);
this.editor.destroyEditor();
},
mounted: function () {
let onlyofficeScript = document.createElement("script");
onlyofficeScript.setAttribute(
"src",
`${onlyOffice.url}/web-apps/apps/api/documents/api.js`
);
document.head.appendChild(onlyofficeScript);
/*eslint-disable */
onlyofficeScript.onload = () => {
let fileUrl = `${window.location.protocol}//${window.location.host}${baseURL}/api/raw${url.encodePath(
this.req.path
)}?auth=${this.jwt}`;
// create a key from the last modified timestamp and the reversed file path (most specific part first)
// replace all special characters (only these symbols are supported: 0-9, a-z, A-Z, -._=)
// and truncate it (max length is 20 characters)
const key = (
Date.parse(this.req.modified).valueOf()
+ url
.encodePath(this.req.path.split('/').reverse().join(''))
.replaceAll(/[!~[\]*'()/,;:\-%+. ]/g, "")
).substring(0, 20);
const config = {
document: {
fileType: this.req.extension.substring(1),
key: key,
title: this.req.name,
url: fileUrl,
permissions: {
edit: this.user.perm.modify,
download: this.user.perm.download,
print: this.user.perm.download
}
},
editorConfig: {
callbackUrl: `${window.location.protocol}//${window.location.host}${baseURL}/api/onlyoffice/callback?auth=${this.jwt}&save=${encodeURIComponent(this.req.path)}`,
user: {
id: this.user.id,
name: `User ${this.user.id}`
},
customization: {
autosave: true,
forcesave: true
},
lang: this.user.locale,
mode: this.user.perm.modify ? "edit" : "view"
}
};
if(onlyOffice.jwtSecret != "") {
const alg = 'HS256';
new jose.SignJWT(config)
.setProtectedHeader({ alg })
.sign(new TextEncoder().encode(onlyOffice.jwtSecret)).then((jwt) => {
config.token = jwt;
this.editor = new DocsAPI.DocEditor("editor", config);
})
} else {
this.editor = new DocsAPI.DocEditor("editor", config);
}
};
/*eslint-enable */
},
methods: {
back() {
let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri });
},
keyEvent(event) {
if (!event.ctrlKey && !event.metaKey) {
return;
}
if (String.fromCharCode(event.which).toLowerCase() !== "s") {
return;
}
event.preventDefault();
this.save();
},
close() {
this.$store.commit("updateRequest", {});
let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri });
},
},
};
</script>

View File

@ -136,6 +136,31 @@
/>
</p>
</div>
<h3>{{ $t("settings.onlyOffice") }}</h3>
<p>
<label for="onlyoffice-url">{{
$t("settings.onlyOfficeUrl")
}}</label>
<input
class="input input--block"
type="text"
v-model="settings.onlyoffice.url"
id="onlyoffice-url"
/>
</p>
<p>
<label for="onlyoffice-jwt">{{
$t("settings.onlyOfficeJwtSecret")
}}</label>
<input
class="input input--block"
type="text"
v-model="settings.onlyoffice.jwtSecret"
id="onlyoffice-jwt"
/>
</p>
</div>
<div class="card-action">

View File

@ -60,6 +60,8 @@ func NewHandler(
users.Handle("/{id:[0-9]+}", monkey(userGetHandler, "")).Methods("GET")
users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE")
api.PathPrefix("/onlyoffice").Handler(monkey(onlyofficeCallbackHandler, "/api/onlyoffice/callback")).Methods("POST")
api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET")
api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler(fileCache), "/api/resources")).Methods("DELETE")
api.PathPrefix("/resources").Handler(monkey(resourcePostHandler(fileCache), "/api/resources")).Methods("POST")

64
http/onlyoffice.go Normal file
View File

@ -0,0 +1,64 @@
package http
import (
"encoding/json"
"errors"
"io"
"net/http"
)
type OnlyOfficeCallback struct {
ChangesURL string `json:"changesurl,omitempty"`
Key string `json:"key"`
Status int `json:"status"`
URL string `json:"url,omitempty"`
Users []string `json:"users,omitempty"`
UserData string `json:"userdata,omitempty"`
}
var onlyofficeCallbackHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
body, err := io.ReadAll(r.Body)
if err != nil {
return http.StatusInternalServerError, err
}
var data OnlyOfficeCallback
err = json.Unmarshal(body, &data)
if err != nil {
return http.StatusInternalServerError, err
}
if data.Status == 2 || data.Status == 6 {
docPath := r.URL.Query().Get("save")
if docPath == "" {
return http.StatusInternalServerError, errors.New("unable to get file save path")
}
if !d.user.Perm.Modify || !d.Check(docPath) {
return http.StatusForbidden, nil
}
doc, err := http.Get(data.URL)
if err != nil {
return http.StatusInternalServerError, err
}
defer doc.Body.Close()
err = d.RunHook(func() error {
_, writeErr := writeFile(d.user.Fs, docPath, doc.Body)
if writeErr != nil {
return writeErr
}
return nil
}, "save", docPath, "", d.user)
if err != nil {
return http.StatusInternalServerError, err
}
}
resp := map[string]int{
"error": 0,
}
return renderJSON(w, r, resp)
})

View File

@ -18,6 +18,7 @@ type settingsData struct {
Tus settings.Tus `json:"tus"`
Shell []string `json:"shell"`
Commands map[string][]string `json:"commands"`
OnlyOffice settings.OnlyOffice `json:"onlyoffice"`
}
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
@ -31,6 +32,7 @@ var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
Tus: d.settings.Tus,
Shell: d.settings.Shell,
Commands: d.settings.Commands,
OnlyOffice: d.settings.OnlyOffice,
}
return renderJSON(w, r, data)
@ -52,6 +54,7 @@ var settingsPutHandler = withAdmin(func(_ http.ResponseWriter, r *http.Request,
d.settings.Tus = req.Tus
d.settings.Shell = req.Shell
d.settings.Commands = req.Commands
d.settings.OnlyOffice = req.OnlyOffice
err = d.store.Settings.Save(d.settings)
return errToStatus(err), err

View File

@ -46,6 +46,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
"ResizePreview": d.server.ResizePreview,
"EnableExec": d.server.EnableExec,
"TusSettings": d.settings.Tus,
"OnlyOffice": d.settings.OnlyOffice,
}
if d.settings.Branding.Files != "" {

7
settings/onlyoffice.go Normal file
View File

@ -0,0 +1,7 @@
package settings
// OnlyOffice contains the onlyoffice server connection settings of the app.
type OnlyOffice struct {
URL string `json:"url"`
JWTSecret string `json:"jwtSecret"`
}

View File

@ -27,6 +27,7 @@ type Settings struct {
Commands map[string][]string `json:"commands"`
Shell []string `json:"shell"`
Rules []rules.Rule `json:"rules"`
OnlyOffice OnlyOffice `json:"onlyoffice"`
}
// GetRules implements rules.Provider.

BIN
test.docx Normal file

Binary file not shown.