001/* 002 * Copyright 2020 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.ametys.plugins.workspaces.documents.onlyoffice; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.net.URL; 022import java.nio.charset.StandardCharsets; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026 027import javax.servlet.http.HttpServletRequest; 028 029import org.apache.avalon.framework.parameters.Parameters; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.cocoon.acting.ServiceableAction; 033import org.apache.cocoon.environment.ObjectModelHelper; 034import org.apache.cocoon.environment.Redirector; 035import org.apache.cocoon.environment.Request; 036import org.apache.cocoon.environment.SourceResolver; 037import org.apache.cocoon.environment.http.HttpEnvironment; 038import org.apache.commons.io.IOUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043import org.ametys.core.cocoon.JSonReader; 044import org.ametys.core.util.JSONUtils; 045import org.ametys.plugins.explorer.resources.Resource; 046import org.ametys.plugins.repository.AmetysObjectResolver; 047import org.ametys.plugins.workspaces.documents.WorkspaceExplorerResourceDAO; 048 049/** 050 * Action to handle OnlyOffice communications 051 */ 052public class GetOnlyOfficeResponse extends ServiceableAction 053{ 054 /** 055 * Enumeration for user connection status 056 */ 057 public enum UserStatus 058 { 059 /** Unknown */ 060 UNKNOWN, 061 /** User connection */ 062 CONNECTION, 063 /** User disconnection */ 064 DISCONNECTION 065 } 066 067 /** The logger */ 068 protected Logger _logger = LoggerFactory.getLogger(GetOnlyOfficeResponse.class.getName()); 069 070 /** JSON utils */ 071 protected JSONUtils _jsonUtils; 072 073 /** The Ametys object resolver */ 074 protected AmetysObjectResolver _resolver; 075 076 /** Workspace explorer resource DAO */ 077 protected WorkspaceExplorerResourceDAO _workspaceExplorerResourceDAO; 078 079 /** The only office manager */ 080 protected OnlyOfficeKeyManager _onlyOfficeManager; 081 082 @Override 083 public void service(ServiceManager smanager) throws ServiceException 084 { 085 super.service(smanager); 086 _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE); 087 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 088 _workspaceExplorerResourceDAO = (WorkspaceExplorerResourceDAO) smanager.lookup(WorkspaceExplorerResourceDAO.ROLE); 089 _onlyOfficeManager = (OnlyOfficeKeyManager) smanager.lookup(OnlyOfficeKeyManager.ROLE); 090 } 091 092 @Override 093 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 094 { 095 Map<String, Object> result = new HashMap<>(); 096 097 Request request = ObjectModelHelper.getRequest(objectModel); 098 099 String resourceId = request.getParameter("id"); 100 101 HttpServletRequest httpRequest = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT); 102 103 try (InputStream is = httpRequest.getInputStream()) 104 { 105 String body = IOUtils.toString(is, StandardCharsets.UTF_8); 106 Map<String, Object> jsonMap = _jsonUtils.convertJsonToMap(body); 107 boolean success = callbackHandler(jsonMap, resourceId); 108 109 // Expected response from the document storage service is { "error": 0 }, otherwise the document editor will display an error message 110 result.put("error", success ? 0 : 1); 111 } 112 113 request.setAttribute(JSonReader.OBJECT_TO_READ, result); 114 return EMPTY_MAP; 115 } 116 117 /** 118 * Analyze the response from document editing service on OnlyOffice Server 119 * See https://api.onlyoffice.com/editors/callback 120 * @param response the response as JSON object 121 * @param resourceId id of edited resource 122 * @return true if the response is ok and the needed process successed. 123 */ 124 protected boolean callbackHandler(Map<String, Object> response, String resourceId) 125 { 126 // edited document identifier 127 String key = (String) response.get("key"); 128 if (key == null) 129 { 130 _logger.error("The edited document identifier is missing in document editing service's response"); 131 return false; 132 } 133 134 // status of document 135 Object oStatus = response.get("status"); 136 if (oStatus == null) 137 { 138 _logger.error("The status of the edited document is missing in document editing service's response"); 139 return false; 140 } 141 142 int status = (Integer) oStatus; 143 144 _logger.debug("Receive status {} for key '{}' and resource '{}' from document editing service", status, key, resourceId); 145 146 switch (status) 147 { 148 case 0: 149 // document not found ?? (0 is not part of documented status) 150 return true; 151 case 1: 152 // document is being edited 153 return getUserConnectionStatus(response) != UserStatus.UNKNOWN; 154 case 2: 155 // document is ready for saving 156 // Status 2 is received 10s after the document is closed for editing with the identifier of the user who was the last to send the changes to the document editing service 157 try 158 { 159 Boolean notModified = (Boolean) response.get("notmodified"); 160 161 if (notModified == null || !notModified) 162 { 163 return saveDocument(response, resourceId); 164 } 165 166 return true; 167 } 168 finally 169 { 170 _onlyOfficeManager.removeKey(resourceId); 171 } 172 case 3: 173 // document saving error has occurred 174 _logger.error("Document editing service returned that document saving error has occurred (3)"); 175 _onlyOfficeManager.removeKey(resourceId); 176 return false; 177 case 4: 178 // document is closed with no changes 179 _logger.info("Document editing service returned that document was closed with no changes (4)"); 180 _onlyOfficeManager.removeKey(resourceId); 181 return getUserConnectionStatus(response) != UserStatus.UNKNOWN; 182 case 6: 183 // document is being edited, but the current document state is saved (force save) 184 return saveDocument(response, resourceId); 185 case 7: 186 // error has occurred while force saving the document 187 _logger.error("Document editing service returned that an error has occurred while force saving the document (7)"); 188 return false; 189 default: 190 _logger.error("Document editing service returned an invalid status: " + status); 191 return false; 192 } 193 } 194 195 /** 196 * Get the status of a user connection 197 * @param response response of OO server as JSON 198 * @return the user status 199 */ 200 protected UserStatus getUserConnectionStatus(Map<String, Object> response) 201 { 202 if (response != null && response.containsKey("actions")) 203 { 204 @SuppressWarnings("unchecked") 205 List<Map<String, Object>> actions = (List<Map<String, Object>>) response.get("actions"); 206 207 for (Map<String, Object> action : actions) 208 { 209 int type = (Integer) action.get("type"); 210 return type == 1 ? UserStatus.CONNECTION : UserStatus.DISCONNECTION; 211 } 212 } 213 214 return UserStatus.UNKNOWN; 215 } 216 217 /** 218 * Save the document if needed 219 * @param response the OO response as JSON 220 * @param resourceId id of document to edit 221 * @return true if save success, false otherwise 222 */ 223 protected boolean saveDocument(Map<String, Object> response, String resourceId) 224 { 225 if (response.get("notmodified") != null && (boolean) response.get("notmodified")) 226 { 227 // No save is needed 228 _logger.debug("No modification was detected by document editing service"); 229 return true; 230 } 231 232 String url = (String) response.get("url"); 233 if (StringUtils.isEmpty(url)) 234 { 235 _logger.error("Unable to save document, the document url is missing"); 236 return false; 237 } 238 239 return processSave(url, resourceId); 240 } 241 242 /** 243 * Save the document 244 * @param documentURI the URI of document to save 245 * @param resourceId id of the resource to update 246 * @return true if success to save document, false otherwise 247 */ 248 protected boolean processSave(String documentURI, String resourceId) 249 { 250 try 251 { 252 URL url = new URL(documentURI); 253 254 Resource resource = _resolver.resolveById(resourceId); 255 256 try (InputStream is = url.openStream()) 257 { 258 // Update the document 259 Map<String, Object> result = _workspaceExplorerResourceDAO.addFile(is, resource.getName(), resource.getParent().getId(), false /* unarchive */, false /* allow rename */, true /* allow update */); 260 if (result != null && result.containsKey("resources")) 261 { 262 return true; 263 } 264 else 265 { 266 _logger.error("Failed to update document with id '{}", resourceId); 267 return false; 268 } 269 } 270 } 271 catch (IOException e) 272 { 273 _logger.error("Unable to get document at uri {}", documentURI, e); 274 return false; 275 } 276 catch (IllegalAccessException e) 277 { 278 _logger.error("User has no sufficient right to edit the document with id '{}'", resourceId, e); 279 return false; 280 } 281 } 282}