001/* 002 * Copyright 2024 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.microsoft365; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.Iterator; 021import java.util.List; 022import java.util.Map; 023import java.util.Optional; 024 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029import org.apache.commons.lang3.StringUtils; 030 031import org.ametys.core.ui.Callable; 032import org.ametys.core.user.CurrentUserProvider; 033import org.ametys.core.user.UserIdentity; 034import org.ametys.plugins.extrausermgt.users.aad.GraphClientProvider; 035import org.ametys.plugins.extrausermgt.users.aad.GraphClientProvider.GraphClientException; 036import org.ametys.plugins.repository.AmetysObjectResolver; 037import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 038import org.ametys.runtime.plugin.component.AbstractLogEnabled; 039import org.ametys.web.repository.page.ZoneItem; 040 041import com.microsoft.graph.http.GraphServiceException; 042import com.microsoft.graph.models.DriveItem; 043import com.microsoft.graph.models.DriveSearchParameterSet; 044import com.microsoft.graph.models.Site; 045import com.microsoft.graph.requests.DriveItemCollectionPage; 046import com.microsoft.graph.requests.DriveItemCollectionRequestBuilder; 047import com.microsoft.graph.requests.DriveRecentCollectionPage; 048import com.microsoft.graph.requests.DriveRecentCollectionRequestBuilder; 049import com.microsoft.graph.requests.DriveRequestBuilder; 050import com.microsoft.graph.requests.DriveSearchCollectionPage; 051import com.microsoft.graph.requests.DriveSearchCollectionRequestBuilder; 052import com.microsoft.graph.requests.DriveSharedWithMeCollectionPage; 053import com.microsoft.graph.requests.DriveSharedWithMeCollectionRequestBuilder; 054import com.microsoft.graph.requests.UserRequestBuilder; 055 056/** 057 * Component for OneDrive connector 058 * 059 */ 060public class OneDriveConnector extends AbstractLogEnabled implements Component, Serviceable 061{ 062 /** The avalon role */ 063 public static final String ROLE = OneDriveConnector.class.getName(); 064 065 private static final String __DRIVE_ITEM_SELECT = "id,type,name,webUrl,webDavUrl,file,size,folder,package,parentReference,remoteItem,sharepointIds,createdBy"; 066 067 /** Graph client provider */ 068 protected GraphClientProvider _graphClientProvider; 069 /** Current user provider */ 070 protected CurrentUserProvider _currentUserProvider; 071 /** Ametys object resolver */ 072 protected AmetysObjectResolver _resolver; 073 074 public void service(ServiceManager manager) throws ServiceException 075 { 076 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 077 _graphClientProvider = (GraphClientProvider) manager.lookup(GraphClientProvider.ROLE); 078 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 079 } 080 081 /** 082 * Enumeration for the type of OneDrive resources 083 * 084 */ 085 public enum ResourceType 086 { 087 /** The user files */ 088 USER_FILES, 089 /** The shared files */ 090 SHARED_FILES, 091 /** The recent files */ 092 RECENT_FILES 093 } 094 095 /** 096 * Get the user OneDrive resources according the type of resources defined in services parameters 097 * @param zoneItemId The zone item id. Can be null or empty if itemId is not blank. 098 * @param itemId the parent folder item id 099 * @param driveId the parent folder drive id or empty for the current user drive 100 * @param contextualParameters the contextual parameters 101 * @return the OneDrive resources 102 */ 103 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 104 public Map<String, Object> getFiles(String zoneItemId, String itemId, String driveId, Map<String, Object> contextualParameters) 105 { 106 UserIdentity currentUser = _currentUserProvider.getUser(); 107 108 if (StringUtils.isNotBlank(itemId)) 109 { 110 return getChildren(currentUser, itemId, driveId, contextualParameters); 111 } 112 else 113 { 114 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 115 ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters(); 116 117 ResourceType type = ResourceType.valueOf(serviceParameters.getValue("type", false, ResourceType.USER_FILES.name())); 118 Long maxResults = serviceParameters.getValue("max-results", false, 10L); 119 120 switch (type) 121 { 122 case USER_FILES: 123 return userFiles(currentUser, zoneItem); 124 case SHARED_FILES: 125 return sharedFiles(currentUser, maxResults.intValue()); 126 case RECENT_FILES: 127 return recentFiles(currentUser, maxResults.intValue()); 128 default: 129 throw new IllegalArgumentException("Unexpected type of OneDrive resources: " + type); 130 } 131 } 132 } 133 134 /** 135 * Get the user OneDrive repository content 136 * @param user The user 137 * @param zoneItem the service zone item 138 * @return the user items 139 */ 140 public Map<String, Object> userFiles(UserIdentity user, ZoneItem zoneItem) 141 { 142 try 143 { 144 UserRequestBuilder requestBuilder = _graphClientProvider.getUserRequestBuilder(user); 145 String userDriveId = requestBuilder.drive().buildRequest().select("id").get().id; 146 147 DriveItemCollectionRequestBuilder driveRequestBuilder = requestBuilder.drive().root() 148 .children(); 149 150 List<Map<String, Object>> files = new ArrayList<>(); 151 152 do 153 { 154 DriveItemCollectionPage driveItemCollectionPage = driveRequestBuilder 155 .buildRequest() 156 .select(__DRIVE_ITEM_SELECT) 157 .get(); 158 for (DriveItem item : driveItemCollectionPage.getCurrentPage()) 159 { 160 Map<String, Object> json = _driveItem2JSON(item, userDriveId); 161 if (json != null) 162 { 163 files.add(json); 164 } 165 } 166 167 // update the request builder to fetch the next result if needed 168 driveRequestBuilder = driveItemCollectionPage.getNextPage(); 169 } 170 // loop until there is no more result to fetch or we have enough results 171 while (driveRequestBuilder != null); 172 173 return Map.of("results", files); 174 } 175 catch (GraphClientException e) 176 { 177 getLogger().warn("Failed to get a Graph Client for user " + user, e); 178 return Map.of("error", "user-unknown"); 179 } 180 catch (GraphServiceException e) 181 { 182 getLogger().warn("An error occured while contacting the graph endpoint for user " + user, e); 183 return Map.of("error", "request-failed"); 184 } 185 } 186 187 /** 188 * Get a list of recent item accessed by the current user 189 * @param user the user 190 * @param maxResults the max number of results to return 191 * @return the list of recent files 192 */ 193 public Map<String, Object> recentFiles(UserIdentity user, int maxResults) 194 { 195 try 196 { 197 UserRequestBuilder requestBuilder = _graphClientProvider.getUserRequestBuilder(user); 198 String userDriveId = requestBuilder.drive().buildRequest().select("id").get().id; 199 200 DriveRecentCollectionRequestBuilder driveRequestBuilder = requestBuilder.drive() 201 .recent(); 202 203 List<Map<String, Object>> files = new ArrayList<>(); 204 do 205 { 206 DriveRecentCollectionPage driveItemCollectionPage = driveRequestBuilder 207 .buildRequest() 208 .select(__DRIVE_ITEM_SELECT) 209 .top(maxResults) 210 .get(); 211 212 List<DriveItem> currentPage = driveItemCollectionPage.getCurrentPage(); 213 Iterator<DriveItem> iterator = currentPage.iterator(); 214 // iterate until there is no more results or we reached the max results 215 while (iterator.hasNext() && files.size() < maxResults) 216 { 217 Map<String, Object> json = _driveItem2JSON(iterator.next(), userDriveId); 218 if (json != null) 219 { 220 files.add(json); 221 } 222 } 223 224 // update the request builder to fetch the next result if needed 225 driveRequestBuilder = driveItemCollectionPage.getNextPage(); 226 } 227 // loop until there is no more result to fetch or we have enough results 228 while (driveRequestBuilder != null && files.size() < maxResults); 229 230 return Map.of("results", files); 231 } 232 catch (GraphClientException e) 233 { 234 getLogger().warn("Failed to get a Graph Client for user " + user, e); 235 return Map.of("error", "user-unknown"); 236 } 237 catch (GraphServiceException e) 238 { 239 getLogger().warn("An error occured while contacting the graph endpoint for user " + user, e); 240 return Map.of("error", "request-failed"); 241 } 242 } 243 244 /** 245 * Get a list of item shared with the current user 246 * @param user the user 247 * @param maxResults the max number of results to return 248 * @return the list of item as JSON 249 */ 250 public Map<String, Object> sharedFiles(UserIdentity user, int maxResults) 251 { 252 try 253 { 254 UserRequestBuilder requestBuilder = _graphClientProvider.getUserRequestBuilder(user); 255 String userDriveId = requestBuilder.drive().buildRequest().select("id").get().id; 256 257 DriveSharedWithMeCollectionRequestBuilder driveRequestBuilder = requestBuilder.drive() 258 .sharedWithMe(); 259 260 List<Map<String, Object>> files = new ArrayList<>(); 261 do 262 { 263 DriveSharedWithMeCollectionPage driveItemCollectionPage = driveRequestBuilder 264 .buildRequest() 265 .select(__DRIVE_ITEM_SELECT) 266 .top(maxResults) 267 .get(); 268 269 Iterator<DriveItem> currentPageIterator = driveItemCollectionPage.getCurrentPage().iterator(); 270 // iterate until there is no more results or we reached the max results 271 while (currentPageIterator.hasNext() && files.size() < maxResults) 272 { 273 Map<String, Object> json = _driveItem2JSON(currentPageIterator.next(), userDriveId); 274 // Filter item without drive Id to remove item that belongs to the user 275 // For some reason, the "sharedWithMe" endpoint also return the "sharedByMe"… 276 if (json != null && json.get("driveId") != null) 277 { 278 files.add(json); 279 } 280 } 281 282 // update the request builder to fetch the next result if needed 283 driveRequestBuilder = driveItemCollectionPage.getNextPage(); 284 } 285 // loop until there is no more result to fetch or we have enough results 286 while (driveRequestBuilder != null && files.size() < maxResults); 287 288 return Map.of("results", files); 289 } 290 catch (GraphClientException e) 291 { 292 getLogger().warn("Failed to get a Graph Client for user " + user, e); 293 return Map.of("error", "user-unknown"); 294 } 295 catch (GraphServiceException e) 296 { 297 getLogger().warn("An error occured while contacting the graph endpoint for user " + user, e); 298 return Map.of("error", "request-failed"); 299 } 300 } 301 302 /** 303 * Get resources of a OneDrive folder 304 * @param user The user identity 305 * @param itemId the parent folder item id 306 * @param driveId the parent folder drive id or empty for the current user drive 307 * @param contextualParameters The contextual parameters 308 * @return the child resources 309 */ 310 public Map<String, Object> getChildren(UserIdentity user, String itemId, String driveId, Map<String, Object> contextualParameters) 311 { 312 try 313 { 314 UserRequestBuilder requestBuilder = _graphClientProvider.getUserRequestBuilder(user); 315 316 String userDriveId = requestBuilder.drive().buildRequest().select("id").get().id; 317 318 DriveRequestBuilder driveRequestBuilder = StringUtils.isNotEmpty(driveId) ? requestBuilder.drives(driveId) : requestBuilder.drive(); 319 320 DriveItemCollectionRequestBuilder childrenRequestBuilder = driveRequestBuilder.items(itemId) 321 .children(); 322 323 List<Map<String, Object>> files = new ArrayList<>(); 324 325 do 326 { 327 DriveItemCollectionPage driveItemCollectionPage = childrenRequestBuilder 328 .buildRequest() 329 .select(__DRIVE_ITEM_SELECT) 330 .get(); 331 332 for (DriveItem item : driveItemCollectionPage.getCurrentPage()) 333 { 334 Map<String, Object> json = _driveItem2JSON(item, userDriveId); 335 if (json != null) 336 { 337 files.add(json); 338 } 339 } 340 341 // update the request builder to fetch the next result if needed 342 childrenRequestBuilder = driveItemCollectionPage.getNextPage(); 343 } 344 // loop until there is no more result to fetch 345 while (childrenRequestBuilder != null); 346 347 return Map.of("results", files); 348 } 349 catch (GraphClientException e) 350 { 351 getLogger().warn("Failed to get a Graph Client for user " + user, e); 352 return Map.of("error", "user-unknown"); 353 } 354 catch (GraphServiceException e) 355 { 356 getLogger().warn("An error occured while contacting the graph endpoint for user " + user, e); 357 return Map.of("error", "request-failed"); 358 } 359 } 360 361 /** 362 * Search resources into a OneDrive folder 363 * @param pattern the search pattern 364 * @param zoneItemId the zone item Id of the service 365 * @param contextualParameters The contextual parameters 366 * @return the child resources 367 */ 368 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 369 public Map<String, Object> searchFiles(String zoneItemId, String pattern, Map<String, Object> contextualParameters) 370 { 371 UserIdentity currentUser = _currentUserProvider.getUser(); 372 373 try 374 { 375 UserRequestBuilder userRequestBuilder = _graphClientProvider.getUserRequestBuilder(currentUser); 376 377 String userId = userRequestBuilder.buildRequest().select("id").get().id; 378 379 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 380 ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters(); 381 382 Long maxResults = serviceParameters.getValue("max-search-results", false, 5L); 383 384 DriveSearchCollectionRequestBuilder driveSearchRequestBuilder = userRequestBuilder.drive() 385 .search(DriveSearchParameterSet.newBuilder().withQ(pattern).build()); 386 387 List<Map<String, Object>> results = new ArrayList<>(); 388 boolean hasMore = true; 389 390 do 391 { 392 DriveSearchCollectionPage driveSearchCollectionPage = driveSearchRequestBuilder 393 .buildRequest() 394 .select(__DRIVE_ITEM_SELECT) 395 .top(maxResults.intValue()) 396 .get(); 397 Iterator<DriveItem> iterator = driveSearchCollectionPage 398 .getCurrentPage() 399 .iterator(); 400 401 while (iterator.hasNext() && results.size() < maxResults) 402 { 403 Map<String, Object> json = _driveItem2JSON(iterator.next(), userId); 404 if (json != null) 405 { 406 results.add(json); 407 } 408 } 409 410 driveSearchRequestBuilder = driveSearchCollectionPage.getNextPage(); 411 hasMore = iterator.hasNext() || driveSearchRequestBuilder != null; 412 } 413 while (driveSearchRequestBuilder != null && results.size() < maxResults && hasMore); 414 415 if (hasMore) 416 { 417 // There is more results, provide a link to display the search in onedrive 418 String oneDriveUrl = userRequestBuilder.drive().buildRequest().select("webUrl").get().webUrl; 419 oneDriveUrl = oneDriveUrl + "?q=" + pattern + "*&searchScope=all"; 420 return Map.of("results", results, 421 "has-more", oneDriveUrl); 422 } 423 return Map.of("results", results); 424 } 425 catch (GraphServiceException e) 426 { 427 getLogger().warn("Failed to search drive for user " + currentUser, e); 428 return Map.of("error", "request-failed"); 429 } 430 catch (GraphClientException e) 431 { 432 getLogger().warn("Failed to get a Graph Client for user " + currentUser, e); 433 return Map.of("error", "user-unknown"); 434 } 435 } 436 437 /** 438 * Get the JSON or null if the item should be ignored 439 */ 440 private Map<String, Object> _driveItem2JSON(DriveItem item, String userDriveId) 441 { 442 if (StringUtils.equals(item.name, "OneNote_RecycleBin") 443 || StringUtils.equals(item.name, "OneNote_DeletedPages.one") 444 || StringUtils.endsWith(item.name, ".onetoc2")) 445 { 446 // Ignore those special folder or files from OneDrive 447 return null; 448 } 449 450 Map<String, Object> json = new HashMap<>(); 451 json.put("itemId", item.id); 452 json.put("name", item.name); 453 if (item.webUrl != null) 454 { 455 json.put("webUrl", item.webUrl); 456 } 457 else if (item.webDavUrl != null) 458 { 459 json.put("webUrl", item.webDavUrl); 460 } 461 462 try 463 { 464 if (item.parentReference != null 465 || item.remoteItem.parentReference != null) 466 { 467 String driveId = (item.parentReference != null) ? item.parentReference.driveId : item.remoteItem.parentReference.driveId; 468 if (!StringUtils.equals(driveId, userDriveId)) 469 { 470 json.put("driveId", driveId); 471 _getItemLocation(item).ifPresent(location -> json.putAll(location)); 472 } 473 } 474 } 475 catch (GraphClientException e) 476 { 477 getLogger().info("An error occured while retrieving the location of file " + item.name, e); 478 } 479 480 // OneNote are represented as folder but we want to make them file 481 if (item.msgraphPackage != null && StringUtils.equals(item.msgraphPackage.type, "oneNote")) 482 { 483 json.putAll(Map.of( 484 "type", "file", 485 "mime-type", "application/msonenote", 486 "size", item.size 487 )); 488 } 489 else if (item.folder != null) 490 { 491 json.putAll(_folder2JSON(item)); 492 } 493 else if (item.file != null) 494 { 495 json.putAll(_file2JSON(item)); 496 } 497 return json; 498 } 499 500 private Map<String, Object> _folder2JSON(DriveItem item) 501 { 502 503 return Map.of( 504 "type", "folder", 505 "hasChildren", item.folder.childCount != 0 506 ); 507 } 508 509 private Map<String, Object> _file2JSON(DriveItem item) 510 { 511 return Map.of( 512 "type", "file", 513 "mime-type", item.file.mimeType, 514 "size", item.size 515 ); 516 } 517 518 private Optional<Map<String, Object>> _getItemLocation(DriveItem item) throws GraphClientException 519 { 520 // Use a sharepoint id as primary means to determine an item location 521 if (item.sharepointIds != null 522 || item.remoteItem != null && item.remoteItem.sharepointIds != null) 523 { 524 String siteId = item.sharepointIds != null ? item.sharepointIds.siteId : item.remoteItem.sharepointIds.siteId; 525 Site site = _graphClientProvider.getGraphClient().sites(siteId) 526 .buildRequest() 527 .select("displayName,webUrl") 528 .get(); 529 return Optional.of(_locationToI18N(site.displayName, StringUtils.contains(site.webUrl, "/personal/"))); 530 } 531 532 // Use the creator as a fallback 533 if (item.createdBy != null && item.createdBy.user != null) 534 { 535 return Optional.of(_locationToI18N(item.createdBy.user.displayName, true)); 536 } 537 else if (item.remoteItem != null && item.remoteItem.createdBy != null && item.remoteItem.createdBy.user != null) 538 { 539 return Optional.of(_locationToI18N(item.remoteItem.createdBy.user.displayName, true)); 540 } 541 542 return Optional.empty(); 543 } 544 545 private Map<String, Object> _locationToI18N(String name, boolean isUser) 546 { 547 return isUser 548 ? Map.of("driveType", "user", 549 "driveLabel", name) 550 : Map.of("driveType", "sharepoint", 551 "driveLabel", name); 552 } 553}