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}