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}