001/* 002 * (C) Copyright 2019 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 * Nuxeo - initial API and implementation 018 */ 019package org.nuxeo.ecm.platform.filemanager.service.extension; 020 021import static org.nuxeo.ecm.core.api.security.SecurityConstants.ADD_CHILDREN; 022import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_PROPERTIES; 023 024import java.io.IOException; 025import java.util.ArrayList; 026import java.util.List; 027import java.util.regex.Pattern; 028 029import org.nuxeo.ecm.core.api.Blob; 030import org.nuxeo.ecm.core.api.CoreSession; 031import org.nuxeo.ecm.core.api.DocumentModel; 032import org.nuxeo.ecm.core.api.DocumentSecurityException; 033import org.nuxeo.ecm.core.api.NuxeoException; 034import org.nuxeo.ecm.core.api.PathRef; 035import org.nuxeo.ecm.core.api.VersioningOption; 036import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 037import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService; 038import org.nuxeo.ecm.core.blob.BlobManager; 039import org.nuxeo.ecm.core.blob.BlobProvider; 040import org.nuxeo.ecm.platform.filemanager.api.FileImporterContext; 041import org.nuxeo.ecm.platform.filemanager.api.FileManager; 042import org.nuxeo.ecm.platform.filemanager.service.FileManagerService; 043import org.nuxeo.ecm.platform.filemanager.utils.FileManagerUtils; 044import org.nuxeo.ecm.platform.types.Type; 045import org.nuxeo.ecm.platform.types.TypeManager; 046import org.nuxeo.runtime.api.Framework; 047 048/** 049 * File importer abstract class. 050 * <p> 051 * Default file importer behavior. 052 * 053 * @see FileImporter 054 * @author <a href="mailto:akalogeropoulos@nuxeo.com">Andreas Kalogeropolos</a> 055 */ 056public abstract class AbstractFileImporter implements FileImporter { 057 058 private static final long serialVersionUID = 1L; 059 060 protected String name = ""; 061 062 protected String docType; 063 064 protected transient List<String> filters = new ArrayList<>(); 065 066 protected transient List<Pattern> patterns; 067 068 protected boolean enabled = true; 069 070 protected Integer order = 0; 071 072 public static final String SKIP_UPDATE_AUDIT_LOGGING = "org.nuxeo.filemanager.skip.audit.logging.forupdates"; 073 074 // duplicated from Audit module to avoid circular dependency 075 public static final String DISABLE_AUDIT_LOGGER = "disableAuditLogger"; 076 077 // to be used by plugin implementation to gain access to standard file 078 // creation utility methods without having to lookup the service 079 /** 080 * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed 081 */ 082 @Deprecated(since = "10.3") 083 protected transient FileManagerService fileManagerService; 084 085 protected AbstractFileImporter() { 086 this.fileManagerService = (FileManagerService) Framework.getService(FileManager.class); 087 } 088 089 @Override 090 public List<String> getFilters() { 091 return filters; 092 } 093 094 @Override 095 public void setFilters(List<String> filters) { 096 this.filters = filters; 097 patterns = new ArrayList<>(); 098 for (String filter : filters) { 099 patterns.add(Pattern.compile(filter)); 100 } 101 } 102 103 @Override 104 public boolean matches(String mimeType) { 105 for (Pattern pattern : patterns) { 106 if (pattern.matcher(mimeType).matches()) { 107 return true; 108 } 109 } 110 return false; 111 } 112 113 @Override 114 public String getName() { 115 return name; 116 } 117 118 @Override 119 public void setName(String name) { 120 this.name = name; 121 } 122 123 @Override 124 public String getDocType() { 125 return docType; 126 } 127 128 @Override 129 public void setDocType(String docType) { 130 this.docType = docType; 131 } 132 133 /** 134 * Gets the doc type to use in the given container. 135 */ 136 protected String getDocType(DocumentModel container) { // NOSONAR 137 return getDocType(); // use XML configuration 138 } 139 140 /** 141 * Default document type to use when the plugin XML configuration does not specify one. 142 * <p> 143 * To implement when the default {@link #createOrUpdate(FileImporterContext)} method is used. 144 */ 145 protected String getDefaultDocType() { 146 throw new UnsupportedOperationException(); 147 } 148 149 /** 150 * Whether document overwrite is detected by checking title or filename. 151 * <p> 152 * To implement when the default {@link #createOrUpdate(FileImporterContext)} method is used. 153 */ 154 protected boolean isOverwriteByTitle() { 155 throw new UnsupportedOperationException(); 156 } 157 158 /** 159 * Creates the document (sets its properties). {@link #updateDocument} will be called after this. 160 * <p> 161 * Default implementation sets the title. 162 */ 163 protected void createDocument(DocumentModel doc, String title) { 164 doc.setPropertyValue("dc:title", title); 165 } 166 167 /** 168 * Tries to update the document <code>doc</code> with the blob <code>content</code>. 169 * <p> 170 * Returns <code>true</code> if the document is really updated. 171 * 172 * @since 7.1 173 */ 174 protected boolean updateDocumentIfPossible(DocumentModel doc, Blob content) { 175 updateDocument(doc, content); 176 return true; 177 } 178 179 /** 180 * Updates the document (sets its properties). 181 * <p> 182 * Default implementation sets the content. 183 */ 184 protected void updateDocument(DocumentModel doc, Blob content) { 185 doc.getAdapter(BlobHolder.class).setBlob(content); 186 } 187 188 protected Blob getBlob(DocumentModel doc) { 189 return doc.getAdapter(BlobHolder.class).getBlob(); 190 } 191 192 @Override 193 public boolean isOneToMany() { 194 return false; 195 } 196 197 @Override 198 public DocumentModel create(CoreSession session, Blob content, String path, boolean overwrite, String fullname, 199 TypeManager typeService) throws IOException { 200 FileImporterContext context = FileImporterContext.builder(session, content, path) 201 .overwrite(overwrite) 202 .fileName(fullname) 203 .build(); 204 return createOrUpdate(context); 205 } 206 207 @Override 208 public DocumentModel createOrUpdate(FileImporterContext context) throws IOException { 209 CoreSession session = context.getSession(); 210 String path = getNearestContainerPath(session, context.getParentPath()); 211 DocumentModel container = session.getDocument(new PathRef(path)); 212 String targetDocType = getDocType(container); // from override or descriptor 213 if (targetDocType == null) { 214 targetDocType = getDefaultDocType(); 215 } 216 // always check security 217 checkSecurity(session, path); 218 // check allowed subtypes unless bypassed 219 if (!context.isBypassAllowedSubtypeCheck()) { 220 checkAllowedSubtypes(session, path, targetDocType); 221 } 222 223 Blob blob = context.getBlob(); 224 String filename = FileManagerUtils.fetchFileName(context.getFileName()); 225 String title = FileManagerUtils.fetchTitle(filename); 226 blob.setFilename(filename); 227 // look for an existing document with same title or filename 228 DocumentModel doc; 229 if (isOverwriteByTitle()) { 230 doc = FileManagerUtils.getExistingDocByTitle(session, path, title); 231 } else { 232 doc = FileManagerUtils.getExistingDocByFileName(session, path, filename); 233 } 234 if (context.isOverwrite() && doc != null) { 235 Blob previousBlob = getBlob(doc); 236 // check that previous blob allows overwrite 237 if (previousBlob != null) { 238 BlobProvider blobProvider = Framework.getService(BlobManager.class).getBlobProvider(previousBlob); 239 if (blobProvider != null && !blobProvider.supportsUserUpdate()) { 240 throw new DocumentSecurityException("Cannot overwrite blob"); 241 } 242 } 243 // update data 244 boolean isDocumentUpdated = updateDocumentIfPossible(doc, blob); 245 if (!isDocumentUpdated) { 246 return null; 247 } 248 if (Framework.isBooleanPropertyTrue(SKIP_UPDATE_AUDIT_LOGGING)) { 249 // skip the update event if configured to do so 250 doc.putContextData(DISABLE_AUDIT_LOGGER, true); 251 } 252 if (context.isPersistDocument()) { 253 // save 254 doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName()); 255 doc = doc.getCoreSession().saveDocument(doc); 256 session.save(); 257 } 258 } else { 259 // create document model 260 doc = session.createDocumentModel(targetDocType); 261 createDocument(doc, title); 262 // set path 263 PathSegmentService pss = Framework.getService(PathSegmentService.class); 264 doc.setPathInfo(path, pss.generatePathSegment(doc)); 265 // update data 266 updateDocument(doc, blob); 267 if (context.isPersistDocument()) { 268 // create 269 doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName()); 270 doc = session.createDocument(doc); 271 session.save(); 272 } 273 } 274 return doc; 275 } 276 277 /** 278 * Avoid checkin for a 0-length blob. Microsoft-WebDAV-MiniRedir first creates a 0-length file and then locks it 279 * before putting the real file. But we don't want this first placeholder to cause a versioning event. 280 * 281 * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning 282 * behaviors from importers 283 */ 284 @Deprecated(since = "9.1") 285 protected boolean skipCheckInForBlob(Blob blob) { 286 return blob == null || blob.getLength() == 0; 287 } 288 289 /** 290 * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed 291 */ 292 @Deprecated(since = "10.3") 293 public FileManagerService getFileManagerService() { 294 return fileManagerService; 295 } 296 297 /** 298 * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed 299 */ 300 @Deprecated(since = "10.3") 301 @Override 302 public void setFileManagerService(FileManagerService fileManagerService) { 303 this.fileManagerService = fileManagerService; 304 } 305 306 @Override 307 public void setEnabled(boolean enabled) { 308 this.enabled = enabled; 309 } 310 311 @Override 312 public boolean isEnabled() { 313 return enabled; 314 } 315 316 @Override 317 public Integer getOrder() { 318 return order; 319 } 320 321 @Override 322 public void setOrder(Integer order) { 323 this.order = order; 324 } 325 326 @Override 327 public int compareTo(FileImporter other) { 328 Integer otherOrder = other.getOrder(); 329 if (order == null && otherOrder == null) { 330 return 0; 331 } else if (order == null) { 332 return 1; 333 } else if (otherOrder == null) { 334 return -1; 335 } 336 return order.compareTo(otherOrder); 337 } 338 339 /** 340 * Returns nearest container path 341 * <p> 342 * If given path points to a folderish document, return it. Else, return parent path. 343 */ 344 protected String getNearestContainerPath(CoreSession documentManager, String path) { 345 DocumentModel currentDocument = documentManager.getDocument(new PathRef(path)); 346 if (!currentDocument.isFolder()) { 347 path = path.substring(0, path.lastIndexOf('/')); 348 } 349 return path; 350 } 351 352 /** 353 * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning 354 * behaviors from importers 355 */ 356 @Deprecated(since = "9.1") 357 protected void checkIn(DocumentModel doc) { 358 VersioningOption option = fileManagerService.getVersioningOption(); 359 if (option != null && option != VersioningOption.NONE && doc.isCheckedOut()) { 360 doc.checkIn(option, null); 361 } 362 } 363 364 /** 365 * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning 366 * behaviors from importers 367 */ 368 @Deprecated(since = "9.1") 369 protected void checkInAfterAdd(DocumentModel doc) { 370 if (fileManagerService.doVersioningAfterAdd()) { 371 checkIn(doc); 372 } 373 } 374 375 /** 376 * @since 10.10 377 * @deprecated since 11.3, use {@link #checkSecurity(CoreSession, String) and #checkAllowedSubtypes(CoreSession, 378 * String, String)} instead 379 */ 380 @Deprecated 381 protected void doSecurityCheck(CoreSession documentManager, String path, String typeName) { 382 doSecurityCheck(documentManager, path, typeName, Framework.getService(TypeManager.class)); 383 } 384 385 /** 386 * @deprecated since 11.3, use {@link #checkSecurity(CoreSession, String) and #checkAllowedSubtypes(CoreSession, 387 * String, String)} instead 388 */ 389 @Deprecated 390 protected void doSecurityCheck(CoreSession documentManager, String path, String typeName, TypeManager typeService) { 391 // perform the security checks 392 PathRef containerRef = new PathRef(path); 393 if (!documentManager.hasPermission(containerRef, READ_PROPERTIES) 394 || !documentManager.hasPermission(containerRef, ADD_CHILDREN)) { 395 throw new DocumentSecurityException("Not enough rights to create folder"); 396 } 397 DocumentModel container = documentManager.getDocument(containerRef); 398 399 Type containerType = typeService.getType(container.getType()); 400 if (containerType == null) { 401 return; 402 } 403 404 if (!typeService.isAllowedSubType(typeName, container.getType(), container)) { 405 throw new NuxeoException(String.format("Cannot create document of type %s in container with type %s", 406 typeName, containerType.getId())); 407 } 408 } 409 410 /** 411 * @since 11.3 412 */ 413 protected void checkSecurity(CoreSession session, String path) { 414 PathRef containerRef = new PathRef(path); 415 if (!session.hasPermission(containerRef, ADD_CHILDREN)) { 416 throw new DocumentSecurityException("Not enough rights to create document"); 417 } 418 } 419 420 /** 421 * @since 11.3 422 */ 423 protected void checkAllowedSubtypes(CoreSession session, String path, String typeName) { 424 PathRef containerRef = new PathRef(path); 425 DocumentModel container = session.getDocument(containerRef); 426 TypeManager typeService = Framework.getService(TypeManager.class); 427 Type containerType = typeService.getType(container.getType()); 428 if (containerType == null) { 429 return; 430 } 431 432 if (!typeService.isAllowedSubType(typeName, container.getType(), container)) { 433 throw new NuxeoException(String.format("Cannot create document of type %s in container with type %s", 434 typeName, containerType.getId())); 435 } 436 } 437 438}