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