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