001/* 002 * (C) Copyright 2006-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 * Bogdan Stefanescu 018 * Florent Guillaume 019 * Benoit Delbosc 020 */ 021package org.nuxeo.ecm.core.api; 022 023import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; 024import static org.nuxeo.ecm.core.api.event.CoreEventConstants.CHANGED_ACL_NAME; 025import static org.nuxeo.ecm.core.api.event.CoreEventConstants.NEW_ACE; 026import static org.nuxeo.ecm.core.api.event.CoreEventConstants.OLD_ACE; 027import static org.nuxeo.ecm.core.api.security.SecurityConstants.ADD_CHILDREN; 028import static org.nuxeo.ecm.core.api.security.SecurityConstants.BROWSE; 029import static org.nuxeo.ecm.core.api.security.SecurityConstants.MAKE_RECORD; 030import static org.nuxeo.ecm.core.api.security.SecurityConstants.MANAGE_LEGAL_HOLD; 031import static org.nuxeo.ecm.core.api.security.SecurityConstants.SET_RETENTION; 032import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ; 033import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_CHILDREN; 034import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_LIFE_CYCLE; 035import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_PROPERTIES; 036import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_SECURITY; 037import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_VERSION; 038import static org.nuxeo.ecm.core.api.security.SecurityConstants.REMOVE; 039import static org.nuxeo.ecm.core.api.security.SecurityConstants.REMOVE_CHILDREN; 040import static org.nuxeo.ecm.core.api.security.SecurityConstants.UNLOCK; 041import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE; 042import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_LIFE_CYCLE; 043import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_PROPERTIES; 044import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_SECURITY; 045import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_VERSION; 046import static org.nuxeo.ecm.core.api.trash.TrashService.Feature.TRASHED_STATE_IS_DEDUCED_FROM_LIFECYCLE; 047import static org.nuxeo.ecm.core.trash.LifeCycleTrashService.FROM_LIFE_CYCLE_TRASH_SERVICE; 048 049import java.io.Serializable; 050import java.util.ArrayList; 051import java.util.Arrays; 052import java.util.Calendar; 053import java.util.Collection; 054import java.util.Collections; 055import java.util.Comparator; 056import java.util.GregorianCalendar; 057import java.util.HashMap; 058import java.util.List; 059import java.util.Map; 060import java.util.Map.Entry; 061import java.util.concurrent.ConcurrentHashMap; 062import java.util.function.Function; 063import java.util.stream.Collectors; 064 065import org.apache.commons.lang3.StringUtils; 066import org.apache.logging.log4j.LogManager; 067import org.apache.logging.log4j.Logger; 068import org.nuxeo.ecm.core.CoreService; 069import org.nuxeo.ecm.core.api.DocumentModel.DocumentModelRefresh; 070import org.nuxeo.ecm.core.api.event.CoreEventConstants; 071import org.nuxeo.ecm.core.api.event.DocumentEventCategories; 072import org.nuxeo.ecm.core.api.event.DocumentEventTypes; 073import org.nuxeo.ecm.core.api.facet.VersioningDocument; 074import org.nuxeo.ecm.core.api.impl.DocumentModelChildrenIterator; 075import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; 076import org.nuxeo.ecm.core.api.impl.FacetFilter; 077import org.nuxeo.ecm.core.api.impl.VersionModelImpl; 078import org.nuxeo.ecm.core.api.security.ACE; 079import org.nuxeo.ecm.core.api.security.ACP; 080import org.nuxeo.ecm.core.api.security.SecurityConstants; 081import org.nuxeo.ecm.core.api.security.UserEntry; 082import org.nuxeo.ecm.core.api.security.impl.ACPImpl; 083import org.nuxeo.ecm.core.api.security.impl.UserEntryImpl; 084import org.nuxeo.ecm.core.api.trash.TrashService; 085import org.nuxeo.ecm.core.api.validation.DocumentValidationException; 086import org.nuxeo.ecm.core.api.validation.DocumentValidationReport; 087import org.nuxeo.ecm.core.api.validation.DocumentValidationService; 088import org.nuxeo.ecm.core.api.versioning.VersioningService; 089import org.nuxeo.ecm.core.event.Event; 090import org.nuxeo.ecm.core.event.EventService; 091import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 092import org.nuxeo.ecm.core.filter.CharacterFilteringService; 093import org.nuxeo.ecm.core.lifecycle.LifeCycleService; 094import org.nuxeo.ecm.core.model.Document; 095import org.nuxeo.ecm.core.model.PathComparator; 096import org.nuxeo.ecm.core.model.Session; 097import org.nuxeo.ecm.core.query.QueryFilter; 098import org.nuxeo.ecm.core.query.QueryParseException; 099import org.nuxeo.ecm.core.query.sql.NXQL; 100import org.nuxeo.ecm.core.query.sql.model.SQLQuery.Transformer; 101import org.nuxeo.ecm.core.schema.DocumentType; 102import org.nuxeo.ecm.core.schema.FacetNames; 103import org.nuxeo.ecm.core.schema.SchemaManager; 104import org.nuxeo.ecm.core.schema.types.CompositeType; 105import org.nuxeo.ecm.core.schema.types.Schema; 106import org.nuxeo.ecm.core.security.LockSecurityPolicy; 107import org.nuxeo.ecm.core.security.SecurityService; 108import org.nuxeo.runtime.api.Framework; 109import org.nuxeo.runtime.metrics.MetricsService; 110import org.nuxeo.runtime.services.config.ConfigurationService; 111 112import io.dropwizard.metrics5.Counter; 113import io.dropwizard.metrics5.MetricName; 114import io.dropwizard.metrics5.MetricRegistry; 115import io.dropwizard.metrics5.SharedMetricRegistries; 116 117import io.opencensus.trace.AttributeValue; 118import io.opencensus.trace.Span; 119import io.opencensus.trace.Tracing; 120 121/** 122 * Abstract implementation of the client interface. 123 * <p> 124 * This handles all the aspects that are independent on the final implementation (like running inside a J2EE platform or 125 * not). 126 * <p> 127 * The only aspect not implemented is the session management that should be handled by subclasses. 128 * 129 * @author Bogdan Stefanescu 130 * @author Florent Guillaume 131 */ 132public abstract class AbstractSession implements CoreSession, Serializable { 133 134 private static final Logger log = LogManager.getLogger(CoreSession.class); 135 136 private static final long serialVersionUID = 1L; 137 138 private static final Comparator<? super Document> pathComparator = new PathComparator(); 139 140 public static final String DEFAULT_MAX_RESULTS = "1000"; 141 142 public static final String MAX_RESULTS_PROPERTY = "org.nuxeo.ecm.core.max.results"; 143 144 public static final String LIMIT_RESULTS_PROPERTY = "org.nuxeo.ecm.core.limit.results"; 145 146 /** 147 * @deprecated since 10.1, new trash behavior is: always keep checkedIn state 148 */ 149 @Deprecated 150 public static final String TRASH_KEEP_CHECKED_IN_PROPERTY = "org.nuxeo.trash.keepCheckedIn"; 151 152 // @since 9.1 disable ecm:isLatestVersion and ecm:isLatestMajorVersion updates for performance purpose 153 public static final String DISABLED_ISLATESTVERSION_PROPERTY = "org.nuxeo.core.isLatestVersion.disabled"; 154 155 public static final String BINARY_TEXT_SYS_PROP = "fulltextBinary"; 156 157 private Boolean limitedResults; 158 159 private Long maxResults; 160 161 // @since 5.7.2 162 protected static final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 163 164 // static caches shared by all threads 165 protected static final Map<String, Counter> CREATE_DOC_COUNT = new ConcurrentHashMap<>(); 166 167 protected static final Map<String, Counter> DELETE_DOC_COUNT = new ConcurrentHashMap<>(); 168 169 protected static final Map<String, Counter> UPDATE_DOC_COUNT = new ConcurrentHashMap<>(); 170 171 // lazily initialized counters 172 protected Counter createDocumentCount; 173 174 protected Counter deleteDocumentCount; 175 176 protected Counter updateDocumentCount; 177 178 protected void createDocumentCountInc() { 179 if (createDocumentCount == null) { 180 createDocumentCount = CREATE_DOC_COUNT.computeIfAbsent(getRepositoryName(), 181 repositoryName -> registry.counter( 182 MetricName.build("nuxeo", "repositories", "repository", "documents", "create") 183 .tagged("repository", repositoryName))); 184 } 185 createDocumentCount.inc(); 186 } 187 188 protected void deleteDocumentCountInc() { 189 if (deleteDocumentCount == null) { 190 deleteDocumentCount = DELETE_DOC_COUNT.computeIfAbsent(getRepositoryName(), 191 repositoryName -> registry.counter( 192 MetricName.build("nuxeo", "repositories", "repository", "documents", "delete") 193 .tagged("repository", repositoryName))); 194 } 195 deleteDocumentCount.inc(); 196 } 197 198 protected void updateDocumentCountInc() { 199 if (updateDocumentCount == null) { 200 updateDocumentCount = UPDATE_DOC_COUNT.computeIfAbsent(getRepositoryName(), 201 repositoryName -> registry.counter( 202 MetricName.build("nuxeo", "repositories", "repository", "documents", "update") 203 .tagged("repository", repositoryName))); 204 } 205 updateDocumentCount.inc(); 206 } 207 208 protected SecurityService getSecurityService() { 209 return Framework.getService(SecurityService.class); 210 } 211 212 protected VersioningService getVersioningService() { 213 return Framework.getService(VersioningService.class); 214 } 215 216 protected DocumentValidationService getValidationService() { 217 return Framework.getService(DocumentValidationService.class); 218 } 219 220 /** 221 * Internal method: Gets the current session based on the client session id. 222 * 223 * @return the repository session 224 */ 225 public abstract Session getSession(); 226 227 @Override 228 public DocumentType getDocumentType(String type) { 229 return Framework.getService(SchemaManager.class).getDocumentType(type); 230 } 231 232 protected final void checkPermission(Document doc, String permission) throws DocumentSecurityException { 233 if (isAdministrator()) { 234 return; 235 } 236 if (!hasPermission(doc, permission)) { 237 log.debug("Permission '{}' is not granted to '{}' on document {} ({} - {})", permission, 238 getPrincipal().getName(), doc.getPath(), doc.getUUID(), doc.getType().getName()); 239 240 throw new DocumentSecurityException( 241 "Privilege '" + permission + "' is not granted to '" + getPrincipal().getName() + "'"); 242 } 243 } 244 245 protected Map<String, Serializable> getContextMapEventInfo(DocumentModel doc) { 246 Map<String, Serializable> options = new HashMap<>(); 247 if (doc != null) { 248 options.putAll(doc.getContextData()); 249 } 250 return options; 251 } 252 253 public DocumentEventContext newEventContext(DocumentModel source) { 254 DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source); 255 ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, getRepositoryName()); 256 return ctx; 257 } 258 259 protected void notifyEvent(String eventId, DocumentModel source, Map<String, Serializable> options, String category, 260 String comment, boolean withLifeCycle, boolean inline) { 261 262 DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source); 263 264 // compatibility with old code (< 5.2.M4) - import info from old event 265 // model 266 if (options != null) { 267 ctx.setProperties(options); 268 } 269 ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, getRepositoryName()); 270 // Document life cycle 271 if (source != null && withLifeCycle) { 272 String currentLifeCycleState = source.getCurrentLifeCycleState(); 273 ctx.setProperty(CoreEventConstants.DOC_LIFE_CYCLE, currentLifeCycleState); 274 } 275 if (comment != null) { 276 ctx.setProperty("comment", comment); 277 } 278 ctx.setProperty("category", category == null ? DocumentEventCategories.EVENT_DOCUMENT_CATEGORY : category); 279 // compat code: mark SAVE event as a commit event 280 Event event = ctx.newEvent(eventId); 281 if (DocumentEventTypes.SESSION_SAVED.equals(eventId)) { 282 event.setIsCommitEvent(true); 283 } 284 if (inline) { 285 event.setInline(true); 286 } 287 Framework.getService(EventService.class).fireEvent(event); 288 } 289 290 /** 291 * Copied from obsolete VersionChangeNotifier. 292 * <p> 293 * Sends change notifications to core event listeners. The event contains info with older document (before version 294 * change) and newer doc (current document). 295 * 296 * @param options additional info to pass to the event 297 */ 298 protected void notifyVersionChange(DocumentModel oldDocument, DocumentModel newDocument, 299 Map<String, Serializable> options) { 300 final Map<String, Serializable> info = new HashMap<>(); 301 if (options != null) { 302 info.putAll(options); 303 } 304 info.put(VersioningChangeNotifier.EVT_INFO_NEW_DOC_KEY, newDocument); 305 info.put(VersioningChangeNotifier.EVT_INFO_OLD_DOC_KEY, oldDocument); 306 notifyEvent(VersioningChangeNotifier.CORE_EVENT_ID_VERSIONING_CHANGE, newDocument, info, 307 DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY, null, false, false); 308 } 309 310 @Override 311 public boolean hasPermission(NuxeoPrincipal principal, DocumentRef docRef, String permission) { 312 Document doc = resolveReference(docRef); 313 return hasPermission(principal, doc, permission); 314 } 315 316 protected final boolean hasPermission(NuxeoPrincipal principal, Document doc, String permission) { 317 return getSecurityService().checkPermission(doc, principal, permission); 318 } 319 320 @Override 321 public boolean hasPermission(DocumentRef docRef, String permission) { 322 Document doc = resolveReference(docRef); 323 return hasPermission(doc, permission); 324 } 325 326 @Override 327 public Collection<String> filterGrantedPermissions(NuxeoPrincipal principal, DocumentRef docRef, 328 Collection<String> permissions) { 329 Document doc = resolveReference(docRef); 330 return getSecurityService().filterGrantedPermissions(doc, principal, permissions); 331 } 332 333 protected final boolean hasPermission(Document doc, String permission) { 334 // TODO: optimize this - usually ACP is already available when calling 335 // this method. 336 // -> cache ACP at securitymanager level or try to reuse the ACP when 337 // it is known 338 return getSecurityService().checkPermission(doc, getPrincipal(), permission); 339 // return doc.getSession().getSecurityManager().checkPermission(doc, 340 // getPrincipal().getName(), permission); 341 } 342 343 protected Document resolveReference(DocumentRef docRef) { 344 if (docRef == null) { 345 throw new IllegalArgumentException("null docRref"); 346 } 347 Object ref = docRef.reference(); 348 if (ref == null) { 349 throw new IllegalArgumentException("null reference"); 350 } 351 int type = docRef.type(); 352 switch (type) { 353 case DocumentRef.ID: 354 return getSession().getDocumentByUUID((String) ref); 355 case DocumentRef.PATH: 356 return getSession().resolvePath((String) ref); 357 case DocumentRef.INSTANCE: 358 return getSession().getDocumentByUUID(((DocumentModel) ref).getId()); 359 default: 360 throw new IllegalArgumentException("Invalid type: " + type); 361 } 362 } 363 364 protected Document resolveParentReference(DocumentRef docRef) { 365 // first let's try to handle a path reference 366 if (docRef != null && docRef.type() == DocumentRef.PATH && docRef.reference() != null) { 367 String docPath = (String) docRef.reference(); 368 if ("/".equals(docPath)) { 369 return null; 370 } 371 String parentPath = docPath.substring(0, docPath.lastIndexOf('/')); 372 if (parentPath.isEmpty()) { 373 parentPath = "/"; 374 } 375 return resolveReference(new PathRef(parentPath)); 376 } 377 return resolveReference(docRef).getParent(); 378 } 379 380 /** 381 * Gets the document model for the given core document. 382 * 383 * @param doc the document 384 * @return the document model 385 */ 386 protected DocumentModel readModel(Document doc) { 387 return DocumentModelFactory.createDocumentModel(doc, this); 388 } 389 390 /** 391 * Gets the document model for the given core document, preserving the contextData. 392 * 393 * @param doc the document 394 * @return the document model 395 */ 396 protected DocumentModel readModel(Document doc, DocumentModel docModel) { 397 DocumentModel newModel = readModel(doc); 398 newModel.copyContextData(docModel); 399 return newModel; 400 } 401 402 protected DocumentModel writeModel(Document doc, DocumentModel docModel) { 403 return writeModel(doc, docModel, false); 404 } 405 406 protected DocumentModel writeModel(Document doc, DocumentModel docModel, boolean create) { 407 return DocumentModelFactory.writeDocumentModel(docModel, doc, create); 408 } 409 410 @Override 411 @Deprecated 412 public DocumentModel copy(DocumentRef src, DocumentRef dst, String name, boolean resetLifeCycle) { 413 if (resetLifeCycle) { 414 return copy(src, dst, name, CopyOption.RESET_LIFE_CYCLE); 415 } 416 return copy(src, dst, name); 417 } 418 419 @Override 420 public DocumentModel copy(DocumentRef src, DocumentRef dst, String name, CopyOption... copyOptions) { 421 Document dstDoc = resolveReference(dst); 422 checkPermission(dstDoc, ADD_CHILDREN); 423 424 Document srcDoc = resolveReference(src); 425 if (name == null) { 426 name = srcDoc.getName(); 427 } else { 428 PathRef.checkName(name); 429 } 430 431 Map<String, Serializable> options = new HashMap<>(); 432 433 // add the destination name, destination, resetLifeCycle flag and 434 // source references in 435 // the options of the event 436 options.put(CoreEventConstants.SOURCE_REF, src); 437 options.put(CoreEventConstants.DESTINATION_REF, dst); 438 options.put(CoreEventConstants.DESTINATION_PATH, dstDoc.getPath()); 439 options.put(CoreEventConstants.DESTINATION_NAME, name); 440 options.put(CoreEventConstants.DESTINATION_EXISTS, dstDoc.hasChild(name)); 441 options.put(CoreEventConstants.RESET_LIFECYCLE, CopyOption.isResetLifeCycle(copyOptions)); 442 options.put(CoreEventConstants.RESET_CREATOR, CopyOption.isResetCreator(copyOptions)); 443 DocumentModel srcDocModel = readModel(srcDoc); 444 notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, options, null, null, true, true); 445 446 name = (String) options.get(CoreEventConstants.DESTINATION_NAME); 447 Document doc = getSession().copy(srcDoc, dstDoc, name); 448 // no need to clear lock, locks table is not copied 449 450 // notify document created by copy 451 DocumentModel docModel = readModel(doc); 452 453 String comment = srcDoc.getRepositoryName() + ':' + src.toString(); 454 455 // initialize the copied document version 456 if (!Boolean.TRUE.equals(docModel.getContextData(VersioningService.SKIP_VERSIONING))) { 457 getVersioningService().doPostCreate(doc, options); 458 } 459 460 notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false); 461 docModel = writeModel(doc, docModel); 462 463 // notify document copied 464 comment = doc.getRepositoryName() + ':' + docModel.getRef().toString(); 465 466 notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false); 467 468 return docModel; 469 } 470 471 @Override 472 @Deprecated 473 public List<DocumentModel> copy(List<DocumentRef> src, DocumentRef dst, boolean resetLifeCycle) { 474 if (resetLifeCycle) { 475 return copy(src, dst, CopyOption.RESET_LIFE_CYCLE); 476 } 477 return copy(src, dst); 478 } 479 480 @Override 481 public List<DocumentModel> copy(List<DocumentRef> src, DocumentRef dst, CopyOption... opts) { 482 return src.stream().map(ref -> copy(ref, dst, null, opts)).collect(Collectors.toList()); 483 } 484 485 @Override 486 @Deprecated 487 public DocumentModel copyProxyAsDocument(DocumentRef src, DocumentRef dst, String name, boolean resetLifeCycle) { 488 if (resetLifeCycle) { 489 return copyProxyAsDocument(src, dst, name, CopyOption.RESET_LIFE_CYCLE); 490 } 491 return copyProxyAsDocument(src, dst, name); 492 } 493 494 @Override 495 public DocumentModel copyProxyAsDocument(DocumentRef src, DocumentRef dst, String name, CopyOption... copyOptions) { 496 Document srcDoc = resolveReference(src); 497 if (!srcDoc.isProxy()) { 498 return copy(src, dst, name); 499 } 500 Document dstDoc = resolveReference(dst); 501 checkPermission(dstDoc, WRITE); 502 503 // create a new document using the expanded proxy 504 DocumentModel srcDocModel = readModel(srcDoc); 505 String docName = (name != null) ? name : srcDocModel.getName(); 506 DocumentModel docModel = createDocumentModel(dstDoc.getPath(), docName, srcDocModel.getType()); 507 docModel.copyContent(srcDocModel); 508 notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, null, null, null, true, true); 509 docModel = createDocument(docModel); 510 Document doc = resolveReference(docModel.getRef()); 511 512 Map<String, Serializable> options = new HashMap<>(); 513 options.put(CoreEventConstants.RESET_LIFECYCLE, CopyOption.isResetLifeCycle(copyOptions)); 514 options.put(CoreEventConstants.RESET_CREATOR, CopyOption.isResetCreator(copyOptions)); 515 // notify document created by copy 516 String comment = srcDoc.getRepositoryName() + ':' + src.toString(); 517 notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false); 518 519 // notify document copied 520 comment = doc.getRepositoryName() + ':' + docModel.getRef().toString(); 521 notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false); 522 523 return docModel; 524 } 525 526 @Override 527 @Deprecated 528 public List<DocumentModel> copyProxyAsDocument(List<DocumentRef> src, DocumentRef dst, boolean resetLifeCycle) { 529 if (resetLifeCycle) { 530 return copyProxyAsDocument(src, dst, CopyOption.RESET_LIFE_CYCLE); 531 } 532 return copyProxyAsDocument(src, dst); 533 } 534 535 @Override 536 public List<DocumentModel> copyProxyAsDocument(List<DocumentRef> src, DocumentRef dst, CopyOption... opts) { 537 return src.stream().map(ref -> copyProxyAsDocument(ref, dst, null, opts)).collect(Collectors.toList()); 538 } 539 540 @Override 541 public DocumentModel move(DocumentRef src, DocumentRef dst, String name) { 542 Document srcDoc = resolveReference(src); 543 Document dstDoc; 544 if (dst == null) { 545 // rename 546 dstDoc = srcDoc.getParent(); 547 checkPermission(dstDoc, WRITE_PROPERTIES); 548 } else { 549 dstDoc = resolveReference(dst); 550 checkPermission(dstDoc, ADD_CHILDREN); 551 checkPermission(srcDoc.getParent(), REMOVE_CHILDREN); 552 checkPermission(srcDoc, REMOVE); 553 } 554 555 DocumentModel srcDocModel = readModel(srcDoc); 556 String originalName = srcDocModel.getName(); 557 if (name == null) { 558 name = srcDocModel.getName(); 559 } else { 560 PathRef.checkName(name); 561 } 562 Map<String, Serializable> options = getContextMapEventInfo(srcDocModel); 563 // add the destination name, destination and source references in 564 // the options of the event 565 options.put(CoreEventConstants.SOURCE_REF, src); 566 options.put(CoreEventConstants.DESTINATION_REF, dst); 567 options.put(CoreEventConstants.DESTINATION_PATH, dstDoc.getPath()); 568 options.put(CoreEventConstants.DESTINATION_NAME, name); 569 options.put(CoreEventConstants.DESTINATION_EXISTS, dstDoc.hasChild(name)); 570 571 notifyEvent(DocumentEventTypes.ABOUT_TO_MOVE, srcDocModel, options, null, null, true, true); 572 573 name = (String) options.get(CoreEventConstants.DESTINATION_NAME); 574 575 if (!originalName.equals(name)) { 576 options.put(CoreEventConstants.ORIGINAL_NAME, originalName); 577 } 578 579 String comment = srcDoc.getRepositoryName() + ':' + srcDoc.getParent().getUUID(); 580 581 Document doc = getSession().move(srcDoc, dstDoc, name); 582 583 // notify document moved 584 DocumentModel docModel = readModel(doc); 585 options.put(CoreEventConstants.PARENT_PATH, srcDocModel.getParentRef()); 586 notifyEvent(DocumentEventTypes.DOCUMENT_MOVED, docModel, options, null, comment, true, false); 587 588 return docModel; 589 } 590 591 @Override 592 public void move(List<DocumentRef> src, DocumentRef dst) { 593 for (DocumentRef ref : src) { 594 move(ref, dst, null); 595 } 596 } 597 598 @Override 599 public ACP getACP(DocumentRef docRef) { 600 Document doc = resolveReference(docRef); 601 checkPermission(doc, READ_SECURITY); 602 return getSession().getMergedACP(doc); 603 } 604 605 @Override 606 public void setACP(DocumentRef docRef, ACP newAcp, boolean overwrite) { 607 Document doc = resolveReference(docRef); 608 checkPermission(doc, WRITE_SECURITY); 609 610 setACP(doc, newAcp, overwrite, null); 611 } 612 613 protected void setACP(Document doc, ACP newAcp, boolean overwrite, Map<String, Serializable> options) { 614 DocumentModel docModel = readModel(doc); 615 if (options == null) { 616 options = new HashMap<>(); 617 } 618 ACP oldAcp = docModel.getACP(); 619 if (oldAcp != null) { 620 options.put(CoreEventConstants.OLD_ACP, oldAcp.clone()); 621 } 622 options.put(CoreEventConstants.NEW_ACP, newAcp); 623 624 notifyEvent(DocumentEventTypes.BEFORE_DOC_SECU_UPDATE, docModel, options, null, null, true, true); 625 getSession().setACP(doc, newAcp, overwrite); 626 docModel = readModel(doc); 627 options.put(CoreEventConstants.NEW_ACP, newAcp.clone()); 628 notifyEvent(DocumentEventTypes.DOCUMENT_SECURITY_UPDATED, docModel, options, null, null, true, false); 629 } 630 631 @Override 632 public void replaceACE(DocumentRef docRef, String aclName, ACE oldACE, ACE newACE) { 633 Document doc = resolveReference(docRef); 634 checkPermission(doc, WRITE_SECURITY); 635 636 ACP acp = getACP(docRef); 637 if (acp.replaceACE(aclName, oldACE, newACE)) { 638 Map<String, Serializable> options = new HashMap<>(); 639 options.put(OLD_ACE, oldACE); 640 options.put(NEW_ACE, newACE); 641 options.put(CHANGED_ACL_NAME, aclName); 642 setACP(doc, acp, true, options); 643 } 644 } 645 646 @Override 647 public void updateReadACLs(Collection<String> docIds) { 648 getSession().updateReadACLs(docIds); 649 } 650 651 @Override 652 public boolean isNegativeAclAllowed() { 653 return getSession().isNegativeAclAllowed(); 654 } 655 656 @Override 657 public void cancel() { 658 // nothing 659 } 660 661 private DocumentModel createDocumentModelFromTypeName(String typeName, Map<String, Serializable> options) { 662 return createDocumentModelFromParentAndType(null, typeName, options); 663 } 664 665 private DocumentModel createDocumentModelFromParentAndType(DocumentRef parentRef, String typeName, 666 Map<String, Serializable> options) { 667 DocumentModel docModel = DocumentModelFactory.createDocumentModel(typeName, parentRef, this); 668 if (options == null) { 669 options = new HashMap<>(); 670 } else if (options.containsKey(CoreEventConstants.PARENT_PATH) 671 && options.containsKey(CoreEventConstants.DESTINATION_NAME)) { 672 docModel.setPathInfo((String) options.get(CoreEventConstants.PARENT_PATH), 673 (String) options.get(CoreEventConstants.DESTINATION_NAME)); 674 } 675 notifyEvent(DocumentEventTypes.EMPTY_DOCUMENTMODEL_CREATED, docModel, options, null, null, false, true); 676 return docModel; 677 } 678 679 @Override 680 public DocumentModel createDocumentModel(String typeName) { 681 Map<String, Serializable> options = new HashMap<>(); 682 return createDocumentModelFromTypeName(typeName, options); 683 } 684 685 @Override 686 public DocumentModel createDocumentModel(String parentPath, String name, String typeName) { 687 if (parentPath == null && name == null) { 688 return createDocumentModel(typeName); 689 } 690 691 Map<String, Serializable> options = new HashMap<>(); 692 options.put(CoreEventConstants.PARENT_PATH, parentPath); 693 options.put(CoreEventConstants.DOCUMENT_MODEL_ID, name); 694 options.put(CoreEventConstants.DESTINATION_NAME, name); 695 return createDocumentModelFromTypeName(typeName, options); 696 } 697 698 @Override 699 public DocumentModel createDocumentModel(String typeName, Map<String, Object> options) { 700 701 Map<String, Serializable> serializableOptions = new HashMap<>(); 702 703 for (Entry<String, Object> entry : options.entrySet()) { 704 serializableOptions.put(entry.getKey(), (Serializable) entry.getValue()); 705 } 706 return createDocumentModelFromTypeName(typeName, serializableOptions); 707 } 708 709 @Override 710 public DocumentModel createDocument(DocumentModel docModel) { 711 712 // start by removing disallowed characters 713 CharacterFilteringService charFilteringService = Framework.getService(CharacterFilteringService.class); 714 charFilteringService.filter(docModel); 715 716 if (!docModel.isAttached()) { 717 docModel.attach(this); 718 } 719 String typeName = docModel.getType(); 720 DocumentRef parentRef = docModel.getParentRef(); 721 if (typeName == null) { 722 throw new NullPointerException("null typeName"); 723 } 724 if (parentRef == null && !isAdministrator()) { 725 throw new NuxeoException("Only Administrators can create placeless documents"); 726 } 727 String childName = docModel.getName(); 728 Map<String, Serializable> options = getContextMapEventInfo(docModel); 729 730 Document parent = fillCreateOptions(parentRef, childName, options); 731 732 // get initial life cycle state info 733 String initialLifecycleState = null; 734 Object lifecycleStateInfo = docModel.getContextData(LifeCycleConstants.INITIAL_LIFECYCLE_STATE_OPTION_NAME); 735 if (lifecycleStateInfo instanceof String) { 736 initialLifecycleState = (String) lifecycleStateInfo; 737 } 738 notifyEvent(DocumentEventTypes.ABOUT_TO_CREATE, docModel, options, null, null, false, true); // no lifecycle yet 739 740 // document validation 741 if (getValidationService().isActivated(DocumentValidationService.CTX_CREATEDOC, options)) { 742 DocumentValidationReport report = getValidationService().validate(docModel, false); 743 if (report.hasError()) { 744 throw new DocumentValidationException(report); 745 } 746 } 747 childName = (String) options.get(CoreEventConstants.DESTINATION_NAME); 748 Document doc = parent.addChild(childName, typeName); 749 750 // update facets too since some of them may be dynamic 751 for (String facetName : docModel.getFacets()) { 752 if (!doc.getAllFacets().contains(facetName) && !FacetNames.IMMUTABLE.equals(facetName)) { 753 doc.addFacet(facetName); 754 } 755 } 756 757 // init document life cycle 758 getLifeCycleService().initialize(doc, initialLifecycleState); 759 760 // init document with data from doc model 761 docModel = writeModel(doc, docModel, true); // in create mode 762 763 if (!Boolean.TRUE.equals(docModel.getContextData(VersioningService.SKIP_VERSIONING))) { 764 // during remote publishing we want to skip versioning 765 // to avoid overwriting the version number 766 getVersioningService().doPostCreate(doc, options); 767 } 768 769 // post-create event 770 docModel = readModel(doc, docModel); 771 // compute auto versioning 772 // no need to fire event, as we use DocumentModel API it's already done 773 // we don't rely on SKIP_VERSIONING because automatic versioning in saveDocument as the same behavior - and it 774 // doesn't erase initial version as it's the case to avoid when setting Skip_VERSIONING 775 getVersioningService().doAutomaticVersioning(null, docModel, false); 776 notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, docModel, options, null, null, true, false); 777 docModel = writeModel(doc, docModel); 778 779 createDocumentCountInc(); 780 return docModel; 781 } 782 783 private LifeCycleService getLifeCycleService() { 784 return Framework.getService(LifeCycleService.class); 785 } 786 787 protected Document fillCreateOptions(DocumentRef parentRef, String childName, Map<String, Serializable> options) 788 throws DocumentSecurityException { 789 Document parent; 790 if (parentRef == null || EMPTY_PATH.equals(parentRef)) { 791 parent = getSession().getNullDocument(); 792 options.put(CoreEventConstants.DESTINATION_REF, null); 793 options.put(CoreEventConstants.DESTINATION_PATH, null); 794 options.put(CoreEventConstants.DESTINATION_NAME, childName); 795 options.put(CoreEventConstants.DESTINATION_EXISTS, false); 796 } else { 797 parent = resolveReference(parentRef); 798 checkPermission(parent, ADD_CHILDREN); 799 options.put(CoreEventConstants.DESTINATION_REF, parentRef); 800 options.put(CoreEventConstants.DESTINATION_PATH, parent.getPath()); 801 options.put(CoreEventConstants.DESTINATION_NAME, childName); 802 if (Boolean.TRUE.equals(options.get(CoreSession.SKIP_DESTINATION_CHECK_ON_CREATE))) { 803 options.put(CoreEventConstants.DESTINATION_EXISTS, false); 804 } else { 805 options.put(CoreEventConstants.DESTINATION_EXISTS, parent.hasChild(childName)); 806 } 807 } 808 return parent; 809 } 810 811 @Override 812 public void importDocuments(List<DocumentModel> docModels) { 813 docModels.forEach(this::importDocument); 814 } 815 816 protected static final PathRef EMPTY_PATH = new PathRef(""); 817 818 protected void importDocument(DocumentModel docModel) { 819 if (!isAdministrator()) { 820 throw new DocumentSecurityException("Only Administrator can import"); 821 } 822 String name = docModel.getName(); 823 if (name == null || name.length() == 0) { 824 throw new IllegalArgumentException("Invalid empty name"); 825 } 826 String typeName = docModel.getType(); 827 if (typeName == null || typeName.length() == 0) { 828 throw new IllegalArgumentException("Invalid empty type"); 829 } 830 String id = docModel.getId(); 831 if (id == null || id.length() == 0) { 832 throw new IllegalArgumentException("Invalid empty id"); 833 } 834 835 DocumentRef parentRef = docModel.getParentRef(); 836 Map<String, Serializable> props = getContextMapEventInfo(docModel); 837 838 if (parentRef != null && EMPTY_PATH.equals(parentRef)) { 839 parentRef = null; 840 } 841 Document parent = fillCreateOptions(parentRef, name, props); 842 notifyEvent(DocumentEventTypes.ABOUT_TO_IMPORT, docModel, props, null, null, false, true); 843 name = (String) props.get(CoreEventConstants.DESTINATION_NAME); 844 845 // document validation 846 if (getValidationService().isActivated(DocumentValidationService.CTX_IMPORTDOC, props)) { 847 DocumentValidationReport report = getValidationService().validate(docModel, true); 848 if (report.hasError()) { 849 throw new DocumentValidationException(report); 850 } 851 } 852 853 // create the document 854 Document doc = getSession().importDocument(id, parentRef == null ? null : parent, name, typeName, props); 855 856 if (typeName.equals(CoreSession.IMPORT_PROXY_TYPE)) { 857 // just reread the final document 858 docModel = readModel(doc); 859 } else { 860 // init document with data from doc model 861 docModel = writeModel(doc, docModel); 862 } 863 864 // send an event about the import 865 notifyEvent(DocumentEventTypes.DOCUMENT_IMPORTED, docModel, null, null, null, true, false); 866 } 867 868 @Override 869 public DocumentModel[] createDocument(DocumentModel[] docModels) { 870 DocumentModel[] models = new DocumentModel[docModels.length]; 871 int i = 0; 872 // TODO: optimize this (do not call at each iteration createDocument()) 873 for (DocumentModel docModel : docModels) { 874 models[i++] = createDocument(docModel); 875 } 876 return models; 877 } 878 879 @Override 880 public boolean exists(DocumentRef docRef) { 881 try { 882 Document doc = resolveReference(docRef); 883 return hasPermission(doc, BROWSE); 884 } catch (DocumentNotFoundException e) { 885 return false; 886 } 887 } 888 889 @Override 890 public DocumentModel getChild(DocumentRef parent, String name) { 891 Document doc = resolveReference(parent); 892 checkPermission(doc, READ_CHILDREN); 893 Document child = doc.getChild(name); 894 checkPermission(child, READ); 895 return readModel(child); 896 } 897 898 @Override 899 public boolean hasChild(DocumentRef parent, String name) { 900 Document doc = resolveReference(parent); 901 checkPermission(doc, READ_CHILDREN); 902 return doc.hasChild(name); 903 } 904 905 @Override 906 public DocumentModelList getChildren(DocumentRef parent) { 907 return getChildren(parent, null, READ, null, null); 908 } 909 910 @Override 911 public DocumentModelList getChildren(DocumentRef parent, String type) { 912 return getChildren(parent, type, READ, null, null); 913 } 914 915 @Override 916 public DocumentModelList getChildren(DocumentRef parent, String type, String perm) { 917 return getChildren(parent, type, perm, null, null); 918 } 919 920 @Override 921 public DocumentModelList getChildren(DocumentRef parent, String type, Filter filter, Sorter sorter) { 922 return getChildren(parent, type, null, filter, sorter); 923 } 924 925 @Override 926 public DocumentModelList getChildren(DocumentRef parent, String type, String perm, Filter filter, Sorter sorter) { 927 if (perm == null) { 928 perm = READ; 929 } 930 Document doc = resolveReference(parent); 931 checkPermission(doc, READ_CHILDREN); 932 DocumentModelList docs = new DocumentModelListImpl(); 933 for (Document child : doc.getChildren()) { 934 if (hasPermission(child, perm)) { 935 if (child.getType() != null && (type == null || type.equals(child.getType().getName()))) { 936 DocumentModel childModel = readModel(child); 937 if (filter == null || filter.accept(childModel)) { 938 docs.add(childModel); 939 } 940 } 941 } 942 } 943 if (sorter != null) { 944 docs.sort(sorter); 945 } 946 return docs; 947 } 948 949 @Override 950 public List<DocumentRef> getChildrenRefs(DocumentRef parentRef, String perm) { 951 if (perm != null) { 952 // XXX TODO 953 throw new NullPointerException("perm != null not implemented"); 954 } 955 Document parent = resolveReference(parentRef); 956 checkPermission(parent, READ_CHILDREN); 957 List<String> ids = parent.getChildrenIds(); 958 List<DocumentRef> refs = new ArrayList<>(ids.size()); 959 for (String id : ids) { 960 refs.add(new IdRef(id)); 961 } 962 return refs; 963 } 964 965 @Override 966 public DocumentModelIterator getChildrenIterator(DocumentRef parent) { 967 return getChildrenIterator(parent, null, null, null); 968 } 969 970 @Override 971 public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type) { 972 return getChildrenIterator(parent, type, null, null); 973 } 974 975 @Override 976 public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type, String perm, Filter filter) { 977 // perm unused, kept for API compat 978 return new DocumentModelChildrenIterator(this, parent, type, filter); 979 } 980 981 @Override 982 public DocumentModel getDocument(DocumentRef docRef) { 983 Document doc = resolveReference(docRef); 984 checkPermission(doc, READ); 985 return readModel(doc); 986 } 987 988 @Override 989 public DocumentModelList getDocuments(DocumentRef[] docRefs) { 990 List<DocumentModel> docs = new ArrayList<>(docRefs.length); 991 for (DocumentRef docRef : docRefs) { 992 Document doc; 993 try { 994 doc = resolveReference(docRef); 995 checkPermission(doc, READ); 996 } catch (DocumentSecurityException e) { 997 // no permission 998 continue; 999 } 1000 docs.add(readModel(doc)); 1001 } 1002 return new DocumentModelListImpl(docs); 1003 } 1004 1005 @Override 1006 public DocumentModelList getFiles(DocumentRef parent) { 1007 Document doc = resolveReference(parent); 1008 checkPermission(doc, READ_CHILDREN); 1009 DocumentModelList docs = new DocumentModelListImpl(); 1010 for (Document child : doc.getChildren()) { 1011 if (!child.isFolder() && hasPermission(child, READ)) { 1012 docs.add(readModel(child)); 1013 } 1014 } 1015 return docs; 1016 } 1017 1018 @Override 1019 public DocumentModelList getFiles(DocumentRef parent, Filter filter, Sorter sorter) { 1020 Document doc = resolveReference(parent); 1021 checkPermission(doc, READ_CHILDREN); 1022 DocumentModelList docs = new DocumentModelListImpl(); 1023 for (Document child : doc.getChildren()) { 1024 if (!child.isFolder() && hasPermission(child, READ)) { 1025 DocumentModel docModel = readModel(doc); 1026 if (filter == null || filter.accept(docModel)) { 1027 docs.add(readModel(child)); 1028 } 1029 } 1030 } 1031 if (sorter != null) { 1032 docs.sort(sorter); 1033 } 1034 return docs; 1035 } 1036 1037 @Override 1038 public DocumentModelList getFolders(DocumentRef parent) { 1039 Document doc = resolveReference(parent); 1040 checkPermission(doc, READ_CHILDREN); 1041 DocumentModelList docs = new DocumentModelListImpl(); 1042 for (Document child : doc.getChildren()) { 1043 if (child.isFolder() && hasPermission(child, READ)) { 1044 docs.add(readModel(child)); 1045 } 1046 } 1047 return docs; 1048 } 1049 1050 @Override 1051 public DocumentModelList getFolders(DocumentRef parent, Filter filter, Sorter sorter) { 1052 Document doc = resolveReference(parent); 1053 checkPermission(doc, READ_CHILDREN); 1054 DocumentModelList docs = new DocumentModelListImpl(); 1055 for (Document child : doc.getChildren()) { 1056 if (child.isFolder() && hasPermission(child, READ)) { 1057 DocumentModel childModel = readModel(child); 1058 if (filter == null || filter.accept(childModel)) { 1059 docs.add(childModel); 1060 } 1061 } 1062 } 1063 if (sorter != null) { 1064 docs.sort(sorter); 1065 } 1066 return docs; 1067 } 1068 1069 @Override 1070 public DocumentRef getParentDocumentRef(DocumentRef docRef) { 1071 Document parentDoc = resolveParentReference(docRef); 1072 return parentDoc != null ? new IdRef(parentDoc.getUUID()) : null; 1073 } 1074 1075 @Override 1076 public DocumentModel getParentDocument(DocumentRef docRef) { 1077 Document parentDoc = resolveParentReference(docRef); 1078 if (parentDoc == null) { 1079 return null; 1080 } 1081 if (!hasPermission(parentDoc, READ)) { 1082 throw new DocumentSecurityException("Privilege READ is not granted to " + getPrincipal().getName()); 1083 } 1084 return readModel(parentDoc); 1085 } 1086 1087 @Override 1088 public List<DocumentModel> getParentDocuments(final DocumentRef docRef) { 1089 1090 if (null == docRef) { 1091 throw new IllegalArgumentException("null docRef"); 1092 } 1093 1094 final List<DocumentModel> docsList = new ArrayList<>(); 1095 Document doc = resolveReference(docRef); 1096 while (doc != null && !"/".equals(doc.getPath())) { 1097 // XXX OG: shouldn't we check BROWSE and READ_PROPERTIES 1098 // instead? 1099 if (!hasPermission(doc, READ)) { 1100 break; 1101 } 1102 docsList.add(readModel(doc)); 1103 doc = doc.getParent(); 1104 } 1105 Collections.reverse(docsList); 1106 return docsList; 1107 } 1108 1109 @Override 1110 public DocumentModel getRootDocument() { 1111 return readModel(getSession().getRootDocument()); 1112 } 1113 1114 @Override 1115 public boolean hasChildren(DocumentRef docRef) { 1116 // TODO: validate permission check with td 1117 Document doc = resolveReference(docRef); 1118 checkPermission(doc, BROWSE); 1119 return doc.hasChildren(); 1120 } 1121 1122 @Override 1123 public DocumentModelList query(String query) { 1124 return query(query, null, 0, 0, false); 1125 } 1126 1127 @Override 1128 public DocumentModelList query(String query, int max) { 1129 return query(query, null, max, 0, false); 1130 } 1131 1132 @Override 1133 public DocumentModelList query(String query, Filter filter) { 1134 return query(query, filter, 0, 0, false); 1135 } 1136 1137 @Override 1138 public DocumentModelList query(String query, Filter filter, int max) { 1139 return query(query, filter, max, 0, false); 1140 } 1141 1142 @Override 1143 public DocumentModelList query(String query, Filter filter, long limit, long offset, boolean countTotal) { 1144 return query(query, NXQL.NXQL, filter, limit, offset, countTotal); 1145 } 1146 1147 @Override 1148 public DocumentModelList query(String query, String queryType, Filter filter, long limit, long offset, 1149 boolean countTotal) { 1150 long countUpTo = computeCountUpTo(countTotal); 1151 return query(query, queryType, filter, limit, offset, countUpTo); 1152 } 1153 1154 /** 1155 * @return the appropriate countUpTo value depending on input {@code countTotal} and configuration. 1156 */ 1157 protected long computeCountUpTo(boolean countTotal) { 1158 long countUpTo; 1159 if (!countTotal) { 1160 countUpTo = 0; 1161 } else { 1162 if (isLimitedResults()) { 1163 countUpTo = getMaxResults(); 1164 } else { 1165 countUpTo = -1; 1166 } 1167 } 1168 return countUpTo; 1169 } 1170 1171 protected long getMaxResults() { 1172 if (maxResults == null) { 1173 maxResults = Long.valueOf(Framework.getProperty(MAX_RESULTS_PROPERTY, DEFAULT_MAX_RESULTS)); 1174 } 1175 return maxResults; 1176 } 1177 1178 protected boolean isLimitedResults() { 1179 if (limitedResults == null) { 1180 limitedResults = Boolean.valueOf(Framework.getProperty(LIMIT_RESULTS_PROPERTY)); 1181 } 1182 return limitedResults; 1183 } 1184 1185 protected void setMaxResults(long maxResults) { 1186 this.maxResults = maxResults; 1187 } 1188 1189 protected void setLimitedResults(boolean limitedResults) { 1190 this.limitedResults = limitedResults; 1191 } 1192 1193 @Override 1194 public DocumentModelList query(String query, Filter filter, long limit, long offset, long countUpTo) { 1195 return query(query, NXQL.NXQL, filter, limit, offset, countUpTo); 1196 } 1197 1198 @Override 1199 public DocumentModelList query(String query, String queryType, Filter filter, long limit, long offset, 1200 long countUpTo) { 1201 Span span = Tracing.getTracer().getCurrentSpan(); 1202 Map<String, AttributeValue> map = new HashMap<>(); 1203 map.put("nxql", AttributeValue.stringAttributeValue(query)); 1204 if (filter != null) { 1205 map.put("filter", AttributeValue.stringAttributeValue(filter.toString())); 1206 } 1207 map.put("limit", AttributeValue.longAttributeValue(limit)); 1208 map.put("offset", AttributeValue.longAttributeValue(offset)); 1209 map.put("countUpTo", AttributeValue.longAttributeValue(countUpTo)); 1210 span.addAnnotation("AbstractSession#query", map); 1211 1212 DocumentModelList ret = tracedQuery(query, queryType, filter, limit, offset, countUpTo); 1213 1214 map.clear(); 1215 map.put("totalSize", AttributeValue.longAttributeValue(ret.totalSize())); 1216 span.addAnnotation("AbstractSession#query.done", map); 1217 return ret; 1218 } 1219 1220 protected DocumentModelList tracedQuery(String query, String queryType, Filter filter, long limit, long offset, 1221 long countUpTo) { 1222 SecurityService securityService = getSecurityService(); 1223 NuxeoPrincipal principal = getPrincipal(); 1224 try { 1225 String permission = BROWSE; 1226 String repoName = getRepositoryName(); 1227 boolean postFilterPolicies = !securityService.arePoliciesExpressibleInQuery(repoName); 1228 boolean postFilterFilter = filter != null && !(filter instanceof FacetFilter); 1229 boolean postFilter = postFilterPolicies || postFilterFilter; 1230 String[] principals = getPrincipalsToCheck(); 1231 String[] permissions = securityService.getPermissionsToCheck(permission); 1232 Collection<Transformer> transformers = getPoliciesQueryTransformers(queryType); 1233 1234 QueryFilter queryFilter = new QueryFilter(principal, principals, permissions, 1235 filter instanceof FacetFilter ? (FacetFilter) filter : null, transformers, postFilter ? 0 : limit, 1236 postFilter ? 0 : offset); 1237 1238 // get document list with total size 1239 PartialList<Document> pl = getSession().query(query, queryType, queryFilter, postFilter ? -1 : countUpTo); 1240 // convert to DocumentModelList 1241 DocumentModelListImpl dms = new DocumentModelListImpl(pl.size()); 1242 dms.setTotalSize(pl.totalSize()); 1243 for (Document doc : pl) { 1244 dms.add(readModel(doc)); 1245 } 1246 1247 if (!postFilter) { 1248 // the backend has done all the needed filtering 1249 return dms; 1250 } 1251 1252 // post-filter the results "by hand", the backend couldn't do it 1253 long start = limit == 0 || offset < 0 ? 0 : offset; 1254 long stop = start + (limit == 0 ? dms.size() : limit); 1255 int n = 0; 1256 DocumentModelListImpl docs = new DocumentModelListImpl(); 1257 for (DocumentModel model : dms) { 1258 if (postFilterPolicies) { 1259 if (!hasPermission(model.getRef(), permission)) { 1260 continue; 1261 } 1262 } 1263 if (postFilterFilter) { 1264 if (!filter.accept(model)) { 1265 continue; 1266 } 1267 } 1268 if (n < start) { 1269 n++; 1270 continue; 1271 } 1272 if (n >= stop) { 1273 if (countUpTo == 0) { 1274 // can break early 1275 break; 1276 } 1277 n++; 1278 continue; 1279 } 1280 n++; 1281 docs.add(model); 1282 } 1283 if (countUpTo != 0) { 1284 docs.setTotalSize(n); 1285 } 1286 return docs; 1287 } catch (QueryParseException e) { 1288 e.addInfo("Failed to execute query: " + query); 1289 throw e; 1290 } 1291 } 1292 1293 @Override 1294 public IterableQueryResult queryAndFetch(String query, String queryType, Object... params) { 1295 return queryAndFetch(query, queryType, false, params); 1296 } 1297 1298 @Override 1299 public IterableQueryResult queryAndFetch(String query, String queryType, boolean distinctDocuments, 1300 Object... params) { 1301 Span span = Tracing.getTracer().getCurrentSpan(); 1302 Map<String, AttributeValue> map = new HashMap<>(); 1303 map.put("nxql", AttributeValue.stringAttributeValue(query)); 1304 map.put("distinct", AttributeValue.booleanAttributeValue(distinctDocuments)); 1305 span.addAnnotation("AbstractSession#queryAndFetch", map); 1306 try { 1307 SecurityService securityService = getSecurityService(); 1308 NuxeoPrincipal principal = getPrincipal(); 1309 String[] principals = getPrincipalsToCheck(); 1310 String[] permissions = securityService.getPermissionsToCheck(BROWSE); 1311 Collection<Transformer> transformers = getPoliciesQueryTransformers(queryType); 1312 1313 QueryFilter queryFilter = new QueryFilter(principal, principals, permissions, null, transformers, 0, 0); 1314 IterableQueryResult result = getSession().queryAndFetch(query, queryType, queryFilter, distinctDocuments, 1315 params); 1316 1317 map.clear(); 1318 map.put("totalSize", AttributeValue.longAttributeValue(result.size())); 1319 span.addAnnotation("AbstractSession#queryAndFetch.done", map); 1320 return result; 1321 } catch (QueryParseException e) { 1322 e.addInfo("Failed to execute query: " + queryType + ": " + query); 1323 throw e; 1324 } 1325 } 1326 1327 @Override 1328 public PartialList<Map<String, Serializable>> queryProjection(String query, long limit, long offset) { 1329 return queryProjection(query, limit, offset, false); 1330 } 1331 1332 @Override 1333 public PartialList<Map<String, Serializable>> queryProjection(String query, long limit, long offset, 1334 boolean countTotal) { 1335 long countUpTo = computeCountUpTo(countTotal); 1336 return queryProjection(query, NXQL.NXQL, false, limit, offset, countUpTo); 1337 } 1338 1339 @Override 1340 public PartialList<Map<String, Serializable>> queryProjection(String query, String queryType, 1341 boolean distinctDocuments, long limit, long offset, long countUpTo, Object... params) { 1342 NuxeoPrincipal principal = getPrincipal(); 1343 String[] principals = getPrincipalsToCheck(); 1344 String[] permissions = getPermissionsToCheck(BROWSE); 1345 Collection<Transformer> transformers = getPoliciesQueryTransformers(queryType); 1346 1347 QueryFilter queryFilter = new QueryFilter(principal, principals, permissions, null, transformers, limit, 1348 offset); 1349 return getSession().queryProjection(query, queryType, queryFilter, distinctDocuments, countUpTo, params); 1350 } 1351 1352 protected String[] getPrincipalsToCheck() { 1353 NuxeoPrincipal principal = getPrincipal(); 1354 String[] principals; 1355 if (isAdministrator()) { 1356 principals = null; // means: no security check needed 1357 } else { 1358 principals = SecurityService.getPrincipalsToCheck(principal); 1359 } 1360 return principals; 1361 } 1362 1363 protected Collection<Transformer> getPoliciesQueryTransformers(String queryType) { 1364 Collection<Transformer> transformers; 1365 if (NXQL.NXQL.equals(queryType)) { 1366 String repoName = getRepositoryName(); 1367 transformers = getSecurityService().getPoliciesQueryTransformers(repoName); 1368 } else { 1369 transformers = Collections.emptyList(); 1370 } 1371 return transformers; 1372 } 1373 1374 @Override 1375 public ScrollResult<String> scroll(String query, int batchSize, int keepAliveSeconds) { 1376 Map<String, AttributeValue> map = new HashMap<>(); 1377 Span span = Tracing.getTracer().getCurrentSpan(); 1378 map.put("nxql", AttributeValue.stringAttributeValue(query)); 1379 map.put("batchSize", AttributeValue.longAttributeValue(batchSize)); 1380 map.put("keepAliveSeconds", AttributeValue.longAttributeValue(keepAliveSeconds)); 1381 if (isAdministrator()) { 1382 span.addAnnotation("AbstractSession#scroll", map); 1383 return getSession().scroll(query, batchSize, keepAliveSeconds); 1384 } 1385 SecurityService securityService = getSecurityService(); 1386 NuxeoPrincipal principal = getPrincipal(); 1387 String[] principals = getPrincipalsToCheck(); 1388 String permission = BROWSE; 1389 String[] permissions = securityService.getPermissionsToCheck(permission); 1390 Collection<Transformer> transformers = getPoliciesQueryTransformers(NXQL.NXQL); 1391 QueryFilter queryFilter = new QueryFilter(principal, principals, permissions, null, transformers, 0, 0); 1392 1393 map.put("queryFilter", AttributeValue.stringAttributeValue(queryFilter.toString())); 1394 span.addAnnotation("AbstractSession#scroll", map); 1395 return getSession().scroll(query, queryFilter, batchSize, keepAliveSeconds); 1396 } 1397 1398 @Override 1399 public ScrollResult<String> scroll(String scrollId) { 1400 Span span = Tracing.getTracer().getCurrentSpan(); 1401 span.addAnnotation("AbstractSession#scroll.next"); 1402 return getSession().scroll(scrollId); 1403 } 1404 1405 @Override 1406 public void removeChildren(DocumentRef docRef) { 1407 // TODO: check req permissions with td 1408 Document doc = resolveReference(docRef); 1409 checkPermission(doc, REMOVE_CHILDREN); 1410 List<Document> children = doc.getChildren(); 1411 // remove proxies first, otherwise they could become dangling 1412 for (Document child : children) { 1413 if (child.isProxy()) { 1414 if (hasPermission(child, REMOVE)) { 1415 removeNotifyOneDoc(child); 1416 } 1417 } 1418 } 1419 // then remove regular docs or versions, both of which could be proxies targets 1420 for (Document child : children) { 1421 if (!child.isProxy()) { 1422 if (hasPermission(child, REMOVE)) { 1423 removeNotifyOneDoc(child); 1424 } 1425 } 1426 } 1427 } 1428 1429 @Override 1430 public boolean canRemoveDocument(DocumentRef docRef) { 1431 Document doc = resolveReference(docRef); 1432 return canRemoveDocument(doc) == null; 1433 } 1434 1435 /** 1436 * Checks if a document can be removed, and returns a failure reason if not. 1437 */ 1438 protected String canRemoveDocument(Document doc) { 1439 // TODO must also check for proxies on live docs (NXP-22312) 1440 if (doc.isVersion()) { 1441 // TODO a hasProxies method would be more efficient 1442 Collection<Document> proxies = getSession().getProxies(doc, null); 1443 if (!proxies.isEmpty()) { 1444 return "Proxy " + proxies.iterator().next().getUUID() + " targets version " + doc.getUUID(); 1445 } 1446 // find a working document to check security 1447 Document working = doc.getSourceDocument(); 1448 if (working != null) { 1449 Document baseVersion = working.getBaseVersion(); 1450 if (baseVersion != null && !baseVersion.isCheckedOut() && baseVersion.getUUID().equals(doc.getUUID())) { 1451 return "Working copy " + working.getUUID() + " is checked in with base version " + doc.getUUID(); 1452 } 1453 return hasPermission(working, WRITE_VERSION) ? null 1454 : "Missing permission '" + WRITE_VERSION + "' on working copy " + working.getUUID(); 1455 } else { 1456 // no working document, only admins can remove 1457 return isAdministrator() ? null : "No working copy and not an Administrator"; 1458 } 1459 } else { 1460 if (isAdministrator()) { 1461 return null; // ok 1462 } 1463 if (!hasPermission(doc, REMOVE)) { 1464 return "Missing permission '" + REMOVE + "' on document " + doc.getUUID(); 1465 } 1466 Document parent = doc.getParent(); 1467 if (parent == null) { 1468 return null; // ok 1469 } 1470 return hasPermission(parent, REMOVE_CHILDREN) ? null 1471 : "Missing permission '" + REMOVE_CHILDREN + "' on parent document " + parent.getUUID(); 1472 } 1473 } 1474 1475 @Override 1476 public void removeDocument(DocumentRef docRef) { 1477 Document doc = resolveReference(docRef); 1478 removeDocument(doc); 1479 } 1480 1481 protected void removeDocument(Document doc) { 1482 try { 1483 String reason = canRemoveDocument(doc); 1484 if (reason != null) { 1485 throw new DocumentSecurityException( 1486 "Permission denied: cannot remove document " + doc.getUUID() + ", " + reason); 1487 } 1488 removeNotifyOneDoc(doc); 1489 1490 } catch (ConcurrentUpdateException e) { 1491 e.addInfo("Failed to remove document " + doc.getUUID()); 1492 throw e; 1493 } 1494 deleteDocumentCountInc(); 1495 } 1496 1497 protected void removeNotifyOneDoc(Document doc) { 1498 // XXX notify with options if needed 1499 DocumentModel docModel = readModel(doc); 1500 Map<String, Serializable> options = new HashMap<>(); 1501 options.put("docTitle", docModel.getTitle()); 1502 String versionLabel = ""; 1503 Document sourceDoc = null; 1504 // notify different events depending on wether the document is a 1505 // version or not 1506 if (!doc.isVersion()) { 1507 notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE, docModel, options, null, null, true, true); 1508 CoreService coreService = Framework.getService(CoreService.class); 1509 coreService.getVersionRemovalPolicy().removeVersions(getSession(), doc, this); 1510 } else { 1511 versionLabel = docModel.getVersionLabel(); 1512 sourceDoc = doc.getSourceDocument(); 1513 notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE_VERSION, docModel, options, null, null, true, true); 1514 1515 } 1516 doc.remove(getPrincipal()); 1517 if (doc.isVersion()) { 1518 if (sourceDoc != null) { 1519 DocumentModel sourceDocModel = readModel(sourceDoc); 1520 if (sourceDocModel != null) { 1521 options.put("comment", versionLabel); // to be used by 1522 // audit service 1523 notifyEvent(DocumentEventTypes.VERSION_REMOVED, sourceDocModel, options, null, null, false, false); 1524 options.remove("comment"); 1525 } 1526 options.put("docSource", sourceDoc.getUUID()); 1527 } 1528 } 1529 notifyEvent(DocumentEventTypes.DOCUMENT_REMOVED, docModel, options, null, null, false, false); 1530 } 1531 1532 /** 1533 * Implementation uses the fact that the lexicographic ordering of paths is a refinement of the "contains" partial 1534 * ordering. 1535 */ 1536 @Override 1537 public void removeDocuments(DocumentRef[] docRefs) { 1538 Document[] docs = new Document[docRefs.length]; 1539 1540 for (int i = 0; i < docs.length; i++) { 1541 docs[i] = resolveReference(docRefs[i]); 1542 } 1543 // TODO OPTIM: it's not guaranteed that getPath is cheap and 1544 // we call it a lot. Should use an object for pairs (document, path) 1545 // to call it just once per doc. 1546 Arrays.sort(docs, pathComparator); // nulls first 1547 String[] paths = new String[docs.length]; 1548 for (int i = 0; i < docs.length; i++) { 1549 paths[i] = docs[i].getPath(); 1550 } 1551 String lastRemovedWithSlash = "\u0000"; 1552 for (int i = 0; i < docs.length; i++) { 1553 String path = paths[i]; 1554 if (i == 0 || path == null || !path.startsWith(lastRemovedWithSlash)) { 1555 removeDocument(docs[i]); 1556 if (path != null) { 1557 lastRemovedWithSlash = path + "/"; 1558 } 1559 } 1560 } 1561 } 1562 1563 @Override 1564 public void save() { 1565 Span span = Tracing.getTracer().getCurrentSpan(); 1566 span.addAnnotation("AbstractSession#save"); 1567 try { 1568 final Map<String, Serializable> options = new HashMap<>(); 1569 getSession().save(); 1570 notifyEvent(DocumentEventTypes.SESSION_SAVED, null, options, null, null, true, false); 1571 } catch (ConcurrentUpdateException e) { 1572 e.addInfo("Failed to save session"); 1573 throw e; 1574 } finally { 1575 span.addAnnotation("AbstractSession#save.done"); 1576 } 1577 } 1578 1579 @Override 1580 public DocumentModel saveDocument(DocumentModel docModel) { 1581 if (docModel.getRef() == null) { 1582 throw new IllegalArgumentException(String.format( 1583 "cannot save document '%s' with null reference: " + "document has probably not yet been created " 1584 + "in the repository with " + "'CoreSession.createDocument(docModel)'", 1585 docModel.getTitle())); 1586 } 1587 1588 Document doc = resolveReference(docModel.getRef()); 1589 checkPermission(doc, WRITE_PROPERTIES); 1590 1591 Map<String, Serializable> options = getContextMapEventInfo(docModel); 1592 1593 boolean dirty = docModel.isDirty(); 1594 1595 if (dirty) { 1596 // remove disallowed characters 1597 CharacterFilteringService charFilteringService = Framework.getService(CharacterFilteringService.class); 1598 charFilteringService.filter(docModel); 1599 } 1600 1601 DocumentModel previousDocModel = readModel(doc); 1602 // load previous data for versioning purpose, we want previous document filled with previous value which could 1603 // not be the case if access to property value is done after the update 1604 Arrays.asList(previousDocModel.getSchemas()).forEach(previousDocModel::getProperties); 1605 options.put(CoreEventConstants.PREVIOUS_DOCUMENT_MODEL, previousDocModel); 1606 // regular event, last chance to modify docModel 1607 options.put(CoreEventConstants.DESTINATION_NAME, docModel.getName()); 1608 options.put(CoreEventConstants.DOCUMENT_DIRTY, dirty); 1609 notifyEvent(DocumentEventTypes.BEFORE_DOC_UPDATE, docModel, options, null, null, true, true); 1610 1611 // recompute the dirty state 1612 dirty = docModel.isDirty(); 1613 options.put(CoreEventConstants.DOCUMENT_DIRTY, dirty); 1614 if (dirty) { 1615 // document validation 1616 if (getValidationService().isActivated(DocumentValidationService.CTX_SAVEDOC, options)) { 1617 DocumentValidationReport report = getValidationService().validate(docModel, true); 1618 if (report.hasError()) { 1619 throw new DocumentValidationException(report); 1620 } 1621 } 1622 } 1623 String name = (String) options.get(CoreEventConstants.DESTINATION_NAME); 1624 // did the event change the name? not applicable to Root whose 1625 // name is null/empty 1626 if (name != null && !name.equals(docModel.getName())) { 1627 doc = getSession().move(doc, doc.getParent(), name); 1628 } 1629 1630 // recompute versioning option as it can be set by listeners 1631 VersioningOption versioningOption = (VersioningOption) docModel.getContextData( 1632 VersioningService.VERSIONING_OPTION); 1633 docModel.putContextData(VersioningService.VERSIONING_OPTION, null); 1634 String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT); 1635 docModel.putContextData(VersioningService.CHECKIN_COMMENT, null); 1636 Boolean disableAutoCheckOut = (Boolean) docModel.getContextData(VersioningService.DISABLE_AUTO_CHECKOUT); 1637 docModel.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, null); 1638 options.put(VersioningService.DISABLE_AUTO_CHECKOUT, disableAutoCheckOut); 1639 1640 boolean manualVersioning = versioningOption != null; 1641 if (!docModel.isImmutable()) { 1642 // compute auto versioning before update - here we create a version of document in order to save previous 1643 // state it's useful if we want to implement rules like create a version if last contributor is not the 1644 // same previous one. So we want to trigger this mechanism if and only if: 1645 // - previous document is checkouted 1646 // - we don't ask for a version without updating the document (manual versioning only) 1647 // no need to fire event, as we use DocumentModel API it's already done 1648 if (previousDocModel.isCheckedOut() && (!manualVersioning || dirty)) { 1649 getVersioningService().doAutomaticVersioning(previousDocModel, docModel, true); 1650 } 1651 // pre-save versioning 1652 boolean checkout = getVersioningService().isPreSaveDoingCheckOut(doc, dirty, versioningOption, options); 1653 if (checkout) { 1654 notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true); 1655 } 1656 versioningOption = getVersioningService().doPreSave(this, doc, dirty, versioningOption, checkinComment, 1657 options); 1658 if (checkout) { 1659 DocumentModel checkedOutDoc = readModel(doc); 1660 notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, checkedOutDoc, options, null, null, true, false); 1661 } 1662 } 1663 1664 boolean allowVersionWrite = Boolean.TRUE.equals(docModel.getContextData(ALLOW_VERSION_WRITE)); 1665 docModel.putContextData(ALLOW_VERSION_WRITE, null); 1666 boolean setReadWrite = allowVersionWrite && doc.isVersion() && doc.isReadOnly(); 1667 1668 // actual save 1669 if (setReadWrite) { 1670 doc.setReadOnly(false); 1671 } 1672 docModel = writeModel(doc, docModel); 1673 if (setReadWrite) { 1674 doc.setReadOnly(true); 1675 } 1676 1677 Document checkedInDoc = null; 1678 if (!docModel.isImmutable()) { 1679 // post-save versioning 1680 boolean checkin = getVersioningService().isPostSaveDoingCheckIn(doc, versioningOption, options); 1681 if (checkin) { 1682 notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true); 1683 } 1684 if (manualVersioning) { 1685 checkedInDoc = getVersioningService().doPostSave(this, doc, versioningOption, checkinComment, options); 1686 } else { 1687 // compute auto versioning - only if it is not deactivated by manual versioning 1688 // no need to fire event, as we use DocumentModel API it's already done 1689 getVersioningService().doAutomaticVersioning(previousDocModel, docModel, false); 1690 } 1691 } 1692 1693 // post-save events 1694 docModel = readModel(doc); 1695 if (checkedInDoc != null) { 1696 DocumentRef checkedInVersionRef = new IdRef(checkedInDoc.getUUID()); 1697 notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment); 1698 } 1699 1700 notifyEvent(DocumentEventTypes.DOCUMENT_UPDATED, docModel, options, null, null, true, false); 1701 updateDocumentCountInc(); 1702 1703 // Notify that proxies have been updated 1704 List<Document> proxies = getSession().getProxies(doc); 1705 if (proxies != null && !proxies.isEmpty()) { 1706 proxies.forEach(proxy -> { 1707 DocumentModel docProxy = readModel(proxy); 1708 if (!docProxy.isImmutable()) { 1709 notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_UPDATED, docProxy, options, null, null, true, false); 1710 } 1711 }); 1712 } 1713 return docModel; 1714 } 1715 1716 @Override 1717 public void saveDocuments(DocumentModel[] docModels) { 1718 // TODO: optimize this - avoid calling at each iteration saveDoc... 1719 for (DocumentModel docModel : docModels) { 1720 saveDocument(docModel); 1721 } 1722 } 1723 1724 @Override 1725 public DocumentModel getSourceDocument(DocumentRef docRef) { 1726 assert null != docRef; 1727 1728 Document doc = resolveReference(docRef); 1729 checkPermission(doc, READ_VERSION); 1730 Document headDocument = doc.getSourceDocument(); 1731 if (headDocument == null) { 1732 throw new DocumentNotFoundException("Source document has been deleted"); 1733 } 1734 return readModel(headDocument); 1735 } 1736 1737 protected VersionModel getVersionModel(Document version) { 1738 VersionModel versionModel = new VersionModelImpl(); 1739 versionModel.setId(version.getUUID()); 1740 versionModel.setCreated(version.getVersionCreationDate()); 1741 versionModel.setDescription(version.getCheckinComment()); 1742 versionModel.setLabel(version.getVersionLabel()); 1743 return versionModel; 1744 } 1745 1746 @Override 1747 public DocumentModel getLastDocumentVersion(DocumentRef docRef) { 1748 Document doc = resolveReference(docRef); 1749 checkPermission(doc, READ_VERSION); 1750 Document version = doc.getLastVersion(); 1751 return version == null ? null : readModel(version); 1752 } 1753 1754 @Override 1755 public DocumentRef getLastDocumentVersionRef(DocumentRef docRef) { 1756 Document doc = resolveReference(docRef); 1757 checkPermission(doc, READ_VERSION); 1758 Document version = doc.getLastVersion(); 1759 return version == null ? null : new IdRef(version.getUUID()); 1760 } 1761 1762 @Override 1763 public List<DocumentRef> getVersionsRefs(DocumentRef docRef) { 1764 Document doc = resolveReference(docRef); 1765 checkPermission(doc, READ_VERSION); 1766 List<String> ids = doc.getVersionsIds(); 1767 List<DocumentRef> refs = new ArrayList<>(ids.size()); 1768 for (String id : ids) { 1769 refs.add(new IdRef(id)); 1770 } 1771 return refs; 1772 } 1773 1774 @Override 1775 public List<DocumentModel> getVersions(DocumentRef docRef) { 1776 Document doc = resolveReference(docRef); 1777 checkPermission(doc, READ_VERSION); 1778 List<Document> docVersions = doc.getVersions(); 1779 List<DocumentModel> versions = new ArrayList<>(docVersions.size()); 1780 for (Document version : docVersions) { 1781 versions.add(readModel(version)); 1782 } 1783 return versions; 1784 } 1785 1786 @Override 1787 public List<VersionModel> getVersionsForDocument(DocumentRef docRef) { 1788 Document doc = resolveReference(docRef); 1789 checkPermission(doc, READ_VERSION); 1790 List<Document> docVersions = doc.getVersions(); 1791 List<VersionModel> versions = new ArrayList<>(docVersions.size()); 1792 for (Document version : docVersions) { 1793 versions.add(getVersionModel(version)); 1794 } 1795 return versions; 1796 1797 } 1798 1799 @Override 1800 public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef) { 1801 Document doc = resolveReference(docRef); 1802 Document ver = resolveReference(versionRef); 1803 return restoreToVersion(doc, ver, false, true); 1804 } 1805 1806 @Override 1807 public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef, boolean skipSnapshotCreation, 1808 boolean skipCheckout) { 1809 Document doc = resolveReference(docRef); 1810 Document ver = resolveReference(versionRef); 1811 return restoreToVersion(doc, ver, skipSnapshotCreation, skipCheckout); 1812 } 1813 1814 protected DocumentModel restoreToVersion(Document doc, Document version, boolean skipSnapshotCreation, 1815 boolean skipCheckout) { 1816 checkPermission(doc, WRITE_VERSION); 1817 if (doc.isRecord()) { 1818 throw new PropertyException("Version cannot be restored on a record: " + doc.getUUID()); 1819 } 1820 1821 DocumentModel docModel = readModel(doc); 1822 1823 Map<String, Serializable> options = new HashMap<>(); 1824 1825 // we're about to overwrite the document, make sure it's archived 1826 if (!skipSnapshotCreation && doc.isCheckedOut()) { 1827 String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT); 1828 docModel.putContextData(VersioningService.CHECKIN_COMMENT, null); 1829 notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true); 1830 Document ver = getVersioningService().doCheckIn(doc, null, checkinComment); 1831 docModel.refresh(DocumentModel.REFRESH_STATE, null); 1832 notifyCheckedInVersion(docModel, new IdRef(ver.getUUID()), null, checkinComment); 1833 } 1834 1835 // FIXME: the fields are hardcoded. should be moved in versioning 1836 // component 1837 // HOW? 1838 final Long majorVer = (Long) doc.getPropertyValue("major_version"); 1839 final Long minorVer = (Long) doc.getPropertyValue("minor_version"); 1840 if (majorVer != null || minorVer != null) { 1841 options.put(VersioningDocument.CURRENT_DOCUMENT_MAJOR_VERSION_KEY, majorVer); 1842 options.put(VersioningDocument.CURRENT_DOCUMENT_MINOR_VERSION_KEY, minorVer); 1843 } 1844 // add the uuid of the version being restored 1845 String versionUUID = version.getUUID(); 1846 options.put(VersioningDocument.RESTORED_VERSION_UUID_KEY, versionUUID); 1847 1848 notifyEvent(DocumentEventTypes.BEFORE_DOC_RESTORE, docModel, options, null, null, true, true); 1849 writeModel(doc, docModel); 1850 1851 doc.restore(version); 1852 // re-read doc model after restoration 1853 docModel = readModel(doc); 1854 notifyEvent(DocumentEventTypes.DOCUMENT_RESTORED, docModel, options, null, docModel.getVersionLabel(), true, 1855 false); 1856 docModel = writeModel(doc, docModel); 1857 1858 if (!skipCheckout) { 1859 // restore gives us a checked in document, so do a checkout 1860 notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true); 1861 getVersioningService().doCheckOut(doc); 1862 docModel = readModel(doc); 1863 notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false); 1864 } 1865 1866 log.debug("Document restored to version:{}", version::getUUID); 1867 return docModel; 1868 } 1869 1870 @Override 1871 public DocumentRef getBaseVersion(DocumentRef docRef) { 1872 Document doc = resolveReference(docRef); 1873 checkPermission(doc, READ); 1874 Document ver = doc.getBaseVersion(); 1875 if (ver == null) { 1876 return null; 1877 } 1878 checkPermission(ver, READ); 1879 return new IdRef(ver.getUUID()); 1880 } 1881 1882 @Override 1883 public DocumentRef checkIn(DocumentRef docRef, VersioningOption option, String checkinComment) { 1884 Document doc = resolveReference(docRef); 1885 checkPermission(doc, WRITE_PROPERTIES); 1886 DocumentModel docModel = readModel(doc); 1887 1888 Map<String, Serializable> options = new HashMap<>(); 1889 notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true); 1890 writeModel(doc, docModel); 1891 1892 Document version = getVersioningService().doCheckIn(doc, option, checkinComment); 1893 1894 docModel = readModel(doc); 1895 DocumentRef checkedInVersionRef = new IdRef(version.getUUID()); 1896 notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment); 1897 writeModel(doc, docModel); 1898 1899 return checkedInVersionRef; 1900 } 1901 1902 /** 1903 * Send a core event for the creation of a new check in version. The source document is the live document model used 1904 * as the source for the checkin, not the archived version it-self. 1905 * 1906 * @param docModel work document that has been checked-in as a version 1907 * @param checkedInVersionRef document ref of the new checked-in version 1908 * @param options initial option map, or null 1909 */ 1910 protected void notifyCheckedInVersion(DocumentModel docModel, DocumentRef checkedInVersionRef, 1911 Map<String, Serializable> options, String checkinComment) { 1912 String label = getVersioningService().getVersionLabel(docModel); 1913 Map<String, Serializable> props = new HashMap<>(); 1914 if (options != null) { 1915 props.putAll(options); 1916 } 1917 props.put("versionLabel", label); 1918 props.put("checkInComment", checkinComment); 1919 props.put("checkedInVersionRef", checkedInVersionRef); 1920 if (checkinComment == null && options != null) { 1921 // check if there's a comment already in options 1922 Object optionsComment = options.get("comment"); 1923 if (optionsComment instanceof String) { 1924 checkinComment = (String) optionsComment; 1925 } 1926 } 1927 String comment = checkinComment == null ? label : label + ' ' + checkinComment; 1928 props.put("comment", comment); // compat, used in audit 1929 // notify checkin on live document 1930 notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDIN, docModel, props, null, null, true, false); 1931 // notify creation on version document 1932 notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, getDocument(checkedInVersionRef), props, null, null, true, 1933 false); 1934 1935 } 1936 1937 @Override 1938 public void checkOut(DocumentRef docRef) { 1939 Document doc = resolveReference(docRef); 1940 // TODO: add a new permission names CHECKOUT and use it instead of 1941 // WRITE_PROPERTIES 1942 checkPermission(doc, WRITE_PROPERTIES); 1943 DocumentModel docModel = readModel(doc); 1944 Map<String, Serializable> options = new HashMap<>(); 1945 1946 notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true); 1947 1948 getVersioningService().doCheckOut(doc); 1949 docModel = readModel(doc); 1950 1951 notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false); 1952 writeModel(doc, docModel); 1953 } 1954 1955 @Override 1956 public boolean isCheckedOut(DocumentRef docRef) { 1957 assert null != docRef; 1958 Document doc = resolveReference(docRef); 1959 checkPermission(doc, BROWSE); 1960 return doc.isCheckedOut(); 1961 } 1962 1963 @Override 1964 public String getVersionSeriesId(DocumentRef docRef) { 1965 Document doc = resolveReference(docRef); 1966 checkPermission(doc, READ); 1967 return doc.getVersionSeriesId(); 1968 } 1969 1970 @Override 1971 public DocumentModel getWorkingCopy(DocumentRef docRef) { 1972 Document doc = resolveReference(docRef); 1973 checkPermission(doc, READ_VERSION); 1974 Document pwc = doc.getWorkingCopy(); 1975 checkPermission(pwc, READ); 1976 return pwc == null ? null : readModel(pwc); 1977 } 1978 1979 @Override 1980 public DocumentModel getVersion(String versionableId, VersionModel versionModel) { 1981 String id = versionModel.getId(); 1982 if (id != null) { 1983 return getDocument(new IdRef(id)); 1984 } 1985 Document doc = getSession().getVersion(versionableId, versionModel); 1986 if (doc == null) { 1987 return null; 1988 } 1989 checkPermission(doc, READ_PROPERTIES); 1990 checkPermission(doc, READ_VERSION); 1991 return readModel(doc); 1992 } 1993 1994 @Override 1995 public String getVersionLabel(DocumentModel docModel) { 1996 return getVersioningService().getVersionLabel(docModel); 1997 } 1998 1999 @Override 2000 public DocumentModel getDocumentWithVersion(DocumentRef docRef, VersionModel version) { 2001 String id = version.getId(); 2002 if (id != null) { 2003 return getDocument(new IdRef(id)); 2004 } 2005 Document doc = resolveReference(docRef); 2006 checkPermission(doc, READ_PROPERTIES); 2007 checkPermission(doc, READ_VERSION); 2008 String docPath = doc.getPath(); 2009 doc = doc.getVersion(version.getLabel()); 2010 if (doc == null) { 2011 // SQL Storage uses to return null if version not found 2012 log.debug("Version {} does not exist for {}", version.getLabel(), docPath); 2013 return null; 2014 } 2015 log.debug("Retrieved the version {} of the document {}", version.getLabel(), docPath); 2016 return readModel(doc); 2017 } 2018 2019 @Override 2020 public DocumentModel createProxy(DocumentRef docRef, DocumentRef folderRef) { 2021 Document doc = resolveReference(docRef); 2022 Document fold = resolveReference(folderRef); 2023 checkPermission(doc, READ); 2024 checkPermission(fold, ADD_CHILDREN); 2025 return createProxyInternal(doc, fold, new HashMap<>()); 2026 } 2027 2028 protected DocumentModel createProxyInternal(Document doc, Document folder, Map<String, Serializable> options) { 2029 // create the new proxy 2030 Document proxy = getSession().createProxy(doc, folder); 2031 DocumentModel proxyModel = readModel(proxy); 2032 2033 notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, proxyModel, options, null, null, true, false); 2034 notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxyModel, options, null, null, true, false); 2035 DocumentModel folderModel = readModel(folder); 2036 notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, folderModel, options, null, null, true, false); 2037 return proxyModel; 2038 } 2039 2040 /** 2041 * Remove proxies for the same base document in the folder. doc may be a normal document or a proxy. 2042 */ 2043 protected List<String> removeExistingProxies(Document doc, Document folder) { 2044 Collection<Document> otherProxies = getSession().getProxies(doc, folder); 2045 List<String> removedProxyIds = new ArrayList<>(otherProxies.size()); 2046 for (Document otherProxy : otherProxies) { 2047 removedProxyIds.add(otherProxy.getUUID()); 2048 removeNotifyOneDoc(otherProxy); 2049 } 2050 return removedProxyIds; 2051 } 2052 2053 /** 2054 * Update the proxy for doc in the given section to point to the new target. Do nothing if there are several 2055 * proxies. 2056 * 2057 * @return the proxy if it was updated, or {@code null} if none or several were found 2058 */ 2059 protected DocumentModel updateExistingProxies(Document doc, Document folder, Document target) { 2060 Collection<Document> proxies = getSession().getProxies(doc, folder); 2061 try { 2062 if (proxies.size() == 1) { 2063 Document proxy = proxies.iterator().next(); 2064 proxy.setTargetDocument(target); 2065 return readModel(proxy); 2066 } 2067 } catch (UnsupportedOperationException e) { 2068 log.error("Cannot update proxy, try to remove"); 2069 } 2070 return null; 2071 } 2072 2073 @Override 2074 public DocumentModelList getProxies(DocumentRef docRef, DocumentRef folderRef) { 2075 Document folder = null; 2076 if (folderRef != null) { 2077 folder = resolveReference(folderRef); 2078 checkPermission(folder, READ_CHILDREN); 2079 } 2080 Document doc = resolveReference(docRef); 2081 Collection<Document> children = getSession().getProxies(doc, folder); 2082 DocumentModelList docs = new DocumentModelListImpl(); 2083 for (Document child : children) { 2084 if (hasPermission(child, READ)) { 2085 docs.add(readModel(child)); 2086 } 2087 } 2088 return docs; 2089 } 2090 2091 @Override 2092 public List<String> getAvailableSecurityPermissions() { 2093 // XXX: add security check? 2094 return Arrays.asList(getSecurityService().getPermissionProvider().getPermissions()); 2095 } 2096 2097 @Override 2098 public DataModel getDataModel(DocumentRef docRef, Schema schema) { 2099 Document doc = resolveReference(docRef); 2100 checkPermission(doc, READ); 2101 return DocumentModelFactory.createDataModel(doc, schema); 2102 } 2103 2104 protected Object getDataModelField(DocumentRef docRef, String schema, String field) { 2105 Document doc = resolveReference(docRef); 2106 if (doc != null) { 2107 checkPermission(doc, READ); 2108 Schema docSchema = doc.getType().getSchema(schema); 2109 if (docSchema != null) { 2110 String prefix = docSchema.getNamespace().prefix; 2111 if (prefix != null && prefix.length() > 0) { 2112 field = prefix + ':' + field; 2113 } 2114 return doc.getPropertyValue(field); 2115 } else { 2116 log.warn("Cannot find schema with name={}", schema); 2117 } 2118 } else { 2119 log.warn("Cannot resolve docRef={}", docRef); 2120 } 2121 return null; 2122 } 2123 2124 @Override 2125 public void makeRecord(DocumentRef docRef) { 2126 Document doc = resolveReference(docRef); 2127 checkPermission(doc, READ); 2128 if (doc.isRecord()) { 2129 // already a record, don't do anything 2130 return; 2131 } 2132 if (doc.isVersion()) { 2133 throw new PropertyException("Version cannot be made record: " + doc.getUUID()); 2134 } 2135 checkPermission(doc, MAKE_RECORD); 2136 DocumentModel docModel = readModel(doc); 2137 notifyEvent(DocumentEventTypes.BEFORE_MAKE_RECORD, docModel, null, null, null, true, false); 2138 doc.makeRecord(); 2139 docModel = readModel(doc); 2140 notifyEvent(DocumentEventTypes.AFTER_MAKE_RECORD, docModel, null, null, null, true, false); 2141 } 2142 2143 @Override 2144 public boolean isRecord(DocumentRef docRef) { 2145 Document doc = resolveReference(docRef); 2146 checkPermission(doc, READ); 2147 return doc.isRecord(); 2148 } 2149 2150 @Override 2151 public void setRetainUntil(DocumentRef docRef, Calendar retainUntil, String comment) throws PropertyException { 2152 Document doc = resolveReference(docRef); 2153 checkPermission(doc, READ); 2154 if (!doc.isRecord()) { 2155 throw new PropertyException("Document is not a record"); 2156 } 2157 Calendar current = doc.getRetainUntil(); 2158 if (current!=null && retainUntil!=null && current.compareTo(retainUntil) == 0) { 2159 // unchanged, don't do anything 2160 return; 2161 } 2162 checkPermission(doc, SET_RETENTION); 2163 Map<String, Serializable> options = new HashMap<>(); 2164 options.put(CoreEventConstants.RETAIN_UNTIL, retainUntil); 2165 boolean retainUntilIndeterminate = retainUntil == null 2166 || RETAIN_UNTIL_INDETERMINATE.compareTo(retainUntil) == 0; 2167 options.put("comment", retainUntilIndeterminate ? "" : retainUntil.toInstant().toString()); 2168 DocumentModel docModel = readModel(doc); 2169 boolean currentIndeterminate = current == null || RETAIN_UNTIL_INDETERMINATE.compareTo(current) == 0; 2170 String beforeRetentionEvent = currentIndeterminate ? DocumentEventTypes.BEFORE_SET_RETENTION 2171 : DocumentEventTypes.BEFORE_EXTEND_RETENTION; 2172 notifyEvent(beforeRetentionEvent, docModel, options, null, null, true, false); 2173 doc.setRetainUntil(retainUntil); 2174 docModel = readModel(doc); 2175 String afterRetentionEvent = currentIndeterminate ? DocumentEventTypes.AFTER_SET_RETENTION 2176 : DocumentEventTypes.AFTER_EXTEND_RETENTION; 2177 notifyEvent(afterRetentionEvent, docModel, options, null, null, true, false); 2178 } 2179 2180 @Override 2181 public Calendar getRetainUntil(DocumentRef docRef) { 2182 Document doc = resolveReference(docRef); 2183 checkPermission(doc, READ); 2184 return doc.getRetainUntil(); 2185 } 2186 2187 @Override 2188 public void setLegalHold(DocumentRef docRef, boolean hold, String comment) { 2189 Document doc = resolveReference(docRef); 2190 if (!isRecord(docRef)) { 2191 throw new PropertyException("Document is not a record"); 2192 } 2193 if (hasLegalHold(docRef) == hold) { 2194 // unchanged, don't do anything 2195 return; 2196 } 2197 checkPermission(doc, MANAGE_LEGAL_HOLD); 2198 DocumentModel docModel = readModel(doc); 2199 Map<String, Serializable> options = new HashMap<>(); 2200 options.put("comment", StringUtils.defaultString(comment)); 2201 String beforeLegalHoldEvent = hold ? DocumentEventTypes.BEFORE_SET_LEGAL_HOLD : DocumentEventTypes.BEFORE_REMOVE_LEGAL_HOLD; 2202 notifyEvent(beforeLegalHoldEvent, docModel, options, null, null, true, false); 2203 doc.setLegalHold(hold); 2204 docModel = readModel(doc); 2205 String afterLegalHoldEvent = hold ? DocumentEventTypes.AFTER_SET_LEGAL_HOLD : DocumentEventTypes.AFTER_REMOVE_LEGAL_HOLD; 2206 notifyEvent(afterLegalHoldEvent, docModel, options, null, null, true, false); 2207 } 2208 2209 @Override 2210 public boolean hasLegalHold(DocumentRef docRef) { 2211 Document doc = resolveReference(docRef); 2212 checkPermission(doc, READ); 2213 return doc.hasLegalHold(); 2214 } 2215 2216 @Override 2217 public boolean isUnderRetentionOrLegalHold(DocumentRef docRef) { 2218 Document doc = resolveReference(docRef); 2219 checkPermission(doc, READ); 2220 return doc.isUnderRetentionOrLegalHold(); 2221 } 2222 2223 @Override 2224 public boolean isRetentionActive(DocumentRef docRef) { 2225 Document doc = resolveReference(docRef); 2226 checkPermission(doc, READ); 2227 return doc.isRetentionActive(); 2228 } 2229 2230 @Override 2231 public void setRetentionActive(DocumentRef docRef, boolean retentionActive) { 2232 Document doc = resolveReference(docRef); 2233 if (isRetentionActive(docRef) == retentionActive) { 2234 // if unchanged don't do anything 2235 return; 2236 } 2237 // require WriteSecurity to unset retention active 2238 checkPermission(doc, retentionActive ? WRITE : WRITE_SECURITY); 2239 doc.setRetentionActive(retentionActive); 2240 // send an event for audit logging 2241 Map<String, Serializable> options = new HashMap<>(); 2242 options.put(CoreEventConstants.RETENTION_ACTIVE, Boolean.valueOf(retentionActive)); 2243 options.put("comment", Boolean.toString(retentionActive)); // for audit 2244 DocumentModel docModel = readModel(doc); 2245 notifyEvent(DocumentEventTypes.RETENTION_ACTIVE_CHANGED, docModel, options, null, null, true, false); 2246 } 2247 2248 @Override 2249 public boolean isTrashed(DocumentRef docRef) { 2250 return Framework.getService(TrashService.class).isTrashed(this, docRef); 2251 } 2252 2253 @Override 2254 public String getCurrentLifeCycleState(DocumentRef docRef) { 2255 Document doc = resolveReference(docRef); 2256 checkPermission(doc, READ_LIFE_CYCLE); 2257 return doc.getLifeCycleState(); 2258 } 2259 2260 @Override 2261 public String getLifeCyclePolicy(DocumentRef docRef) { 2262 Document doc = resolveReference(docRef); 2263 checkPermission(doc, READ_LIFE_CYCLE); 2264 return doc.getLifeCyclePolicy(); 2265 } 2266 2267 /** 2268 * Make a document follow a transition. 2269 * 2270 * @param docRef a {@link DocumentRef} 2271 * @param transition the transition to follow 2272 * @param options an option map than can be used by callers to pass additional params 2273 * @since 5.9.3 2274 */ 2275 private boolean followTransition(DocumentRef docRef, String transition, Map<String, Serializable> options) 2276 throws LifeCycleException { 2277 Document doc = resolveReference(docRef); 2278 checkPermission(doc, WRITE_LIFE_CYCLE); 2279 2280 // backward compat - used to forward deprecated call followTransition("deleted") to trash service 2281 TrashService trashService = Framework.getService(TrashService.class); 2282 boolean deleteTransitions = LifeCycleConstants.DELETE_TRANSITION.equals(transition) 2283 || LifeCycleConstants.UNDELETE_TRANSITION.equals(transition); 2284 if (deleteTransitions && !trashService.hasFeature(TRASHED_STATE_IS_DEDUCED_FROM_LIFECYCLE) 2285 && !Boolean.TRUE.equals(options.get(FROM_LIFE_CYCLE_TRASH_SERVICE))) { 2286 String message = "Following the transition '" + transition + "' is deprecated. Please use TrashService."; 2287 if (log.isTraceEnabled()) { 2288 log.warn(message, new Throwable("stack trace")); 2289 } else { 2290 log.warn(message); 2291 } 2292 DocumentModel docModel = readModel(doc); 2293 if (LifeCycleConstants.DELETE_TRANSITION.equals(transition)) { 2294 trashService.trashDocument(docModel); 2295 } else { 2296 trashService.untrashDocument(docModel); 2297 } 2298 return true; 2299 } 2300 2301 if (!doc.isVersion() && !doc.isProxy() && !doc.isCheckedOut()) { 2302 if (!deleteTransitions || Framework.getService(ConfigurationService.class) 2303 .isBooleanFalse(TRASH_KEEP_CHECKED_IN_PROPERTY)) { 2304 checkOut(docRef); 2305 doc = resolveReference(docRef); 2306 } 2307 } 2308 String formerStateName = doc.getLifeCycleState(); 2309 doc.followTransition(transition); 2310 2311 // Construct a map holding meta information about the event. 2312 Map<String, Serializable> eventOptions = new HashMap<>(options); 2313 eventOptions.put(LifeCycleConstants.TRANSTION_EVENT_OPTION_FROM, formerStateName); 2314 eventOptions.put(LifeCycleConstants.TRANSTION_EVENT_OPTION_TO, doc.getLifeCycleState()); 2315 eventOptions.put(LifeCycleConstants.TRANSTION_EVENT_OPTION_TRANSITION, transition); 2316 String comment = (String) options.get("comment"); 2317 DocumentModel docModel = readModel(doc); 2318 notifyEvent(LifeCycleConstants.TRANSITION_EVENT, docModel, eventOptions, 2319 DocumentEventCategories.EVENT_LIFE_CYCLE_CATEGORY, comment, true, false); 2320 if (!docModel.isImmutable()) { 2321 writeModel(doc, docModel); 2322 } 2323 return true; // throws if error 2324 } 2325 2326 @Override 2327 public boolean followTransition(DocumentModel docModel, String transition) throws LifeCycleException { 2328 return followTransition(docModel.getRef(), transition, docModel.getContextData()); 2329 } 2330 2331 @Override 2332 public boolean followTransition(DocumentRef docRef, String transition) throws LifeCycleException { 2333 return followTransition(docRef, transition, Collections.emptyMap()); 2334 } 2335 2336 @Override 2337 public Collection<String> getAllowedStateTransitions(DocumentRef docRef) { 2338 Document doc = resolveReference(docRef); 2339 checkPermission(doc, READ_LIFE_CYCLE); 2340 return doc.getAllowedStateTransitions(); 2341 } 2342 2343 @Override 2344 public void reinitLifeCycleState(DocumentRef docRef) { 2345 Document doc = resolveReference(docRef); 2346 checkPermission(doc, WRITE_LIFE_CYCLE); 2347 getLifeCycleService().reinitLifeCycle(doc); 2348 } 2349 2350 @Override 2351 public Object[] getDataModelsField(DocumentRef[] docRefs, String schema, String field) { 2352 2353 assert docRefs != null; 2354 assert schema != null; 2355 assert field != null; 2356 2357 final Object[] values = new Object[docRefs.length]; 2358 int i = 0; 2359 for (DocumentRef docRef : docRefs) { 2360 final Object value = getDataModelField(docRef, schema, field); 2361 values[i++] = value; 2362 } 2363 2364 return values; 2365 } 2366 2367 @Override 2368 public DocumentRef[] getParentDocumentRefs(DocumentRef docRef) { 2369 final List<DocumentRef> docRefs = new ArrayList<>(); 2370 Document parentDoc = resolveParentReference(docRef); 2371 while (parentDoc != null) { 2372 final DocumentRef parentDocRef = new IdRef(parentDoc.getUUID()); 2373 docRefs.add(parentDocRef); 2374 parentDoc = parentDoc.getParent(); 2375 } 2376 DocumentRef[] refs = new DocumentRef[docRefs.size()]; 2377 return docRefs.toArray(refs); 2378 } 2379 2380 @Override 2381 public Object[] getDataModelsFieldUp(DocumentRef docRef, String schema, String field) { 2382 2383 final DocumentRef[] parentRefs = getParentDocumentRefs(docRef); 2384 final DocumentRef[] allRefs = new DocumentRef[parentRefs.length + 1]; 2385 allRefs[0] = docRef; 2386 System.arraycopy(parentRefs, 0, allRefs, 1, parentRefs.length); 2387 2388 return getDataModelsField(allRefs, schema, field); 2389 } 2390 2391 @Override 2392 public Lock setLock(DocumentRef docRef) throws LockException { 2393 Document doc = resolveReference(docRef); 2394 // TODO: add a new permission named LOCK and use it instead of 2395 // WRITE_PROPERTIES 2396 2397 // ignore the lock policy that would always deny a write on a locked document, because we want to have a 2398 // specific LockException when the document is already locked by another user 2399 try { 2400 LockSecurityPolicy.setIgnorePolicy(true); 2401 checkPermission(doc, WRITE_PROPERTIES); 2402 } finally { 2403 LockSecurityPolicy.setIgnorePolicy(false); 2404 } 2405 Lock lock = new Lock(getPrincipal().getName(), new GregorianCalendar()); 2406 Lock oldLock = doc.setLock(lock); 2407 if (oldLock == null) { 2408 // not previously locked 2409 DocumentModel docModel = readModel(doc); 2410 Map<String, Serializable> options = new HashMap<>(); 2411 options.put("lock", lock); 2412 notifyEvent(DocumentEventTypes.DOCUMENT_LOCKED, docModel, options, null, null, true, false); 2413 return lock; 2414 } 2415 if (getPrincipal().getName().equals(oldLock.getOwner())) { 2416 return oldLock; 2417 } 2418 throw new LockException("Document already locked by " + oldLock.getOwner() + ": " + docRef, SC_CONFLICT); 2419 } 2420 2421 @Override 2422 public Lock getLockInfo(DocumentRef docRef) { 2423 Document doc = resolveReference(docRef); 2424 checkPermission(doc, READ); 2425 return doc.getLock(); 2426 } 2427 2428 @Override 2429 public Lock removeLock(DocumentRef docRef) throws LockException { 2430 Document doc = resolveReference(docRef); 2431 String owner; 2432 if (hasPermission(docRef, UNLOCK)) { 2433 // always unlock 2434 owner = null; 2435 } else { 2436 owner = getPrincipal().getName(); 2437 } 2438 Lock lock = doc.removeLock(owner); 2439 if (lock == null) { 2440 // there was no lock, we're done 2441 return null; 2442 } 2443 if (lock.getFailed()) { 2444 // lock removal failed due to owner check 2445 throw new LockException("Document already locked by " + lock.getOwner() + ": " + docRef, SC_CONFLICT); 2446 } 2447 DocumentModel docModel = readModel(doc); 2448 Map<String, Serializable> options = new HashMap<>(); 2449 options.put("lock", lock); 2450 notifyEvent(DocumentEventTypes.DOCUMENT_UNLOCKED, docModel, options, null, null, true, false); 2451 return lock; 2452 } 2453 2454 protected boolean isAdministrator() { 2455 NuxeoPrincipal principal = getPrincipal(); 2456 // FIXME: this is inconsistent with NuxeoPrincipal#isAdministrator 2457 // method because it allows hardcoded Administrator user 2458 if (Framework.isTestModeSet()) { 2459 if (SecurityConstants.ADMINISTRATOR.equals(principal.getName())) { 2460 return true; 2461 } 2462 } 2463 return principal.isAdministrator(); 2464 } 2465 2466 @Override 2467 public void applyDefaultPermissions(String userOrGroupName) { 2468 if (userOrGroupName == null) { 2469 throw new NullPointerException("null userOrGroupName"); 2470 } 2471 if (!isAdministrator()) { 2472 throw new DocumentSecurityException("You need to be an Administrator to do this."); 2473 } 2474 DocumentModel rootDocument = getRootDocument(); 2475 ACP acp = new ACPImpl(); 2476 2477 UserEntry userEntry = new UserEntryImpl(userOrGroupName); 2478 userEntry.addPrivilege(READ); 2479 2480 acp.setRules(new UserEntry[] { userEntry }); 2481 2482 setACP(rootDocument.getRef(), acp, false); 2483 } 2484 2485 @Override 2486 public DocumentModel publishDocument(DocumentModel docToPublish, DocumentModel section) { 2487 return publishDocument(docToPublish, section, true); 2488 } 2489 2490 @Override 2491 public DocumentModel publishDocument(DocumentModel docModel, DocumentModel section, 2492 boolean overwriteExistingProxy) { 2493 Document doc = resolveReference(docModel.getRef()); 2494 Document sec = resolveReference(section.getRef()); 2495 checkPermission(doc, READ); 2496 checkPermission(sec, ADD_CHILDREN); 2497 if (doc.isRecord()) { 2498 throw new PropertyException("Record cannot be published: " + doc.getUUID()); 2499 } 2500 2501 Map<String, Serializable> options = new HashMap<>(); 2502 DocumentModel proxy = null; 2503 Document target; 2504 if (docModel.isProxy() || docModel.isVersion()) { 2505 target = doc; 2506 if (overwriteExistingProxy) { 2507 if (docModel.isVersion()) { 2508 Document base = resolveReference(new IdRef(doc.getVersionSeriesId())); 2509 proxy = updateExistingProxies(base, sec, target); 2510 } 2511 if (proxy == null) { 2512 // remove previous 2513 List<String> removedProxyIds = removeExistingProxies(doc, sec); 2514 options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds); 2515 } 2516 } 2517 2518 } else { 2519 String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT); 2520 docModel.putContextData(VersioningService.CHECKIN_COMMENT, null); 2521 if (doc.isCheckedOut() || doc.getLastVersion() == null) { 2522 if (!doc.isCheckedOut()) { 2523 // last version was deleted while leaving a checked in 2524 // doc. recreate a version 2525 notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true); 2526 getVersioningService().doCheckOut(doc); 2527 docModel = readModel(doc); 2528 notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false); 2529 } 2530 notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true); 2531 Document version = getVersioningService().doCheckIn(doc, null, checkinComment); 2532 docModel.refresh(DocumentModel.REFRESH_STATE | DocumentModel.REFRESH_CONTENT_LAZY, null); 2533 notifyCheckedInVersion(docModel, new IdRef(version.getUUID()), null, checkinComment); 2534 } 2535 // NXP-12921: use base version because we could need to publish 2536 // a previous version (after restoring for example) 2537 target = doc.getBaseVersion(); 2538 if (overwriteExistingProxy) { 2539 proxy = updateExistingProxies(doc, sec, target); 2540 if (proxy == null) { 2541 // no or several proxies, remove them 2542 List<String> removedProxyIds = removeExistingProxies(doc, sec); 2543 options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds); 2544 } else { 2545 // notify proxy updates 2546 notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_UPDATED, proxy, options, null, null, true, false); 2547 notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxy, options, null, null, true, false); 2548 notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, section, options, null, null, true, 2549 false); 2550 } 2551 } 2552 } 2553 if (proxy == null) { 2554 proxy = createProxyInternal(target, sec, options); 2555 } 2556 return proxy; 2557 } 2558 2559 @Override 2560 public String getSuperParentType(DocumentModel doc) { 2561 DocumentModel superSpace = getSuperSpace(doc); 2562 if (superSpace == null) { 2563 return null; 2564 } else { 2565 return superSpace.getType(); 2566 } 2567 } 2568 2569 @Override 2570 public DocumentModel getSuperSpace(DocumentModel doc) { 2571 if (doc == null) { 2572 throw new IllegalArgumentException("null document"); 2573 } 2574 if (doc.hasFacet(FacetNames.SUPER_SPACE)) { 2575 return doc; 2576 } else { 2577 2578 DocumentModel parent = getDirectAccessibleParent(doc.getRef()); 2579 if (parent == null || "/".equals(parent.getPathAsString())) { 2580 // return Root instead of null 2581 return getRootDocument(); 2582 } else { 2583 return getSuperSpace(parent); 2584 } 2585 } 2586 } 2587 2588 // walk the tree up until a accessible doc is found 2589 private DocumentModel getDirectAccessibleParent(DocumentRef docRef) { 2590 Document doc = resolveReference(docRef); 2591 Document parentDoc = doc.getParent(); 2592 if (parentDoc == null) { 2593 // return null for placeless document 2594 return null; 2595 } 2596 if (!hasPermission(parentDoc, READ)) { 2597 String parentPath = parentDoc.getPath(); 2598 if ("/".equals(parentPath)) { 2599 return getRootDocument(); 2600 } else { 2601 // try on parent 2602 return getDirectAccessibleParent(new PathRef(parentDoc.getPath())); 2603 } 2604 } 2605 return readModel(parentDoc); 2606 } 2607 2608 @Override 2609 public <T extends Serializable> T getDocumentSystemProp(DocumentRef ref, String systemProperty, Class<T> type) { 2610 Document doc = resolveReference(ref); 2611 return doc.getSystemProp(systemProperty, type); 2612 } 2613 2614 @Override 2615 public <T extends Serializable> void setDocumentSystemProp(DocumentRef ref, String systemProperty, T value) { 2616 Document doc = resolveReference(ref); 2617 if (systemProperty != null && systemProperty.startsWith(BINARY_TEXT_SYS_PROP)) { 2618 DocumentModel docModel = readModel(doc); 2619 Map<String, Serializable> options = new HashMap<>(); 2620 options.put(systemProperty, value != null); // deprecated, not very useful 2621 options.put(DocumentEventTypes.SYSTEM_PROPERTY, systemProperty); 2622 options.put(DocumentEventTypes.SYSTEM_PROPERTY_VALUE, value); 2623 // note: event is sent "inline", so never available to async processing (because it's big) 2624 notifyEvent(DocumentEventTypes.BINARYTEXT_UPDATED, docModel, options, null, null, false, true); 2625 } 2626 doc.setSystemProp(systemProperty, value); 2627 } 2628 2629 @Override 2630 public String getChangeToken(DocumentRef ref) { 2631 Document doc = resolveReference(ref); 2632 return doc.getChangeToken(); 2633 } 2634 2635 @Override 2636 public void orderBefore(DocumentRef parent, String src, String dest) { 2637 if ((src == null) || (src.equals(dest))) { 2638 return; 2639 } 2640 Document doc = resolveReference(parent); 2641 doc.orderBefore(src, dest); 2642 Map<String, Serializable> options = new HashMap<>(); 2643 2644 // send event on container passing the reordered child as parameter 2645 DocumentModel docModel = readModel(doc); 2646 options.put(CoreEventConstants.REORDERED_CHILD, src); 2647 notifyEvent(DocumentEventTypes.DOCUMENT_CHILDREN_ORDER_CHANGED, docModel, options, null, src, true, false); 2648 } 2649 2650 @Override 2651 public DocumentModelRefresh refreshDocument(DocumentRef ref, int refreshFlags, String[] schemas) { 2652 Document doc = resolveReference(ref); 2653 2654 // permission checks 2655 if ((refreshFlags & (DocumentModel.REFRESH_STATE | DocumentModel.REFRESH_CONTENT)) != 0) { 2656 checkPermission(doc, READ); 2657 } 2658 if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) { 2659 checkPermission(doc, READ_SECURITY); 2660 } 2661 2662 DocumentModelRefresh refresh = DocumentModelFactory.refreshDocumentModel(doc, refreshFlags, schemas); 2663 2664 // ACPs need the session, so aren't done in the factory method 2665 if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) { 2666 refresh.acp = getSession().getMergedACP(doc); 2667 } 2668 2669 if ((refreshFlags & DocumentModel.REFRESH_STATE) != 0) { 2670 refresh.isTrashed = isTrashed(ref); 2671 } 2672 2673 return refresh; 2674 } 2675 2676 @Override 2677 public String[] getPermissionsToCheck(String permission) { 2678 return getSecurityService().getPermissionsToCheck(permission); 2679 } 2680 2681 @Override 2682 public <T extends DetachedAdapter> T adaptFirstMatchingDocumentWithFacet(DocumentRef docRef, String facet, 2683 Class<T> adapterClass) { 2684 Document doc = getFirstParentDocumentWithFacet(docRef, facet); 2685 if (doc != null) { 2686 DocumentModel docModel = readModel(doc); 2687 loadDataModelsForFacet(docModel, doc, facet); 2688 docModel.detach(false); 2689 return docModel.getAdapter(adapterClass); 2690 } 2691 return null; 2692 } 2693 2694 protected void loadDataModelsForFacet(DocumentModel docModel, Document doc, String facetName) { 2695 // Load all the data related to facet's schemas 2696 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 2697 CompositeType facet = schemaManager.getFacet(facetName); 2698 if (facet == null) { 2699 return; 2700 } 2701 2702 String[] facetSchemas = facet.getSchemaNames(); 2703 for (String schema : facetSchemas) { 2704 DataModel dm = DocumentModelFactory.createDataModel(doc, schemaManager.getSchema(schema)); 2705 docModel.getDataModels().put(schema, dm); 2706 } 2707 } 2708 2709 /** 2710 * Returns the first {@code Document} with the given {@code facet}, recursively going up the parent hierarchy. 2711 * Returns {@code null} if there is no more parent. 2712 * <p> 2713 * This method does not check security rights. 2714 */ 2715 protected Document getFirstParentDocumentWithFacet(DocumentRef docRef, String facet) { 2716 Document doc = resolveReference(docRef); 2717 while (doc != null && !doc.hasFacet(facet)) { 2718 doc = doc.getParent(); 2719 } 2720 return doc; 2721 } 2722 2723 @Override 2724 public Map<String, String> getBinaryFulltext(DocumentRef ref) { 2725 Document doc = resolveReference(ref); 2726 checkPermission(doc, READ); 2727 // Use an id whether than system properties to avoid to store fulltext properties in cache 2728 String id = doc.getUUID(); 2729 if (doc.isProxy()) { 2730 id = doc.getTargetDocument().getUUID(); 2731 } 2732 return getSession().getBinaryFulltext(id); 2733 } 2734 2735 @Override 2736 public DocumentModel getOrCreateDocument(DocumentModel docModel) { 2737 return getOrCreateDocument(docModel, Function.identity()); 2738 } 2739 2740 @Override 2741 public DocumentModel getOrCreateDocument(DocumentModel docModel, 2742 Function<DocumentModel, DocumentModel> postCreate) { 2743 DocumentRef ref = docModel.getRef(); 2744 // Check if the document exists 2745 if (exists(ref)) { 2746 return getDocument(ref); 2747 } 2748 // handle placeless documents, no locks are needed in this case 2749 if (docModel.getParentRef() == null) { 2750 return postCreate.apply(createDocument(docModel)); 2751 } 2752 String key = computeKeyForAtomicCreation(docModel); 2753 return LockHelper.doAtomically(key, () -> { 2754 if (exists(ref)) { 2755 return getDocument(ref); 2756 } 2757 return postCreate.apply(createDocument(docModel)); 2758 }); 2759 } 2760 2761 @Override 2762 public DocumentModel newDocumentModel(DocumentRef parentRef, String name, String typeName) { 2763 if (parentRef == null && name == null) { 2764 return createDocumentModel(typeName); 2765 } 2766 2767 String parentPath = parentRef == null ? null : resolveReference(parentRef).getPath(); 2768 Map<String, Serializable> options = new HashMap<>(); 2769 options.put(CoreEventConstants.PARENT_PATH, parentPath); 2770 options.put(CoreEventConstants.DOCUMENT_MODEL_ID, name); 2771 options.put(CoreEventConstants.DESTINATION_NAME, name); 2772 return createDocumentModelFromParentAndType(parentRef, typeName, options); 2773 } 2774 2775 protected String computeKeyForAtomicCreation(DocumentModel docModel) { 2776 String repositoryName = docModel.getRepositoryName(); 2777 if (repositoryName == null) { 2778 repositoryName = getRepositoryName(); 2779 } 2780 String parentId = getDocument(docModel.getParentRef()).getId(); 2781 String name = docModel.getName(); 2782 return repositoryName + "-" + parentId + "-" + name; 2783 } 2784 2785}