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