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