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