001/*
002 * (C) Copyright 2013-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 *     Olivier Grisel <ogrisel@nuxeo.com>
018 *     Antoine Taillefer <ataillefer@nuxeo.com>
019 */
020package org.nuxeo.drive.seam;
021
022import static java.nio.charset.StandardCharsets.UTF_8;
023
024import java.io.UnsupportedEncodingException;
025import java.net.URLDecoder;
026import java.util.ArrayList;
027import java.util.List;
028import java.util.Set;
029
030import javax.faces.context.FacesContext;
031import javax.servlet.ServletRequest;
032
033import org.apache.commons.lang3.ObjectUtils;
034import org.apache.logging.log4j.LogManager;
035import org.apache.logging.log4j.Logger;
036import org.jboss.seam.Component;
037import org.jboss.seam.ScopeType;
038import org.jboss.seam.annotations.Factory;
039import org.jboss.seam.annotations.In;
040import org.jboss.seam.annotations.Install;
041import org.jboss.seam.annotations.Name;
042import org.jboss.seam.annotations.Scope;
043import org.jboss.seam.contexts.Context;
044import org.jboss.seam.contexts.Contexts;
045import org.nuxeo.common.utils.URIUtils;
046import org.nuxeo.drive.NuxeoDriveConstants;
047import org.nuxeo.drive.adapter.FileSystemItem;
048import org.nuxeo.drive.hierarchy.userworkspace.adapter.UserWorkspaceHelper;
049import org.nuxeo.drive.service.FileSystemItemAdapterService;
050import org.nuxeo.drive.service.NuxeoDriveManager;
051import org.nuxeo.ecm.core.api.Blob;
052import org.nuxeo.ecm.core.api.CoreSession;
053import org.nuxeo.ecm.core.api.DocumentModel;
054import org.nuxeo.ecm.core.api.DocumentModelList;
055import org.nuxeo.ecm.core.api.DocumentRef;
056import org.nuxeo.ecm.core.api.IdRef;
057import org.nuxeo.ecm.core.api.NuxeoException;
058import org.nuxeo.ecm.core.api.NuxeoPrincipal;
059import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
060import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
061import org.nuxeo.ecm.core.api.security.SecurityConstants;
062import org.nuxeo.ecm.core.io.download.DownloadService;
063import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
064import org.nuxeo.ecm.tokenauth.service.TokenAuthenticationService;
065import org.nuxeo.ecm.user.center.UserCenterViewManager;
066import org.nuxeo.ecm.webapp.base.InputController;
067import org.nuxeo.ecm.webapp.contentbrowser.DocumentActions;
068import org.nuxeo.ecm.webapp.security.AbstractUserGroupManagement;
069import org.nuxeo.runtime.api.Framework;
070
071/**
072 * @since 5.7
073 */
074@Name("nuxeoDriveActions")
075@Scope(ScopeType.PAGE)
076@Install(precedence = Install.FRAMEWORK)
077public class NuxeoDriveActions extends InputController {
078
079    private static final Logger log = LogManager.getLogger(NuxeoDriveActions.class);
080
081    /** @since 9.3 */
082    public static final String NUXEO_DRIVE_APPLICATION_NAME = "Nuxeo Drive";
083
084    protected static final String IS_UNDER_SYNCHRONIZATION_ROOT = "nuxeoDriveIsUnderSynchronizationRoot";
085
086    protected static final String CURRENT_SYNCHRONIZATION_ROOT = "nuxeoDriveCurrentSynchronizationRoot";
087
088    public static final String NXDRIVE_PROTOCOL = "nxdrive";
089
090    public static final String PROTOCOL_COMMAND_EDIT = "edit";
091
092    /**
093     * @deprecated since 10.2
094     */
095    @Deprecated
096    public static final String DESKTOP_PACKAGE_URL_LATEST_SEGMENT = "latest";
097
098    public static final String DESKTOP_PACKAGE_PREFIX = "nuxeo-drive.";
099
100    public static final String MSI_EXTENSION = "exe";
101
102    public static final String DMG_EXTENSION = "dmg";
103
104    public static final String WINDOWS_PLATFORM = "windows";
105
106    public static final String OSX_PLATFORM = "osx";
107
108    private static final String DRIVE_METADATA_VIEW = "view_drive_metadata";
109
110    @In(create = true, required = false)
111    protected CoreSession documentManager;
112
113    @In(create = true, required = false)
114    protected UserCenterViewManager userCenterViews;
115
116    @In(create = true)
117    protected DocumentActions documentActions;
118
119    @Factory(value = CURRENT_SYNCHRONIZATION_ROOT, scope = ScopeType.EVENT)
120    public DocumentModel getCurrentSynchronizationRoot() {
121        // Use the event context as request cache
122        Context cache = Contexts.getEventContext();
123        Boolean isUnderSync = (Boolean) cache.get(IS_UNDER_SYNCHRONIZATION_ROOT);
124        if (isUnderSync == null) {
125            NuxeoDriveManager driveManager = Framework.getService(NuxeoDriveManager.class);
126            Set<IdRef> references = driveManager.getSynchronizationRootReferences(documentManager);
127            DocumentModelList path = navigationContext.getCurrentPath();
128            DocumentModel root = null;
129            // list is ordered such as closest synchronized ancestor is
130            // considered the current synchronization root
131            for (DocumentModel parent : path) {
132                if (references.contains(parent.getRef())) {
133                    root = parent;
134                    break;
135                }
136            }
137            cache.set(CURRENT_SYNCHRONIZATION_ROOT, root);
138            cache.set(IS_UNDER_SYNCHRONIZATION_ROOT, root != null);
139        }
140        return (DocumentModel) cache.get(CURRENT_SYNCHRONIZATION_ROOT);
141    }
142
143    public boolean canEditDocument(DocumentModel doc) {
144        if (doc == null || !documentManager.exists(doc.getRef())) {
145            return false;
146        }
147        if (doc.isFolder() || doc.isProxy()) {
148            return false;
149        }
150        if (!documentManager.hasPermission(doc.getRef(), SecurityConstants.WRITE)) {
151            return false;
152        }
153        // Check if current document can be adapted as a FileSystemItem
154        return getFileSystemItem(doc) != null;
155    }
156
157    public boolean canEditBlob(DocumentModel doc, String xPath) {
158        return canEditDocument(doc) && doc.getPropertyValue(xPath) instanceof Blob;
159    }
160
161    public boolean hasOneDriveToken(NuxeoPrincipal user) throws UnsupportedEncodingException {
162        TokenAuthenticationService tokenService = Framework.getService(TokenAuthenticationService.class);
163        for (DocumentModel token : tokenService.getTokenBindings(user.getName())) {
164            String applicationName = (String) token.getPropertyValue("authtoken:applicationName");
165            if (applicationName == null) {
166                continue;
167            }
168            // We do the URL decoding for backward compatibility reasons, but in the future token parameters should be
169            // stored in their natural format (i.e. not needing re-decoding).
170            if (NUXEO_DRIVE_APPLICATION_NAME.equals(URLDecoder.decode(applicationName, UTF_8.toString()))) {
171                return true;
172            }
173        }
174        return false;
175    }
176
177    /**
178     * Returns the Drive edit URL for the current document.
179     *
180     * @see #getDriveEditURL(DocumentModel)
181     */
182    public String getDriveEditURL() {
183        DocumentModel currentDocument = navigationContext.getCurrentDocument();
184        return getDriveEditURL(currentDocument);
185    }
186
187    /**
188     * Returns the Drive edit URL for the given document.
189     * <p>
190     * {@link #NXDRIVE_PROTOCOL} must be handled by a protocol handler configured on the client side (either on the
191     * browser, or on the OS).
192     *
193     * @since 7.4
194     * @return Drive edit URL in the form "{@link #NXDRIVE_PROTOCOL}:// {@link #PROTOCOL_COMMAND_EDIT}
195     *         /protocol/server[:port]/webappName/[user/userName/]repo/repoName/nxdocid/docId/filename/fileName[/
196     *         downloadUrl/downloadUrl]"
197     */
198    public String getDriveEditURL(DocumentModel currentDocument) {
199        if (currentDocument == null) {
200            return null;
201        }
202        // TODO NXP-15397: handle Drive not started exception
203        BlobHolder bh = currentDocument.getAdapter(BlobHolder.class);
204        if (bh == null) {
205            throw new NuxeoException(String.format("Document %s (%s) is not a BlobHolder, cannot get Drive Edit URL.",
206                    currentDocument.getPathAsString(), currentDocument.getId()));
207        }
208        Blob blob = bh.getBlob();
209        if (blob == null) {
210            throw new NuxeoException(String.format("Document %s (%s) has no blob, cannot get Drive Edit URL.",
211                    currentDocument.getPathAsString(), currentDocument.getId()));
212        }
213        return getDriveEditURL(currentDocument, blob, null);
214    }
215
216    public String getDriveEditURL(DocumentModel doc, String xPath) {
217        if (doc == null) {
218            return null;
219        }
220
221        Object obj = doc.getPropertyValue(xPath);
222        if (!(obj instanceof Blob)) {
223            throw new NuxeoException(
224                    String.format("Property %s of document %s (%s) is not a blob, cannot get Drive Edit URL.", xPath,
225                            doc.getPathAsString(), doc.getId()));
226        }
227        Blob blob = (Blob) obj;
228
229        return getDriveEditURL(doc, blob, xPath);
230    }
231
232    public String getDriveEditURL(DocumentModel doc, Blob blob, String xPath) {
233        if (doc == null || blob == null) {
234            return null;
235        }
236
237        String editURL = "%s://%s/%suser/%s/repo/%s/nxdocid/%s/filename/%s/downloadUrl/%s";
238        ServletRequest servletRequest = (ServletRequest) FacesContext.getCurrentInstance()
239                                                                     .getExternalContext()
240                                                                     .getRequest();
241        String baseURL = VirtualHostHelper.getBaseURL(servletRequest).replaceFirst("://", "/");
242
243        String user = documentManager.getPrincipal().getName();
244        String repo = documentManager.getRepositoryName();
245        String docId = doc.getId();
246        String filename = blob.getFilename();
247        filename = filename.replaceAll("(/|\\\\|\\*|<|>|\\?|\"|:|\\|)", "-");
248        filename = URIUtils.quoteURIPathComponent(filename, true);
249        DownloadService downloadService = Framework.getService(DownloadService.class);
250        if (xPath == null) {
251            xPath = DownloadService.BLOBHOLDER_0;
252        }
253        String downloadUrl = downloadService.getDownloadUrl(doc, xPath, filename);
254
255        return String.format(editURL, NXDRIVE_PROTOCOL, PROTOCOL_COMMAND_EDIT, baseURL, user, repo, docId, filename,
256                downloadUrl);
257    }
258
259    public String navigateToUserCenterNuxeoDrive() {
260        return getUserCenterNuxeoDriveView();
261    }
262
263    @Factory(value = "canSynchronizeCurrentDocument")
264    public boolean canSynchronizeCurrentDocument() {
265        DocumentModel currentDocument = navigationContext.getCurrentDocument();
266        if (currentDocument == null) {
267            return false;
268        }
269        return isSyncRootCandidate(currentDocument) && getCurrentSynchronizationRoot() == null;
270    }
271
272    @Factory(value = "canUnSynchronizeCurrentDocument")
273    public boolean canUnSynchronizeCurrentDocument() {
274        DocumentModel currentDocument = navigationContext.getCurrentDocument();
275        if (currentDocument == null) {
276            return false;
277        }
278        if (!isSyncRootCandidate(currentDocument)) {
279            return false;
280        }
281        DocumentRef currentDocRef = currentDocument.getRef();
282        DocumentModel currentSyncRoot = getCurrentSynchronizationRoot();
283        if (currentSyncRoot == null) {
284            return false;
285        }
286        return currentDocRef.equals(currentSyncRoot.getRef());
287    }
288
289    @Factory(value = "canNavigateToCurrentSynchronizationRoot")
290    public boolean canNavigateToCurrentSynchronizationRoot() {
291        DocumentModel currentDocument = navigationContext.getCurrentDocument();
292        if (currentDocument == null) {
293            return false;
294        }
295        if (currentDocument.isTrashed()) {
296            return false;
297        }
298        DocumentRef currentDocRef = currentDocument.getRef();
299        DocumentModel currentSyncRoot = getCurrentSynchronizationRoot();
300        if (currentSyncRoot == null) {
301            return false;
302        }
303        return !currentDocRef.equals(currentSyncRoot.getRef());
304    }
305
306    @Factory(value = "currentDocumentUserWorkspace", scope = ScopeType.PAGE)
307    public boolean isCurrentDocumentUserWorkspace() {
308        DocumentModel currentDocument = navigationContext.getCurrentDocument();
309        if (currentDocument == null) {
310            return false;
311        }
312        return UserWorkspaceHelper.isUserWorkspace(currentDocument);
313    }
314
315    public String synchronizeCurrentDocument() throws UnsupportedEncodingException {
316        NuxeoDriveManager driveManager = Framework.getService(NuxeoDriveManager.class);
317        NuxeoPrincipal principal = documentManager.getPrincipal();
318        DocumentModel newSyncRoot = navigationContext.getCurrentDocument();
319        driveManager.registerSynchronizationRoot(principal, newSyncRoot, documentManager);
320        boolean hasOneNuxeoDriveToken = hasOneDriveToken(principal);
321        if (hasOneNuxeoDriveToken) {
322            return null;
323        } else {
324            // redirect to user center
325            return getUserCenterNuxeoDriveView();
326        }
327    }
328
329    public void unsynchronizeCurrentDocument() {
330        NuxeoDriveManager driveManager = Framework.getService(NuxeoDriveManager.class);
331        NuxeoPrincipal principal = documentManager.getPrincipal();
332        DocumentModel syncRoot = navigationContext.getCurrentDocument();
333        driveManager.unregisterSynchronizationRoot(principal, syncRoot, documentManager);
334    }
335
336    public String navigateToCurrentSynchronizationRoot() {
337        DocumentModel currentRoot = getCurrentSynchronizationRoot();
338        if (currentRoot == null) {
339            return "";
340        }
341        return navigationContext.navigateToDocument(currentRoot);
342    }
343
344    public DocumentModelList getSynchronizationRoots() {
345        DocumentModelList syncRoots = new DocumentModelListImpl();
346        NuxeoDriveManager driveManager = Framework.getService(NuxeoDriveManager.class);
347        Set<IdRef> syncRootRefs = driveManager.getSynchronizationRootReferences(documentManager);
348        for (IdRef syncRootRef : syncRootRefs) {
349            syncRoots.add(documentManager.getDocument(syncRootRef));
350        }
351        return syncRoots;
352    }
353
354    public void unsynchronizeRoot(DocumentModel syncRoot) {
355        NuxeoDriveManager driveManager = Framework.getService(NuxeoDriveManager.class);
356        NuxeoPrincipal principal = documentManager.getPrincipal();
357        driveManager.unregisterSynchronizationRoot(principal, syncRoot, documentManager);
358    }
359
360    @Factory(value = "nuxeoDriveClientPackages", scope = ScopeType.CONVERSATION)
361    public List<DesktopPackageDefinition> getClientPackages() {
362        List<DesktopPackageDefinition> packages = new ArrayList<>();
363        Object desktopPackageBaseURL = Component.getInstance("desktopPackageBaseURL", ScopeType.APPLICATION);
364        // Add link to packages from the update site
365        if (desktopPackageBaseURL != ObjectUtils.NULL) {
366            // Mac OS X
367            String packageName = DESKTOP_PACKAGE_PREFIX + DMG_EXTENSION;
368            String packageURL = desktopPackageBaseURL + packageName;
369            packages.add(new DesktopPackageDefinition(packageURL, packageName, OSX_PLATFORM));
370            log.debug("Added {} to the list of desktop packages available for download.", packageURL);
371            // Windows
372            packageName = DESKTOP_PACKAGE_PREFIX + MSI_EXTENSION;
373            packageURL = desktopPackageBaseURL + packageName;
374            packages.add(new DesktopPackageDefinition(packageURL, packageName, WINDOWS_PLATFORM));
375            log.debug("Added {} to the list of desktop packages available for download.", packageURL);
376        }
377        // Debian / Ubuntu
378        // TODO: remove when Debian package is available
379        packages.add(new DesktopPackageDefinition(
380                "https://github.com/nuxeo/nuxeo-drive#debian-based-distributions-and-other-gnulinux-variants-client",
381                "user.center.nuxeoDrive.platform.ubuntu.docLinkTitle", "ubuntu"));
382        return packages;
383    }
384
385    @Factory(value = "desktopPackageBaseURL", scope = ScopeType.APPLICATION)
386    public Object getDesktopPackageBaseURL() {
387        String url = Framework.getProperty(NuxeoDriveConstants.UPDATE_SITE_URL_PROP_KEY);
388        if (url == null) {
389            return ObjectUtils.NULL;
390        }
391        StringBuilder sb = new StringBuilder(url);
392        if (!url.endsWith("/")) {
393            sb.append("/");
394        }
395        return sb.toString();
396    }
397
398    protected boolean isSyncRootCandidate(DocumentModel doc) {
399        return doc.isFolder() && !doc.isTrashed();
400    }
401
402    protected FileSystemItem getFileSystemItem(DocumentModel doc) {
403        // Force parentItem to null to avoid computing ancestors
404        // NXP-19442: Avoid useless and costly call to DocumentModel#getLockInfo
405        FileSystemItem fileSystemItem = Framework.getService(FileSystemItemAdapterService.class).getFileSystemItem(doc,
406                null, false, false, false);
407        if (fileSystemItem == null) {
408            log.debug("Document {} ({}) is not adaptable as a FileSystemItem.", doc::getPathAsString, doc::getId);
409        }
410        return fileSystemItem;
411    }
412
413    protected String getUserCenterNuxeoDriveView() {
414        userCenterViews.setCurrentViewId("userCenterNuxeoDrive");
415        return AbstractUserGroupManagement.VIEW_HOME;
416    }
417
418    /**
419     * Update document model and redirect to drive view.
420     */
421    public String updateCurrentDocument() {
422        documentActions.updateCurrentDocument();
423        return DRIVE_METADATA_VIEW;
424    }
425
426}