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 *     Kevin Leturc
018 */
019package org.nuxeo.ecm.liveconnect.onedrive;
020
021import java.io.IOException;
022import java.io.Serializable;
023import java.util.HashMap;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Optional;
027
028import javax.faces.application.Application;
029import javax.faces.component.UIComponent;
030import javax.faces.component.UIInput;
031import javax.faces.component.UINamingContainer;
032import javax.faces.component.html.HtmlInputText;
033import javax.faces.context.FacesContext;
034import javax.faces.context.ResponseWriter;
035import javax.servlet.http.HttpServletRequest;
036
037import org.apache.commons.lang.StringUtils;
038import org.nuxeo.common.utils.i18n.I18NUtils;
039import org.nuxeo.ecm.core.api.Blob;
040import org.nuxeo.ecm.core.api.NuxeoException;
041import org.nuxeo.ecm.core.blob.BlobManager;
042import org.nuxeo.ecm.liveconnect.core.LiveConnectFileInfo;
043import org.nuxeo.ecm.liveconnect.onedrive.OneDriveBlobProvider;
044import org.nuxeo.ecm.liveconnect.onedrive.OneDriveOAuth2ServiceProvider;
045import org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token;
046import org.nuxeo.ecm.platform.ui.web.component.file.InputFileChoice;
047import org.nuxeo.ecm.platform.ui.web.component.file.InputFileInfo;
048import org.nuxeo.ecm.platform.ui.web.component.file.JSFBlobUploader;
049import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
050import org.nuxeo.runtime.api.Framework;
051
052import com.google.api.client.auth.oauth2.Credential;
053
054/**
055 * JSF Blob Upload based on one drive blobs.
056 *
057 * @since 8.2
058 */
059public class OneDriveBlobUploader implements JSFBlobUploader {
060
061    public static final String UPLOAD_ONEDRIVE_FACET_NAME = InputFileChoice.UPLOAD + "OneDrive";
062
063    protected final String id;
064
065    public OneDriveBlobUploader(String id) {
066        this.id = id;
067        try {
068            getOneDriveBlobProvider();
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 UPLOAD_ONEDRIVE_FACET_NAME;
079    }
080
081    @Override
082    public void hookSubComponent(UIInput parent) {
083        Application app = FacesContext.getCurrentInstance().getApplication();
084        ComponentUtils.initiateSubComponent(parent, UPLOAD_ONEDRIVE_FACET_NAME,
085                app.createComponent(HtmlInputText.COMPONENT_TYPE));
086    }
087
088    @Override
089    public void encodeBeginUpload(UIInput parent, FacesContext context, String onClick) throws IOException {
090        UIComponent facet = parent.getFacet(UPLOAD_ONEDRIVE_FACET_NAME);
091        if (!(facet instanceof HtmlInputText)) {
092            return;
093        }
094        HtmlInputText inputText = (HtmlInputText) facet;
095
096        // not ours to close
097        @SuppressWarnings("resource")
098        ResponseWriter writer = context.getResponseWriter();
099        OneDriveBlobProvider blobProvider = getOneDriveBlobProvider();
100        OneDriveOAuth2ServiceProvider oauthProvider = blobProvider.getOAuth2Provider();
101
102        String inputId = facet.getClientId(context);
103        String prefix = parent.getClientId(context) + UINamingContainer.getSeparatorChar(context);
104        String pickId = prefix + "OneDrivePickMsg";
105        String infoId = prefix + "OneDriveInfo";
106        Locale locale = context.getViewRoot().getLocale();
107        String message;
108        boolean isProviderAvailable = oauthProvider != null && oauthProvider.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 accessToken = getCurrentUserAccessToken(blobProvider);
118            String authorizationUrl = getOAuthAuthorizationUrl(oauthProvider);
119            String baseUrl = oauthProvider.getAPIInitializer().apply("").getBaseURL();
120
121            String onButtonClick = onClick
122                    + ";"
123                    + String.format("new nuxeo.utils.OneDrivePicker('%s', '%s','%s', '%s', '%s', '%s')",
124                            getClientId(oauthProvider), inputId, infoId, accessToken, authorizationUrl, baseUrl);
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.oneDriveUploadPicker", null, locale);
131        writer.write(message);
132        writer.endElement("span");
133
134        writer.endElement("button");
135
136        if (isProviderAvailable) {
137            writer.write(ComponentUtils.WHITE_SPACE_CHARACTER);
138            writer.startElement("span", parent);
139            writer.writeAttribute("id", infoId, null);
140            message = I18NUtils.getMessageString("messages", "error.inputFile.noFileSelected", null, locale);
141            writer.write(message);
142            writer.endElement("span");
143        } else {
144            // if oauth service provider not properly setup, add warning message
145            writer.startElement("span", parent);
146            writer.writeAttribute("class", "processMessage completeWarning", null);
147            writer.writeAttribute(
148                    "style",
149                    "margin: 0 0 .5em 0; font-size: 11px; padding: 0.4em 0.5em 0.5em 2.2em; background-position-y: 0.6em",
150                    null);
151            message = I18NUtils.getMessageString("messages", "error.oneDrive.providerUnavailable", null, locale);
152            writer.write(message);
153            writer.endElement("span");
154        }
155
156        inputText.setLocalValueSet(false);
157        inputText.setStyle("display: none");
158        ComponentUtils.encodeComponent(context, inputText);
159    }
160
161    @Override
162    public void validateUpload(UIInput parent, FacesContext context, InputFileInfo submitted) {
163        UIComponent facet = parent.getFacet(UPLOAD_ONEDRIVE_FACET_NAME);
164        if (!(facet instanceof HtmlInputText)) {
165            return;
166        }
167        HtmlInputText inputText = (HtmlInputText) facet;
168        Object value = inputText.getSubmittedValue();
169        if (value != null && !(value instanceof String)) {
170            ComponentUtils.addErrorMessage(context, parent, "error.inputFile.invalidSpecialBlob");
171            parent.setValid(false);
172            return;
173        }
174        String fileId = (String) value;
175        if (StringUtils.isBlank(fileId)) {
176            String message = context.getPartialViewContext().isAjaxRequest() ? InputFileInfo.INVALID_WITH_AJAX_MESSAGE
177                    : InputFileInfo.INVALID_FILE_MESSAGE;
178            ComponentUtils.addErrorMessage(context, parent, message);
179            parent.setValid(false);
180            return;
181        }
182
183        OneDriveBlobProvider blobProvider = getOneDriveBlobProvider();
184        OneDriveOAuth2ServiceProvider oauthProvider = blobProvider.getOAuth2Provider();
185        if (oauthProvider == null) {
186            ComponentUtils.addErrorMessage(context, parent, "error.inputFile.oneDriveInvalidConfiguration");
187            parent.setValid(false);
188            return;
189        }
190
191        Optional<NuxeoOAuth2Token> nuxeoToken = getCurrentNuxeoToken(blobProvider);
192        if (!nuxeoToken.isPresent()) {
193            String link = String.format(
194                    "<a href='#' onclick=\"openPopup('%s'); return false;\">Register a new token</a> and try again.",
195                    getOAuthAuthorizationUrl(oauthProvider));
196            ComponentUtils.addErrorMessage(context, parent, "error.inputFile.oneDriveInvalidPermissions",
197                    new Object[] { link });
198            parent.setValid(false);
199            return;
200        }
201
202        try {
203            LiveConnectFileInfo fileInfo = new LiveConnectFileInfo(nuxeoToken.get().getServiceLogin(), fileId);
204            Blob blob = blobProvider.toBlob(fileInfo);
205            submitted.setBlob(blob);
206            submitted.setFilename(blob.getFilename());
207            submitted.setMimeType(blob.getMimeType());
208        } catch (IOException e) {
209            throw new RuntimeException(e); // TODO better feedback
210        }
211    }
212
213    /**
214     * OneDrive upload button is added to the file widget if and only if OneDrive OAuth service provider is enabled
215     *
216     * @return {@code true} if OneDrive OAuth service provider is enabled or {@code false} otherwise
217     */
218    @Override
219    public boolean isEnabled() {
220        OneDriveOAuth2ServiceProvider provider = getOneDriveBlobProvider().getOAuth2Provider();
221        return provider != null && provider.isEnabled();
222    }
223
224    protected String getClientId(OneDriveOAuth2ServiceProvider provider) {
225        return Optional.ofNullable(provider).map(OneDriveOAuth2ServiceProvider::getClientId).orElse("");
226    }
227
228    protected OneDriveBlobProvider getOneDriveBlobProvider() {
229        return (OneDriveBlobProvider) Framework.getService(BlobManager.class).getBlobProvider(id);
230    }
231
232    private String getCurrentUserAccessToken(OneDriveBlobProvider provider) throws IOException {
233        Optional<NuxeoOAuth2Token> nuxeoToken = getCurrentNuxeoToken(provider);
234        if (nuxeoToken.isPresent()) {
235            // Here we don't need to handle NuxeoException as we just retrieved the token
236            Credential credential = provider.getCredential(nuxeoToken.get().getServiceLogin());
237            Long expiresInSeconds = credential.getExpiresInSeconds();
238            if (expiresInSeconds != null && expiresInSeconds > 0) {
239                return credential.getAccessToken();
240            }
241        }
242        return "";
243    }
244
245    private Optional<NuxeoOAuth2Token> getCurrentNuxeoToken(OneDriveBlobProvider provider) {
246        Map<String, Serializable> filter = new HashMap<>();
247        filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, FacesContext.getCurrentInstance()
248                                                                 .getExternalContext()
249                                                                 .getUserPrincipal()
250                                                                 .getName());
251        return provider.getOAuth2Provider()
252                       .getCredentialDataStore()
253                       .query(filter)
254                       .stream()
255                       .map(NuxeoOAuth2Token::new)
256                       .findFirst();
257    }
258
259    private String getOAuthAuthorizationUrl(OneDriveOAuth2ServiceProvider provider) {
260        HttpServletRequest request = getHttpServletRequest();
261        return (provider != null && provider.getClientId() != null) ? provider.getAuthorizationUrl(request) : "";
262    }
263
264    private HttpServletRequest getHttpServletRequest() {
265        return (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
266    }
267}