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}