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