001/* 002 * Copyright 2016 Anyware Services 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 */ 016package org.ametys.plugins.queriesdirectory; 017 018import java.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.LinkedList; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.stream.Stream; 027 028import javax.jcr.Node; 029import javax.jcr.RepositoryException; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.commons.lang3.StringUtils; 036import org.apache.jackrabbit.util.Text; 037 038import org.ametys.core.observation.Event; 039import org.ametys.core.observation.ObservationManager; 040import org.ametys.core.right.RightManager; 041import org.ametys.core.right.RightManager.RightResult; 042import org.ametys.core.ui.Callable; 043import org.ametys.core.user.CurrentUserProvider; 044import org.ametys.core.user.UserIdentity; 045import org.ametys.core.util.DateUtils; 046import org.ametys.core.util.LambdaUtils; 047import org.ametys.plugins.core.user.UserHelper; 048import org.ametys.plugins.queriesdirectory.observation.ObservationConstants; 049import org.ametys.plugins.repository.AmetysObject; 050import org.ametys.plugins.repository.AmetysObjectIterable; 051import org.ametys.plugins.repository.AmetysObjectResolver; 052import org.ametys.plugins.repository.AmetysRepositoryException; 053import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 054import org.ametys.plugins.repository.MovableAmetysObject; 055import org.ametys.plugins.repository.UnknownAmetysObjectException; 056import org.ametys.plugins.repository.jcr.NameHelper; 057import org.ametys.runtime.authentication.AccessDeniedException; 058import org.ametys.runtime.plugin.component.AbstractLogEnabled; 059 060import com.google.common.base.Predicates; 061import com.google.common.collect.ImmutableMap; 062 063/** 064 * DAO for manipulating queries 065 */ 066public class QueryDAO extends AbstractLogEnabled implements Serviceable, Component 067{ 068 /** The Avalon role */ 069 public static final String ROLE = QueryDAO.class.getName(); 070 071 /** The right id to handle query */ 072 public static final String QUERY_HANDLE_RIGHT_ID = "QueriesDirectory_Rights_Admin"; 073 074 /** The right id to handle query container */ 075 public static final String QUERY_CONTAINER_HANDLE_RIGHT_ID = "QueriesDirectory_Rights_Containers"; 076 077 /** The alias id of the root {@link QueryContainer} */ 078 public static final String ROOT_QUERY_CONTAINER_ID = "root"; 079 080 /** Propery key for read access */ 081 public static final String READ_ACCESS_PROPERTY = "canRead"; 082 /** Propery key for write access */ 083 public static final String WRITE_ACCESS_PROPERTY = "canWrite"; 084 /** Propery key for rename access */ 085 public static final String RENAME_ACCESS_PROPERTY = "canRename"; 086 /** Propery key for delete access */ 087 public static final String DELETE_ACCESS_PROPERTY = "canDelete"; 088 /** Propery key for edit rights access */ 089 public static final String EDIT_RIGHTS_ACCESS_PROPERTY = "canAssignRights"; 090 091 private static final String __PLUGIN_NODE_NAME = "queriesdirectory"; 092 093 /** The current user provider */ 094 protected CurrentUserProvider _userProvider; 095 096 /** The observation manager */ 097 protected ObservationManager _observationManager; 098 099 /** The Ametys object resolver */ 100 private AmetysObjectResolver _resolver; 101 102 private UserHelper _userHelper; 103 104 private RightManager _rightManager; 105 106 @Override 107 public void service(ServiceManager serviceManager) throws ServiceException 108 { 109 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 110 _userProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 111 _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE); 112 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 113 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 114 } 115 116 /** 117 * Get the root plugin storage object. 118 * @return the root plugin storage object. 119 * @throws AmetysRepositoryException if a repository error occurs. 120 */ 121 public QueryContainer getQueriesRootNode() throws AmetysRepositoryException 122 { 123 try 124 { 125 return _getOrCreateRootNode(); 126 } 127 catch (AmetysRepositoryException e) 128 { 129 throw new AmetysRepositoryException("Unable to get the queries root node", e); 130 } 131 } 132 133 private QueryContainer _getOrCreateRootNode() throws AmetysRepositoryException 134 { 135 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins"); 136 137 ModifiableTraversableAmetysObject pluginNode = (ModifiableTraversableAmetysObject) _getOrCreateNode(pluginsNode, __PLUGIN_NODE_NAME, "ametys:unstructured"); 138 139 return (QueryContainer) _getOrCreateNode(pluginNode, "ametys:queries", QueryContainerFactory.QUERY_CONTAINER_NODETYPE); 140 } 141 142 private static AmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException 143 { 144 AmetysObject definitionsNode; 145 if (parentNode.hasChild(nodeName)) 146 { 147 definitionsNode = parentNode.getChild(nodeName); 148 } 149 else 150 { 151 definitionsNode = parentNode.createChild(nodeName, nodeType); 152 parentNode.saveChanges(); 153 } 154 return definitionsNode; 155 } 156 157 /** 158 * Get queries' properties 159 * @param queryIds The ids of queries to retrieve 160 * @return The queries' properties 161 */ 162 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 163 public Map<String, Object> getQueriesProperties(List<String> queryIds) 164 { 165 Map<String, Object> result = new HashMap<>(); 166 167 List<Map<String, Object>> queries = new LinkedList<>(); 168 List<String> notAllowedQueries = new LinkedList<>(); 169 Set<String> unknownQueries = new HashSet<>(); 170 171 UserIdentity user = _userProvider.getUser(); 172 173 for (String id : queryIds) 174 { 175 try 176 { 177 Query query = _resolver.resolveById(id); 178 179 if (canRead(user, query)) 180 { 181 queries.add(getQueryProperties(query)); 182 } 183 else 184 { 185 notAllowedQueries.add(query.getId()); 186 } 187 } 188 catch (UnknownAmetysObjectException e) 189 { 190 unknownQueries.add(id); 191 } 192 } 193 194 result.put("queries", queries); 195 result.put("notAllowedQueries", notAllowedQueries); 196 result.put("unknownQueries", unknownQueries); 197 198 return result; 199 } 200 201 /** 202 * Get the query properties 203 * @param query The query 204 * @return The query properties 205 */ 206 public Map<String, Object> getQueryProperties (Query query) 207 { 208 Map<String, Object> infos = new HashMap<>(); 209 210 List<String> fullPath = new ArrayList<>(); 211 fullPath.add(query.getTitle()); 212 213 AmetysObject node = query.getParent(); 214 while (node instanceof QueryContainer 215 && node.getParent() instanceof QueryContainer) // The parent must also be a container to avoid the root 216 { 217 fullPath.add(0, node.getName()); 218 node = node.getParent(); 219 } 220 221 222 infos.put("isQuery", true); 223 infos.put("id", query.getId()); 224 infos.put("title", query.getTitle()); 225 infos.put("fullPath", String.join(" > ", fullPath)); 226 infos.put("type", query.getType()); 227 infos.put("description", query.getDescription()); 228 infos.put("documentation", query.getDocumentation()); 229 infos.put("author", _userHelper.user2json(query.getAuthor())); 230 infos.put("contributor", _userHelper.user2json(query.getContributor())); 231 infos.put("content", query.getContent()); 232 infos.put("lastModificationDate", DateUtils.zonedDateTimeToString(query.getLastModificationDate())); 233 infos.put("creationDate", DateUtils.zonedDateTimeToString(query.getCreationDate())); 234 235 UserIdentity currentUser = _userProvider.getUser(); 236 infos.put(READ_ACCESS_PROPERTY, canRead(currentUser, query)); 237 infos.put(WRITE_ACCESS_PROPERTY, canWrite(currentUser, query)); 238 infos.put(DELETE_ACCESS_PROPERTY, canDelete(currentUser, query)); 239 infos.put(EDIT_RIGHTS_ACCESS_PROPERTY, canAssignRights(currentUser, query)); 240 241 return infos; 242 } 243 244 /** 245 * Gets the ids of the path elements of a query or query container, i.e. the parent ids. 246 * <br>For instance, if the query path is 'a/b/c', then the result list will be ["id-of-a", "id-of-b", "id-of-c"] 247 * @param queryId The id of the query 248 * @return the ids of the path elements of a query 249 */ 250 @Callable (rights = {QUERY_CONTAINER_HANDLE_RIGHT_ID, QUERY_HANDLE_RIGHT_ID}, rightContext = QueriesDirectoryRightAssignmentContext.ID, paramIndex = 0) 251 public List<String> getIdsOfPath(String queryId) 252 { 253 AmetysObject queryOrQueryContainer = _resolver.resolveById(queryId); 254 QueryContainer queriesRootNode = getQueriesRootNode(); 255 256 if (!(queryOrQueryContainer instanceof Query) && !(queryOrQueryContainer instanceof QueryContainer)) 257 { 258 throw new IllegalArgumentException("The given id is not a query nor a query container"); 259 } 260 261 List<String> pathElements = new ArrayList<>(); 262 QueryContainer current = queryOrQueryContainer.getParent(); 263 while (!queriesRootNode.equals(current)) 264 { 265 pathElements.add(0, current.getId()); 266 current = current.getParent(); 267 } 268 269 return pathElements; 270 } 271 272 /** 273 * Get the root container properties 274 * @return The root container properties 275 */ 276 @Callable(rights = {"QueriesDirectory_Rights_Tool", "CMS_Rights_Delegate_Rights", "Runtime_Rights_Rights_Handle"}) 277 public Map<String, Object> getRootProperties() 278 { 279 return getQueryContainerProperties(getQueriesRootNode()); 280 } 281 282 /** 283 * Get the query container properties 284 * @param id The query container id. Can be {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 285 * @return The query container properties 286 */ 287 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 288 public Map<String, Object> getQueryContainerProperties(String id) 289 { 290 UserIdentity user = _userProvider.getUser(); 291 QueryContainer container = _getQueryContainer(id); 292 if (!canRead(user, container)) 293 { 294 throw new AccessDeniedException(user + " tried to access the properties of container " + id + " without sufficients rights"); 295 } 296 return getQueryContainerProperties(container); 297 } 298 299 /** 300 * Get the query container properties 301 * @param queryContainer The query container 302 * @return The query container properties 303 */ 304 public Map<String, Object> getQueryContainerProperties(QueryContainer queryContainer) 305 { 306 Map<String, Object> infos = new HashMap<>(); 307 308 infos.put("isQuery", false); 309 infos.put("id", queryContainer.getId()); 310 infos.put("name", queryContainer.getName()); 311 infos.put("title", queryContainer.getName()); 312 infos.put("fullPath", _getFullPath(queryContainer)); 313 314 UserIdentity currentUser = _userProvider.getUser(); 315 316 infos.put(READ_ACCESS_PROPERTY, canRead(currentUser, queryContainer)); 317 infos.put(WRITE_ACCESS_PROPERTY, canWrite(currentUser, queryContainer)); // create container, add query 318 infos.put(RENAME_ACCESS_PROPERTY, canRename(currentUser, queryContainer)); // rename 319 infos.put(DELETE_ACCESS_PROPERTY, canDelete(currentUser, queryContainer)); // delete or move 320 infos.put(EDIT_RIGHTS_ACCESS_PROPERTY, canAssignRights(currentUser, queryContainer)); // edit rights 321 322 return infos; 323 } 324 325 private String _getFullPath(QueryContainer queryContainer) 326 { 327 List<String> fullPath = new ArrayList<>(); 328 fullPath.add(queryContainer.getName()); 329 330 AmetysObject node = queryContainer.getParent(); 331 while (node instanceof QueryContainer 332 && node.getParent() instanceof QueryContainer) // The parent must also be a container to avoid the root 333 { 334 fullPath.add(0, node.getName()); 335 node = node.getParent(); 336 } 337 return String.join(" > ", fullPath); 338 } 339 340 /** 341 * Filter queries on the server side 342 * @param rootNode The root node where to seek (to refresh subtrees) 343 * @param search Textual search 344 * @param ownerOnly Only queries of the owner will be returned 345 * @param requestType Only simple/advanced requests will be returned 346 * @param solrType Only Solr requests will be returned 347 * @param scriptType Only script requests will be returned 348 * @param formattingType Only formatting will be returned 349 * @return The list of query path 350 */ 351 @Callable (rights = {"QueriesDirectory_Rights_Tool", "CMS_Rights_Delegate_Rights", "Runtime_Rights_Rights_Handle"}) 352 public List<String> filterQueries(String rootNode, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType) 353 { 354 List<String> matchingPaths = new ArrayList<>(); 355 _getMatchingQueries(_getQueryContainer(rootNode), 356 matchingPaths, StringUtils.stripAccents(search.toLowerCase()), ownerOnly, requestType, solrType, scriptType, formattingType); 357 358 return matchingPaths; 359 } 360 361 private void _getMatchingQueries(QueryContainer queryContainer, List<String> matchingPaths, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType) 362 { 363 if (StringUtils.isBlank(search)) 364 { 365 return; 366 } 367 368 String containerName = StringUtils.stripAccents(queryContainer.getName().toLowerCase()); 369 if (containerName.contains(search)) 370 { 371 matchingPaths.add(_getQueryPath(queryContainer)); 372 } 373 374 if (!hasAnyReadableDescendant(_userProvider.getUser(), queryContainer)) 375 { 376 return; 377 } 378 379 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 380 { 381 for (AmetysObject child : children) 382 { 383 if (child instanceof QueryContainer childQueryContainer) 384 { 385 _getMatchingQueries(childQueryContainer, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType); 386 } 387 else if (child instanceof Query childQuery) 388 { 389 _getMatchingQuery(childQuery, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType); 390 } 391 } 392 } 393 } 394 395 private void _getMatchingQuery(Query query, List<String> matchingPaths, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType) 396 { 397 String type = query.getType(); 398 399 if (!canRead(_userProvider.getUser(), query) 400 || formattingType && !"formatting".equals(type) 401 || requestType && !Query.Type.SIMPLE.toString().equals(type) && !Query.Type.ADVANCED.toString().equals(type) 402 || solrType && !"solr".equals(type) 403 || scriptType && !Query.Type.SCRIPT.toString().equals(type) 404 || ownerOnly && !query.getAuthor().equals(_userProvider.getUser())) 405 { 406 return; 407 } 408 409 if (_contains(query, search)) 410 { 411 matchingPaths.add(_getQueryPath(query)); 412 } 413 } 414 415 /** 416 * Check it the query contains the search string in its title, content, description or documentation 417 */ 418 private boolean _contains(Query query, String search) 419 { 420 if (StringUtils.stripAccents(query.getTitle().toLowerCase()).contains(search)) 421 { 422 return true; 423 } 424 425 if (StringUtils.stripAccents(query.getContent().toLowerCase()).contains(search)) 426 { 427 return true; 428 } 429 430 if (StringUtils.stripAccents(query.getDescription().toLowerCase()).contains(search)) 431 { 432 return true; 433 } 434 return false; 435 } 436 437 private String _getQueryPath(AmetysObject queryOrContainer) 438 { 439 if (queryOrContainer instanceof Query 440 || queryOrContainer instanceof QueryContainer 441 && queryOrContainer.getParent() instanceof QueryContainer) 442 { 443 return _getQueryPath(queryOrContainer.getParent()) + "#" + queryOrContainer.getId(); 444 } 445 else 446 { 447 return "#root"; 448 } 449 } 450 451 /** 452 * Creates a new {@link Query} 453 * @param title The title of the query 454 * @param desc The description of the query 455 * @param documentation The documentation of the query 456 * @param type The type of the query 457 * @param content The content of the query 458 * @param parentId The id of the parent of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 459 * @return A result map 460 */ 461 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 462 public Map<String, Object> createQuery(String title, String desc, String documentation, String type, String content, String parentId) 463 { 464 Map<String, Object> results = new HashMap<>(); 465 466 QueryContainer queriesNode = _getQueryContainer(parentId); 467 468 if (!canWrite(_userProvider.getUser(), queriesNode)) 469 { 470 results.put("message", "not-allowed"); 471 return results; 472 } 473 474 String name = NameHelper.filterName(title); 475 476 // Find unique name 477 String uniqueName = name; 478 int index = 2; 479 while (queriesNode.hasChild(uniqueName)) 480 { 481 uniqueName = name + "-" + (index++); 482 } 483 484 Query query = queriesNode.createChild(uniqueName, QueryFactory.QUERY_NODETYPE); 485 query.setTitle(title); 486 query.setDescription(desc); 487 query.setDocumentation(documentation); 488 query.setAuthor(_userProvider.getUser()); 489 query.setContributor(_userProvider.getUser()); 490 query.setType(type); 491 query.setContent(content); 492 query.setCreationDate(ZonedDateTime.now()); 493 query.setLastModificationDate(ZonedDateTime.now()); 494 495 queriesNode.saveChanges(); 496 497 results.put("id", query.getId()); 498 results.put("title", query.getTitle()); 499 results.put("content", query.getContent()); 500 501 return results; 502 } 503 504 /** 505 * Creates a new {@link QueryContainer} 506 * @param parentId The id of the parent. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 507 * @param name The desired name for the new {@link QueryContainer} 508 * @return A result map 509 */ 510 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 511 public Map<String, Object> createQueryContainer(String parentId, String name) 512 { 513 QueryContainer parent = _getQueryContainer(parentId); 514 515 if (!canWrite(_userProvider.getUser(), parent)) 516 { 517 return ImmutableMap.of("message", "not-allowed"); 518 } 519 520 int index = 2; 521 String legalName = Text.escapeIllegalJcrChars(name); 522 String realName = legalName; 523 while (parent.hasChild(realName)) 524 { 525 realName = legalName + " (" + index + ")"; 526 index++; 527 } 528 529 QueryContainer createdChild = parent.createChild(realName, QueryContainerFactory.QUERY_CONTAINER_NODETYPE); 530 parent.saveChanges(); 531 532 return getQueryContainerProperties(createdChild); 533 } 534 535 /** 536 * Edits a {@link Query} 537 * @param id The id of the query 538 * @param title The title of the query 539 * @param desc The description of the query 540 * @param documentation The documentation of the query 541 * @return A result map 542 */ 543 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 544 public Map<String, Object> updateQuery(String id, String title, String desc, String documentation) 545 { 546 Map<String, Object> results = new HashMap<>(); 547 548 Query query = _resolver.resolveById(id); 549 550 if (canWrite(_userProvider.getUser(), query)) 551 { 552 query.setTitle(title); 553 query.setDescription(desc); 554 query.setDocumentation(documentation); 555 query.setContributor(_userProvider.getUser()); 556 query.setLastModificationDate(ZonedDateTime.now()); 557 query.saveChanges(); 558 } 559 else 560 { 561 results.put("message", "not-allowed"); 562 } 563 564 results.put("id", query.getId()); 565 results.put("title", query.getTitle()); 566 567 return results; 568 } 569 570 /** 571 * Renames a {@link QueryContainer} 572 * @param id The id of the query container 573 * @param newName The new name of the container 574 * @return A result map 575 */ 576 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 577 public Map<String, Object> renameQueryContainer(String id, String newName) 578 { 579 QueryContainer queryContainer = _resolver.resolveById(id); 580 581 UserIdentity currentUser = _userProvider.getUser(); 582 583 if (canRename(currentUser, queryContainer)) 584 { 585 String legalName = Text.escapeIllegalJcrChars(newName); 586 Node node = queryContainer.getNode(); 587 try 588 { 589 node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName); 590 node.getSession().save(); 591 592 return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id)); 593 } 594 catch (RepositoryException e) 595 { 596 getLogger().error("Unable to rename query container '{}'", id, e); 597 return Map.of("message", "cannot-rename"); 598 } 599 } 600 else 601 { 602 return Map.of("message", "not-allowed"); 603 } 604 } 605 606 /** 607 * Moves a {@link Query} 608 * @param id The id of the query 609 * @param newParentId The id of the new parent container of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 610 * @return A result map 611 */ 612 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 613 public Map<String, Object> moveQuery(String id, String newParentId) 614 { 615 Map<String, Object> results = new HashMap<>(); 616 Query query = _resolver.resolveById(id); 617 QueryContainer queryContainer = _getQueryContainer(newParentId); 618 619 if (canDelete(_userProvider.getUser(), query) && canWrite(_userProvider.getUser(), queryContainer)) 620 { 621 if (!_move(query, newParentId)) 622 { 623 results.put("message", "cannot-move"); 624 } 625 } 626 else 627 { 628 results.put("message", "not-allowed"); 629 } 630 631 results.put("id", query.getId()); 632 return results; 633 } 634 635 /** 636 * Moves a {@link QueryContainer} 637 * @param id The id of the query container 638 * @param newParentId The id of the new parent container of the query container. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 639 * @return A result map 640 */ 641 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 642 public Map<String, Object> moveQueryContainer(String id, String newParentId) 643 { 644 QueryContainer queryContainer = _resolver.resolveById(id); 645 QueryContainer parentQueryContainer = _getQueryContainer(newParentId); 646 UserIdentity currentUser = _userProvider.getUser(); 647 648 if (canDelete(currentUser, queryContainer) && canWrite(currentUser, parentQueryContainer)) 649 { 650 if (_move(queryContainer, newParentId)) 651 { 652 // returns updated properties 653 return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id)); 654 } 655 else 656 { 657 return Map.of("id", id, "message", "cannot-move"); 658 } 659 660 } 661 else 662 { 663 return Map.of("id", id, "message", "not-allowed"); 664 } 665 } 666 667 private boolean _move(MovableAmetysObject obj, String newParentId) 668 { 669 QueryContainer newParent = _getQueryContainer(newParentId); 670 if (obj.canMoveTo(newParent)) 671 { 672 try 673 { 674 obj.moveTo(newParent, false); 675 return true; 676 } 677 catch (AmetysRepositoryException e) 678 { 679 getLogger().error("Unable to move '{}' to query container '{}'", obj.getId(), newParentId, e); 680 } 681 } 682 return false; 683 } 684 685 private QueryContainer _getQueryContainer(String id) 686 { 687 return ROOT_QUERY_CONTAINER_ID.equals(id) 688 ? getQueriesRootNode() 689 : _resolver.resolveById(id); 690 } 691 692 /** 693 * Saves a {@link Query} 694 * @param id The id of the query 695 * @param type The type of the query 696 * @param content The content of the query 697 * @return A result map 698 */ 699 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 700 public Map<String, Object> saveQuery(String id, String type, String content) 701 { 702 Map<String, Object> results = new HashMap<>(); 703 704 Query query = _resolver.resolveById(id); 705 UserIdentity user = _userProvider.getUser(); 706 if (canWrite(user, query)) 707 { 708 query.setType(type); 709 query.setContent(content); 710 query.setContributor(user); 711 query.setLastModificationDate(ZonedDateTime.now()); 712 query.saveChanges(); 713 714 results.put("id", query.getId()); 715 results.put("content", query.getContent()); 716 results.put("title", query.getTitle()); 717 } 718 else 719 { 720 results.put("message", "not-allowed"); 721 } 722 723 return results; 724 } 725 726 /** 727 * Deletes {@link Query}(ies) 728 * @param ids The ids of the queries to delete 729 * @return A result map 730 */ 731 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 732 public Map<String, Object> deleteQuery(List<String> ids) 733 { 734 Map<String, Object> results = new HashMap<>(); 735 736 List<String> deletedQueries = new ArrayList<>(); 737 List<String> unknownQueries = new ArrayList<>(); 738 List<String> notallowedQueries = new ArrayList<>(); 739 740 for (String id : ids) 741 { 742 try 743 { 744 Query query = _resolver.resolveById(id); 745 746 if (canDelete(_userProvider.getUser(), query)) 747 { 748 Map<String, Object> params = new HashMap<>(); 749 params.put(ObservationConstants.ARGS_QUERY_ID, query.getId()); 750 751 query.remove(); 752 query.saveChanges(); 753 deletedQueries.add(id); 754 755 _observationManager.notify(new Event(ObservationConstants.EVENT_QUERY_DELETED, _userProvider.getUser(), params)); 756 } 757 else 758 { 759 notallowedQueries.add(query.getTitle()); 760 } 761 } 762 catch (UnknownAmetysObjectException e) 763 { 764 unknownQueries.add(id); 765 getLogger().error("Unable to delete query. The query of id '{}' doesn't exist", id, e); 766 } 767 } 768 769 results.put("deletedQueries", deletedQueries); 770 results.put("notallowedQueries", notallowedQueries); 771 results.put("unknownQueries", unknownQueries); 772 773 return results; 774 } 775 776 /** 777 * Determines if application must warn before deleting the given {@link QueryContainer}s 778 * @param ids The {@link QueryContainer} ids 779 * @return <code>true</code> if application must warn 780 */ 781 @Callable (rights = Callable.NO_CHECK_REQUIRED) 782 public boolean mustWarnBeforeDeletion(List<String> ids) 783 { 784 return ids.stream() 785 .anyMatch(LambdaUtils.wrapPredicate(this::_mustWarnBeforeDeletion)); 786 } 787 788 private boolean _mustWarnBeforeDeletion(String id) 789 { 790 QueryContainer container = _resolver.resolveById(id); 791 AmetysObjectIterable<Query> allQueries = getChildQueriesForAdministrator(container, false, List.of()); 792 return _containsNotOwnQueries(allQueries); 793 } 794 795 private boolean _containsNotOwnQueries(AmetysObjectIterable<Query> allQueries) 796 { 797 UserIdentity currentUser = _userProvider.getUser(); 798 return allQueries.stream() 799 .map(Query::getAuthor) 800 .anyMatch(Predicates.not(currentUser::equals)); 801 } 802 803 804 /** 805 * Deletes {@link QueryContainer}(s) 806 * @param ids The ids of the query containers to delete 807 * @return A result map 808 */ 809 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 810 public Map<String, Object> deleteQueryContainer(List<String> ids) 811 { 812 Map<String, Object> results = new HashMap<>(); 813 814 List<Object> deletedQueryContainers = new ArrayList<>(); 815 List<String> unknownQueryContainers = new ArrayList<>(); 816 List<String> notallowedQueryContainers = new ArrayList<>(); 817 818 for (String id : ids) 819 { 820 try 821 { 822 QueryContainer queryContainer = _resolver.resolveById(id); 823 824 if (canDelete(_userProvider.getUser(), queryContainer)) 825 { 826 Map<String, Object> props = getQueryContainerProperties(queryContainer); 827 queryContainer.remove(); 828 queryContainer.saveChanges(); 829 deletedQueryContainers.add(props); 830 } 831 else 832 { 833 notallowedQueryContainers.add(queryContainer.getName()); 834 } 835 } 836 catch (UnknownAmetysObjectException e) 837 { 838 unknownQueryContainers.add(id); 839 getLogger().error("Unable to delete query container. The query container of id '{}' doesn't exist", id, e); 840 } 841 } 842 843 results.put("deletedQueryContainers", deletedQueryContainers); 844 results.put("notallowedQueryContainers", notallowedQueryContainers); 845 results.put("unknownQueryContainers", unknownQueryContainers); 846 847 return results; 848 } 849 850 /** 851 * Gets all queries for administrator for given parent 852 * @param parent The {@link QueryContainer}, defining the context from which getting children 853 * @param onlyDirect <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path 854 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 855 * @return all queries for administrator for given parent 856 */ 857 public AmetysObjectIterable<Query> getChildQueriesForAdministrator(QueryContainer parent, boolean onlyDirect, List<String> acceptedTypes) 858 { 859 return _resolver.query(QueryHelper.getXPathForQueriesForAdministrator(parent, onlyDirect, acceptedTypes)); 860 } 861 862 /** 863 * Gets all queries in READ access for given parent 864 * @param parent The {@link QueryContainer}, defining the context from which getting children 865 * @param onlyDirect <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path 866 * @param user The user 867 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 868 * @return all queries in READ access for given parent 869 */ 870 public Stream<Query> getChildQueriesInReadAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes) 871 { 872 return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes)) 873 .stream() 874 .filter(Query.class::isInstance) 875 .map(obj -> (Query) obj) 876 .filter(query -> canRead(user, query)); 877 } 878 879 /** 880 * Determine if user has read access on a query 881 * @param userIdentity the user 882 * @param query the query 883 * @return true if the user have read rights on a query 884 */ 885 public boolean canRead(UserIdentity userIdentity, Query query) 886 { 887 return _rightManager.hasReadAccess(userIdentity, query) || canWrite(userIdentity, query); 888 } 889 890 /** 891 * Determines if the user has read access on a query container 892 * @param userIdentity the user 893 * @param queryContainer the query container 894 * @return true if the user has read access on the query container 895 */ 896 public boolean canRead(UserIdentity userIdentity, QueryContainer queryContainer) 897 { 898 return _rightManager.hasReadAccess(userIdentity, queryContainer) || canWrite(userIdentity, queryContainer); 899 } 900 901 /** 902 * Gets all queries in WRITE access for given parent 903 * @param parent The {@link QueryContainer}, defining the context from which getting children 904 * @param onlyDirect <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path 905 * @param user The user 906 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 907 * @return all queries in WRITE access for given parent 908 */ 909 public Stream<Query> getChildQueriesInWriteAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes) 910 { 911 return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes)) 912 .stream() 913 .filter(Query.class::isInstance) 914 .map(obj -> (Query) obj) 915 .filter(query -> canWrite(user, query)); 916 } 917 918 /** 919 * Determines if the user has write access on a query 920 * @param userIdentity the user 921 * @param query the query 922 * @return true if the user has write access on query 923 */ 924 public boolean canWrite(UserIdentity userIdentity, Query query) 925 { 926 return _rightManager.hasRight(userIdentity, QUERY_HANDLE_RIGHT_ID, query) == RightResult.RIGHT_ALLOW; 927 } 928 929 /** 930 * Determines if the user can delete a query 931 * @param userIdentity the user 932 * @param query the query 933 * @return true if the user can delete the query 934 */ 935 public boolean canDelete(UserIdentity userIdentity, Query query) 936 { 937 return canWrite(userIdentity, query) && canWrite(userIdentity, (QueryContainer) query.getParent()); 938 } 939 940 /** 941 * Determines if the user can rename a query container 942 * @param userIdentity the user 943 * @param queryContainer the query container 944 * @return true if the user can delete the query 945 */ 946 public boolean canRename(UserIdentity userIdentity, QueryContainer queryContainer) 947 { 948 return !_isRoot(queryContainer) && canWrite(userIdentity, queryContainer) && canWrite(userIdentity, (QueryContainer) queryContainer.getParent()); 949 } 950 951 /** 952 * Determines if the user can delete a query container 953 * @param userIdentity the user 954 * @param queryContainer the query container 955 * @return true if the user can delete the query container 956 */ 957 public boolean canDelete(UserIdentity userIdentity, QueryContainer queryContainer) 958 { 959 return !_isRoot(queryContainer) // is not root 960 && canWrite(userIdentity, (QueryContainer) queryContainer.getParent()) // has write access on parent 961 && canWrite(userIdentity, queryContainer, true); // has write access on itselft and each descendant 962 } 963 964 /** 965 * Determines if the query container is the root node 966 * @param queryContainer the query container 967 * @return true if is root 968 */ 969 protected boolean _isRoot(QueryContainer queryContainer) 970 { 971 return getQueriesRootNode().equals(queryContainer); 972 } 973 974 /** 975 * Gets all queries in WRITE access for given parent 976 * @param parent The {@link QueryContainer}, defining the context from which getting children 977 * @param onlyDirect <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path 978 * @param user The user 979 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 980 * @return all queries in WRITE access for given parent 981 */ 982 public Stream<Query> getChildQueriesInRightAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes) 983 { 984 return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes)) 985 .stream() 986 .filter(Query.class::isInstance) 987 .map(obj -> (Query) obj) 988 .filter(query -> canAssignRights(user, query)); 989 } 990 991 /** 992 * Check if a user can edit rights on a query 993 * @param userIdentity the user 994 * @param query the query 995 * @return true if the user can edit rights on a query 996 */ 997 public boolean canAssignRights(UserIdentity userIdentity, Query query) 998 { 999 return canWrite(userIdentity, query) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW; 1000 } 1001 1002 /** 1003 * Check if a user has creation rights on a query container 1004 * @param userIdentity the user identity 1005 * @param queryContainer the query container 1006 * @return true if the user has creation rights on a query container 1007 */ 1008 public boolean canCreate(UserIdentity userIdentity, QueryContainer queryContainer) 1009 { 1010 return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, "/cms") == RightResult.RIGHT_ALLOW; 1011 } 1012 1013 1014 /** 1015 * Check if a user has write access on a query container 1016 * @param userIdentity the user identity 1017 * @param queryContainer the query container 1018 * @return true if the user has write access on the a query container 1019 */ 1020 public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer) 1021 { 1022 return canWrite(userIdentity, queryContainer, false); 1023 } 1024 1025 /** 1026 * Check if a user has write access on a query container 1027 * @param userIdentity the user user identity 1028 * @param queryContainer the query container 1029 * @param recursively true to check write access on all descendants recursively 1030 * @return true if the user has write access on the a query container 1031 */ 1032 public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer, boolean recursively) 1033 { 1034 boolean hasRight = _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, queryContainer) == RightResult.RIGHT_ALLOW; 1035 if (!hasRight) 1036 { 1037 return false; 1038 } 1039 1040 if (recursively) 1041 { 1042 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1043 { 1044 for (AmetysObject child : children) 1045 { 1046 if (child instanceof QueryContainer) 1047 { 1048 hasRight = hasRight && canWrite(userIdentity, (QueryContainer) child, true); 1049 } 1050 else if (child instanceof Query) 1051 { 1052 hasRight = hasRight && canWrite(userIdentity, (Query) child); 1053 } 1054 1055 if (!hasRight) 1056 { 1057 return false; 1058 } 1059 } 1060 } 1061 } 1062 1063 return hasRight; 1064 } 1065 1066 /** 1067 * Check if a user can edit rights on a query container 1068 * @param userIdentity the user 1069 * @param queryContainer the query container 1070 * @return true if the user can edit rights on a query 1071 */ 1072 public boolean canAssignRights(UserIdentity userIdentity, QueryContainer queryContainer) 1073 { 1074 return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW; 1075 } 1076 1077 /** 1078 * Gets all query containers for given parent 1079 * @param parent The {@link QueryContainer}, defining the context from which getting children 1080 * @return all query containers for given parent 1081 */ 1082 public AmetysObjectIterable<QueryContainer> getChildQueryContainers(QueryContainer parent) 1083 { 1084 return _resolver.query(QueryHelper.getXPathForQueryContainers(parent)); 1085 } 1086 1087 /** 1088 * Check if a folder have a descendant in read access for a given user 1089 * @param userIdentity the user 1090 * @param queryContainer the query container 1091 * @return true if the user have read right for at least one child of this container 1092 */ 1093 public boolean hasAnyReadableDescendant(UserIdentity userIdentity, QueryContainer queryContainer) 1094 { 1095 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1096 { 1097 for (AmetysObject child : children) 1098 { 1099 if (child instanceof QueryContainer) 1100 { 1101 if (canRead(userIdentity, (QueryContainer) child) || hasAnyReadableDescendant(userIdentity, (QueryContainer) child)) 1102 { 1103 return true; 1104 } 1105 } 1106 else if (child instanceof Query && canRead(userIdentity, (Query) child)) 1107 { 1108 return true; 1109 } 1110 } 1111 } 1112 1113 return false; 1114 } 1115 1116 /** 1117 * Check if a query container have descendant in write access for a given user 1118 * @param userIdentity the user identity 1119 * @param queryContainer the query container 1120 * @return true if the user have write right for at least one child of this container 1121 */ 1122 public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, QueryContainer queryContainer) 1123 { 1124 boolean canWrite = false; 1125 1126 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1127 { 1128 for (AmetysObject child : children) 1129 { 1130 if (child instanceof QueryContainer) 1131 { 1132 if (canWrite(userIdentity, (QueryContainer) child) || hasAnyWritableDescendant(userIdentity, (QueryContainer) child)) 1133 { 1134 return true; 1135 } 1136 } 1137 else if (child instanceof Query && canWrite(userIdentity, (Query) child)) 1138 { 1139 return true; 1140 } 1141 } 1142 } 1143 1144 return canWrite; 1145 } 1146 1147 /** 1148 * Check if a folder have descendant in right assignment access for a given user 1149 * @param userIdentity the user identity 1150 * @param queryContainer the query container 1151 * @return true if the user have right assignment right for at least one child of this container 1152 */ 1153 public boolean hasAnyAssignableDescendant(UserIdentity userIdentity, QueryContainer queryContainer) 1154 { 1155 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1156 { 1157 for (AmetysObject child : children) 1158 { 1159 if (child instanceof QueryContainer) 1160 { 1161 if (canAssignRights(userIdentity, (QueryContainer) child) || hasAnyAssignableDescendant(userIdentity, (QueryContainer) child)) 1162 { 1163 return true; 1164 } 1165 } 1166 else if (child instanceof Query) 1167 { 1168 if (canAssignRights(userIdentity, (Query) child)) 1169 { 1170 return true; 1171 } 1172 } 1173 } 1174 return false; 1175 } 1176 } 1177}