diff --git a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/OnlyOfficeEditor.java b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/OnlyOfficeEditor.java index 664410d3dbc467060a3f99a468b3ddae3da4f112..3d1544722f783c1caacac992abe0ef004085a107 100644 --- a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/OnlyOfficeEditor.java +++ b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/OnlyOfficeEditor.java @@ -72,7 +72,9 @@ public class OnlyOfficeEditor implements DocEditor { @Override public boolean isLockedForMe(VFSLeaf vfsLeaf, Identity identity, Mode mode) { - // TODO uh Auto-generated method stub + if (onlyOfficeService.isLockNeeded(mode)) { + return onlyOfficeService.isLockedForMe(vfsLeaf, identity); + } return false; } diff --git a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/OnlyOfficeService.java b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/OnlyOfficeService.java index beb9ee803d9ec6d5e1dde98742b8df144395b92c..c09561ab0694de7e9c43cd350c11bf9248cc0c2c 100644 --- a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/OnlyOfficeService.java +++ b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/OnlyOfficeService.java @@ -24,6 +24,8 @@ import java.io.File; import org.olat.core.commons.services.doceditor.DocEditor.Mode; import org.olat.core.commons.services.vfs.VFSMetadata; import org.olat.core.id.Identity; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.lock.LockResult; /** * @@ -37,9 +39,19 @@ public interface OnlyOfficeService { File getFile(String fileId); - boolean canUpdateContent(String fileId, Identity identity); + VFSLeaf getVfsLeaf(String fileId); - boolean updateContent(String fileId, Identity identity, String url); + boolean canUpdateContent(VFSLeaf vfsLeaf, Identity identity, String documentKey); + + boolean updateContent(VFSLeaf vfsLeaf, Identity identity, String url); + + boolean isLockNeeded(Mode mode); + + boolean isLockedForMe(VFSLeaf vfsLeaf, Identity identity); + + LockResult lock(VFSLeaf vfsLeaf, Identity identity); + + void unlock(VFSLeaf vfsLeaf); boolean isSupportedFormat(String suffix, Mode mode); diff --git a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/manager/OnlyOfficeServiceImpl.java b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/manager/OnlyOfficeServiceImpl.java index bc3e6d0a1dd796930c2fe47fde2244d727bced69..b3bd9cd27fb229c5ee082b68dc7ac4980989a9ff 100644 --- a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/manager/OnlyOfficeServiceImpl.java +++ b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/manager/OnlyOfficeServiceImpl.java @@ -22,21 +22,26 @@ package org.olat.core.commons.services.doceditor.onlyoffice.manager; import java.io.File; import java.io.InputStream; import java.net.URL; +import java.nio.file.Paths; import java.text.DateFormat; import java.text.SimpleDateFormat; import org.olat.basesecurity.BaseSecurityManager; import org.olat.core.commons.services.doceditor.DocEditor.Mode; import org.olat.core.commons.services.doceditor.onlyoffice.OnlyOfficeService; -import org.olat.core.commons.services.doceditor.wopi.WopiService; import org.olat.core.commons.services.vfs.VFSMetadata; import org.olat.core.commons.services.vfs.VFSRepositoryService; import org.olat.core.id.Identity; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.vfs.VFSConstants; +import org.olat.core.util.vfs.VFSItem; import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.VFSLockApplicationType; +import org.olat.core.util.vfs.VFSLockManager; import org.olat.core.util.vfs.VFSManager; +import org.olat.core.util.vfs.lock.LockInfo; +import org.olat.core.util.vfs.lock.LockResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -51,36 +56,53 @@ public class OnlyOfficeServiceImpl implements OnlyOfficeService { private static final OLog log = Tracing.createLoggerFor(OnlyOfficeServiceImpl.class); + private static final String LOCK_APP_NAME = "onlyoffice"; + private static DateFormat LAST_MODIFIED = new SimpleDateFormat("yyyyMMddHHmmSS"); - @Autowired - private WopiService wopiService; @Autowired private VFSRepositoryService vfsRepositoryService; @Autowired + private VFSLockManager lockManager; + @Autowired private BaseSecurityManager securityManager; @Override public boolean fileExists(String fileId) { - return wopiService.fileExists(fileId); + return vfsRepositoryService.getItemFor(fileId) != null? true: false; } @Override public File getFile(String fileId) { - return wopiService.getFile(fileId); + VFSLeaf vfsLeaf = getVfsLeaf(fileId); + if (vfsLeaf != null) { + String uri = vfsLeaf.getMetaInfo().getUri(); + try { + return Paths.get(new URL(uri).toURI()).toFile(); + } catch (Exception e) { + log.error("", e); + } + } + return null; + } + + @Override + public VFSLeaf getVfsLeaf(String fileId) { + VFSItem item = vfsRepositoryService.getItemFor(fileId); + if (item instanceof VFSLeaf) { + return (VFSLeaf) item; + } + return null; } @Override - public boolean canUpdateContent(String fileId, Identity identity) { - return true; - //TODO uh check lock -// VFSLeaf vfsLeaf = wopiService.getVfsLeaf(fileId); -// return !isLockedForMe(vfsLeaf, access.getIdentity()); + public boolean canUpdateContent(VFSLeaf vfsLeaf, Identity identity, String documentKey) { + String currentDocumentKey = getDocumentKey(vfsLeaf.getMetaInfo()); + return currentDocumentKey.equals(documentKey) && !isLockedForMe(vfsLeaf, identity); } @Override - public boolean updateContent(String fileId, Identity identity, String url) { - VFSLeaf vfsLeaf = wopiService.getVfsLeaf(fileId); + public boolean updateContent(VFSLeaf vfsLeaf, Identity identity, String url) { boolean updated = false; try (InputStream in = new URL(url).openStream()) { //TODO uh versionCntrolled @@ -94,12 +116,46 @@ public class OnlyOfficeServiceImpl implements OnlyOfficeService { log.error("", e); } if (updated) { - log.debug("File updated. File ID: " + fileId); - //TODO uh lock -// refreshLock(vfsLeaf); + log.debug("File updated. File name: " + vfsLeaf.getName()); + refreshLock(vfsLeaf); } return updated; } + + private void refreshLock(VFSLeaf vfsLeaf) { + LockInfo lock = lockManager.getLock(vfsLeaf); + if (lock != null) { + long inADay = System.currentTimeMillis() + (24 * 60 * 60 * 1000); + lock.setExpiresAt(inADay); + } + } + + @Override + public boolean isLockNeeded(Mode mode) { + return Mode.EDIT.equals(mode); + } + + @Override + public boolean isLockedForMe(VFSLeaf vfsLeaf, Identity identity) { + return lockManager.isLockedForMe(vfsLeaf, identity, VFSLockApplicationType.collaboration, LOCK_APP_NAME); + } + + @Override + public LockResult lock(VFSLeaf vfsLeaf, Identity identity) { + LockResult lock = lockManager.lock(vfsLeaf, identity, VFSLockApplicationType.collaboration, LOCK_APP_NAME); + log.debug("Locked file. File name: " + vfsLeaf.getName() + ", Identity: " + identity); + return lock; + } + + @Override + public void unlock(VFSLeaf vfsLeaf) { + LockInfo lock = lockManager.getLock(vfsLeaf); + if (lock != null && LOCK_APP_NAME.equals(lock.getAppName())) { + lock.getTokens().clear(); + lockManager.unlock(vfsLeaf, VFSLockApplicationType.collaboration); + log.debug("Unlocked file. File name: " + vfsLeaf.getName()); + } + } @Override public boolean isSupportedFormat(String suffix, Mode mode) { diff --git a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/CallbackStatus.java b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/CallbackStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..f153f01377a67dfbb3d0386d120b59ec6c2f9da8 --- /dev/null +++ b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/CallbackStatus.java @@ -0,0 +1,53 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.core.commons.services.doceditor.onlyoffice.restapi; + +/** + * + * Initial date: 16 Apr 2019<br> + * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com + * + */ +enum CallbackStatus { + + ErrorDocumentNotFound(0), + Editing(1), + MustSave(2), + ErrorCorrupted(3), + ClosedWithoutChanges(4), + MustForceSave(6), + ErrorCorruptedForce(7); + + private final int value; + + private CallbackStatus(int value) { + this.value = value; + } + + static CallbackStatus valueOf(int value) { + for (CallbackStatus callbackStatus : CallbackStatus.values()) { + if (callbackStatus.value == value) { + return callbackStatus; + } + } + return null; + } + +} diff --git a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/CallbackVO.java b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/CallbackVO.java index 7cb85686fef6c65148603be9759057a99f661d07..40b6660e36193833a5292e7a352a5d4471778a0f 100644 --- a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/CallbackVO.java +++ b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/CallbackVO.java @@ -35,8 +35,6 @@ public class CallbackVO { // see https://api.onlyoffice.com/editors/callback - static final int STATUS_READY_FOR_SAVING = 2; - private List<Action> actions; private Integer forcesavetype; private String key; diff --git a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/OnlyOfficeWebService.java b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/OnlyOfficeWebService.java index 33be973f60664200047cb8326717d99927d5dd6d..013da213468196de86ef0b102475cd5bbdf0605a 100644 --- a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/OnlyOfficeWebService.java +++ b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/restapi/OnlyOfficeWebService.java @@ -21,7 +21,6 @@ package org.olat.core.commons.services.doceditor.onlyoffice.restapi; import static org.olat.core.commons.services.doceditor.onlyoffice.restapi.CallbackResponseVO.error; import static org.olat.core.commons.services.doceditor.onlyoffice.restapi.CallbackResponseVO.success; -import static org.olat.core.commons.services.doceditor.onlyoffice.restapi.CallbackVO.STATUS_READY_FOR_SAVING; import java.io.File; import java.util.List; @@ -45,6 +44,8 @@ import org.olat.core.commons.services.doceditor.onlyoffice.OnlyOfficeService; import org.olat.core.id.Identity; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.lock.LockResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -87,10 +88,37 @@ public class OnlyOfficeWebService { } CallbackResponseVO responseVO; - switch(callbackVO.getStatus()) { - case STATUS_READY_FOR_SAVING: + CallbackStatus status = CallbackStatus.valueOf(callbackVO.getStatus()); + switch(status) { + case Editing: + responseVO = lock(fileId, callbackVO); + break; + case ClosedWithoutChanges: + // This callback is called + // A) if a user closes a document without changes. + // B) if a user does not edit a document for about one minute. + // Case B) results in a opened, unlocked file. If the user starts to edit the file again callback "Editing" is called and the file is locked again. + // However, is is possible to edit the file in the meantime in another editor and all changes made in ONLYOFFICE are lost! + // (This is the same implementation like in Alfresco, ownCloud, Nextcloud etc.) + responseVO = unlock(fileId, callbackVO); + break; + case MustSave: + case MustForceSave: responseVO = updateContent(fileId, callbackVO); break; + case ErrorCorrupted: + log.warn("ONLYOFFICE has reported that saving the document has failed. File ID: " + fileId); + responseVO = success(); + break; + case ErrorCorruptedForce: + log.warn("ONLYOFFICE has reported that saving the document has failed. File ID: " + fileId); + responseVO = success(); + break; + case ErrorDocumentNotFound: + // I never get that status, so I do not know, how to reproduce it. + log.warn("ONLYOFFICE has reported that no doc with the specified key can be found. File ID: " + fileId); + responseVO = success(); + break; default: // nothing to do responseVO = success(); @@ -99,19 +127,53 @@ public class OnlyOfficeWebService { return Response.ok(responseVO).build(); } - private CallbackResponseVO updateContent(String fileId, CallbackVO callbackVO) { + private CallbackResponseVO lock(String fileId, CallbackVO callbackVO) { String IdentityId = callbackVO.getUsers()[0]; Identity identity = onlyOfficeService.getIdentity(IdentityId); - if (identity == null) { + if (identity == null) return error(); + + VFSLeaf vfsLeaf = onlyOfficeService.getVfsLeaf(fileId); + if (vfsLeaf == null) return error(); + + boolean isLockedForMe = onlyOfficeService.isLockedForMe(vfsLeaf, identity); + if (isLockedForMe) return error(); + + boolean canUpdate = onlyOfficeService.canUpdateContent(vfsLeaf, identity, callbackVO.getKey()); + if (!canUpdate) { + log.debug("ONLYOFFICE has no right to update file. File ID: " + fileId + ", identity: " + IdentityId); return error(); } - boolean canUpdate = onlyOfficeService.canUpdateContent(fileId, identity); + LockResult lock = onlyOfficeService.lock(vfsLeaf, identity); + return lock != null? success(): error(); + } + + private CallbackResponseVO unlock(String fileId, CallbackVO callbackVO) { + VFSLeaf vfsLeaf = onlyOfficeService.getVfsLeaf(fileId); + if (vfsLeaf == null) return error(); + + boolean lastUser = callbackVO.getUsers() == null || callbackVO.getUsers().length == 0; + if (lastUser) { + onlyOfficeService.unlock(vfsLeaf); + } + return success(); + } + + private CallbackResponseVO updateContent(String fileId, CallbackVO callbackVO) { + String IdentityId = callbackVO.getUsers()[0]; + Identity identity = onlyOfficeService.getIdentity(IdentityId); + if (identity == null) return error(); + + VFSLeaf vfsLeaf = onlyOfficeService.getVfsLeaf(fileId); + if (vfsLeaf == null) return error(); + + boolean canUpdate = onlyOfficeService.canUpdateContent(vfsLeaf, identity, callbackVO.getKey()); if (!canUpdate) { - log.debug("Access has not right to update file. File ID: " + fileId + ", identity: " + IdentityId); + log.debug("ONLYOFFICE has no right to update file. File ID: " + fileId + ", identity: " + IdentityId); return error(); } - boolean updated = onlyOfficeService.updateContent(fileId, identity, callbackVO.getUrl()); + boolean updated = onlyOfficeService.updateContent(vfsLeaf, identity, callbackVO.getUrl()); + onlyOfficeService.unlock(vfsLeaf); return updated? success(): error(); } diff --git a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/ui/ApiConfigBuilder.java b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/ui/ApiConfigBuilder.java index a6d9eff81eb6137f2d7bfbc8799ece4047aed6b2..946500f5e84e3286101fd490071303cd7a7be4e1 100644 --- a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/ui/ApiConfigBuilder.java +++ b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/ui/ApiConfigBuilder.java @@ -402,11 +402,10 @@ public class ApiConfigBuilder { private String mode; private User user; // not implemented yet + // private Customization customization; // private Object recent; -// private String createUrl; - // private Customization costumisazion; + // private String createUrl; // private Embedded embedded; - // private Customization costumisazion; public String getCallbackUrl() { return callbackUrl; diff --git a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/ui/OnlyOfficeEditorController.java b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/ui/OnlyOfficeEditorController.java index 45ca2514bc5ca0f03ec34ef2eb00ba5c0b763c29..e203d61930266d3e8fb193fa92f69a36b5e150e7 100644 --- a/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/ui/OnlyOfficeEditorController.java +++ b/src/main/java/org/olat/core/commons/services/doceditor/onlyoffice/ui/OnlyOfficeEditorController.java @@ -21,7 +21,9 @@ package org.olat.core.commons.services.doceditor.onlyoffice.ui; import org.olat.core.commons.services.doceditor.DocEditor.Mode; import org.olat.core.commons.services.doceditor.DocEditorSecurityCallback; +import org.olat.core.commons.services.doceditor.DocEditorSecurityCallbackBuilder; import org.olat.core.commons.services.doceditor.onlyoffice.OnlyOfficeModule; +import org.olat.core.commons.services.doceditor.onlyoffice.OnlyOfficeService; import org.olat.core.commons.services.vfs.VFSMetadata; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; @@ -47,11 +49,25 @@ public class OnlyOfficeEditorController extends BasicController { @Autowired private OnlyOfficeModule onlyOfficeModule; + @Autowired + private OnlyOfficeService onlyOfficeService; public OnlyOfficeEditorController(UserRequest ureq, WindowControl wControl, VFSLeaf vfsLeaf, final DocEditorSecurityCallback securityCallback) { super(ureq, wControl); + + DocEditorSecurityCallback secCallback = securityCallback; + if (onlyOfficeService.isLockNeeded(secCallback.getMode())) { + if (onlyOfficeService.isLockedForMe(vfsLeaf, getIdentity())) { + secCallback = DocEditorSecurityCallbackBuilder.clone(secCallback) + .withMode(Mode.VIEW) + .build(); + showWarning("editor.warning.locked"); + } else { + onlyOfficeService.lock(vfsLeaf, getIdentity()); + } + } VelocityContainer mainVC = createVelocityContainer("editor"); VFSMetadata vfsMetadata = vfsLeaf.getMetaInfo(); @@ -59,7 +75,7 @@ public class OnlyOfficeEditorController extends BasicController { mainVC.contextPut("warning", translate("editor.warning.no.metadata")); } else { String apiConfig = ApiConfigBuilder.builder(vfsMetadata, getIdentity()) - .withEdit(Mode.EDIT.equals(securityCallback.getMode())) + .withEdit(Mode.EDIT.equals(secCallback.getMode())) .buildJson(); log.debug("OnlyOffice ApiConfig: " + apiConfig);