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 *     Florent Guillaume
016 *     Nelson Silva
017 */
018package org.nuxeo.ecm.liveconnect.google.drive;
019
020import java.io.IOException;
021import java.util.Locale;
022
023import javax.faces.application.Application;
024import javax.faces.component.NamingContainer;
025import javax.faces.component.UIComponent;
026import javax.faces.component.UIInput;
027import javax.faces.component.html.HtmlInputText;
028import javax.faces.context.FacesContext;
029import javax.faces.context.ResponseWriter;
030import javax.servlet.http.HttpServletRequest;
031
032import org.apache.commons.lang.StringUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.common.utils.i18n.I18NUtils;
036import org.nuxeo.ecm.core.api.Blob;
037import org.nuxeo.ecm.core.api.DocumentModelList;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.blob.BlobManager;
040import org.nuxeo.ecm.liveconnect.google.drive.GoogleDriveBlobProvider.FileInfo;
041import org.nuxeo.ecm.platform.ui.web.component.file.InputFileChoice;
042import org.nuxeo.ecm.platform.ui.web.component.file.InputFileInfo;
043import org.nuxeo.ecm.platform.ui.web.component.file.JSFBlobUploader;
044import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
045import org.nuxeo.runtime.api.Framework;
046
047import com.google.api.client.auth.oauth2.Credential;
048
049/**
050 * JSF Blob Upload based on Google Drive blobs.
051 *
052 * @since 7.3
053 */
054public class GoogleDriveBlobUploader implements JSFBlobUploader {
055
056    private static final Log log = LogFactory.getLog(GoogleDriveBlobUploader.class);
057
058    public static final String UPLOAD_GOOGLE_DRIVE_FACET_NAME = "uploadGoogleDrive";
059
060    // restrict sign-in to accounts at this domain
061    public static final String GOOGLE_DOMAIN_PROP = "nuxeo.google.domain";
062
063    protected final String id;
064
065    public GoogleDriveBlobUploader(String id) {
066        this.id = id;
067        try {
068            getGoogleDriveBlobProvider();
069        } catch (NuxeoException e) {
070            // this exception is caught by JSFBlobUploaderDescriptor.getJSFBlobUploader
071            // to mean that the uploader is not available because badly configured
072            throw new IllegalStateException(e);
073        }
074    }
075
076    @Override
077    public String getChoice() {
078        return InputFileChoice.UPLOAD + "GoogleDrive";
079    }
080
081    @Override
082    public void hookSubComponent(UIInput parent) {
083        Application app = FacesContext.getCurrentInstance().getApplication();
084        ComponentUtils.initiateSubComponent(parent, UPLOAD_GOOGLE_DRIVE_FACET_NAME,
085                app.createComponent(HtmlInputText.COMPONENT_TYPE));
086
087    }
088
089    // Needs supporting JavaScript code for nuxeo.utils.pickFromGoogleDrive defined in googleclient.js.
090    @Override
091    public void encodeBeginUpload(UIInput parent, FacesContext context, String onClick) throws IOException {
092        UIComponent facet = parent.getFacet(UPLOAD_GOOGLE_DRIVE_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 + "GoogleDrivePickMsg";
105        String authId = prefix + "GoogleDriveAuthMsg";
106        String infoId = prefix + "GoogleDriveInfo";
107        String authorizationUrl = hasServiceAccount() ? "" : getOAuthAuthorizationUrl();
108        Locale locale = context.getViewRoot().getLocale();
109        String message;
110        boolean isProviderAvailable = getGoogleDriveBlobProvider().getOAuth2Provider().isProviderAvailable();
111
112        writer.startElement("button", parent);
113        writer.writeAttribute("type", "button", null);
114        writer.writeAttribute("class", "button GoogleDrivePickerButton", null);
115
116        // only add onclick event to button if oauth service provider is available
117        // this prevents users from using the picker if some configuration is missing
118        if (isProviderAvailable) {
119            // TODO pass existing access token
120            String onButtonClick = onClick
121                + ";"
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("<a href='#' onclick=\"openPopup('%s'); return false;\">Register a new token</a> and try again.", getOAuthAuthorizationUrl());
195            ComponentUtils.addErrorMessage(context, parent, "error.inputFile.accessToken", new Object[] { user, link });
196            parent.setValid(false);
197            return;
198        }
199
200        Blob blob = createBlob(new FileInfo(user, fileId, null)); // no revisionId
201        submitted.setBlob(blob);
202        submitted.setFilename(blob.getFilename());
203        submitted.setMimeType(blob.getMimeType());
204    }
205
206    /**
207     * Google Drive upload button is added to the file widget if and only if Google Drive OAuth service provider is enabled
208     *
209     * @return true if Google Drive OAuth service provider is enabled or false otherwise.
210     */
211    @Override
212    public boolean isEnabled() {
213        return getGoogleDriveBlobProvider().getOAuth2Provider().isEnabled();
214    }
215
216    /**
217     * Creates a Google Drive managed blob.
218     *
219     * @param fileInfo the Google Drive file info
220     * @return the blob
221     */
222    protected Blob createBlob(FileInfo fileInfo) {
223        try {
224            return getGoogleDriveBlobProvider().getBlob(fileInfo);
225        } catch (IOException e) {
226            throw new RuntimeException(e); // TODO better feedback
227        }
228    }
229
230    protected GoogleDriveBlobProvider getGoogleDriveBlobProvider() {
231        return (GoogleDriveBlobProvider) Framework.getService(BlobManager.class)
232            .getBlobProvider(id);
233    }
234
235    protected String getGoogleDomain() {
236        String domain = Framework.getProperty(GOOGLE_DOMAIN_PROP);
237        return (domain != null) ? domain : "";
238    }
239
240    protected String getClientId() {
241        String clientId = getGoogleDriveBlobProvider().getClientId();
242        return (clientId != null) ? clientId : "";
243    }
244
245    protected String getAccessToken(String user) {
246        try {
247            Credential credential = getGoogleDriveBlobProvider().getCredential(user);
248            if (credential != null) {
249                String accessToken = credential.getAccessToken();
250                if (accessToken != null) {
251                    return accessToken;
252                }
253            }
254        } catch (IOException e) {
255            log.error("Failed to get access token for " + user, e);
256        }
257        return null;
258    }
259
260    private boolean hasServiceAccount() {
261        HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
262        String username = request.getUserPrincipal().getName();
263        GoogleOAuth2ServiceProvider provider = (GoogleOAuth2ServiceProvider) getGoogleDriveBlobProvider().getOAuth2Provider();
264        return provider != null && provider.getServiceUser(username) != null;
265    }
266
267    private String getOAuthAuthorizationUrl() {
268        HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
269        GoogleOAuth2ServiceProvider provider = (GoogleOAuth2ServiceProvider) getGoogleDriveBlobProvider().getOAuth2Provider();
270        return (provider != null && provider.getClientId() != null) ? provider.getAuthorizationUrl(request) : "";
271    }
272}