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