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