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.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.1
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                    + ";"
121                    + String.format("new nuxeo.utils.OneDrivePicker('%s', '%s','%s', '%s', '%s', '%s')",
122                            getClientId(oauthProvider), inputId, infoId, accessToken, authorizationUrl, baseUrl);
123            writer.writeAttribute("onclick", onButtonClick, null);
124        }
125
126        writer.startElement("span", parent);
127        writer.writeAttribute("id", pickId, null);
128        message = I18NUtils.getMessageString("messages", "label.inputFile.oneDriveUploadPicker", null, locale);
129        writer.write(message);
130        writer.endElement("span");
131
132        writer.endElement("button");
133
134        if (isProviderAvailable) {
135            writer.write(ComponentUtils.WHITE_SPACE_CHARACTER);
136            writer.startElement("span", parent);
137            writer.writeAttribute("id", infoId, null);
138            message = I18NUtils.getMessageString("messages", "error.inputFile.noFileSelected", null, locale);
139            writer.write(message);
140            writer.endElement("span");
141        } else {
142            // if oauth service provider not properly setup, add warning message
143            writer.startElement("span", parent);
144            writer.writeAttribute("class", "processMessage completeWarning", null);
145            writer.writeAttribute(
146                    "style",
147                    "margin: 0 0 .5em 0; font-size: 11px; padding: 0.4em 0.5em 0.5em 2.2em; background-position-y: 0.6em",
148                    null);
149            message = I18NUtils.getMessageString("messages", "error.oneDrive.providerUnavailable", null, locale);
150            writer.write(message);
151            writer.endElement("span");
152        }
153
154        inputText.setLocalValueSet(false);
155        inputText.setStyle("display: none");
156        ComponentUtils.encodeComponent(context, inputText);
157    }
158
159    @Override
160    public void validateUpload(UIInput parent, FacesContext context, InputFileInfo submitted) {
161        UIComponent facet = parent.getFacet(UPLOAD_ONEDRIVE_FACET_NAME);
162        if (!(facet instanceof HtmlInputText)) {
163            return;
164        }
165        HtmlInputText inputText = (HtmlInputText) facet;
166        Object value = inputText.getSubmittedValue();
167        if (value != null && !(value instanceof String)) {
168            ComponentUtils.addErrorMessage(context, parent, "error.inputFile.invalidSpecialBlob");
169            parent.setValid(false);
170            return;
171        }
172        String fileId = (String) value;
173        if (StringUtils.isBlank(fileId)) {
174            String message = context.getPartialViewContext().isAjaxRequest() ? InputFileInfo.INVALID_WITH_AJAX_MESSAGE
175                    : InputFileInfo.INVALID_FILE_MESSAGE;
176            ComponentUtils.addErrorMessage(context, parent, message);
177            parent.setValid(false);
178            return;
179        }
180
181        OneDriveBlobProvider blobProvider = getOneDriveBlobProvider();
182        OneDriveOAuth2ServiceProvider oauthProvider = blobProvider.getOAuth2Provider();
183        if (oauthProvider == null) {
184            ComponentUtils.addErrorMessage(context, parent, "error.inputFile.oneDriveInvalidConfiguration");
185            parent.setValid(false);
186            return;
187        }
188
189        Optional<NuxeoOAuth2Token> nuxeoToken = getCurrentNuxeoToken(blobProvider);
190        if (!nuxeoToken.isPresent()) {
191            String link = String.format(
192                    "<a href='#' onclick=\"openPopup('%s'); return false;\">Register a new token</a> and try again.",
193                    getOAuthAuthorizationUrl(oauthProvider));
194            ComponentUtils.addErrorMessage(context, parent, "error.inputFile.oneDriveInvalidPermissions",
195                    new Object[] { link });
196            parent.setValid(false);
197            return;
198        }
199
200        try {
201            LiveConnectFileInfo fileInfo = new LiveConnectFileInfo(nuxeoToken.get().getServiceLogin(), fileId);
202            Blob blob = blobProvider.toBlob(fileInfo);
203            submitted.setBlob(blob);
204            submitted.setFilename(blob.getFilename());
205            submitted.setMimeType(blob.getMimeType());
206        } catch (IOException e) {
207            throw new RuntimeException(e); // TODO better feedback
208        }
209    }
210
211    /**
212     * OneDrive upload button is added to the file widget if and only if OneDrive OAuth service provider is enabled
213     *
214     * @return {@code true} if OneDrive OAuth service provider is enabled or {@code false} otherwise
215     */
216    @Override
217    public boolean isEnabled() {
218        OneDriveOAuth2ServiceProvider provider = getOneDriveBlobProvider().getOAuth2Provider();
219        return provider != null && provider.isEnabled();
220    }
221
222    protected String getClientId(OneDriveOAuth2ServiceProvider provider) {
223        return Optional.ofNullable(provider).map(OneDriveOAuth2ServiceProvider::getClientId).orElse("");
224    }
225
226    protected OneDriveBlobProvider getOneDriveBlobProvider() {
227        return (OneDriveBlobProvider) Framework.getService(BlobManager.class).getBlobProvider(id);
228    }
229
230    private String getCurrentUserAccessToken(OneDriveBlobProvider provider) throws IOException {
231        Optional<NuxeoOAuth2Token> nuxeoToken = getCurrentNuxeoToken(provider);
232        if (nuxeoToken.isPresent()) {
233            // Here we don't need to handle NuxeoException as we just retrieved the token
234            Credential credential = provider.getCredential(nuxeoToken.get().getServiceLogin());
235            Long expiresInSeconds = credential.getExpiresInSeconds();
236            if (expiresInSeconds != null && expiresInSeconds > 0) {
237                return credential.getAccessToken();
238            }
239        }
240        return "";
241    }
242
243    private Optional<NuxeoOAuth2Token> getCurrentNuxeoToken(OneDriveBlobProvider provider) {
244        Map<String, Serializable> filter = new HashMap<>();
245        filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, FacesContext.getCurrentInstance()
246                                                                 .getExternalContext()
247                                                                 .getUserPrincipal()
248                                                                 .getName());
249        return provider.getOAuth2Provider()
250                       .getCredentialDataStore()
251                       .query(filter)
252                       .stream()
253                       .map(NuxeoOAuth2Token::new)
254                       .findFirst();
255    }
256
257    private String getOAuthAuthorizationUrl(OneDriveOAuth2ServiceProvider provider) {
258        HttpServletRequest request = getHttpServletRequest();
259        return (provider != null && provider.getClientId() != null) ? provider.getAuthorizationUrl(request) : "";
260    }
261
262    private HttpServletRequest getHttpServletRequest() {
263        return (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
264    }
265}