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