001/*
002 * (C) Copyright 2015-2016 Nuxeo SA (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.lang.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                + ";"
123                + String.format("new nuxeo.utils.GoogleDrivePicker('%s','%s','%s','%s','%s','%s', '%s')",
124                getClientId(), pickId, authId, inputId, infoId, getGoogleDomain(), authorizationUrl);
125            writer.writeAttribute("onclick", onButtonClick, null);
126        }
127
128        writer.startElement("span", parent);
129        writer.writeAttribute("id", pickId, null);
130        message = I18NUtils.getMessageString("messages", "label.inputFile.googleDriveUploadPicker", null, locale);
131        writer.write(message);
132        writer.endElement("span");
133
134        writer.startElement("span", parent);
135        writer.writeAttribute("id", authId, null);
136        writer.writeAttribute("style", "display:none", null); // hidden
137        message = I18NUtils.getMessageString("messages", "label.inputFile.authenticate", null, locale);
138        writer.write(message);
139        writer.endElement("span");
140
141        writer.endElement("button");
142
143        if (isProviderAvailable) {
144            writer.write(ComponentUtils.WHITE_SPACE_CHARACTER);
145            writer.startElement("span", parent);
146            writer.writeAttribute("id", infoId, null);
147            message = I18NUtils.getMessageString("messages", "error.inputFile.noFileSelected", null, locale);
148            writer.write(message);
149            writer.endElement("span");
150        } else {
151            // if oauth service provider not properly setup, add warning message
152            writer.startElement("span", parent);
153            writer.writeAttribute("class", "processMessage completeWarning", null);
154            writer.writeAttribute("style", "margin: 0 0 .5em 0; font-size: 11px; background-position-y: 0.6em", null);
155            message = I18NUtils.getMessageString("messages", "error.googledrive.providerUnavailable", null, locale);
156            writer.write(message);
157            writer.endElement("span");
158        }
159
160        inputText.setLocalValueSet(false);
161        inputText.setStyle("display:none"); // hidden
162        ComponentUtils.encodeComponent(context, inputText);
163    }
164
165    @Override
166    public void validateUpload(UIInput parent, FacesContext context, InputFileInfo submitted) {
167        UIComponent facet = parent.getFacet(UPLOAD_GOOGLE_DRIVE_FACET_NAME);
168        if (!(facet instanceof HtmlInputText)) {
169            return;
170        }
171        HtmlInputText inputText = (HtmlInputText) facet;
172        Object value = inputText.getSubmittedValue();
173        if (value != null && !(value instanceof String)) {
174            ComponentUtils.addErrorMessage(context, parent, "error.inputFile.invalidSpecialBlob");
175            parent.setValid(false);
176            return;
177        }
178        String string = (String) value;
179        if (StringUtils.isBlank(string) || string.indexOf(':') < 0) {
180            String message = context.getPartialViewContext().isAjaxRequest() ? InputFileInfo.INVALID_WITH_AJAX_MESSAGE
181                    : InputFileInfo.INVALID_FILE_MESSAGE;
182            ComponentUtils.addErrorMessage(context, parent, message);
183            parent.setValid(false);
184            return;
185        }
186
187        // micro parse the string (user:fileId)
188        String[] parts = string.split(":");
189        String user = parts[0];
190        String fileId = parts[1];
191
192        // check if we can get an access token
193        String accessToken = getAccessToken(user);
194        if (accessToken == null) {
195            String link = String.format("<a href='#' onclick=\"openPopup('%s'); return false;\">Register a new token</a> and try again.", getOAuthAuthorizationUrl());
196            ComponentUtils.addErrorMessage(context, parent, "error.inputFile.accessToken", new Object[] { user, link });
197            parent.setValid(false);
198            return;
199        }
200
201        Blob blob = toBlob(new LiveConnectFileInfo(user, fileId)); // no revisionId
202        submitted.setBlob(blob);
203        submitted.setFilename(blob.getFilename());
204        submitted.setMimeType(blob.getMimeType());
205    }
206
207    /**
208     * Google Drive upload button is added to the file widget if and only if Google Drive OAuth service provider is enabled
209     *
210     * @return true if Google Drive OAuth service provider is enabled or false otherwise.
211     */
212    @Override
213    public boolean isEnabled() {
214        return getGoogleDriveBlobProvider().getOAuth2Provider().isEnabled();
215    }
216
217    /**
218     * Creates a Google Drive managed blob.
219     *
220     * @param fileInfo the Google Drive file info
221     * @return the blob
222     */
223    protected Blob toBlob(LiveConnectFileInfo fileInfo) {
224        try {
225            return getGoogleDriveBlobProvider().toBlob(fileInfo);
226        } catch (IOException e) {
227            throw new RuntimeException(e); // TODO better feedback
228        }
229    }
230
231    protected GoogleDriveBlobProvider getGoogleDriveBlobProvider() {
232        return (GoogleDriveBlobProvider) Framework.getService(BlobManager.class)
233            .getBlobProvider(id);
234    }
235
236    protected String getGoogleDomain() {
237        String domain = Framework.getProperty(GOOGLE_DOMAIN_PROP);
238        return (domain != null) ? domain : "";
239    }
240
241    protected String getClientId() {
242        String clientId = getGoogleDriveBlobProvider().getClientId();
243        return (clientId != null) ? clientId : "";
244    }
245
246    protected String getAccessToken(String user) {
247        try {
248            Credential credential = getGoogleDriveBlobProvider().getCredential(user);
249            if (credential != null) {
250                String accessToken = credential.getAccessToken();
251                if (accessToken != null) {
252                    return accessToken;
253                }
254            }
255        } catch (IOException e) {
256            log.error("Failed to get access token for " + user, e);
257        }
258        return null;
259    }
260
261    private boolean hasServiceAccount() {
262        HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
263        String username = request.getUserPrincipal().getName();
264        GoogleOAuth2ServiceProvider provider = getGoogleDriveBlobProvider().getOAuth2Provider();
265        return provider != null && provider.getServiceUser(username) != null;
266    }
267
268    private String getOAuthAuthorizationUrl() {
269        HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
270        GoogleOAuth2ServiceProvider provider = getGoogleDriveBlobProvider().getOAuth2Provider();
271        return (provider != null && provider.getClientId() != null) ? provider.getAuthorizationUrl(request) : "";
272    }
273}