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}