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