001/* 002 * (C) Copyright 2015-2016 Nuxeo SA (http://nuxeo.com/) and others. 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 * Contributors: 017 * Andre Justo 018 */ 019package org.nuxeo.ecm.liveconnect.dropbox; 020 021import java.io.IOException; 022import java.io.Serializable; 023import java.io.UnsupportedEncodingException; 024import java.net.URLDecoder; 025import java.security.Principal; 026import java.util.HashMap; 027import java.util.Locale; 028import java.util.Map; 029 030import javax.faces.application.Application; 031import javax.faces.component.NamingContainer; 032import javax.faces.component.UIComponent; 033import javax.faces.component.UIInput; 034import javax.faces.component.html.HtmlInputText; 035import javax.faces.context.FacesContext; 036import javax.faces.context.ResponseWriter; 037import javax.servlet.http.HttpServletRequest; 038 039import org.apache.commons.lang.StringUtils; 040import org.nuxeo.common.utils.i18n.I18NUtils; 041import org.nuxeo.ecm.core.api.Blob; 042import org.nuxeo.ecm.core.api.DocumentModel; 043import org.nuxeo.ecm.core.api.DocumentModelList; 044import org.nuxeo.ecm.core.api.NuxeoException; 045import org.nuxeo.ecm.core.blob.BlobManager; 046import org.nuxeo.ecm.liveconnect.core.LiveConnectFileInfo; 047import org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token; 048import org.nuxeo.ecm.platform.ui.web.component.file.InputFileChoice; 049import org.nuxeo.ecm.platform.ui.web.component.file.InputFileInfo; 050import org.nuxeo.ecm.platform.ui.web.component.file.JSFBlobUploader; 051import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; 052import org.nuxeo.runtime.api.Framework; 053 054import com.dropbox.core.DbxException; 055 056/** 057 * JSF Blob Upload based on Dropbox blobs. 058 * 059 * @since 7.3 060 */ 061public class DropboxBlobUploader implements JSFBlobUploader { 062 063 public static final String UPLOAD_DROPBOX_FACET_NAME = "uploadDropbox"; 064 065 protected final String id; 066 067 public DropboxBlobUploader(String id) { 068 this.id = id; 069 try { 070 getDropboxBlobProvider(); 071 } catch (NuxeoException e) { 072 // this exception is caught by JSFBlobUploaderDescriptor.getJSFBlobUploader 073 // to mean that the uploader is not available because badly configured 074 throw new IllegalStateException(e); 075 } 076 } 077 078 @Override 079 public String getChoice() { 080 return InputFileChoice.UPLOAD + "Dropbox"; 081 } 082 083 @Override 084 public void hookSubComponent(UIInput parent) { 085 Application app = FacesContext.getCurrentInstance().getApplication(); 086 ComponentUtils.initiateSubComponent(parent, UPLOAD_DROPBOX_FACET_NAME, 087 app.createComponent(HtmlInputText.COMPONENT_TYPE)); 088 } 089 090 @Override 091 public void encodeBeginUpload(UIInput parent, FacesContext context, String onClick) throws IOException { 092 UIComponent facet = parent.getFacet(UPLOAD_DROPBOX_FACET_NAME); 093 if (!(facet instanceof HtmlInputText)) { 094 return; 095 } 096 HtmlInputText inputText = (HtmlInputText) facet; 097 098 // not ours to close 099 @SuppressWarnings("resource") 100 ResponseWriter writer = context.getResponseWriter(); 101 102 String inputId = facet.getClientId(context); 103 String prefix = parent.getClientId(context) + NamingContainer.SEPARATOR_CHAR; 104 String pickId = prefix + "DropboxPickMsg"; 105 String infoId = prefix + "DropboxInfo"; 106 String authorizationUrl = hasServiceAccount() ? "" : getOAuthAuthorizationUrl(); 107 Locale locale = context.getViewRoot().getLocale(); 108 String message; 109 boolean isProviderAvailable = getDropboxBlobProvider().getOAuth2Provider().isProviderAvailable(); 110 111 writer.startElement("button", parent); 112 writer.writeAttribute("type", "button", null); 113 writer.writeAttribute("class", "button", null); 114 115 // only add onclick event to button if oauth service provider is available 116 // this prevents users from using the picker if some configuration is missing 117 if (isProviderAvailable) { 118 String onButtonClick = onClick 119 + ";" 120 + String.format("new nuxeo.utils.DropboxPicker('%s', '%s','%s', '%s')", inputId, infoId, 121 authorizationUrl, getClientId()); 122 writer.writeAttribute("onclick", onButtonClick, null); 123 } 124 125 writer.startElement("span", parent); 126 writer.writeAttribute("id", pickId, null); 127 message = I18NUtils.getMessageString("messages", "label.inputFile.dropboxUploadPicker", null, locale); 128 writer.write(message); 129 writer.endElement("span"); 130 131 writer.endElement("button"); 132 133 if (isProviderAvailable) { 134 writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); 135 writer.startElement("span", parent); 136 writer.writeAttribute("id", infoId, null); 137 message = I18NUtils.getMessageString("messages", "error.inputFile.noFileSelected", null, locale); 138 writer.write(message); 139 writer.endElement("span"); 140 } else { 141 // if oauth service provider not properly setup, add warning message 142 writer.startElement("span", parent); 143 writer.writeAttribute("class", "processMessage completeWarning", null); 144 writer.writeAttribute( 145 "style", 146 "margin: 0 0 .5em 0; font-size: 11px; padding: 0.4em 0.5em 0.5em 2.2em; background-position-y: 0.6em", 147 null); 148 message = I18NUtils.getMessageString("messages", "error.dropbox.providerUnavailable", null, locale); 149 writer.write(message); 150 writer.endElement("span"); 151 } 152 153 inputText.setLocalValueSet(false); 154 inputText.setStyle("display: none"); 155 ComponentUtils.encodeComponent(context, inputText); 156 } 157 158 @Override 159 public void validateUpload(UIInput parent, FacesContext context, InputFileInfo submitted) { 160 UIComponent facet = parent.getFacet(UPLOAD_DROPBOX_FACET_NAME); 161 if (!(facet instanceof HtmlInputText)) { 162 return; 163 } 164 HtmlInputText inputText = (HtmlInputText) facet; 165 Object value = inputText.getSubmittedValue(); 166 String string; 167 if (value == null || value instanceof String) { 168 string = (String) value; 169 } else { 170 ComponentUtils.addErrorMessage(context, parent, "error.inputFile.invalidSpecialBlob"); 171 parent.setValid(false); 172 return; 173 } 174 if (StringUtils.isBlank(string)) { 175 String message = context.getPartialViewContext().isAjaxRequest() ? InputFileInfo.INVALID_WITH_AJAX_MESSAGE 176 : InputFileInfo.INVALID_FILE_MESSAGE; 177 ComponentUtils.addErrorMessage(context, parent, message); 178 parent.setValid(false); 179 return; 180 } 181 182 if (getDropboxBlobProvider().getOAuth2Provider() == null) { 183 ComponentUtils.addErrorMessage(context, parent, "error.inputFile.dropboxInvalidConfiguration"); 184 parent.setValid(false); 185 return; 186 } 187 188 String filePath = getPathFromUrl(string); 189 if (StringUtils.isBlank(filePath)) { 190 ComponentUtils.addErrorMessage(context, parent, "error.inputFile.invalidFilePath"); 191 parent.setValid(false); 192 return; 193 } 194 195 String serviceUserId = getServiceUserId(filePath, FacesContext.getCurrentInstance() 196 .getExternalContext() 197 .getUserPrincipal()); 198 if (StringUtils.isBlank(serviceUserId)) { 199 String link = String.format( 200 "<a href='#' onclick=\"openPopup('%s'); return false;\">Register a new token</a> and try again.", 201 getOAuthAuthorizationUrl()); 202 ComponentUtils.addErrorMessage(context, parent, "error.inputFile.dropboxInvalidPermissions", new Object[] { link }); 203 parent.setValid(false); 204 return; 205 } 206 207 LiveConnectFileInfo fileInfo = new LiveConnectFileInfo(serviceUserId, filePath); 208 Blob blob = createBlob(fileInfo); 209 submitted.setBlob(blob); 210 submitted.setFilename(blob.getFilename()); 211 submitted.setMimeType(blob.getMimeType()); 212 } 213 214 /** 215 * Dropbox upload button is added to the file widget if and only if Dropbox OAuth service provider is enabled 216 * 217 * @return true if Dropbox OAuth service provider is enabled or false otherwise. 218 */ 219 @Override 220 public boolean isEnabled() { 221 return getDropboxBlobProvider().getOAuth2Provider().isEnabled(); 222 } 223 224 /** 225 * Creates a Dropbox managed blob. 226 * 227 * @param fileInfo the Dropbox file info 228 * @return the blob 229 */ 230 protected Blob createBlob(LiveConnectFileInfo fileInfo) { 231 try { 232 return getDropboxBlobProvider().toBlob(fileInfo); 233 } catch (IOException e) { 234 throw new RuntimeException(e); // TODO better feedback 235 } 236 } 237 238 protected String getClientId() { 239 String clientId = getDropboxBlobProvider().getClientId(); 240 return (clientId != null) ? clientId : ""; 241 } 242 243 protected DropboxBlobProvider getDropboxBlobProvider() { 244 return (DropboxBlobProvider) Framework.getService(BlobManager.class).getBlobProvider(id); 245 } 246 247 /** 248 * Retrieves a file path from a Dropbox sharable URL. 249 * 250 * @param url 251 * @return 252 */ 253 private String getPathFromUrl(String url) { 254 String pattern = "https://dl.dropboxusercontent.com/1/view/[\\w]*"; 255 String path = url.replaceAll(pattern, ""); 256 try { 257 path = URLDecoder.decode(path, "UTF-8"); 258 } catch (UnsupportedEncodingException e) { 259 throw new RuntimeException(e); // TODO better feedback 260 } 261 return path; 262 } 263 264 /** 265 * Iterates all registered Dropbox tokens of a {@link Principal} to get the serviceLogin of a token with access to a 266 * Dropbox file. We need this because Dropbox file picker doesn't provide any information about the account that was 267 * used to select the file, and therefore we need to "guess". 268 * 269 * @param filePath 270 * @param principal 271 * @return 272 */ 273 private String getServiceUserId(String filePath, Principal principal) { 274 Map<String, Serializable> filter = new HashMap<>(); 275 filter.put("nuxeoLogin", principal.getName()); 276 277 DocumentModelList userTokens = getDropboxBlobProvider().getOAuth2Provider() 278 .getCredentialDataStore() 279 .query(filter); 280 for (DocumentModel entry : userTokens) { 281 NuxeoOAuth2Token token = new NuxeoOAuth2Token(entry); 282 if (hasAccessToFile(filePath, token.getAccessToken())) { 283 return token.getServiceLogin(); 284 } 285 } 286 return null; 287 } 288 289 /** 290 * Attempts to retrieve a Dropbox file's metadata to check if an accessToken has permissions to access the file. 291 * 292 * @param filePath 293 * @param accessToken 294 * @return true if metadata was successfully retrieved, or false otherwise. 295 */ 296 private boolean hasAccessToFile(String filePath, String accessToken) { 297 try { 298 return getDropboxBlobProvider().getDropboxClient(accessToken).getMetadata(filePath) != null; 299 } catch (DbxException | IOException e) { 300 throw new RuntimeException(e); // TODO better feedback 301 } 302 } 303 304 private boolean hasServiceAccount() { 305 HttpServletRequest request = getHttpServletRequest(); 306 String username = request.getUserPrincipal().getName(); 307 DropboxOAuth2ServiceProvider provider = getDropboxBlobProvider().getOAuth2Provider(); 308 return provider != null && provider.getServiceUser(username) != null; 309 } 310 311 private String getOAuthAuthorizationUrl() { 312 HttpServletRequest request = getHttpServletRequest(); 313 DropboxOAuth2ServiceProvider provider = getDropboxBlobProvider().getOAuth2Provider(); 314 return (provider != null && provider.getClientId() != null) ? provider.getAuthorizationUrl(request) : ""; 315 } 316 317 private HttpServletRequest getHttpServletRequest() { 318 return (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); 319 } 320}