001/* 002 * (C) Copyright 2015-2018 Nuxeo (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 * Florent Guillaume 018 * Nelson Silva 019 */ 020package org.nuxeo.ecm.liveconnect.google.drive; 021 022import java.io.IOException; 023import java.util.Locale; 024 025import javax.faces.application.Application; 026import javax.faces.component.NamingContainer; 027import javax.faces.component.UIComponent; 028import javax.faces.component.UIInput; 029import javax.faces.component.html.HtmlInputText; 030import javax.faces.context.FacesContext; 031import javax.faces.context.ResponseWriter; 032import javax.servlet.http.HttpServletRequest; 033 034import org.apache.commons.lang3.StringUtils; 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037import org.nuxeo.common.utils.i18n.I18NUtils; 038import org.nuxeo.ecm.core.api.Blob; 039import org.nuxeo.ecm.core.api.NuxeoException; 040import org.nuxeo.ecm.core.blob.BlobManager; 041import org.nuxeo.ecm.liveconnect.core.LiveConnectFileInfo; 042import org.nuxeo.ecm.platform.ui.web.component.file.InputFileChoice; 043import org.nuxeo.ecm.platform.ui.web.component.file.InputFileInfo; 044import org.nuxeo.ecm.platform.ui.web.component.file.JSFBlobUploader; 045import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; 046import org.nuxeo.runtime.api.Framework; 047 048import com.google.api.client.auth.oauth2.Credential; 049 050/** 051 * JSF Blob Upload based on Google Drive blobs. 052 * 053 * @since 7.3 054 */ 055public class GoogleDriveBlobUploader implements JSFBlobUploader { 056 057 private static final Log log = LogFactory.getLog(GoogleDriveBlobUploader.class); 058 059 public static final String UPLOAD_GOOGLE_DRIVE_FACET_NAME = "uploadGoogleDrive"; 060 061 // restrict sign-in to accounts at this domain 062 public static final String GOOGLE_DOMAIN_PROP = "nuxeo.google.domain"; 063 064 protected final String id; 065 066 public GoogleDriveBlobUploader(String id) { 067 this.id = id; 068 try { 069 getGoogleDriveBlobProvider(); 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 + "GoogleDrive"; 080 } 081 082 @Override 083 public void hookSubComponent(UIInput parent) { 084 Application app = FacesContext.getCurrentInstance().getApplication(); 085 ComponentUtils.initiateSubComponent(parent, UPLOAD_GOOGLE_DRIVE_FACET_NAME, 086 app.createComponent(HtmlInputText.COMPONENT_TYPE)); 087 088 } 089 090 // Needs supporting JavaScript code for nuxeo.utils.pickFromGoogleDrive defined in googleclient.js. 091 @Override 092 public void encodeBeginUpload(UIInput parent, FacesContext context, String onClick) throws IOException { 093 UIComponent facet = parent.getFacet(UPLOAD_GOOGLE_DRIVE_FACET_NAME); 094 if (!(facet instanceof HtmlInputText)) { 095 return; 096 } 097 HtmlInputText inputText = (HtmlInputText) facet; 098 099 // not ours to close 100 @SuppressWarnings("resource") 101 ResponseWriter writer = context.getResponseWriter(); 102 103 String inputId = facet.getClientId(context); 104 String prefix = parent.getClientId(context) + NamingContainer.SEPARATOR_CHAR; 105 String pickId = prefix + "GoogleDrivePickMsg"; 106 String authId = prefix + "GoogleDriveAuthMsg"; 107 String infoId = prefix + "GoogleDriveInfo"; 108 String authorizationUrl = hasServiceAccount() ? "" : getOAuthAuthorizationUrl(); 109 Locale locale = context.getViewRoot().getLocale(); 110 String message; 111 boolean isProviderAvailable = getGoogleDriveBlobProvider().getOAuth2Provider().isProviderAvailable(); 112 113 writer.startElement("button", parent); 114 writer.writeAttribute("type", "button", null); 115 writer.writeAttribute("class", "button GoogleDrivePickerButton", null); 116 117 // only add onclick event to button if oauth service provider is available 118 // this prevents users from using the picker if some configuration is missing 119 if (isProviderAvailable) { 120 // TODO pass existing access token 121 String onButtonClick = onClick + ";" 122 + String.format("new nuxeo.utils.GoogleDrivePicker('%s','%s','%s','%s','%s','%s', '%s')", 123 getClientId(), pickId, authId, inputId, infoId, getGoogleDomain(), authorizationUrl); 124 writer.writeAttribute("onclick", onButtonClick, null); 125 } 126 127 writer.startElement("span", parent); 128 writer.writeAttribute("id", pickId, null); 129 message = I18NUtils.getMessageString("messages", "label.inputFile.googleDriveUploadPicker", null, locale); 130 writer.write(message); 131 writer.endElement("span"); 132 133 writer.startElement("span", parent); 134 writer.writeAttribute("id", authId, null); 135 writer.writeAttribute("style", "display:none", null); // hidden 136 message = I18NUtils.getMessageString("messages", "label.inputFile.authenticate", null, locale); 137 writer.write(message); 138 writer.endElement("span"); 139 140 writer.endElement("button"); 141 142 if (isProviderAvailable) { 143 writer.write(ComponentUtils.WHITE_SPACE_CHARACTER); 144 writer.startElement("span", parent); 145 writer.writeAttribute("id", infoId, null); 146 message = I18NUtils.getMessageString("messages", "error.inputFile.noFileSelected", null, locale); 147 writer.write(message); 148 writer.endElement("span"); 149 } else { 150 // if oauth service provider not properly setup, add warning message 151 writer.startElement("span", parent); 152 writer.writeAttribute("class", "processMessage completeWarning", null); 153 writer.writeAttribute("style", "margin: 0 0 .5em 0; font-size: 11px; background-position-y: 0.6em", null); 154 message = I18NUtils.getMessageString("messages", "error.googledrive.providerUnavailable", null, locale); 155 writer.write(message); 156 writer.endElement("span"); 157 } 158 159 inputText.setLocalValueSet(false); 160 inputText.setStyle("display:none"); // hidden 161 ComponentUtils.encodeComponent(context, inputText); 162 } 163 164 @Override 165 public void validateUpload(UIInput parent, FacesContext context, InputFileInfo submitted) { 166 UIComponent facet = parent.getFacet(UPLOAD_GOOGLE_DRIVE_FACET_NAME); 167 if (!(facet instanceof HtmlInputText)) { 168 return; 169 } 170 HtmlInputText inputText = (HtmlInputText) facet; 171 Object value = inputText.getSubmittedValue(); 172 if (value != null && !(value instanceof String)) { 173 ComponentUtils.addErrorMessage(context, parent, "error.inputFile.invalidSpecialBlob"); 174 parent.setValid(false); 175 return; 176 } 177 String string = (String) value; 178 if (StringUtils.isBlank(string) || string.indexOf(':') < 0) { 179 String message = context.getPartialViewContext().isAjaxRequest() ? InputFileInfo.INVALID_WITH_AJAX_MESSAGE 180 : InputFileInfo.INVALID_FILE_MESSAGE; 181 ComponentUtils.addErrorMessage(context, parent, message); 182 parent.setValid(false); 183 return; 184 } 185 186 // micro parse the string (user:fileId) 187 String[] parts = string.split(":"); 188 String user = parts[0]; 189 String fileId = parts[1]; 190 191 // check if we can get an access token 192 String accessToken = getAccessToken(user); 193 if (accessToken == null) { 194 String link = String.format( 195 "<a href='#' onclick=\"openPopup('%s'); return false;\">Register a new token</a> and try again.", 196 getOAuthAuthorizationUrl()); 197 ComponentUtils.addErrorMessage(context, parent, "error.inputFile.accessToken", new Object[] { user, link }); 198 parent.setValid(false); 199 return; 200 } 201 202 Blob blob = toBlob(new LiveConnectFileInfo(user, fileId)); // no revisionId 203 submitted.setBlob(blob); 204 submitted.setFilename(blob.getFilename()); 205 submitted.setMimeType(blob.getMimeType()); 206 } 207 208 /** 209 * Google Drive upload button is added to the file widget if and only if Google Drive OAuth service provider is 210 * enabled 211 * 212 * @return true if Google Drive OAuth service provider is enabled or false otherwise. 213 */ 214 @Override 215 public boolean isEnabled() { 216 return getGoogleDriveBlobProvider().getOAuth2Provider().isEnabled(); 217 } 218 219 /** 220 * Creates a Google Drive managed blob. 221 * 222 * @param fileInfo the Google Drive file info 223 * @return the blob 224 */ 225 protected Blob toBlob(LiveConnectFileInfo fileInfo) { 226 try { 227 return getGoogleDriveBlobProvider().toBlob(fileInfo); 228 } catch (IOException e) { 229 throw new RuntimeException(e); // TODO better feedback 230 } 231 } 232 233 protected GoogleDriveBlobProvider getGoogleDriveBlobProvider() { 234 return (GoogleDriveBlobProvider) Framework.getService(BlobManager.class).getBlobProvider(id); 235 } 236 237 protected String getGoogleDomain() { 238 String domain = Framework.getProperty(GOOGLE_DOMAIN_PROP); 239 return (domain != null) ? domain : ""; 240 } 241 242 protected String getClientId() { 243 String clientId = getGoogleDriveBlobProvider().getClientId(); 244 return (clientId != null) ? clientId : ""; 245 } 246 247 protected String getAccessToken(String user) { 248 try { 249 Credential credential = getGoogleDriveBlobProvider().getCredential(user); 250 if (credential != null) { 251 String accessToken = credential.getAccessToken(); 252 if (accessToken != null) { 253 return accessToken; 254 } 255 } 256 } catch (IOException e) { 257 log.error("Failed to get access token for " + user, e); 258 } 259 return null; 260 } 261 262 private boolean hasServiceAccount() { 263 HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance() 264 .getExternalContext() 265 .getRequest(); 266 String username = request.getUserPrincipal().getName(); 267 GoogleOAuth2ServiceProvider provider = getGoogleDriveBlobProvider().getOAuth2Provider(); 268 return provider != null && provider.getServiceUser(username) != null; 269 } 270 271 private String getOAuthAuthorizationUrl() { 272 HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance() 273 .getExternalContext() 274 .getRequest(); 275 GoogleOAuth2ServiceProvider provider = getGoogleDriveBlobProvider().getOAuth2Provider(); 276 return (provider != null && provider.getClientId() != null) ? provider.getAuthorizationUrl(request) : ""; 277 } 278}