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