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.right.RightManager; 045import org.ametys.core.right.RightManager.RightResult; 046import org.ametys.core.user.CurrentUserProvider; 047import org.ametys.core.util.JSONUtils; 048import org.ametys.plugins.explorer.resources.Resource; 049import org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO; 050import org.ametys.plugins.repository.AmetysObjectResolver; 051import org.ametys.plugins.workspaces.documents.WorkspaceExplorerResourceDAO; 052 053/** 054 * Action to handle OnlyOffice communications 055 */ 056public class GetOnlyOfficeResponse extends ServiceableAction 057{ 058 /** 059 * Enumeration for user connection status 060 */ 061 public enum UserStatus 062 { 063 /** Unknown */ 064 UNKNOWN, 065 /** User connection */ 066 CONNECTION, 067 /** User disconnection */ 068 DISCONNECTION 069 } 070 071 /** The logger */ 072 protected Logger _logger = LoggerFactory.getLogger(GetOnlyOfficeResponse.class.getName()); 073 074 /** JSON utils */ 075 protected JSONUtils _jsonUtils; 076 077 /** The Ametys object resolver */ 078 protected AmetysObjectResolver _resolver; 079 080 /** Workspace explorer resource DAO */ 081 protected WorkspaceExplorerResourceDAO _workspaceExplorerResourceDAO; 082 083 /** The only office manager */ 084 protected OnlyOfficeKeyManager _onlyOfficeManager; 085 086 /** The current user provider */ 087 protected CurrentUserProvider _currentUserProvider; 088 089 /** The right manager */ 090 protected RightManager _rightManager; 091 092 @Override 093 public void service(ServiceManager smanager) throws ServiceException 094 { 095 super.service(smanager); 096 _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE); 097 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 098 _workspaceExplorerResourceDAO = (WorkspaceExplorerResourceDAO) smanager.lookup(WorkspaceExplorerResourceDAO.ROLE); 099 _onlyOfficeManager = (OnlyOfficeKeyManager) smanager.lookup(OnlyOfficeKeyManager.ROLE); 100 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 101 _rightManager = (RightManager) smanager.lookup(RightManager.ROLE); 102 } 103 104 @Override 105 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 106 { 107 Map<String, Object> result = new HashMap<>(); 108 109 Request request = ObjectModelHelper.getRequest(objectModel); 110 111 String resourceId = request.getParameter("id"); 112 113 HttpServletRequest httpRequest = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT); 114 115 try (InputStream is = httpRequest.getInputStream()) 116 { 117 String body = IOUtils.toString(is, StandardCharsets.UTF_8); 118 Map<String, Object> jsonMap = _jsonUtils.convertJsonToMap(body); 119 boolean success = callbackHandler(jsonMap, resourceId); 120 121 // Expected response from the document storage service is { "error": 0 }, otherwise the document editor will display an error message 122 result.put("error", success ? 0 : 1); 123 } 124 125 request.setAttribute(JSonReader.OBJECT_TO_READ, result); 126 return EMPTY_MAP; 127 } 128 129 /** 130 * Analyze the response from document editing service on OnlyOffice Server 131 * See https://api.onlyoffice.com/editors/callback 132 * @param response the response as JSON object 133 * @param resourceId id of edited resource 134 * @return true if the response is ok and the needed process successed. 135 */ 136 protected boolean callbackHandler(Map<String, Object> response, String resourceId) 137 { 138 // edited document identifier 139 String key = (String) response.get("key"); 140 if (key == null) 141 { 142 _logger.error("The edited document identifier is missing in document editing service's response"); 143 return false; 144 } 145 146 // status of document 147 Object oStatus = response.get("status"); 148 if (oStatus == null) 149 { 150 _logger.error("The status of the edited document is missing in document editing service's response"); 151 return false; 152 } 153 154 int status = (Integer) oStatus; 155 156 _logger.debug("Receive status {} for key '{}' and resource '{}' from document editing service", status, key, resourceId); 157 158 switch (status) 159 { 160 case 0: 161 // document not found ?? (0 is not part of documented status) 162 return true; 163 case 1: 164 // document is being edited 165 return getUserConnectionStatus(response) != UserStatus.UNKNOWN; 166 case 2: 167 // document is ready for saving 168 // 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 169 try 170 { 171 Boolean notModified = (Boolean) response.get("notmodified"); 172 173 if (notModified == null || !notModified) 174 { 175 return saveDocument(response, resourceId); 176 } 177 178 return true; 179 } 180 finally 181 { 182 _onlyOfficeManager.removeKey(resourceId); 183 } 184 case 3: 185 // document saving error has occurred 186 _logger.error("Document editing service returned that document saving error has occurred (3)"); 187 _onlyOfficeManager.removeKey(resourceId); 188 return false; 189 case 4: 190 // document is closed with no changes 191 _logger.info("Document editing service returned that document was closed with no changes (4)"); 192 _onlyOfficeManager.removeKey(resourceId); 193 return getUserConnectionStatus(response) != UserStatus.UNKNOWN; 194 case 6: 195 // document is being edited, but the current document state is saved (force save) 196 return saveDocument(response, resourceId); 197 case 7: 198 // error has occurred while force saving the document 199 _logger.error("Document editing service returned that an error has occurred while force saving the document (7)"); 200 return false; 201 default: 202 _logger.error("Document editing service returned an invalid status: " + status); 203 return false; 204 } 205 } 206 207 /** 208 * Get the status of a user connection 209 * @param response response of OO server as JSON 210 * @return the user status 211 */ 212 protected UserStatus getUserConnectionStatus(Map<String, Object> response) 213 { 214 if (response != null && response.containsKey("actions")) 215 { 216 @SuppressWarnings("unchecked") 217 List<Map<String, Object>> actions = (List<Map<String, Object>>) response.get("actions"); 218 219 for (Map<String, Object> action : actions) 220 { 221 int type = (Integer) action.get("type"); 222 return type == 1 ? UserStatus.CONNECTION : UserStatus.DISCONNECTION; 223 } 224 } 225 226 return UserStatus.UNKNOWN; 227 } 228 229 /** 230 * Save the document if needed 231 * @param response the OO response as JSON 232 * @param resourceId id of document to edit 233 * @return true if save success, false otherwise 234 */ 235 protected boolean saveDocument(Map<String, Object> response, String resourceId) 236 { 237 if (response.get("notmodified") != null && (boolean) response.get("notmodified")) 238 { 239 // No save is needed 240 _logger.debug("No modification was detected by document editing service"); 241 return true; 242 } 243 244 String url = (String) response.get("url"); 245 if (StringUtils.isEmpty(url)) 246 { 247 _logger.error("Unable to save document, the document url is missing"); 248 return false; 249 } 250 251 return processSave(url, resourceId); 252 } 253 254 /** 255 * Save the document 256 * @param documentURI the URI of document to save 257 * @param resourceId id of the resource to update 258 * @return true if success to save document, false otherwise 259 */ 260 protected boolean processSave(String documentURI, String resourceId) 261 { 262 try 263 { 264 URL url = new URL(documentURI); 265 266 Resource resource = _resolver.resolveById(resourceId); 267 if (_rightManager.hasRight(_currentUserProvider.getUser(), ExplorerResourcesDAO.RIGHTS_RESOURCE_ADD, resource.getParent()) != RightResult.RIGHT_ALLOW) 268 { 269 _logger.error("User '" + _currentUserProvider.getUser() + "' tried to add file without convenient right [" + ExplorerResourcesDAO.RIGHTS_RESOURCE_ADD + "]"); 270 return false; 271 } 272 273 try (InputStream is = url.openStream()) 274 { 275 // Update the document 276 Map<String, Object> result = _workspaceExplorerResourceDAO.addFile(is, resource.getName(), resource.getParent().getId(), false /* unarchive */, false /* allow rename */, true /* allow update */); 277 if (result != null && result.containsKey("resources")) 278 { 279 return true; 280 } 281 else 282 { 283 _logger.error("Failed to update document with id '{}", resourceId); 284 return false; 285 } 286 } 287 } 288 catch (IOException e) 289 { 290 _logger.error("Unable to get document at uri {}", documentURI, e); 291 return false; 292 } 293 } 294}