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