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