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