From 8c67bd97a1d9fe07c0d8616e1c6dd0994a844245 Mon Sep 17 00:00:00 2001 From: stetsche <53173679+stetsche@users.noreply.github.com> Date: Tue, 23 Jan 2024 13:29:53 +0100 Subject: [PATCH] S2U-4 Microsoft integrations working package - MASTER (#12273) --- .gitignore | 3 +- cloud-storage/README.md | 2 +- cloud-storage/onedrive/README.md | 38 - cloud-storage/onedrive/api/pom.xml | 45 - .../onedrive/model/OneDriveItem.java | 66 - .../onedrive/model/OneDriveUser.java | 48 - .../onedrive/service/OneDriveService.java | 71 - cloud-storage/onedrive/impl/pom.xml | 97 - .../onedrive/service/OneDriveServiceImpl.java | 311 --- .../onedrive/util/HTTPConnectionUtil.java | 124 - .../impl/src/webapp/WEB-INF/components.xml | 28 - cloud-storage/onedrive/pom.xml | 43 - .../onedrive/tool/MainController.java | 66 - .../src/main/resources/Messages.properties | 4 - .../src/main/resources/Messages_ar.properties | 4 - .../src/main/resources/Messages_ca.properties | 4 - .../src/main/resources/Messages_es.properties | 4 - .../main/resources/Messages_fr_FR.properties | 4 - .../main/resources/Messages_ro_RO.properties | 4 - .../main/resources/Messages_tr_TR.properties | 4 - .../main/webapp/WEB-INF/templates/index.html | 23 - cloud-storage/pom.xml | 1 - .../localization/bundle/tool/tools.properties | 6 + .../bundle/tool/tools_ca.properties | 6 + .../bundle/tool/tools_es.properties | 6 + .../bundle/tool/tools_eu.properties | 6 + content/content-tool/tool/pom.xml | 10 +- .../tool/src/bundle/helper.properties | 5 + .../tool/src/bundle/helper_ca.properties | 5 + .../tool/src/bundle/helper_es.properties | 5 + .../tool/src/bundle/helper_eu.properties | 26 + .../content/tool/FilePickerAction.java | 443 +++- .../content/cloudstorage_onedrive_picker.vm | 249 +- .../vm/content/sakai_filepicker_attach.vm | 5 + .../content/types/FileUploadType.java | 9 + .../content/api/ResourceType.java | 1 + .../messaging/api/MicrosoftMessage.java | 79 + .../api/MicrosoftMessageListener.java | 17 +- .../api/MicrosoftMessagingService.java | 23 + .../org/sakaiproject/config/kernel.properties | 3 +- .../authz/impl/AuthzGroupServiceTest.java | 6 + .../authz/impl/BaseAuthzGroup.java | 27 + .../authz/impl/BaseAuthzGroupService.java | 3 + .../content/impl/BaseContentService.java | 8 +- .../impl/MicrosoftMessagingServiceImpl.java | 69 + .../org/sakaiproject/site/impl/BaseSite.java | 33 +- .../site/impl/BaseSiteService.java | 32 + .../src/main/sql/mysql/sakai_site.sql | 5 + .../main/webapp/WEB-INF/authz-components.xml | 1 + .../webapp/WEB-INF/messaging-components.xml | 5 + .../main/webapp/WEB-INF/site-components.xml | 1 + .../impl/test/ContreteAuthzGroupService.java | 7 + .../site/impl/SiteServiceTest.java | 7 + .../skins/default/src/sass/base/_icons.scss | 3 + .../microsoft-admin/_microsoft-admin.scss | 250 ++ .../_microsoft-collaborativedocuments.scss | 150 ++ .../_microsoft-mediagallery.scss | 217 ++ library/src/skins/default/src/sass/tool.scss | 3 + master/pom.xml | 12 +- meetings/README.md | 111 + meetings/api/pom.xml | 37 + .../meetings/api/MeetingService.java | 38 + .../meetings/api/model/AttendeeType.java | 22 + .../meetings/api/model/Meeting.java | 104 + .../meetings/api/model/MeetingAttendee.java | 59 + .../meetings/api/model/MeetingProperty.java | 60 + .../meetings/api/model/MeetingsProvider.java | 50 + .../MeetingAttendeeRepository.java | 26 + .../MeetingPropertyRepository.java | 29 + .../api/persistence/MeetingRepository.java | 32 + meetings/impl/pom.xml | 110 + .../meetings/impl/MeetingServiceImpl.java | 117 + .../MeetingAttendeeRepositoryImpl.java | 39 + .../MeetingPropertyRepositoryImpl.java | 53 + .../persistence/MeetingRepositoryImpl.java | 97 + .../meetings/impl/MeetingServiceImplTest.java | 252 ++ .../impl/MeetingsImplTestConfiguration.java | 106 + .../MeetingAttendeeRepositoryImplTest.java | 84 + .../MeetingPropertyRepositoryImplTest.java | 72 + .../MeetingRepositoryImplTest.java | 118 + .../src/test/resources/hibernate.properties | 14 + .../impl/src/webapp/WEB-INF/components.xml | 47 + meetings/pom.xml | 24 + .../onedrive => meetings}/tool/pom.xml | 88 +- .../meetings}/ThymeleafConfig.java | 17 +- .../meetings/WebAppConfiguration.java | 54 + .../meetings/controller/GlobalController.java | 54 + .../meetings/controller/MainController.java | 42 + .../controller/MeetingsController.java | 700 +++++ .../meetings/controller/data/GroupData.java | 22 +- .../meetings/controller/data/MeetingData.java | 47 + .../controller/data/NotificationType.java | 21 + .../controller/data/ParticipantData.java | 25 +- .../exceptions/MeetingsException.java | 26 + .../src/main/resources/Messages.properties | 2 + .../src/main/resources/Messages_ca.properties | 2 + .../src/main/resources/Messages_es.properties | 2 + .../src/main/resources/Messages_eu.properties | 2 + .../tool/src/main/resources/card.properties | 20 + .../src/main/resources/card_ca.properties | 20 + .../src/main/resources/card_es.properties | 20 + .../src/main/resources/card_eu.properties | 20 + .../main/resources/create-meeting.properties | 26 + .../resources/create-meeting_ca.properties | 26 + .../resources/create-meeting_es.properties | 26 + .../resources/create-meeting_eu.properties | 26 + .../tool/src/main/resources/main.properties | 10 + .../src/main/resources/main_ca.properties | 11 + .../src/main/resources/main_es.properties | 10 + .../src/main/resources/main_eu.properties | 10 + .../resources/meeting-recordings.properties | 5 + .../meeting-recordings_ca.properties | 5 + .../meeting-recordings_es.properties | 5 + .../meeting-recordings_eu.properties | 5 + .../src/main/resources/validations.properties | 3 + .../main/resources/validations_ca.properties | 3 + .../main/resources/validations_es.properties | 3 + .../main/resources/validations_eu.properties | 3 + .../main/webapp/WEB-INF/templates/index.html | 17 + .../webapp/WEB-INF/tools/sakai.meetings.xml | 10 + meetings/tool/src/main/webapp/WEB-INF/web.xml | 7 + meetings/ui/pom.xml | 102 + meetings/ui/src/main/frontend/README.md | 7 + meetings/ui/src/main/frontend/index.html | 13 + .../ui/src/main/frontend/package-lock.json | 1030 ++++++++ meetings/ui/src/main/frontend/package.json | 25 + .../ui/src/main/frontend/public/favicon.ico | Bin 0 -> 4286 bytes meetings/ui/src/main/frontend/src/App.vue | 36 + .../src/components/sakai-accordion-item.vue | 114 + .../src/components/sakai-accordion.vue | 24 + .../src/components/sakai-avatar-list.vue | 66 + .../frontend/src/components/sakai-avatar.vue | 128 + .../frontend/src/components/sakai-button.vue | 146 ++ .../src/components/sakai-dropdown-button.vue | 109 + .../src/components/sakai-dropdown.vue | 145 ++ .../frontend/src/components/sakai-icon.vue | 42 + .../src/components/sakai-input-labelled.vue | 124 + .../frontend/src/components/sakai-input.vue | 177 ++ .../src/components/sakai-meeting-card.vue | 418 +++ .../frontend/src/components/sakai-modal.vue | 107 + .../src/components/sakai-radio-group.vue | 63 + .../src/components/sakai-recording.vue | 52 + .../frontend/src/components/sakai-select.vue | 77 + .../sakai-selected-participants.vue | 79 + .../src/components/sakai-textarea.vue | 100 + meetings/ui/src/main/frontend/src/main.js | 14 + .../src/main/frontend/src/mixins/i18n-mixn.js | 24 + .../src/main/frontend/src/mixins/uid-mixin.js | 13 + .../frontend/src/mixins/validation-mixin.js | 115 + .../main/frontend/src/resources/constants.js | 5 + .../src/main/frontend/src/resources/icons.js | 49 + .../frontend/src/resources/meetings-i18n.js | 100 + .../ui/src/main/frontend/src/router/index.js | 43 + .../src/main/frontend/src/stores/dataStore.js | 14 + .../main/frontend/src/views/CreateMeeting.vue | 392 +++ .../ui/src/main/frontend/src/views/Main.vue | 312 +++ .../frontend/src/views/MeetingRecordings.vue | 105 + .../main/frontend/src/views/Permissions.vue | 36 + meetings/ui/src/main/frontend/vite.config.js | 16 + microsoft-integration/README.md | 180 ++ microsoft-integration/admin-tool/pom.xml | 117 + .../microsoft/ThymeleafConfig.java | 102 + .../microsoft/WebAppConfiguration.java | 54 + .../controller/AutoConfigController.java | 386 +++ .../controller/ConfigController.java | 96 + .../controller/CredentialsController.java | 87 + .../controller/GlobalController.java | 99 + .../GroupSynchronizationController.java | 167 ++ .../microsoft/controller/MainController.java | 366 +++ .../SiteSynchronizationController.java | 178 ++ .../controller/auxiliar/AjaxResponse.java | 27 + .../auxiliar/AutoConfigConfirmRequest.java | 30 +- .../controller/auxiliar/AutoConfigError.java | 27 + .../auxiliar/AutoConfigRequest.java | 30 + .../auxiliar/AutoConfigSessionBean.java | 69 + .../controller/auxiliar/ConfigRequest.java | 39 + .../auxiliar/GroupSynchronizationRequest.java | 27 + .../controller/auxiliar/MainSessionBean.java | 29 + .../auxiliar/SiteSynchronizationRequest.java | 36 + .../src/main/resources/Messages.properties | 208 ++ .../src/main/resources/Messages_ca.properties | 208 ++ .../src/main/resources/Messages_es.properties | 208 ++ .../src/main/resources/Messages_eu.properties | 205 ++ .../main/webapp/WEB-INF/templates/body.html | 117 + .../main/webapp/WEB-INF/templates/config.html | 121 + .../webapp/WEB-INF/templates/credentials.html | 87 + .../templates/editGroupSynchronization.html | 214 ++ .../WEB-INF/templates/errorNoAdmin.html | 11 + .../templates/fragments/autoConfig.html | 378 +++ .../WEB-INF/templates/fragments/common.html | 42 + .../WEB-INF/templates/fragments/menus.html | 39 + .../templates/fragments/siteFilter.html | 56 + .../fragments/synchronizationRow.html | 98 + .../main/webapp/WEB-INF/templates/index.html | 266 ++ .../templates/newSiteSynchronization.html | 329 +++ .../webapp/WEB-INF/tools/microsoft.admin.xml | 7 + .../src/main/webapp/WEB-INF/web.xml | 7 + .../admin-tool/src/main/webapp/js/common.js | 48 + microsoft-integration/api/pom.xml | 61 + .../api/src/bundle/microsoft.properties | 6 + .../api/src/bundle/microsoft_ca.properties | 6 + .../api/src/bundle/microsoft_es.properties | 6 + .../api/src/bundle/microsoft_eu.properties | 6 + .../api/MicrosoftAuthorizationService.java | 61 + .../microsoft/api/MicrosoftCommonService.java | 146 ++ .../api/MicrosoftConfigurationService.java | 65 + .../api/MicrosoftLoggingService.java | 38 + .../api/MicrosoftSynchronizationService.java | 55 + .../microsoft/api/SakaiProxy.java | 78 + .../api/converters/JpaConverterMap.java | 56 + ...onverterMicrosoftAuthorizationAccount.java | 55 + .../JpaConverterSynchronizationStatus.java | 42 + .../api/data/GenericMembersCollection.java | 71 + .../api/data/MeetingRecordingData.java | 28 + .../data/MicrosoftAuthorizationAccount.java | 33 + .../microsoft/api/data/MicrosoftChannel.java | 32 + .../api/data/MicrosoftCredentials.java | 80 + .../api/data/MicrosoftDriveItem.java | 159 ++ .../api/data/MicrosoftDriveItemFilter.java | 66 + .../api/data/MicrosoftMembersCollection.java | 88 + .../api/data/MicrosoftRedirectURL.java | 32 + .../microsoft/api/data/MicrosoftTeam.java | 34 + .../api/data/MicrosoftTeamWrapper.java | 57 + .../microsoft/api/data/MicrosoftUser.java | 47 + .../api/data/MicrosoftUserIdentifier.java | 43 + .../api/data/SakaiCalendarEvent.java | 38 + .../api/data/SakaiMembersCollection.java | 78 + .../microsoft/api/data/SakaiSiteFilter.java | 107 + .../api/data/SakaiUserIdentifier.java | 46 + .../api/data/SynchronizationStatus.java | 43 +- .../microsoft/api/data/TeamsMeetingData.java | 26 + .../api/exceptions/AjaxException.java | 24 + .../MicrosoftCredentialsException.java | 24 + .../exceptions/MicrosoftGenericException.java | 24 + .../MicrosoftInvalidCredentialsException.java | 24 + .../MicrosoftInvalidInvitationException.java | 24 + .../MicrosoftInvalidTeamException.java | 24 + .../MicrosoftInvalidTokenException.java | 24 + .../MicrosoftNoCredentialsException.java | 24 + .../api/exceptions/NoAdminException.java | 24 + .../api/model/GroupSynchronization.java | 75 + .../api/model/MicrosoftAccessToken.java | 83 + .../api/model/MicrosoftConfigItem.java | 54 + .../microsoft/api/model/MicrosoftLog.java | 120 + .../api/model/SiteSynchronization.java | 104 + .../MicrosoftAccessTokenRepository.java | 24 + .../MicrosoftConfigRepository.java | 91 + ...crosoftGroupSynchronizationRepository.java | 32 + .../MicrosoftLoggingRepository.java | 12 +- ...icrosoftSiteSynchronizationRepository.java | 34 + .../authorization-tool/pom.xml | 104 + .../authorization/ThymeleafConfig.java | 87 + .../authorization}/WebAppConfiguration.java | 6 +- .../controller/MainController.java | 106 + .../src/main/resources/Messages.properties | 4 + .../src/main/resources/Messages_ar.properties | 4 + .../src/main/resources/Messages_bg.properties | 0 .../src/main/resources/Messages_ca.properties | 4 + .../main/resources/Messages_de_DE.properties | 2 +- .../src/main/resources/Messages_es.properties | 4 + .../src/main/resources/Messages_eu.properties | 0 .../main/resources/Messages_fr_FR.properties | 4 + .../main/resources/Messages_ro_RO.properties | 4 + .../main/resources/Messages_tr_TR.properties | 4 + .../main/webapp/WEB-INF/templates/index.html | 27 + .../main/webapp/WEB-INF/templates/token.html | 25 + .../src/main/webapp/WEB-INF/web.xml | 0 .../collaborative-documents/pom.xml | 324 +++ .../ThymeleafConfig.java | 106 + .../WebAppConfiguration.java | 52 + .../CollaborativeDocumentsSessionBean.java | 53 + .../controller/MainController.java | 633 +++++ .../src/main/resources/Messages.properties | 77 + .../src/main/resources/Messages_ca.properties | 77 + .../src/main/resources/Messages_es.properties | 77 + .../src/main/resources/Messages_eu.properties | 77 + .../main/webapp/WEB-INF/templates/body.html | 35 + .../main/webapp/WEB-INF/templates/error.html | 13 + .../WEB-INF/templates/fragments/common.html | 25 + .../templates/fragments/driveItems.html | 147 ++ .../WEB-INF/templates/fragments/menus.html | 22 + .../main/webapp/WEB-INF/templates/index.html | 114 + .../webapp/WEB-INF/templates/js/common.js | 513 ++++ .../webapp/WEB-INF/templates/permissions.html | 13 + .../microsoft.collaborativedocuments.xml | 8 + .../src/main/webapp/WEB-INF/web.xml | 7 + microsoft-integration/docs/images/1.png | Bin 0 -> 113873 bytes microsoft-integration/docs/images/2.png | Bin 0 -> 46784 bytes microsoft-integration/docs/images/3.png | Bin 0 -> 113515 bytes microsoft-integration/docs/images/4.png | Bin 0 -> 102303 bytes microsoft-integration/impl/pom.xml | 96 + .../MicrosoftAuthorizationServiceImpl.java | 288 +++ .../impl/MicrosoftCommonServiceImpl.java | 2241 +++++++++++++++++ .../MicrosoftConfigurationServiceImpl.java | 147 ++ .../impl/MicrosoftLoggingServiceImpl.java | 44 + .../MicrosoftSynchronizationServiceImpl.java | 1628 ++++++++++++ .../microsoft/impl/SakaiProxyImpl.java | 417 +++ .../impl/jobs/RunSynchronizationsJob.java | 88 + .../MicrosoftAccessTokenRepositoryImpl.java | 19 +- .../MicrosoftConfigRepositoryImpl.java | 172 ++ ...oftGroupSynchronizationRepositoryImpl.java | 89 + .../MicrosoftLoggingRepositoryImpl.java | 24 + ...softSiteSynchronizationRepositoryImpl.java | 107 + .../microsoft/provider/AdminAuthProvider.java | 89 + .../provider/DelegatedAuthProvider.java | 172 ++ .../impl/src/webapp/WEB-INF/components.xml | 102 + .../media-gallery-tool/pom.xml | 120 + .../mediagallery/ThymeleafConfig.java | 97 + .../mediagallery/WebAppConfiguration.java | 53 + .../auxiliar/MediaGallerySessionBean.java | 116 + .../controller/MainController.java | 456 ++++ .../src/main/resources/Messages.properties | 59 + .../src/main/resources/Messages_ca.properties | 59 + .../src/main/resources/Messages_es.properties | 59 + .../src/main/resources/Messages_eu.properties | 59 + .../main/webapp/WEB-INF/templates/body.html | 158 ++ .../main/webapp/WEB-INF/templates/error.html | 11 + .../WEB-INF/templates/fragments/common.html | 24 + .../templates/fragments/driveItems.html | 71 + .../WEB-INF/templates/fragments/menus.html | 22 + .../main/webapp/WEB-INF/templates/index.html | 27 + .../webapp/WEB-INF/templates/index_ws.html | 29 + .../main/webapp/WEB-INF/templates/info.html | 52 + .../webapp/WEB-INF/templates/js/common.js | 375 +++ .../webapp/WEB-INF/templates/permissions.html | 13 + .../WEB-INF/tools/microsoft.mediagallery.xml | 9 + .../src/main/webapp/WEB-INF/web.xml | 7 + microsoft-integration/pom.xml | 48 + pom.xml | 2 + .../controller/GroupController.java | 86 +- 330 files changed, 26891 insertions(+), 1286 deletions(-) delete mode 100644 cloud-storage/onedrive/README.md delete mode 100644 cloud-storage/onedrive/api/pom.xml delete mode 100644 cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveItem.java delete mode 100644 cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveUser.java delete mode 100644 cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/service/OneDriveService.java delete mode 100644 cloud-storage/onedrive/impl/pom.xml delete mode 100644 cloud-storage/onedrive/impl/src/java/org/sakaiproject/onedrive/service/OneDriveServiceImpl.java delete mode 100644 cloud-storage/onedrive/impl/src/java/org/sakaiproject/onedrive/util/HTTPConnectionUtil.java delete mode 100644 cloud-storage/onedrive/impl/src/webapp/WEB-INF/components.xml delete mode 100644 cloud-storage/onedrive/pom.xml delete mode 100644 cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/tool/MainController.java delete mode 100644 cloud-storage/onedrive/tool/src/main/resources/Messages.properties delete mode 100644 cloud-storage/onedrive/tool/src/main/resources/Messages_ar.properties delete mode 100644 cloud-storage/onedrive/tool/src/main/resources/Messages_ca.properties delete mode 100644 cloud-storage/onedrive/tool/src/main/resources/Messages_es.properties delete mode 100644 cloud-storage/onedrive/tool/src/main/resources/Messages_fr_FR.properties delete mode 100644 cloud-storage/onedrive/tool/src/main/resources/Messages_ro_RO.properties delete mode 100644 cloud-storage/onedrive/tool/src/main/resources/Messages_tr_TR.properties delete mode 100644 cloud-storage/onedrive/tool/src/main/webapp/WEB-INF/templates/index.html create mode 100644 kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessage.java rename cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFolder.java => kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessageListener.java (62%) create mode 100644 kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessagingService.java create mode 100644 kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/MicrosoftMessagingServiceImpl.java create mode 100644 library/src/skins/default/src/sass/modules/tool/microsoft-admin/_microsoft-admin.scss create mode 100644 library/src/skins/default/src/sass/modules/tool/microsoft-collaborativedocuments/_microsoft-collaborativedocuments.scss create mode 100644 library/src/skins/default/src/sass/modules/tool/microsoft-mediagallery/_microsoft-mediagallery.scss create mode 100644 meetings/README.md create mode 100644 meetings/api/pom.xml create mode 100644 meetings/api/src/java/org/sakaiproject/meetings/api/MeetingService.java create mode 100644 meetings/api/src/java/org/sakaiproject/meetings/api/model/AttendeeType.java create mode 100644 meetings/api/src/java/org/sakaiproject/meetings/api/model/Meeting.java create mode 100644 meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingAttendee.java create mode 100644 meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingProperty.java create mode 100644 meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingsProvider.java create mode 100644 meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingAttendeeRepository.java create mode 100644 meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingPropertyRepository.java create mode 100644 meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingRepository.java create mode 100644 meetings/impl/pom.xml create mode 100644 meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java create mode 100644 meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImpl.java create mode 100644 meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImpl.java create mode 100644 meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImpl.java create mode 100644 meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingServiceImplTest.java create mode 100644 meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingsImplTestConfiguration.java create mode 100644 meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImplTest.java create mode 100644 meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImplTest.java create mode 100644 meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImplTest.java create mode 100644 meetings/impl/src/test/resources/hibernate.properties create mode 100644 meetings/impl/src/webapp/WEB-INF/components.xml create mode 100644 meetings/pom.xml rename {cloud-storage/onedrive => meetings}/tool/pom.xml (66%) rename {cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive => meetings/tool/src/main/java/org/sakaiproject/meetings}/ThymeleafConfig.java (88%) create mode 100644 meetings/tool/src/main/java/org/sakaiproject/meetings/WebAppConfiguration.java create mode 100644 meetings/tool/src/main/java/org/sakaiproject/meetings/controller/GlobalController.java create mode 100644 meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MainController.java create mode 100644 meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MeetingsController.java rename cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFile.java => meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/GroupData.java (63%) create mode 100644 meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/MeetingData.java create mode 100644 meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/NotificationType.java rename cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveParent.java => meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/ParticipantData.java (57%) create mode 100644 meetings/tool/src/main/java/org/sakaiproject/meetings/exceptions/MeetingsException.java create mode 100644 meetings/tool/src/main/resources/Messages.properties create mode 100644 meetings/tool/src/main/resources/Messages_ca.properties create mode 100644 meetings/tool/src/main/resources/Messages_es.properties create mode 100644 meetings/tool/src/main/resources/Messages_eu.properties create mode 100644 meetings/tool/src/main/resources/card.properties create mode 100644 meetings/tool/src/main/resources/card_ca.properties create mode 100644 meetings/tool/src/main/resources/card_es.properties create mode 100644 meetings/tool/src/main/resources/card_eu.properties create mode 100644 meetings/tool/src/main/resources/create-meeting.properties create mode 100644 meetings/tool/src/main/resources/create-meeting_ca.properties create mode 100644 meetings/tool/src/main/resources/create-meeting_es.properties create mode 100644 meetings/tool/src/main/resources/create-meeting_eu.properties create mode 100644 meetings/tool/src/main/resources/main.properties create mode 100644 meetings/tool/src/main/resources/main_ca.properties create mode 100644 meetings/tool/src/main/resources/main_es.properties create mode 100644 meetings/tool/src/main/resources/main_eu.properties create mode 100644 meetings/tool/src/main/resources/meeting-recordings.properties create mode 100644 meetings/tool/src/main/resources/meeting-recordings_ca.properties create mode 100644 meetings/tool/src/main/resources/meeting-recordings_es.properties create mode 100644 meetings/tool/src/main/resources/meeting-recordings_eu.properties create mode 100644 meetings/tool/src/main/resources/validations.properties create mode 100644 meetings/tool/src/main/resources/validations_ca.properties create mode 100644 meetings/tool/src/main/resources/validations_es.properties create mode 100644 meetings/tool/src/main/resources/validations_eu.properties create mode 100644 meetings/tool/src/main/webapp/WEB-INF/templates/index.html create mode 100644 meetings/tool/src/main/webapp/WEB-INF/tools/sakai.meetings.xml create mode 100644 meetings/tool/src/main/webapp/WEB-INF/web.xml create mode 100644 meetings/ui/pom.xml create mode 100644 meetings/ui/src/main/frontend/README.md create mode 100644 meetings/ui/src/main/frontend/index.html create mode 100644 meetings/ui/src/main/frontend/package-lock.json create mode 100644 meetings/ui/src/main/frontend/package.json create mode 100644 meetings/ui/src/main/frontend/public/favicon.ico create mode 100644 meetings/ui/src/main/frontend/src/App.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-accordion-item.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-accordion.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-avatar-list.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-avatar.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-button.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-dropdown-button.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-dropdown.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-icon.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-input-labelled.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-input.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-modal.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-radio-group.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-recording.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-select.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-selected-participants.vue create mode 100644 meetings/ui/src/main/frontend/src/components/sakai-textarea.vue create mode 100644 meetings/ui/src/main/frontend/src/main.js create mode 100644 meetings/ui/src/main/frontend/src/mixins/i18n-mixn.js create mode 100644 meetings/ui/src/main/frontend/src/mixins/uid-mixin.js create mode 100644 meetings/ui/src/main/frontend/src/mixins/validation-mixin.js create mode 100644 meetings/ui/src/main/frontend/src/resources/constants.js create mode 100644 meetings/ui/src/main/frontend/src/resources/icons.js create mode 100644 meetings/ui/src/main/frontend/src/resources/meetings-i18n.js create mode 100644 meetings/ui/src/main/frontend/src/router/index.js create mode 100644 meetings/ui/src/main/frontend/src/stores/dataStore.js create mode 100644 meetings/ui/src/main/frontend/src/views/CreateMeeting.vue create mode 100644 meetings/ui/src/main/frontend/src/views/Main.vue create mode 100644 meetings/ui/src/main/frontend/src/views/MeetingRecordings.vue create mode 100644 meetings/ui/src/main/frontend/src/views/Permissions.vue create mode 100644 meetings/ui/src/main/frontend/vite.config.js create mode 100644 microsoft-integration/README.md create mode 100644 microsoft-integration/admin-tool/pom.xml create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/ThymeleafConfig.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/WebAppConfiguration.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/AutoConfigController.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/ConfigController.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/CredentialsController.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/GlobalController.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/GroupSynchronizationController.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/MainController.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/SiteSynchronizationController.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AjaxResponse.java rename cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveToken.java => microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigConfirmRequest.java (52%) create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigError.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigRequest.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/AutoConfigSessionBean.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/ConfigRequest.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/GroupSynchronizationRequest.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/MainSessionBean.java create mode 100644 microsoft-integration/admin-tool/src/main/java/org/sakaiproject/microsoft/controller/auxiliar/SiteSynchronizationRequest.java create mode 100644 microsoft-integration/admin-tool/src/main/resources/Messages.properties create mode 100644 microsoft-integration/admin-tool/src/main/resources/Messages_ca.properties create mode 100644 microsoft-integration/admin-tool/src/main/resources/Messages_es.properties create mode 100644 microsoft-integration/admin-tool/src/main/resources/Messages_eu.properties create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/body.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/config.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/credentials.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/editGroupSynchronization.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/errorNoAdmin.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/fragments/autoConfig.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/fragments/common.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/fragments/menus.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/fragments/siteFilter.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/fragments/synchronizationRow.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/index.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/templates/newSiteSynchronization.html create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/tools/microsoft.admin.xml create mode 100644 microsoft-integration/admin-tool/src/main/webapp/WEB-INF/web.xml create mode 100644 microsoft-integration/admin-tool/src/main/webapp/js/common.js create mode 100644 microsoft-integration/api/pom.xml create mode 100644 microsoft-integration/api/src/bundle/microsoft.properties create mode 100644 microsoft-integration/api/src/bundle/microsoft_ca.properties create mode 100644 microsoft-integration/api/src/bundle/microsoft_es.properties create mode 100644 microsoft-integration/api/src/bundle/microsoft_eu.properties create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftAuthorizationService.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftCommonService.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftConfigurationService.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftLoggingService.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/MicrosoftSynchronizationService.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/SakaiProxy.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/converters/JpaConverterMap.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/converters/JpaConverterMicrosoftAuthorizationAccount.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/converters/JpaConverterSynchronizationStatus.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/GenericMembersCollection.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MeetingRecordingData.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftAuthorizationAccount.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftChannel.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftCredentials.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftDriveItem.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftDriveItemFilter.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftMembersCollection.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftRedirectURL.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftTeam.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftTeamWrapper.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftUser.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/MicrosoftUserIdentifier.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/SakaiCalendarEvent.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/SakaiMembersCollection.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/SakaiSiteFilter.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/SakaiUserIdentifier.java rename cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveItemComparator.java => microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/SynchronizationStatus.java (50%) create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/data/TeamsMeetingData.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/exceptions/AjaxException.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/exceptions/MicrosoftCredentialsException.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/exceptions/MicrosoftGenericException.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/exceptions/MicrosoftInvalidCredentialsException.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/exceptions/MicrosoftInvalidInvitationException.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/exceptions/MicrosoftInvalidTeamException.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/exceptions/MicrosoftInvalidTokenException.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/exceptions/MicrosoftNoCredentialsException.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/exceptions/NoAdminException.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/model/GroupSynchronization.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/model/MicrosoftAccessToken.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/model/MicrosoftConfigItem.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/model/MicrosoftLog.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/model/SiteSynchronization.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/persistence/MicrosoftAccessTokenRepository.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/persistence/MicrosoftConfigRepository.java create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/persistence/MicrosoftGroupSynchronizationRepository.java rename cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/repository/OneDriveUserRepository.java => microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/persistence/MicrosoftLoggingRepository.java (68%) create mode 100644 microsoft-integration/api/src/java/org/sakaiproject/microsoft/api/persistence/MicrosoftSiteSynchronizationRepository.java create mode 100644 microsoft-integration/authorization-tool/pom.xml create mode 100644 microsoft-integration/authorization-tool/src/main/java/org/sakaiproject/microsoft/authorization/ThymeleafConfig.java rename {cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive => microsoft-integration/authorization-tool/src/main/java/org/sakaiproject/microsoft/authorization}/WebAppConfiguration.java (90%) create mode 100644 microsoft-integration/authorization-tool/src/main/java/org/sakaiproject/microsoft/authorization/controller/MainController.java create mode 100644 microsoft-integration/authorization-tool/src/main/resources/Messages.properties create mode 100644 microsoft-integration/authorization-tool/src/main/resources/Messages_ar.properties rename {cloud-storage/onedrive/tool => microsoft-integration/authorization-tool}/src/main/resources/Messages_bg.properties (100%) create mode 100644 microsoft-integration/authorization-tool/src/main/resources/Messages_ca.properties rename {cloud-storage/onedrive/tool => microsoft-integration/authorization-tool}/src/main/resources/Messages_de_DE.properties (69%) create mode 100644 microsoft-integration/authorization-tool/src/main/resources/Messages_es.properties rename {cloud-storage/onedrive/tool => microsoft-integration/authorization-tool}/src/main/resources/Messages_eu.properties (100%) create mode 100644 microsoft-integration/authorization-tool/src/main/resources/Messages_fr_FR.properties create mode 100644 microsoft-integration/authorization-tool/src/main/resources/Messages_ro_RO.properties create mode 100644 microsoft-integration/authorization-tool/src/main/resources/Messages_tr_TR.properties create mode 100644 microsoft-integration/authorization-tool/src/main/webapp/WEB-INF/templates/index.html create mode 100644 microsoft-integration/authorization-tool/src/main/webapp/WEB-INF/templates/token.html rename {cloud-storage/onedrive/tool => microsoft-integration/authorization-tool}/src/main/webapp/WEB-INF/web.xml (100%) create mode 100644 microsoft-integration/collaborative-documents/pom.xml create mode 100644 microsoft-integration/collaborative-documents/src/main/java/org/sakaiproject/microsoft/collaborativedocuments/ThymeleafConfig.java create mode 100644 microsoft-integration/collaborative-documents/src/main/java/org/sakaiproject/microsoft/collaborativedocuments/WebAppConfiguration.java create mode 100644 microsoft-integration/collaborative-documents/src/main/java/org/sakaiproject/microsoft/collaborativedocuments/auxiliar/CollaborativeDocumentsSessionBean.java create mode 100644 microsoft-integration/collaborative-documents/src/main/java/org/sakaiproject/microsoft/collaborativedocuments/controller/MainController.java create mode 100644 microsoft-integration/collaborative-documents/src/main/resources/Messages.properties create mode 100644 microsoft-integration/collaborative-documents/src/main/resources/Messages_ca.properties create mode 100644 microsoft-integration/collaborative-documents/src/main/resources/Messages_es.properties create mode 100644 microsoft-integration/collaborative-documents/src/main/resources/Messages_eu.properties create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/templates/body.html create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/templates/error.html create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/templates/fragments/common.html create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/templates/fragments/driveItems.html create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/templates/fragments/menus.html create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/templates/index.html create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/templates/js/common.js create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/templates/permissions.html create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/tools/microsoft.collaborativedocuments.xml create mode 100644 microsoft-integration/collaborative-documents/src/main/webapp/WEB-INF/web.xml create mode 100644 microsoft-integration/docs/images/1.png create mode 100644 microsoft-integration/docs/images/2.png create mode 100644 microsoft-integration/docs/images/3.png create mode 100644 microsoft-integration/docs/images/4.png create mode 100644 microsoft-integration/impl/pom.xml create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftAuthorizationServiceImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftCommonServiceImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftConfigurationServiceImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftLoggingServiceImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/MicrosoftSynchronizationServiceImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/SakaiProxyImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/jobs/RunSynchronizationsJob.java rename cloud-storage/onedrive/impl/src/java/org/sakaiproject/onedrive/repository/OneDriveUserRepositoryImpl.java => microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/persistence/MicrosoftAccessTokenRepositoryImpl.java (63%) create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/persistence/MicrosoftConfigRepositoryImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/persistence/MicrosoftGroupSynchronizationRepositoryImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/persistence/MicrosoftLoggingRepositoryImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/impl/persistence/MicrosoftSiteSynchronizationRepositoryImpl.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/provider/AdminAuthProvider.java create mode 100644 microsoft-integration/impl/src/main/java/org/sakaiproject/microsoft/provider/DelegatedAuthProvider.java create mode 100644 microsoft-integration/impl/src/webapp/WEB-INF/components.xml create mode 100644 microsoft-integration/media-gallery-tool/pom.xml create mode 100644 microsoft-integration/media-gallery-tool/src/main/java/org/sakaiproject/microsoft/mediagallery/ThymeleafConfig.java create mode 100644 microsoft-integration/media-gallery-tool/src/main/java/org/sakaiproject/microsoft/mediagallery/WebAppConfiguration.java create mode 100644 microsoft-integration/media-gallery-tool/src/main/java/org/sakaiproject/microsoft/mediagallery/auxiliar/MediaGallerySessionBean.java create mode 100644 microsoft-integration/media-gallery-tool/src/main/java/org/sakaiproject/microsoft/mediagallery/controller/MainController.java create mode 100644 microsoft-integration/media-gallery-tool/src/main/resources/Messages.properties create mode 100644 microsoft-integration/media-gallery-tool/src/main/resources/Messages_ca.properties create mode 100644 microsoft-integration/media-gallery-tool/src/main/resources/Messages_es.properties create mode 100644 microsoft-integration/media-gallery-tool/src/main/resources/Messages_eu.properties create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/body.html create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/error.html create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/fragments/common.html create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/fragments/driveItems.html create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/fragments/menus.html create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/index.html create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/index_ws.html create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/info.html create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/js/common.js create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/templates/permissions.html create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/tools/microsoft.mediagallery.xml create mode 100644 microsoft-integration/media-gallery-tool/src/main/webapp/WEB-INF/web.xml create mode 100644 microsoft-integration/pom.xml diff --git a/.gitignore b/.gitignore index f123da48dfe0..a98d41bfb9cd 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ assets library/src/morpheus-master/ library/src/webapp/skin/morpheus-default/ *.key -*.key.pub \ No newline at end of file +*.key.pub +**/src/main/frontend/node/ diff --git a/cloud-storage/README.md b/cloud-storage/README.md index 709e1be8c44a..c8ff13e103ef 100644 --- a/cloud-storage/README.md +++ b/cloud-storage/README.md @@ -7,7 +7,7 @@ This module provides an integration within Sakai for implementations of differen At the moment, the current supported cloud providers are: - - Microsoft OneDrive. + - Microsoft OneDrive -> Migrated to microsoft-integration. - Google Drive. diff --git a/cloud-storage/onedrive/README.md b/cloud-storage/onedrive/README.md deleted file mode 100644 index a51b3bbb76f1..000000000000 --- a/cloud-storage/onedrive/README.md +++ /dev/null @@ -1,38 +0,0 @@ -Sakai OneDrive Integration -========================== - -Overview --------- -This integration is part of the cloud storage options provided by Sakai. - - -User Guide ----------- -Please read the README file on parent folder. - - -APP Registration ----------------- -In order to make this integration work, you'll have to register an APP via Microsoft's Azure portal: - - 1. Access https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade using your Microsoft account login. - 2. On the App registrations menu, click on "New registration". - 3. Introduce a unique name for your application. - 4. The redirect uri (Web) must match the one you add on your sakai.properties files, by default it is "https://YOUR-SERVER-URL/sakai-onedrive-tool". - 5. Click on "Register". Get the Application (client) ID, as you'll have to set it on your sakai.properties (onedrive.client_id). - 6. Under Call APIs, click on "View Api Permissions". - 7. Click on "Add a permission". You'll need to allow two Microsoft Graph delegated permissions: "User.Read" and "Files.Read.All". - 8. Access the "Certificates & secrets" section on the menu. Click on "New client secret" and choose your preferred expiration configuration. - NOTE: You can only copy the secret's value just after creating it, and you can only have one valid secret at any moment. - 9. Click on "Add" and copy the Value for the generated client secret, you'll have to set it on your sakai.properties (onedrive.client_secret). - - -Sakai Configuration -------------------- -Once you have your App successfully registered, you can make use of the related properties. The one you'll need for enabling this functionality on sakai are these: - - onedrive.enabled=true - onedrive.client_id=--CLIENT_ID-- - onedrive.client_secret=--SECRET-- - onedrive.redirect_uri=${serverUrl}/sakai-onedrive-tool - onedrive.endpoint_uri=https://login.microsoftonline.com/{TenantID}/oauth2/v2.0/ diff --git a/cloud-storage/onedrive/api/pom.xml b/cloud-storage/onedrive/api/pom.xml deleted file mode 100644 index 74c20b03a0ec..000000000000 --- a/cloud-storage/onedrive/api/pom.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - 4.0.0 - - sakai-onedrive - org.sakaiproject.onedrive - 24-SNAPSHOT - - - Sakai OneDrive Integration - API - org.sakaiproject.onedrive - sakai-onedrive-api - jar - - - shared - - - - - - org.sakaiproject.kernel - sakai-kernel-api - - - - com.fasterxml.jackson.core - jackson-annotations - - - com.google.guava - guava - 30.1.1-jre - - - org.apache.commons - commons-lang3 - - - org.hibernate - hibernate-core - - - - diff --git a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveItem.java b/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveItem.java deleted file mode 100644 index 640caf143d40..000000000000 --- a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveItem.java +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2003-2019 The Apereo Foundation - * - * Licensed under the Educational Community License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://opensource.org/licenses/ecl2 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sakaiproject.onedrive.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -@Getter @Setter -@ToString -@JsonIgnoreProperties(ignoreUnknown = true) -public class OneDriveItem { - - @JsonProperty("id") - private String oneDriveItemId; - - private String name; - - private Long size; - - @JsonProperty(value = "@microsoft.graph.downloadUrl")//this is always public - //@JsonProperty(value = "webUrl")//this checks against onedrive permissions - private String downloadUrl; - - private OneDriveFolder folder; - private OneDriveFile file; - - @JsonProperty(value = "parentReference") - private OneDriveParent parent; - - public boolean isFolder() { - return folder != null; - } - public boolean hasChildren() { - return isFolder() && folder.childCount != 0; - } - - private int depth = 0; - private boolean expanded = false; - - @Override - public boolean equals(Object obj) { - boolean retVal = false; - if (obj instanceof OneDriveItem){ - OneDriveItem ptr = (OneDriveItem) obj; - return this.oneDriveItemId.equals(ptr.getOneDriveItemId()); - } - return retVal; - } -} diff --git a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveUser.java b/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveUser.java deleted file mode 100644 index 669ca69b1c03..000000000000 --- a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveUser.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2003-2019 The Apereo Foundation - * - * Licensed under the Educational Community License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://opensource.org/licenses/ecl2 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sakaiproject.onedrive.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Lob; -import javax.persistence.Table; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "ONEDRIVE_USER") -@Data @NoArgsConstructor -@JsonIgnoreProperties(ignoreUnknown = true) -public class OneDriveUser { - - @Id - @JsonProperty("id") - private String oneDriveUserId; - - private String sakaiUserId; - - @Lob - private String token; - - @Lob - private String refreshToken; - - @JsonProperty("userPrincipalName") - private String oneDriveName; - -} diff --git a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/service/OneDriveService.java b/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/service/OneDriveService.java deleted file mode 100644 index 97a2eedf4a3a..000000000000 --- a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/service/OneDriveService.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2003-2019 The Apereo Foundation - * - * Licensed under the Educational Community License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://opensource.org/licenses/ecl2 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sakaiproject.onedrive.service; - -import java.util.List; - -import org.sakaiproject.onedrive.model.OneDriveItem; -import org.sakaiproject.onedrive.model.OneDriveUser; - -/** - * Interface for communicating with the OneDrive API. - */ -public interface OneDriveService { - - // ONEDRIVE CONSTANTS - public final String ONEDRIVE_PREFIX = "onedrive."; - public final String ONEDRIVE_ENABLED = ONEDRIVE_PREFIX + "enabled"; - public final String ONEDRIVE_CLIENT_ID = "client_id"; - public final String ONEDRIVE_CLIENT_SECRET = "client_secret"; - public final String ONEDRIVE_CODE = "code"; - public final String ONEDRIVE_ENDPOINT_URI = "endpoint_uri"; - public final String ONEDRIVE_GRANT_TYPE = "grant_type"; - public final String ONEDRIVE_GRANT_TYPE_DEFAULT = "authorization_code"; - public final String ONEDRIVE_REDIRECT_URI = "redirect_uri"; - public final String ONEDRIVE_REFRESH_TOKEN = "refresh_token"; - public final String ONEDRIVE_RESPONSE_MODE = "response_mode"; - public final String ONEDRIVE_RESPONSE_MODE_DEFAULT = "query"; - public final String ONEDRIVE_RESPONSE_TYPE = "response_type"; - public final String ONEDRIVE_RESPONSE_TYPE_DEFAULT = "code"; - public final String ONEDRIVE_SCOPE = "scope"; - public final String ONEDRIVE_SCOPE_DEFAULT_VALUES = "offline_access user.read files.read.all";//all in one variable, separate if necessary - public final String ONEDRIVE_STATE = "state"; - - // ENDPOINTS - public final String ENDPOINT_AUTHORIZE = "authorize"; - public final String ENDPOINT_GRAPH = "https://graph.microsoft.com/v1.0/"; - public final String ENDPOINT_COMMON_LOGIN = "https://login.microsoftonline.com/common/oauth2/v2.0/"; - public final String ENDPOINT_DRIVES = "drives/"; - public final String ENDPOINT_ME = "me"; - public final String ENDPOINT_CHILDREN = "/children"; - public final String ENDPOINT_ITEMS = "/items/"; - public final String ENDPOINT_ROOT_CHILDREN = "/root/children"; - public final String ENDPOINT_TOKEN = "token"; - public final String JSON_ENTRY_VALUE = "value"; - public final String Q_MARK = "?"; - public final String SEPARATOR = "/"; - - // FUNCTIONS - public String formAuthenticationUrl(); - public List getDriveRootItems(String userId); - public List getDriveChildrenItems(String userId, String itemId, int depth); - public OneDriveUser getOneDriveUser(String userId); - public OneDriveUser refreshToken(String userId); - public boolean token(String userId, String code); - public boolean revokeOneDriveConfiguration(String userId); - public void cleanOneDriveCacheForUser(String userId); - -} diff --git a/cloud-storage/onedrive/impl/pom.xml b/cloud-storage/onedrive/impl/pom.xml deleted file mode 100644 index 021c2fe60e92..000000000000 --- a/cloud-storage/onedrive/impl/pom.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - 4.0.0 - - - org.sakaiproject.onedrive - sakai-onedrive - 24-SNAPSHOT - - - Sakai OneDrive Integration - IMPL - org.sakaiproject.onedrive - sakai-onedrive-impl - - sakai-component - - components - - - - - org.sakaiproject.onedrive - sakai-onedrive-api - - - - org.sakaiproject.kernel - sakai-kernel-api - - - org.sakaiproject.kernel - sakai-kernel-util - - - org.sakaiproject.kernel - sakai-component-manager - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - - - - org.springframework - spring-core - - - org.springframework - spring-context - - - org.springframework - spring-orm - - - org.springframework - spring-tx - - - - org.apache.commons - commons-lang3 - - - commons-io - commons-io - - - org.apache.httpcomponents.client5 - httpclient5 - - - org.apache.httpcomponents.core5 - httpcore5 - - - org.hibernate - hibernate-core - - - - - - - ${basedir}/src/resources - - **/*.properties - - - - - - diff --git a/cloud-storage/onedrive/impl/src/java/org/sakaiproject/onedrive/service/OneDriveServiceImpl.java b/cloud-storage/onedrive/impl/src/java/org/sakaiproject/onedrive/service/OneDriveServiceImpl.java deleted file mode 100644 index 873e6a3dda9b..000000000000 --- a/cloud-storage/onedrive/impl/src/java/org/sakaiproject/onedrive/service/OneDriveServiceImpl.java +++ /dev/null @@ -1,311 +0,0 @@ -/** - * Copyright (c) 2003-2019 The Apereo Foundation - * - * Licensed under the Educational Community License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://opensource.org/licenses/ecl2 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sakaiproject.onedrive.service; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.hc.core5.http.NameValuePair; -import org.apache.hc.core5.http.io.entity.StringEntity; -import org.sakaiproject.component.api.ServerConfigurationService; -import org.sakaiproject.memory.api.Cache; -import org.sakaiproject.memory.api.MemoryService; -import org.sakaiproject.onedrive.model.OneDriveItem; -import org.sakaiproject.onedrive.model.OneDriveToken; -import org.sakaiproject.onedrive.model.OneDriveUser; -import org.sakaiproject.onedrive.repository.OneDriveUserRepository; -import org.sakaiproject.onedrive.util.HTTPConnectionUtil; - -/** - * Implementation of the OneDriveService interface. - - * @see OneDriveService - */ -@Slf4j -public class OneDriveServiceImpl implements OneDriveService { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().setSerializationInclusion(Include.NON_NULL); - - @Setter private OneDriveUserRepository onedriveRepo; - - @Getter @Setter - private ServerConfigurationService serverConfigurationService; - - @Getter @Setter - private MemoryService memoryService; - - private Cache tokenCache; - private Cache onedriveUserCache; - private Cache> driveRootItemsCache; - private Cache> driveChildrenItemsCache; - - private String clientId = null; - private String clientSecret = null; - private String redirectUri = null; - private String endpointLogin = null; - private static final String state = ONEDRIVE_STATE; - private String bearer = null; - - public void init() { - log.debug("OneDriveServiceImpl init"); - OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - clientId = serverConfigurationService.getString(ONEDRIVE_PREFIX + ONEDRIVE_CLIENT_ID, null); - clientSecret = serverConfigurationService.getString(ONEDRIVE_PREFIX + ONEDRIVE_CLIENT_SECRET, null); - redirectUri = serverConfigurationService.getString(ONEDRIVE_PREFIX + ONEDRIVE_REDIRECT_URI, "http://localhost:8080/sakai-onedrive-tool"); - endpointLogin = serverConfigurationService.getString(ONEDRIVE_PREFIX + ONEDRIVE_ENDPOINT_URI, ENDPOINT_COMMON_LOGIN); - - tokenCache = memoryService.getCache("org.sakaiproject.onedrive.service.tokenCache"); - driveRootItemsCache = memoryService.>getCache("org.sakaiproject.onedrive.service.driveRootItemsCache"); - driveChildrenItemsCache = memoryService.>getCache("org.sakaiproject.onedrive.service.driveChildrenItemsCache"); - onedriveUserCache = memoryService.getCache("org.sakaiproject.onedrive.service.onedriveUserCache"); - } - - public String formAuthenticationUrl() { - log.debug("formAuthenticationUrl"); - if(!isConfigured()){ - return null; - } - - Map params = new HashMap<>(); - params.put(ONEDRIVE_CLIENT_ID, clientId); - params.put(ONEDRIVE_RESPONSE_MODE, ONEDRIVE_RESPONSE_MODE_DEFAULT); - params.put(ONEDRIVE_RESPONSE_TYPE, ONEDRIVE_RESPONSE_TYPE_DEFAULT); - params.put(ONEDRIVE_SCOPE, ONEDRIVE_SCOPE_DEFAULT_VALUES); - params.put(ONEDRIVE_STATE, state); - params.put(ONEDRIVE_REDIRECT_URI, redirectUri); - String authUrl = endpointLogin + ENDPOINT_AUTHORIZE + Q_MARK + HTTPConnectionUtil.formUrlencodedString(params); - log.debug("authUrl : {}", authUrl); - return authUrl; - } - - public boolean token(String userId, String code) { - if (!isConfigured()) { - return false; - } - String prevToken = tokenCache.get(userId); - if(prevToken != null) { - log.debug("token : Reusing previous token {}", prevToken); - return true; - } - - Map params = new HashMap<>(); - params.put(ONEDRIVE_CLIENT_ID, clientId); - params.put(ONEDRIVE_SCOPE, ONEDRIVE_SCOPE_DEFAULT_VALUES); - params.put(ONEDRIVE_CODE, code); - params.put(ONEDRIVE_GRANT_TYPE, ONEDRIVE_GRANT_TYPE_DEFAULT); - params.put(ONEDRIVE_CLIENT_SECRET, clientSecret); - params.put(ONEDRIVE_REDIRECT_URI, redirectUri); - try { - StringEntity entity = new StringEntity(HTTPConnectionUtil.formUrlencodedString(params)); - String postResponse = HTTPConnectionUtil.makePostCall(endpointLogin + ENDPOINT_TOKEN, entity); - log.debug(postResponse); - OneDriveToken ot = OBJECT_MAPPER.readValue(postResponse, OneDriveToken.class); - log.debug(ot.toString()); - if(ot == null || ot.getCurrentToken() == null){ - log.warn("OneDrive: Error retrieving token for user {} : {}", userId, postResponse); - return false; - } - bearer = ot.getCurrentToken(); - OneDriveUser ou = getCurrentDriveUser(); - ou.setSakaiUserId(userId); - ou.setToken(ot.getCurrentToken()); - ou.setRefreshToken(ot.getRefreshToken()); - log.debug(ou.toString()); - ou = onedriveRepo.save(ou); - if(ou != null){ - tokenCache.put(userId, ot.getCurrentToken()); - return true; - } - } catch(Exception e) { - log.warn("OneDrive: Error while retrieving or saving the token for user {} : {}", userId, e.getMessage()); - } - return false; - } - - public OneDriveUser refreshToken(String userId){ - if(!isConfigured()){ - return null; - } - OneDriveUser onedriveUser = onedriveUserCache.get(userId); - if(onedriveUser != null) { - log.debug("refreshToken : Reusing previous user data {}", onedriveUser); - return onedriveUser; - } - - OneDriveUser ou = getOneDriveUser(userId); - if(ou == null){ - log.debug("No OneDrive account found for user {}", userId); - return null; - } - log.debug(ou.toString()); - Map params = new HashMap<>(); - params.put(ONEDRIVE_CLIENT_ID, clientId); - params.put(ONEDRIVE_SCOPE, ONEDRIVE_SCOPE_DEFAULT_VALUES); - params.put(ONEDRIVE_REFRESH_TOKEN, ou.getRefreshToken()); - params.put(ONEDRIVE_GRANT_TYPE, ONEDRIVE_REFRESH_TOKEN); - params.put(ONEDRIVE_CLIENT_SECRET, clientSecret); - params.put(ONEDRIVE_REDIRECT_URI, redirectUri); - try { - StringEntity entity = new StringEntity(HTTPConnectionUtil.formUrlencodedString(params)); - String postResponse = HTTPConnectionUtil.makePostCall(endpointLogin + ENDPOINT_TOKEN, entity); - log.debug(postResponse); - OneDriveToken ot = OBJECT_MAPPER.readValue(postResponse, OneDriveToken.class); - log.debug(ot.toString()); - if(ot == null || ot.getCurrentToken() == null){ - log.warn("OneDrive: Error refreshing token for user {} : {}", userId, postResponse); - return null; - } - bearer = ot.getCurrentToken(); - ou.setToken(ot.getCurrentToken()); - log.debug(ou.toString()); - onedriveRepo.update(ou); - tokenCache.put(userId, ot.getCurrentToken()); - onedriveUserCache.put(userId, ou); - return ou; - } catch(Exception e) { - log.warn("OneDrive: Error while refreshing or saving the token for user {} : {}", userId, e.getMessage()); - } - return null; - } - - private OneDriveUser getCurrentDriveUser(){ - if(!isConfigured()){ - return null; - } - OneDriveUser ou = null; - try { - List params = new ArrayList<>(); - String getResponse = HTTPConnectionUtil.makeGetCall(ENDPOINT_GRAPH + ENDPOINT_ME, params, bearer); - ou = OBJECT_MAPPER.readValue(getResponse, OneDriveUser.class); - } catch(Exception e) { - log.error("getCurrentDriveUser: {}", e.getMessage()); - } - return ou; - } - - public OneDriveUser getOneDriveUser(String userId) { - return onedriveRepo.findBySakaiId(userId); - } - - public List getDriveRootItems(String userId) { - if(!isConfigured()){ - return null; - } - List cachedItems = driveRootItemsCache.get(userId); - if(cachedItems != null) { - log.debug("getDriveRootItems : Returning cached items {}", cachedItems); - return cachedItems; - } - try { - OneDriveUser ou = refreshToken(userId); - if(ou == null){ - return null; - } - List params = new ArrayList<>(); - bearer = ou.getToken(); - String getResponse = HTTPConnectionUtil.makeGetCall(ENDPOINT_GRAPH + ENDPOINT_DRIVES + ou.getOneDriveUserId() + ENDPOINT_ROOT_CHILDREN, params, bearer); - JsonNode jsonNode = OBJECT_MAPPER.readValue(getResponse, JsonNode.class); - JsonNode valueNode = jsonNode.get(JSON_ENTRY_VALUE); - if(valueNode == null) { - log.warn("Couldn't retrieve root items for user id {} : response is {}", userId, getResponse); - return null; - } - List items = OBJECT_MAPPER.readValue(valueNode.toString(), OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, OneDriveItem.class)); - driveRootItemsCache.put(userId, items); - return items; - } catch(Exception e) { - log.error("getDriveRootItems: id {} - error {}", userId, e.getMessage()); - } - return null; - } - - public List getDriveChildrenItems(String userId, String itemId, int depth) { - if(!isConfigured()){ - return null; - } - String cacheId = userId + "#" + itemId; - List cachedItems = driveChildrenItemsCache.get(cacheId); - if(cachedItems != null) { - log.debug("getDriveChildrenItems : Returning cached items " + cachedItems); - return cachedItems; - } - try { - OneDriveUser ou = refreshToken(userId); - if(ou == null){ - return null; - } - List params = new ArrayList<>(); - bearer = ou.getToken(); - String getResponse = HTTPConnectionUtil.makeGetCall(ENDPOINT_GRAPH + ENDPOINT_DRIVES + ou.getOneDriveUserId() + ENDPOINT_ITEMS + itemId + ENDPOINT_CHILDREN, params, bearer); - JsonNode jsonNode = OBJECT_MAPPER.readValue(getResponse, JsonNode.class); - JsonNode valueNode = jsonNode.get(JSON_ENTRY_VALUE); - if(valueNode == null) { - log.warn("Couldn't retrieve children items for item id {} : response is {}", itemId, getResponse); - return null; - } - List items = OBJECT_MAPPER.readValue(valueNode.toString(), OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, OneDriveItem.class)); - items.forEach(it -> it.setDepth(depth+1)); - driveChildrenItemsCache.put(cacheId, items); - return items; - } catch(Exception e) { - log.error("getDriveChildrenItems: id {} - error {}", userId, e.getMessage()); - } - return null; - } - - private boolean isConfigured(){ - if (StringUtils.isBlank(clientId) || StringUtils.isBlank(clientSecret) || StringUtils.isBlank(redirectUri)) { - log.warn("ONEDRIVE CONFIGURATION IS MISSING"); - return false; - } - return true; - } - - public boolean revokeOneDriveConfiguration(String userId){ - log.info("revokeOneDriveConfiguration for user {}", userId); - try { - // delete onedrive user ddbb entry - onedriveRepo.delete(getOneDriveUser(userId).getOneDriveUserId()); - cleanOneDriveCacheForUser(userId); - return true; - } catch (Exception e) { - log.warn("Error while trying to remove OneDrive configuration : {}", e.getMessage()); - } - return false; - } - - public void cleanOneDriveCacheForUser(String userId){ - log.debug("cleanOneDriveCacheForUser {}", userId); - // clean caches - tokenCache.remove(userId); - onedriveUserCache.remove(userId); - driveRootItemsCache.remove(userId); - driveChildrenItemsCache.clear(); - } - -} diff --git a/cloud-storage/onedrive/impl/src/java/org/sakaiproject/onedrive/util/HTTPConnectionUtil.java b/cloud-storage/onedrive/impl/src/java/org/sakaiproject/onedrive/util/HTTPConnectionUtil.java deleted file mode 100644 index d10295206080..000000000000 --- a/cloud-storage/onedrive/impl/src/java/org/sakaiproject/onedrive/util/HTTPConnectionUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) 2003-2019 The Apereo Foundation - * - * Licensed under the Educational Community License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://opensource.org/licenses/ecl2 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sakaiproject.onedrive.util; - -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; -import java.util.Map; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.IOUtils; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.NameValuePair; -import org.apache.hc.core5.http.io.entity.StringEntity; -import org.apache.hc.core5.net.URIBuilder; - -/** - * HTTP methods required by the service - */ -@Slf4j -public class HTTPConnectionUtil { - - public static String formUrlencodedString(Map params) { - StringBuilder param = new StringBuilder(""); - for (Map.Entry item : params.entrySet()) { - if (param.length() != 0) { - param.append('&'); - } - param.append(item.getKey()); - param.append('='); - param.append(item.getValue().toString()); - } - log.debug("formUrlencodedString : " + param.toString()); - return param.toString(); - } - - public static String makePostCall(String endpoint, StringEntity body) throws Exception { - try { - URIBuilder uriBuilder = new URIBuilder(endpoint); - URI apiUri = uriBuilder.build(); - - HttpPost request = new HttpPost(apiUri); - request.addHeader("content-type", "application/x-www-form-urlencoded"); - request.setEntity(body); - - // Configure request timeouts. - RequestConfig requestConfig = RequestConfig.custom().build(); - request.setConfig(requestConfig); - - CloseableHttpResponse response = null; - log.debug(request.toString()); - InputStream stream = null; - try (CloseableHttpClient client = HttpClients.createDefault()) { - response = client.execute(request); - HttpEntity entity = response.getEntity(); - if (entity != null) { - stream = entity.getContent(); - String streamString = IOUtils.toString(stream, "UTF-8"); - return streamString; - } - } catch (Exception e) { - log.warn("Could not fetch results from OneDrive API." + e.getMessage()); - } - - } catch (URISyntaxException e) { - log.error("Incorrect OneDrive API url syntax.", e); - } - return null; - } - - public static String makeGetCall(String endpoint, List params, String bearer) throws Exception { - try { - URIBuilder uriBuilder = new URIBuilder(endpoint).addParameters(params); - URI apiUri = uriBuilder.build(); - - HttpGet request = new HttpGet(apiUri); - request.addHeader("Authorization", "Bearer " + bearer); - - // Configure request timeouts. - RequestConfig requestConfig = RequestConfig.custom().build(); - request.setConfig(requestConfig); - - CloseableHttpResponse response = null; - log.debug(request.toString()); - InputStream stream = null; - try (CloseableHttpClient client = HttpClients.createDefault()) { - response = client.execute(request); - HttpEntity entity = response.getEntity(); - if (entity != null) { - stream = entity.getContent(); - String streamString = IOUtils.toString(stream, "UTF-8"); - return streamString; - } - } catch (Exception e) { - log.warn("Could not fetch results from OneDrive API." + e.getMessage()); - } - } catch (URISyntaxException e) { - log.error("Incorrect OneDrive API url syntax.", e); - } - - return null; - } - -} diff --git a/cloud-storage/onedrive/impl/src/webapp/WEB-INF/components.xml b/cloud-storage/onedrive/impl/src/webapp/WEB-INF/components.xml deleted file mode 100644 index acc655ba2433..000000000000 --- a/cloud-storage/onedrive/impl/src/webapp/WEB-INF/components.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - org.sakaiproject.onedrive.model.OneDriveUser - - - - - diff --git a/cloud-storage/onedrive/pom.xml b/cloud-storage/onedrive/pom.xml deleted file mode 100644 index 9a84034a7124..000000000000 --- a/cloud-storage/onedrive/pom.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - 4.0.0 - Sakai OneDrive Integration - BASE - org.sakaiproject.onedrive - sakai-onedrive - pom - - - org.sakaiproject.cloudstorage - cloud-storage - 24-SNAPSHOT - - - - api - impl - tool - - - - UTF-8 - - - - - - - org.sakaiproject.onedrive - sakai-onedrive-api - ${project.version} - provided - - - org.sakaiproject.onedrive - sakai-onedrive-impl - ${project.version} - runtime - - - - diff --git a/cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/tool/MainController.java b/cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/tool/MainController.java deleted file mode 100644 index 69a1425a347c..000000000000 --- a/cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/tool/MainController.java +++ /dev/null @@ -1,66 +0,0 @@ -/****************************************************************************** - * Copyright 2015 sakaiproject.org Licensed under the Educational - * Community License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/ECL-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - ******************************************************************************/ -package org.sakaiproject.onedrive.tool; - -import javax.inject.Inject; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.sakaiproject.onedrive.service.OneDriveService; -import org.sakaiproject.tool.api.SessionManager; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; - -/** - * MainController - * - * This is the controller used by Spring MVC to handle requests - * - * @author Miguel Pellicer (mpellicer@edf.globañ) - * - */ -@Slf4j -@Controller -public class MainController { - - @Inject - private OneDriveService oneDriveService; - @Inject - private SessionManager sessionManager; - - @RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET) - public String showIndex(@RequestParam(required=false) String code, Model model) { - log.debug("OneDriveServlet : Called the main servlet."); - String userId = sessionManager.getCurrentSessionUserId(); - Object pickerRedirectUrlObject = sessionManager.getCurrentSession().getAttribute(oneDriveService.ONEDRIVE_REDIRECT_URI); - sessionManager.getCurrentSession().removeAttribute(oneDriveService.ONEDRIVE_REDIRECT_URI); - String pickerRedirectUrl = pickerRedirectUrlObject != null ? pickerRedirectUrlObject.toString() : null; - log.debug("OneDriveServlet : request code {}", code); - log.debug("OneDriveServlet : sakai user {}", userId); - log.debug("OneDriveServlet : pickerRedirectUrl {}", pickerRedirectUrl); - boolean configured = false; - if(StringUtils.isNotEmpty(code) && StringUtils.isNotEmpty(userId)) { - configured = oneDriveService.token(userId, code); - } - log.debug("OneDriveServlet : configured token {} ", configured); - model.addAttribute("pickerRedirectUrl", pickerRedirectUrl != null ? pickerRedirectUrl : "/portal"); - model.addAttribute("onedriveConfigured", configured); - log.debug("OneDriveServlet : Finished action and returning."); - return "index"; - } - -} diff --git a/cloud-storage/onedrive/tool/src/main/resources/Messages.properties b/cloud-storage/onedrive/tool/src/main/resources/Messages.properties deleted file mode 100644 index f23cf0a4e148..000000000000 --- a/cloud-storage/onedrive/tool/src/main/resources/Messages.properties +++ /dev/null @@ -1,4 +0,0 @@ -onedrive_configured= Your OneDrive account has been successfully integrated in the platform. -onedrive_error= Error while setting your OneDrive account: Contact the admin of the platform for more information. -onedrive_button_msg=Please click here to return to your home site: -onedrive_back=Back diff --git a/cloud-storage/onedrive/tool/src/main/resources/Messages_ar.properties b/cloud-storage/onedrive/tool/src/main/resources/Messages_ar.properties deleted file mode 100644 index a2ee785f4044..000000000000 --- a/cloud-storage/onedrive/tool/src/main/resources/Messages_ar.properties +++ /dev/null @@ -1,4 +0,0 @@ -onedrive_configured=\u062a\u0645 \u062f\u0645\u062c \u062d\u0633\u0627\u0628 OneDrive \u0627\u0644\u062e\u0627\u0635 \u0628\u0643 \u0628\u0646\u062c\u0627\u062d \u0641\u064a \u0627\u0644\u0646\u0638\u0627\u0645 \u0627\u0644\u0623\u0633\u0627\u0633\u064a. -onedrive_error=\u062e\u0637\u0623 \u0623\u062b\u0646\u0627\u0621 \u062a\u0639\u064a\u064a\u0646 \u062d\u0633\u0627\u0628 OneDrive \u0627\u0644\u062e\u0627\u0635 \u0628\u0643\: \u0627\u062a\u0635\u0644 \u0628\u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u0646\u0638\u0627\u0645 \u0627\u0644\u0623\u0633\u0627\u0633\u064a \u0644\u0645\u0632\u064a\u062f \u0645\u0646 \u0627\u0644\u0645\u0639\u0644\u0648\u0645\u0627\u062a. -onedrive_button_msg=\u0627\u0644\u0631\u062c\u0627\u0621 \u0627\u0644\u0636\u063a\u0637 \u0647\u0646\u0627 \u0644\u0644\u0639\u0648\u062f\u0629 \u0625\u0644\u0649 \u0645\u0648\u0642\u0639 \u0645\u0646\u0632\u0644\u0643\: -onedrive_back=\u0627\u0644\u0639\u0648\u062f\u0629 diff --git a/cloud-storage/onedrive/tool/src/main/resources/Messages_ca.properties b/cloud-storage/onedrive/tool/src/main/resources/Messages_ca.properties deleted file mode 100644 index 2af7250b628b..000000000000 --- a/cloud-storage/onedrive/tool/src/main/resources/Messages_ca.properties +++ /dev/null @@ -1,4 +0,0 @@ -onedrive_configured=El vostre compte de OneDrive s\u2019ha integrat correctament a la plataforma. -onedrive_error=S\u2019ha produ\u00eft un error al configurar el vostre compte de OneDrive\: contacteu l\u2019administrador de la plataforma per a obtenir m\u00e9s informaci\u00f3. -onedrive_button_msg=Feu clic per tornar al vostre espai personal\: -onedrive_back=Enrere diff --git a/cloud-storage/onedrive/tool/src/main/resources/Messages_es.properties b/cloud-storage/onedrive/tool/src/main/resources/Messages_es.properties deleted file mode 100644 index 2ee8587204c9..000000000000 --- a/cloud-storage/onedrive/tool/src/main/resources/Messages_es.properties +++ /dev/null @@ -1,4 +0,0 @@ -onedrive_configured=Su cuenta de OneDrive se ha integrado correctamente. -onedrive_error=Error estableciendo su cuenta de OneDrive\: Contacte con el administrador para m\u00e1s informaci\u00f3n. -onedrive_button_msg=Por favor, haga click aqu\u00ed para volver a su Sitio\: -onedrive_back=Volver diff --git a/cloud-storage/onedrive/tool/src/main/resources/Messages_fr_FR.properties b/cloud-storage/onedrive/tool/src/main/resources/Messages_fr_FR.properties deleted file mode 100644 index f86ba464c85c..000000000000 --- a/cloud-storage/onedrive/tool/src/main/resources/Messages_fr_FR.properties +++ /dev/null @@ -1,4 +0,0 @@ -onedrive_configured=Votre compte OneDrive a \u00e9t\u00e9 int\u00e9gr\u00e9 avec succ\u00e8s dans la plate-forme. -onedrive_error=Erreur lors de la configuration de votre compte OneDrive \: Contactez l'administrateur de la plate-forme pour plus d'informations. -onedrive_button_msg=Veuillez cliquer ici pour retourner \u00e0 votre espace d'origine \: -onedrive_back=Retour diff --git a/cloud-storage/onedrive/tool/src/main/resources/Messages_ro_RO.properties b/cloud-storage/onedrive/tool/src/main/resources/Messages_ro_RO.properties deleted file mode 100644 index f721e35029b7..000000000000 --- a/cloud-storage/onedrive/tool/src/main/resources/Messages_ro_RO.properties +++ /dev/null @@ -1,4 +0,0 @@ -onedrive_configured=Contul dumneavoastra OneDrive a fost integrat cu succes in platforma. -onedrive_error=Eroare la setarea contului dumneavoastra OneDrive\: Contactati administratorul platformei pentru mai multe informatii. -onedrive_button_msg=Va rugam sa faceti click aici pentru a va reintoarce la pagina principala a site-ului -onedrive_back=Inapoi diff --git a/cloud-storage/onedrive/tool/src/main/resources/Messages_tr_TR.properties b/cloud-storage/onedrive/tool/src/main/resources/Messages_tr_TR.properties deleted file mode 100644 index 1bdff942178e..000000000000 --- a/cloud-storage/onedrive/tool/src/main/resources/Messages_tr_TR.properties +++ /dev/null @@ -1,4 +0,0 @@ -onedrive_configured=OneDrive hesab\u0131n\u0131z ortama ba\u015far\u0131yla dahil edildi. -onedrive_error=OneDrive hesab\u0131n\u0131z\u0131 ayarlarken hata olu\u015ftu\: Detayl\u0131 bilgi i\u00e7in ortam y\u00f6neticisi ile ileti\u015fime ge\u00e7in. -onedrive_button_msg=L\u00fctfen Ana Siteye d\u00f6nmek i\u00e7in buraya t\u0131klay\u0131n\: -onedrive_back=Geri diff --git a/cloud-storage/onedrive/tool/src/main/webapp/WEB-INF/templates/index.html b/cloud-storage/onedrive/tool/src/main/webapp/WEB-INF/templates/index.html deleted file mode 100644 index 9dd5a07c927d..000000000000 --- a/cloud-storage/onedrive/tool/src/main/webapp/WEB-INF/templates/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - -
-
-

ONE_DRIVE_INTEGRATED

-

ONE_DRIVE_ERROR

-

- BACK_BUTTON_MESSAGE - -

-
-
- - diff --git a/cloud-storage/pom.xml b/cloud-storage/pom.xml index 3ab0e1b223ff..b7d73f84df55 100644 --- a/cloud-storage/pom.xml +++ b/cloud-storage/pom.xml @@ -14,7 +14,6 @@ - onedrive googledrive diff --git a/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools.properties b/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools.properties index 78eabe2ece68..ef85b802ee31 100644 --- a/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools.properties +++ b/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools.properties @@ -211,6 +211,12 @@ sakai.usermembership.title = User Membership sakai.usermembership.description = For retrieving site membership information about users sakai.users.description = For editing local user accounts sakai.users.title = Users +sakai.meetings.description = A tool for online meetings management +sakai.meetings.title = Meetings +microsoft.mediagallery.description = A Microsoft tool to browse Media Files +microsoft.mediagallery.title = Media Gallery +microsoft.collaborativedocuments.description = A Microsoft tool to browse Collaborative Documents +microsoft.collaborativedocuments.title = Collaborative Documents # Contrib tools blogger.title = Blogger diff --git a/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_ca.properties b/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_ca.properties index 2245271de526..d34cd2202b6b 100644 --- a/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_ca.properties +++ b/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_ca.properties @@ -211,6 +211,12 @@ sakai.usermembership.title=Pertinen\u00e7a dels usuaris sakai.usermembership.description=Eina per a recuperar informaci\u00f3 sobre la pertinen\u00e7a a espais dels usuaris sakai.users.description=Eina per a editar comptes d\u2019usuari locals sakai.users.title=Edici\u00f3 d\u2019usuaris +sakai.meetings.description=Una eina per a la gesti\u00f3 de reunions en l\u00ednia +sakai.meetings.title=Reunions +microsoft.mediagallery.description= Eina de Microsoft per a explorar fitxers multim\u00E8dia +microsoft.mediagallery.title= Media Gallery +microsoft.collaborativedocuments.description= Eina de Microsoft per a explorar documents col\u00b7laboratius +microsoft.collaborativedocuments.title= Documents Col\u00b7laboratis # Contrib tools blogger.title=Blogger diff --git a/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_es.properties b/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_es.properties index ae01ae8e6096..a6a220bd36d9 100644 --- a/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_es.properties +++ b/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_es.properties @@ -211,6 +211,12 @@ sakai.usermembership.title=Pertenencia de usuario sakai.usermembership.description=Para recoger informaci\u00f3n de participaci\u00f3n en sitios sakai.users.description=Para editar cuentas de usuario locales sakai.users.title=Edici\u00f3n de usuarios +sakai.meetings.description=Una herramienta para la gesti\u00f3n de reuniones en l\u00ednea +sakai.meetings.title=Reuniones +microsoft.mediagallery.description= Herramienta de Microsoft para explorar ficheros multimedia +microsoft.mediagallery.title= Media Gallery +microsoft.collaborativedocuments.description= Herramienta de Microsoft para explorar documentos colaborativos +microsoft.collaborativedocuments.title= Documentos colaborativos # Contrib tools blogger.title=Blogger diff --git a/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_eu.properties b/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_eu.properties index 9eb53b08721a..118c814d4ac5 100644 --- a/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_eu.properties +++ b/config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/tool/tools_eu.properties @@ -211,6 +211,12 @@ sakai.usermembership.title=Erabiltzaile kidearen izaera sakai.usermembership.description=Jakiteko zein gunetakoa den edozein erabiltzaile sakai.users.description=Erabiltzaile kontu lokalak editatzeko sakai.users.title=Erabiltzaileentzako editorea +sakai.meetings.description= A tool for online meetings management +sakai.meetings.title= Meetings +microsoft.mediagallery.description= Ikusentzunezko fitxategiak bilatzeko Microsoft-en tresna bat da hau. +microsoft.mediagallery.title= Galeria +microsoft.collaborativedocuments.description= A Microsoft tool to browse Colaborative Documents +microsoft.collaborativedocuments.title= Collaborative Documents # Contrib tools blogger.title=Blogaria diff --git a/content/content-tool/tool/pom.xml b/content/content-tool/tool/pom.xml index 140c7a32c8bc..a29eeabfbdbd 100644 --- a/content/content-tool/tool/pom.xml +++ b/content/content-tool/tool/pom.xml @@ -86,18 +86,16 @@ org.quartz-scheduler quartz - - org.sakaiproject.onedrive - sakai-onedrive-api - ${project.version} - provided - org.sakaiproject.googledrive sakai-googledrive-api ${project.version} provided + + org.sakaiproject.microsoft + microsoft-api + com.googlecode.json-simple json-simple diff --git a/content/content-tool/tool/src/bundle/helper.properties b/content/content-tool/tool/src/bundle/helper.properties index 0e2d2926c19e..bf818d440a8a 100644 --- a/content/content-tool/tool/src/bundle/helper.properties +++ b/content/content-tool/tool/src/bundle/helper.properties @@ -149,6 +149,11 @@ onedrive.integration=Select From OneDrive onedrive.set_account=Please, set your Office account data: onedrive.configured=Your Office account ({0}) has already been configured. onedrive.table_summary=List of the files and folders on your OneDrive account +onedrive.user=User +onedrive.shared=Shared +onedrive.site=Site +onedrive.empty=No items found +onedrive.select_type=Please, select one tab # GOOGLEDRIVE googledrive.integration=Select From Google Drive googledrive.set_account=Please, set your Google account data: diff --git a/content/content-tool/tool/src/bundle/helper_ca.properties b/content/content-tool/tool/src/bundle/helper_ca.properties index 0c87c774ff40..05f3661e3a72 100644 --- a/content/content-tool/tool/src/bundle/helper_ca.properties +++ b/content/content-tool/tool/src/bundle/helper_ca.properties @@ -149,6 +149,11 @@ onedrive.integration=Selecciona de OneDrive onedrive.set_account=Cal que introdu\u00efu les dades del vostre compte Office\: onedrive.configured=El vostre compte a Office ({0}) ja ha estat configurat. onedrive.table_summary=Mostra la llista de fitxers i les carpetes del meu compte OneDrive +onedrive.user=Usuari +onedrive.shared=Compartit +onedrive.site=Espai +onedrive.empty=No s\u2019ha trobat cap element +onedrive.select_type=Per favor, seleccione una de les pestanyes # GOOGLEDRIVE googledrive.integration=Selecciona de Google Drive googledrive.set_account=Cal que introdu\u00efu les dades del vostre compte de Google\: diff --git a/content/content-tool/tool/src/bundle/helper_es.properties b/content/content-tool/tool/src/bundle/helper_es.properties index 00a87ddbeafd..5559674c262d 100644 --- a/content/content-tool/tool/src/bundle/helper_es.properties +++ b/content/content-tool/tool/src/bundle/helper_es.properties @@ -149,6 +149,11 @@ onedrive.integration=Seleccionar desde OneDrive onedrive.set_account=Por favor, introduce tu cuenta de Office\: onedrive.configured=Tu cuenta Office ({0}) ha sido configurada. onedrive.table_summary=Lista de archivos y carpetas en tu cuenta OneDrive +onedrive.user=Usuario +onedrive.shared=Compartido +onedrive.site=Sitio +onedrive.empty=No se han encontrado elementos +onedrive.select_type=Por favor, seleccione una de las pesta\u00F1as # GOOGLEDRIVE googledrive.integration=Seleccionar desde Google Drive googledrive.set_account=Por favor, introduce tu cuenta de Google\: diff --git a/content/content-tool/tool/src/bundle/helper_eu.properties b/content/content-tool/tool/src/bundle/helper_eu.properties index 3acbe4bd40ca..20fe316dcf1d 100644 --- a/content/content-tool/tool/src/bundle/helper_eu.properties +++ b/content/content-tool/tool/src/bundle/helper_eu.properties @@ -133,3 +133,29 @@ gen.copycomf=Berretsi copyright-a viewing=Bistaratzen of=of items=itemetatik + +att.resource=Aukeratu baliabideetatik + +# CLOUD STORAGE +cloudstorage.configure=Configure +cloudstorage.revoke=Revoke +cloudstorage.title=Title +cloudstorage.clone=Clone +cloudstorage.link=Link +cloudstorage.attach_link=Attach as link +cloudstorage.refresh=Refresh +# ONEDRIVE +onedrive.integration=Select From OneDrive +onedrive.set_account=Please, set your Office account data: +onedrive.configured=Your Office account ({0}) has already been configured. +onedrive.table_summary=List of the files and folders on your OneDrive account +onedrive.user=User +onedrive.shared=Shared +onedrive.site=Site +onedrive.empty=No items found +onedrive.select_type=Please, select a type tab +# GOOGLEDRIVE +googledrive.integration=Select From Google Drive +googledrive.set_account=Please, set your Google account data: +googledrive.configured=Your Google account ({0}) has already been configured. +googledrive.table_summary=List of the files and folders on your GoogleDrive account \ No newline at end of file diff --git a/content/content-tool/tool/src/java/org/sakaiproject/content/tool/FilePickerAction.java b/content/content-tool/tool/src/java/org/sakaiproject/content/tool/FilePickerAction.java index 68600f7693f0..94ebb14525d3 100755 --- a/content/content-tool/tool/src/java/org/sakaiproject/content/tool/FilePickerAction.java +++ b/content/content-tool/tool/src/java/org/sakaiproject/content/tool/FilePickerAction.java @@ -44,8 +44,10 @@ import java.util.TreeSet; import java.util.Vector; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -100,10 +102,19 @@ import org.sakaiproject.googledrive.model.GoogleDriveItem; import org.sakaiproject.googledrive.model.GoogleDriveUser; import org.sakaiproject.googledrive.service.GoogleDriveService; -import org.sakaiproject.onedrive.model.OneDriveItem; -import org.sakaiproject.onedrive.model.OneDriveItemComparator; -import org.sakaiproject.onedrive.model.OneDriveUser; -import org.sakaiproject.onedrive.service.OneDriveService; +import org.sakaiproject.microsoft.api.MicrosoftAuthorizationService; +import org.sakaiproject.microsoft.api.MicrosoftCommonService; +import org.sakaiproject.microsoft.api.MicrosoftConfigurationService; +import org.sakaiproject.microsoft.api.MicrosoftSynchronizationService; +import org.sakaiproject.microsoft.api.data.MicrosoftDriveItem; +import org.sakaiproject.microsoft.api.data.MicrosoftRedirectURL; +import org.sakaiproject.microsoft.api.data.MicrosoftTeam; +import org.sakaiproject.microsoft.api.data.MicrosoftTeamWrapper; +import org.sakaiproject.microsoft.api.exceptions.MicrosoftCredentialsException; +import org.sakaiproject.microsoft.api.exceptions.MicrosoftInvalidTokenException; +import org.sakaiproject.microsoft.api.exceptions.MicrosoftNoCredentialsException; +import org.sakaiproject.microsoft.api.model.MicrosoftAccessToken; +import org.sakaiproject.microsoft.api.model.SiteSynchronization; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SiteService; import org.sakaiproject.time.api.Time; @@ -153,7 +164,10 @@ public class FilePickerAction extends PagedResourceHelperAction private static ToolManager toolManager = ComponentManager.get(ToolManager.class); private static UserDirectoryService userDirectoryService = ComponentManager.get(UserDirectoryService.class); private static TimeService timeService = ComponentManager.get(TimeService.class); - private static OneDriveService onedriveService = ComponentManager.get(OneDriveService.class); + private static MicrosoftConfigurationService microsoftConfigurationService = ComponentManager.get(MicrosoftConfigurationService.class); + private static MicrosoftCommonService microsoftCommonService = ComponentManager.get(MicrosoftCommonService.class); + private static MicrosoftAuthorizationService microsoftAuthorizationService = ComponentManager.get(MicrosoftAuthorizationService.class); + private static MicrosoftSynchronizationService microsoftSynchronizationService = ComponentManager.get(MicrosoftSynchronizationService.class); private static GoogleDriveService googledriveService = ComponentManager.get(GoogleDriveService.class); /** State attribute for where there is at least one attachment before invoking attachment tool */ @@ -169,7 +183,6 @@ public class FilePickerAction extends PagedResourceHelperAction private ResourceLoader srb = Resource.getResourceLoader(resourceClass, resourceBundle); /** CloudStorage **/ - private boolean onedriveOn = ServerConfigurationService.getBoolean(OneDriveService.ONEDRIVE_ENABLED, Boolean.FALSE); private boolean googledriveOn = ServerConfigurationService.getBoolean(GoogleDriveService.GOOGLEDRIVE_ENABLED, Boolean.FALSE); protected static final String PREFIX = "filepicker."; @@ -185,7 +198,7 @@ public class FilePickerAction extends PagedResourceHelperAction protected static final String MODE_ATTACHMENT_SELECT = "mode_attachment_select"; protected static final String MODE_ATTACHMENT_SELECT_INIT = "mode_attachment_select_init"; protected static final String MODE_HELPER = "mode_helper"; - protected static final String MODE_ONEDRIVE = "mode_onedrive"; + protected static final String MODE_CONFIGURE_ONEDRIVE = "mode_configure_onedrive"; protected static final String MODE_GOOGLEDRIVE = "mode_googledrive"; /** The null/empty string */ @@ -241,14 +254,22 @@ public class FilePickerAction extends PagedResourceHelperAction protected static final String STATE_NAVIGATING_RESOURCES = "navigating_resources"; protected static final String STATE_NAVIGATING_ONEDRIVE = "navigating_onedrive"; protected static final String STATE_NAVIGATING_GOOGLEDRIVE = "navigating_googledrive"; + + protected static final String STATE_NAVIGATING_ONEDRIVE_TYPE = "navigating_onedrive_type"; + protected static final String ONEDRIVE_TYPE_SITE = "onedrive_type_site"; + protected static final String ONEDRIVE_TYPE_USER = "onedrive_type_user"; + protected static final String ONEDRIVE_TYPE_SHARED = "onedrive_type_shared"; /** The sort by */ private static final String STATE_SORT_BY = PREFIX + "sort_by"; protected static final String STATE_TOP_MESSAGE_INDEX = PREFIX + "top_message_index"; - public static final String STATE_ONEDRIVE_CHILDREN = PREFIX + "state_onedrive_children"; - public static final String STATE_ONEDRIVE_ITEMS = PREFIX + "state_onedrive_items"; + public static final String STATE_ONEDRIVE_ITEMS_USER = PREFIX + "state_onedrive_items_user"; + public static final String STATE_ONEDRIVE_ITEMS_SHARED = PREFIX + "state_onedrive_items_shared"; + public static final String STATE_ONEDRIVE_ITEMS_SITE = PREFIX + "state_onedrive_items_site"; + public static final String STATE_ONEDRIVE_ITEMS_MAP = PREFIX + "state_onedrive_items_map"; + public static final String STATE_GOOGLEDRIVE_JSON = PREFIX + "state_googledrive_json"; /** The sort ascending or decending */ @@ -348,13 +369,6 @@ else if(MODE_ADD_METADATA.equals(helper_mode)) template = buildAddMetadataContext(portlet, context, data, state); } - context.put("onedriveOn", onedriveOn); - if(onedriveOn) { - OneDriveUser ou = onedriveService.getOneDriveUser(userDirectoryService.getCurrentUser().getId()); - if(ou != null) { - context.put("onedriveUserAccount", ou.getOneDriveName()); - } - } boolean isGoogleDriveOn = this.isGoogleDriveOn(); context.put("googledriveOn", isGoogleDriveOn); if (isGoogleDriveOn) { @@ -920,30 +934,91 @@ else if(contentService.isDropboxMaintainer()) } // ONEDRIVE - if(onedriveOn) { - List onedriveItems = new ArrayList<>(); - if(toolSession.getAttribute(STATE_ONEDRIVE_ITEMS) == null) { - onedriveItems = onedriveService.getDriveRootItems(userDirectoryService.getCurrentUser().getId()); - } else { - onedriveItems = (List) toolSession.getAttribute(STATE_ONEDRIVE_ITEMS); + if(isOneDriveOn()) { + buildOneDriveContext(portlet, context, data, state); + } + context.put("MIME_TYPE_MICROSOFT", ResourceType.MIME_TYPE_MICROSOFT); + + return template; + } + + // ONEDRIVE context + protected void buildOneDriveContext(VelocityPortlet portlet, Context context, RunData data, SessionState state) { + context.put("onedriveOn", true); + + ToolSession toolSession = sessionManager.getCurrentToolSession(); + + String siteId = toolManager.getCurrentPlacement().getContext(); + List ssList = microsoftSynchronizationService.getSiteSynchronizationsBySite(siteId); + + context.put("siteSynchronized", !ssList.isEmpty()); + + String onedriveType = (String)state.getAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE); + if(onedriveType != null) { + Map> onedriveItemsMap = (Map>)toolSession.getAttribute(STATE_ONEDRIVE_ITEMS_MAP); + if(onedriveItemsMap == null) { + onedriveItemsMap = new HashMap<>(); + toolSession.setAttribute(STATE_ONEDRIVE_ITEMS_MAP, onedriveItemsMap); } - if(state.getAttribute(STATE_ONEDRIVE_CHILDREN) != null) { - List childrenIt = (List) state.getAttribute(STATE_ONEDRIVE_CHILDREN); - for(OneDriveItem oi : childrenIt) { - if(!onedriveItems.contains(oi)) { - onedriveItems.add(oi); + + if(onedriveType.equals(ONEDRIVE_TYPE_SITE)) { + //get teams linked with this site + Map onedriveItemsByTeam = (Map)toolSession.getAttribute(STATE_ONEDRIVE_ITEMS_SITE); + if(onedriveItemsByTeam == null) { + onedriveItemsByTeam = new HashMap<>(); + + for(SiteSynchronization ss : ssList) { + try { + //add every Team (without items) to the map -> they will be expanded later (doNavigateOneDrive) + MicrosoftTeam team = microsoftCommonService.getTeam(ss.getTeamId()); + onedriveItemsByTeam.put(ss.getTeamId(), MicrosoftTeamWrapper.builder(team).build()); + }catch(Exception e) { + log.warn("Error getting Microsoft Team: {}", ss.getTeamId()); + } } + toolSession.setAttribute(STATE_ONEDRIVE_ITEMS_SITE, onedriveItemsByTeam); + } + + context.put("onedriveItemsByTeam", onedriveItemsByTeam); + } else if(onedriveType.equals(ONEDRIVE_TYPE_USER) || onedriveType.equals(ONEDRIVE_TYPE_SHARED)) { + MicrosoftAccessToken mcAccessToken = microsoftAuthorizationService.getAccessToken(userDirectoryService.getCurrentUser().getId()); + if(mcAccessToken != null) { + context.put("onedriveUserAccount", mcAccessToken.getMicrosoftUserId()); + + List onedriveItems = (List)toolSession.getAttribute(onedriveType.equals(ONEDRIVE_TYPE_USER) ? STATE_ONEDRIVE_ITEMS_USER : STATE_ONEDRIVE_ITEMS_SHARED); + if(onedriveItems == null) { + try { + //add initial items/folders -> folders will be expanded later (doNavigateOneDrive) + switch(onedriveType) { + case ONEDRIVE_TYPE_USER: + onedriveItems = microsoftCommonService.getMyDriveItems(userDirectoryService.getCurrentUser().getId()); + toolSession.setAttribute(STATE_ONEDRIVE_ITEMS_USER, onedriveItems); + break; + case ONEDRIVE_TYPE_SHARED: + onedriveItems = microsoftCommonService.getMySharedDriveItems(userDirectoryService.getCurrentUser().getId()); + toolSession.setAttribute(STATE_ONEDRIVE_ITEMS_SHARED, onedriveItems); + break; + default: + break; + } + }catch(MicrosoftCredentialsException e) { + //this should not be reached + log.warn("Error getting Drive Items for User or Shared : MicrosoftCredentialsException"); + } + + Collections.sort(onedriveItems); + + //add to items map + Map aux = onedriveItems.stream() + .collect(Collectors.toMap(MicrosoftDriveItem::getId, Function.identity())); + onedriveItemsMap.computeIfAbsent(onedriveType, k -> new HashMap<>()).putAll(aux); + } + + context.put("onedriveItems", onedriveItems); } - } - if(onedriveItems != null) { - Collections.sort(onedriveItems, new OneDriveItemComparator()); - toolSession.setAttribute(STATE_ONEDRIVE_ITEMS, onedriveItems); - context.put("onedriveItems", onedriveItems); } } - - return template; - } + } /** * remove all security advisors @@ -1016,6 +1091,7 @@ protected void cleanup(SessionState state) state.removeAttribute(FilePickerHelper.FILE_PICKER_ATTACH_LINKS); state.removeAttribute(STATE_NAVIGATING_RESOURCES); state.removeAttribute(STATE_NAVIGATING_ONEDRIVE); + state.removeAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE); state.removeAttribute(STATE_NAVIGATING_GOOGLEDRIVE); } @@ -1206,7 +1282,6 @@ protected void initState(SessionState state, VelocityPortlet portlet, RunData da @SuppressWarnings("unchecked") public void doAttachitem(RunData data) { - if (!"POST".equals(data.getRequest().getMethod())) { return; } @@ -1240,18 +1315,15 @@ public void doAttachitem(RunData data) { attachLink(itemId, state); } - } else if (onedriveOn && StringUtils.isNotBlank(onedriveItemId)) { + } else if (isOneDriveOn() && StringUtils.isNotBlank(onedriveItemId)) { boolean onedriveItemClone = params.getBoolean("onedriveItemClone"); - List items = (List) toolSession.getAttribute(STATE_ONEDRIVE_ITEMS); - OneDriveItem oi = null; - for(OneDriveItem off : items) { - if(onedriveItemId.equals(off.getOneDriveItemId())) { - oi = off; - break; - } - } - if(oi != null) { - doAttachOneDrive(oi, state, onedriveItemClone); + + String onedriveType = (String)state.getAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE); + Map> onedriveItemsMap = (Map>)toolSession.getAttribute(STATE_ONEDRIVE_ITEMS_MAP); + MicrosoftDriveItem item = (onedriveItemsMap != null) ? onedriveItemsMap.computeIfAbsent(onedriveType, k -> new HashMap<>()).get(onedriveItemId) : null; + + if(item != null) { + doAttachOneDrive(item, state, onedriveItemClone); } } else if (this.isGoogleDriveOn() && StringUtils.isNotBlank(googledriveItemId)) { boolean googledriveItemClone = params.getBoolean("googledriveItemClone"); @@ -1718,7 +1790,7 @@ public void doAddattachments(RunData data) } @SuppressWarnings("unchecked") - public void doAttachOneDrive(OneDriveItem onedriveItem, SessionState state, boolean onedriveItemClone) { + public void doAttachOneDrive(MicrosoftDriveItem microsoftDriveItem, SessionState state, boolean onedriveItemClone) { ToolSession toolSession = sessionManager.getCurrentToolSession(); ContentHostingService contentService = (ContentHostingService) toolSession.getAttribute (STATE_CONTENT_SERVICE); ResourceTypeRegistry registry = (ResourceTypeRegistry) toolSession.getAttribute(STATE_RESOURCES_TYPE_REGISTRY); @@ -1731,9 +1803,8 @@ public void doAttachOneDrive(OneDriveItem onedriveItem, SessionState state, bool try { ContentResource attachment = null; ResourcePropertiesEdit newprops = contentService.newResourceProperties(); - String onedriveUrl = onedriveItem.getDownloadUrl(); - String contentType = onedriveItem.getFile().getMimeType(); - String filename = onedriveItem.getName(); + String contentType = microsoftDriveItem.getMimeType(); + String filename = microsoftDriveItem.getName(); String resourceId = Validator.escapeResourceName(filename); String siteId = toolManager.getCurrentPlacement().getContext(); String toolName = (String) toolSession.getAttribute(STATE_ATTACH_TOOL_NAME); @@ -1744,6 +1815,7 @@ public void doAttachOneDrive(OneDriveItem onedriveItem, SessionState state, bool String typeId = ResourceType.TYPE_UPLOAD; newprops.addProperty(ResourceProperties.PROP_DISPLAY_NAME, filename); newprops.addProperty(ResourceProperties.PROP_DESCRIPTION, filename); + //copy to Sakai resources if(onedriveItemClone) { String max_file_size_mb = (String) toolSession.getAttribute(STATE_FILE_UPLOAD_MAX_SIZE); long max_bytes = 1024L * 1024L; @@ -1753,23 +1825,31 @@ public void doAttachOneDrive(OneDriveItem onedriveItem, SessionState state, bool max_file_size_mb = "1"; max_bytes = 1024L * 1024L; } - if(onedriveItem.getSize() >= max_bytes) { + if(microsoftDriveItem.getSize() >= max_bytes) { addAlert(state, trb.getFormattedMessage("size.exceeded", new Object[]{ max_file_size_mb })); return; } - InputStream contentStream = new URL(onedriveUrl).openStream(); + //shared items does not contain DownloadURL. We need to ask for it + if(microsoftDriveItem.isShared() && microsoftDriveItem.getDownloadURL() == null) { + MicrosoftDriveItem aux = microsoftCommonService.getDriveItem(microsoftDriveItem.getDriveId(), microsoftDriveItem.getId(), userDirectoryService.getCurrentUser().getId()); + microsoftDriveItem.setDownloadURL((aux != null) ? aux.getDownloadURL() : null); + } + InputStream contentStream = new URL(microsoftDriveItem.getDownloadURL()).openStream(); attachment = contentService.addAttachmentResource(resourceId, siteId, toolName, contentType, contentStream, newprops); - } else { - //typeId = ResourceType.TYPE_URL; - contentType = ResourceProperties.TYPE_URL; - attachment = contentService.addAttachmentResource(resourceId, siteId, toolName, contentType, onedriveUrl.getBytes(), newprops); + } else { //link to Microsoft + contentType = ResourceType.MIME_TYPE_MICROSOFT + contentType; + attachment = contentService.addAttachmentResource(resourceId, siteId, toolName, contentType, microsoftDriveItem.getUrl().getBytes(), newprops); + //private URL. We need to grant permissions to every Team linked with this Site + List ssList = microsoftSynchronizationService.getSiteSynchronizationsBySite(siteId); + for(SiteSynchronization ss : ssList) { + microsoftCommonService.grantReadPermissionToTeam(microsoftDriveItem.getDriveId(), microsoftDriveItem.getId(), ss.getTeamId()); + } } - String displayName = filename; String containerId = contentService.getContainingCollectionId(attachment.getId()); String accessUrl = attachment.getUrl(); log.debug("OneDrive item accessUrl {}", accessUrl); - AttachItem item = new AttachItem(attachment.getId(), displayName, containerId, accessUrl); + AttachItem item = new AttachItem(attachment.getId(), filename, containerId, accessUrl); item.setContentType(contentType); typeId = attachment.getResourceType(); item.setResourceType(typeId); @@ -1780,6 +1860,7 @@ public void doAttachOneDrive(OneDriveItem onedriveItem, SessionState state, bool new_items.add(item); toolSession.setAttribute(STATE_HELPER_CHANGED, Boolean.TRUE.toString()); state.setAttribute(STATE_NAVIGATING_ONEDRIVE, true); + state.removeAttribute(STATE_NAVIGATING_RESOURCES); state.removeAttribute(STATE_NAVIGATING_GOOGLEDRIVE); } catch(Exception e) { log.error("doAttachOneDrive : {}", e.getMessage()); @@ -1794,7 +1875,16 @@ public void doRevokeOneDrive(RunData data) { state.setAttribute(STATE_NAVIGATING_ONEDRIVE, true); state.removeAttribute(STATE_NAVIGATING_RESOURCES); state.removeAttribute(STATE_NAVIGATING_GOOGLEDRIVE); - onedriveService.revokeOneDriveConfiguration(userDirectoryService.getCurrentUser().getId()); + state.removeAttribute(STATE_ONEDRIVE_ITEMS_USER); + state.removeAttribute(STATE_ONEDRIVE_ITEMS_SHARED); + + microsoftAuthorizationService.revokeAccessToken(userDirectoryService.getCurrentUser().getId()); + microsoftCommonService.resetUserDriveItemsCache(userDirectoryService.getCurrentUser().getId()); + microsoftCommonService.resetDriveItemsCache(); + + Map> onedriveItemsMap = (Map>)state.getAttribute(STATE_ONEDRIVE_ITEMS_MAP); + onedriveItemsMap.remove(ONEDRIVE_TYPE_USER); + onedriveItemsMap.remove(ONEDRIVE_TYPE_SHARED); } public void doRefreshOneDrive(RunData data) { @@ -1802,9 +1892,34 @@ public void doRefreshOneDrive(RunData data) { state.setAttribute(STATE_NAVIGATING_ONEDRIVE, true); state.removeAttribute(STATE_NAVIGATING_RESOURCES); state.removeAttribute(STATE_NAVIGATING_GOOGLEDRIVE); - state.removeAttribute(STATE_ONEDRIVE_ITEMS); - state.removeAttribute(STATE_ONEDRIVE_CHILDREN); - onedriveService.cleanOneDriveCacheForUser(userDirectoryService.getCurrentUser().getId()); + + String onedriveType = (String)state.getAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE); + switch(onedriveType) { + case ONEDRIVE_TYPE_SITE: + state.removeAttribute(STATE_ONEDRIVE_ITEMS_SITE); + + String siteId = toolManager.getCurrentPlacement().getContext(); + List ssList = microsoftSynchronizationService.getSiteSynchronizationsBySite(siteId); + for(SiteSynchronization ss : ssList) { + microsoftCommonService.resetGroupDriveItemsCache(ss.getTeamId()); + } + microsoftCommonService.resetDriveItemsCache(); + break; + case ONEDRIVE_TYPE_USER: + state.removeAttribute(STATE_ONEDRIVE_ITEMS_USER); + microsoftCommonService.resetUserDriveItemsCache(userDirectoryService.getCurrentUser().getId()); + break; + case ONEDRIVE_TYPE_SHARED: + state.removeAttribute(STATE_ONEDRIVE_ITEMS_SHARED); + microsoftCommonService.resetUserDriveItemsCache(userDirectoryService.getCurrentUser().getId()); + microsoftCommonService.resetDriveItemsCache(); + break; + default: + break; + } + + Map> onedriveItemsMap = (Map>)state.getAttribute(STATE_ONEDRIVE_ITEMS_MAP); + onedriveItemsMap.remove(onedriveType); } @SuppressWarnings("unchecked") @@ -2208,23 +2323,33 @@ protected void toolModeDispatch(String methodBase, String methodExt, HttpServlet return; } - if (onedriveOn && MODE_ONEDRIVE.equals(toolSession.getAttribute(STATE_FILEPICKER_MODE))) { - try { - cleanup(state); - log.debug("Requesting OneDrive access data for this user"); - String onedriveUrl = onedriveService.formAuthenticationUrl(); - //Add the picker URL to the session to go back after setting the credentials - sessionManager.getCurrentSession().setAttribute(onedriveService.ONEDRIVE_REDIRECT_URI, req.getRequestURL()); - state.setAttribute(STATE_NAVIGATING_ONEDRIVE, true); - state.removeAttribute(STATE_NAVIGATING_RESOURCES); - state.removeAttribute(STATE_NAVIGATING_GOOGLEDRIVE); - res.sendRedirect(onedriveUrl); - } catch (IOException e) { - log.warn("IOException: ", e); + if (isOneDriveOn()) { + //doConfigureOneDrive Action is launched -> starts the authorization process + if(MODE_CONFIGURE_ONEDRIVE.equals(toolSession.getAttribute(STATE_FILEPICKER_MODE))) { + sendOneDriveAuthorizationRedirect(false, req, res); + return; + } + + String onedriveType = (String)state.getAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE); + //before display "user" or "shared" items, check if current access token is valid + if(onedriveType != null && !ONEDRIVE_TYPE_SITE.equals(onedriveType)) { + try { + //check delegated client -> will throw an exception if access token is invalid + microsoftAuthorizationService.checkDelegatedClient(userDirectoryService.getCurrentUser().getId()); + }catch(MicrosoftInvalidTokenException e) { + //our configured access token is not valid. Try to get a new one through the automatic authorization process + //(automatic: no confirmation screen will be shown) + sendOneDriveAuthorizationRedirect(true, req, res); + return; + }catch(MicrosoftNoCredentialsException e) { + //this means there is no access token configured. Do nothing, continue and show the "configure" button. + }catch(MicrosoftCredentialsException e) { + //unexpected error. Remove current access token and let the process starts from the beginning + microsoftAuthorizationService.revokeAccessToken(userDirectoryService.getCurrentUser().getId()); + } } - return; } - else if (this.isGoogleDriveOn() && MODE_GOOGLEDRIVE.equals(toolSession.getAttribute(STATE_FILEPICKER_MODE))) + if (this.isGoogleDriveOn() && MODE_GOOGLEDRIVE.equals(toolSession.getAttribute(STATE_FILEPICKER_MODE))) { try { cleanup(state); @@ -2289,6 +2414,38 @@ else if(sendToHelper(req, res, req.getPathInfo())) super.toolModeDispatch(methodBase, methodExt, req, res); } } + + protected void sendOneDriveAuthorizationRedirect(boolean autoReturn, HttpServletRequest req, HttpServletResponse res) { + try { + SessionState state = getState(req); + String onedriveType = (String)state.getAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE); + Map> onedriveItemsMap = (Map>)state.getAttribute(STATE_ONEDRIVE_ITEMS_MAP); + + cleanup(state); + + log.debug("Requesting OneDrive access data for this user"); + MicrosoftRedirectURL authURL = microsoftAuthorizationService.getAuthenticationUrl(); + //store in cache where to redirect back after authorization code is received + //also store "state" (from authURL) sent to Microsoft, so we can check it when the request returns + MicrosoftRedirectURL afterTokenURL = authURL.toBuilder().URL(req.getRequestURL().toString()).auto(autoReturn).build(); + sessionManager.getCurrentSession().setAttribute(MicrosoftAuthorizationService.MICROSOFT_SESSION_REDIRECT, afterTokenURL); + state.setAttribute(STATE_NAVIGATING_ONEDRIVE, true); + state.removeAttribute(STATE_NAVIGATING_RESOURCES); + state.removeAttribute(STATE_NAVIGATING_GOOGLEDRIVE); + if(onedriveType != null) { + state.setAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE, onedriveType); + } + if(onedriveItemsMap != null) { + state.setAttribute(STATE_ONEDRIVE_ITEMS_MAP, onedriveItemsMap); + } + res.sendRedirect(authURL.getURL()); + } catch (IOException e) { + log.warn("IOException: ", e); + } catch (MicrosoftCredentialsException e) { + log.warn("MicrosoftCredentialsException: ", e); + } + } + /** * @param data @@ -2818,10 +2975,10 @@ public void doUnexpandall ( RunData data) * @param data */ @SuppressWarnings("unchecked") - public void doOneDrive(RunData data) + public void doConfigureOneDrive(RunData data) { ToolSession toolSession = sessionManager.getCurrentToolSession(); - toolSession.setAttribute(STATE_FILEPICKER_MODE, MODE_ONEDRIVE); + toolSession.setAttribute(STATE_FILEPICKER_MODE, MODE_CONFIGURE_ONEDRIVE); } @SuppressWarnings("unchecked") @@ -3528,8 +3685,7 @@ public void doNavigate ( RunData data ) } state.setAttribute(STATE_LIST_SELECTIONS, selectedSet); - String collectionId = data.getParameters().getString ("collectionId"); - String onedriveCollectionId = data.getParameters().getString ("onedriveCollectionId"); + String collectionId = data.getParameters().getString ("collectionId"); String navRoot = data.getParameters().getString("navRoot"); state.setAttribute(STATE_NAVIGATION_ROOT, navRoot); @@ -3574,23 +3730,116 @@ public void doNavigate ( RunData data ) currentMap.clear(); currentMap.addAll(newCurrentMap); } - } else if (onedriveOn && onedriveCollectionId != null){ - int depth = data.getParameters().getInt("onedriveCollectionDepth"); - List children = onedriveService.getDriveChildrenItems(userDirectoryService.getCurrentUser().getId(), onedriveCollectionId, depth); - state.setAttribute(STATE_NAVIGATING_ONEDRIVE, true); - state.removeAttribute(STATE_NAVIGATING_RESOURCES); - state.removeAttribute(STATE_NAVIGATING_GOOGLEDRIVE); - state.setAttribute(STATE_ONEDRIVE_CHILDREN, children); - List items = (List) toolSession.getAttribute(STATE_ONEDRIVE_ITEMS); - items.forEach( it -> { - if (onedriveCollectionId.equals(it.getOneDriveItemId())) { - it.setExpanded(true); - } - }); } } // doNavigate + /** + * Navigate in the Microsoft resource hireachy + */ + @SuppressWarnings("unchecked") + public void doNavigateOneDrive ( RunData data ) { + + SessionState state = ((JetspeedRunData)data).getPortletSessionState (((JetspeedRunData)data).getJs_peid ()); + ToolSession toolSession = sessionManager.getCurrentToolSession(); + + String onedriveType = data.getParameters().getString ("onedriveType"); + String onedriveTeamId = data.getParameters().getString ("onedriveTeamId"); + String onedriveCollectionId = data.getParameters().getString ("onedriveCollectionId"); + + Map> onedriveItemsMap = (Map>)toolSession.getAttribute(STATE_ONEDRIVE_ITEMS_MAP); + + //Type (left tab) changed + if(onedriveType != null) { + state.setAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE, onedriveType); + } + + //Team expanded/collapsed (only for SITE tab) + if(onedriveTeamId != null && onedriveCollectionId == null) { + onedriveType = (String)state.getAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE); + Map onedriveItemsByTeam = (Map)toolSession.getAttribute(STATE_ONEDRIVE_ITEMS_SITE); + MicrosoftTeamWrapper item = (onedriveItemsByTeam != null) ? onedriveItemsByTeam.get(onedriveTeamId) : null; + if(item != null) { + if(item.isExpanded()) { + item.setExpanded(false); + } else { + item.setExpanded(true); + + List onedriveItems = item.getItems(); + if(onedriveItems == null) { + try { + onedriveItems = microsoftCommonService.getGroupDriveItems(item.getTeam().getId()); + + item.setItems(onedriveItems); + + Collections.sort(onedriveItems); + + //add to items map + Map aux = onedriveItems.stream() + .collect(Collectors.toMap(MicrosoftDriveItem::getId, Function.identity())); + onedriveItemsMap.computeIfAbsent(onedriveType, k -> new HashMap<>()).putAll(aux); + }catch(Exception e) { + log.warn("Error getting OneDrive Items for Team: {}", item.getTeam().getId()); + } + } + } + } + } + + //Folder expanded/collapsed + if(onedriveCollectionId != null) { + onedriveType = (String)state.getAttribute(STATE_NAVIGATING_ONEDRIVE_TYPE); + + MicrosoftDriveItem item = (onedriveItemsMap != null) ? onedriveItemsMap.computeIfAbsent(onedriveType, k -> new HashMap<>()).get(onedriveCollectionId) : null; + + if(item != null && item.isFolder() && item.hasChildren()) { + if(item.isExpanded()) { + item.setExpanded(false); + } else { + item.setExpanded(true); + + List children = item.getChildren(); + if(children == null) { + try { + switch(onedriveType) { + case ONEDRIVE_TYPE_SITE: + children = microsoftCommonService.getDriveItemsByItemId(item.getDriveId(), onedriveCollectionId, null); + break; + + case ONEDRIVE_TYPE_USER: + children = microsoftCommonService.getMyDriveItemsByItemId(userDirectoryService.getCurrentUser().getId(), onedriveCollectionId); + break; + + case ONEDRIVE_TYPE_SHARED: + children = microsoftCommonService.getDriveItemsByItemId(item.getDriveId(), onedriveCollectionId, userDirectoryService.getCurrentUser().getId()); + break; + + default: + break; + } + Collections.sort(children); + + //attach children to parent + item.setChildren(children); + + //add children to map, so we can find them + Map aux = children.stream() + .collect(Collectors.toMap(MicrosoftDriveItem::getId, Function.identity())); + onedriveItemsMap.computeIfAbsent(onedriveType, k -> new HashMap<>()).putAll(aux); + } catch (MicrosoftCredentialsException e) { + //this should not be reached + log.warn("doNavigate (OneDrive) : MicrosoftCredentialsException"); + } + } + } + } + } + + state.setAttribute(STATE_NAVIGATING_ONEDRIVE, true); + state.removeAttribute(STATE_NAVIGATING_RESOURCES); + state.removeAttribute(STATE_NAVIGATING_GOOGLEDRIVE); + } // doNavigateOneDrive + /** * Find the resource with this id in the list. * @param resources The list of messages. @@ -3762,6 +4011,10 @@ private static Set getExpandedCollections(ToolSession session) { } return current; } + + private boolean isOneDriveOn() { + return microsoftConfigurationService.isOneDriveEnabled(); + } // This method checks if Google Drive is enabled for the instance and it's enabled for the user's tenant. private boolean isGoogleDriveOn() { diff --git a/content/content-tool/tool/src/webapp/vm/content/cloudstorage_onedrive_picker.vm b/content/content-tool/tool/src/webapp/vm/content/cloudstorage_onedrive_picker.vm index cdfa12f04f2a..c4b49149ae06 100644 --- a/content/content-tool/tool/src/webapp/vm/content/cloudstorage_onedrive_picker.vm +++ b/content/content-tool/tool/src/webapp/vm/content/cloudstorage_onedrive_picker.vm @@ -1,38 +1,103 @@ -

+ + + + + +

$tlang.getString('onedrive.integration')

-#if(!$!onedriveUserAccount) - - - - -#else - - - - - - - - - - #foreach($onedriveItem in $onedriveItems) - #set($itemcount = $itemcount + 1) - #set ($width = "$!{onedriveItem.depth}${unit}") - + +#macro( renderDriveItems $items $teamId) + #set($baseDepth = 0) + #if($!teamId) + #set($baseDepth = 1) + #end + #if($!items && $items.size() > 0) + #foreach($onedriveItem in $items) + #set ($widthAux = $baseDepth + $!{onedriveItem.depth}) + #set ($width = "$!{widthAux}${unit}") + + #if ($onedriveItem.isFolder() && $onedriveItem.isExpanded()) + #renderDriveItems($onedriveItem.children $teamId) + #end #end -
- $tlang.getString("cloudstorage.title") - - $tlang.getString("cloudstorage.clone") - - $tlang.getString("cloudstorage.link") -
#if (!$onedriveItem.isFolder()) - $tlang.getString("att.copy") $formattedText.escapeHtml($onedriveItem.name) + $tlang.getString("att.copy") $formattedText.escapeHtml($onedriveItem.name) #end #if (!$onedriveItem.isFolder()) - $tlang.getString('cloudstorage.attach_link') $formattedText.escapeHtml($onedriveItem.name) + $tlang.getString('cloudstorage.attach_link') $formattedText.escapeHtml($onedriveItem.name) #end
+ #else + + $tlang.getString("onedrive.empty") + + #end #end + +#macro( renderTeams $teams ) + #foreach ($entry in $teams.entrySet()) + #set($team = $entry.value) + + + #set($folderClass = "fa fa-chevron-right") + #if(${team.isExpanded()}) + #set($folderClass = "fa fa-chevron-down") + #end + + + + $tlang.getString('gen.folder1') + $formattedText.escapeHtml(${team.team.name}) + + + + + + #if($team.isExpanded()) + #renderDriveItems($team.items $team.team.id) + #end + #end +#end + +
+
+ +
+ #if($navigating_onedrive_type == 'onedrive_type_user' || $navigating_onedrive_type == 'onedrive_type_shared') + #if(!$!onedriveUserAccount) + + + + + #else + + + + #if($onedriveItems && $onedriveItems.size() > 0) + + + + + + + #renderDriveItems($onedriveItems) +
+ $tlang.getString("cloudstorage.title") + + $tlang.getString("cloudstorage.clone") + + $tlang.getString("cloudstorage.link") +
+ #else +
$tlang.getString("onedrive.empty")
+ #end + #end + #elseif($navigating_onedrive_type == 'onedrive_type_site') + #if($onedriveItemsByTeam && $onedriveItemsByTeam.size() > 0) + + + + + + + + #renderTeams($onedriveItemsByTeam) +
+ $tlang.getString("cloudstorage.title") + + $tlang.getString("cloudstorage.clone") + + $tlang.getString("cloudstorage.link") +
+ #else +
$tlang.getString("onedrive.empty")
+ #end + #else +
$tlang.getString("onedrive.select_type")
+ #end +
+
+
diff --git a/content/content-tool/tool/src/webapp/vm/content/sakai_filepicker_attach.vm b/content/content-tool/tool/src/webapp/vm/content/sakai_filepicker_attach.vm index f2015ae61602..dd80e3bdc69b 100644 --- a/content/content-tool/tool/src/webapp/vm/content/sakai_filepicker_attach.vm +++ b/content/content-tool/tool/src/webapp/vm/content/sakai_filepicker_attach.vm @@ -125,6 +125,9 @@ includeLatestJQuery('sakai_filepicker_attach.vm'); #foreach($att_item in $attached) + #if($att_item.contentType.startsWith($MIME_TYPE_MICROSOFT)) + + #end $!att_item.hoverText $formattedText.escapeHtml($att_item.displayName) @@ -599,6 +602,8 @@ $(document).ready(function () { $(this).addClass('googlethumbnailsmall'); $(this).removeClass('googlethumbnailbig'); }); + + $('.nav-item.active').find('a').focus(); }); if ( $(window).width() > portalSmallBreakPoint() ) { ## Don't go modal when mobile.. diff --git a/content/content-types/src/java/org/sakaiproject/content/types/FileUploadType.java b/content/content-types/src/java/org/sakaiproject/content/types/FileUploadType.java index d986dd4af4e7..72537c92c4da 100644 --- a/content/content-types/src/java/org/sakaiproject/content/types/FileUploadType.java +++ b/content/content-types/src/java/org/sakaiproject/content/types/FileUploadType.java @@ -111,6 +111,9 @@ public String getIconLocation(ContentEntity entity) { if (entity instanceof ContentResource) { String mimetype = ((ContentResource) entity).getContentType(); if (mimetype != null && !"".equals(mimetype.trim())) { + if(mimetype.startsWith(ResourceType.MIME_TYPE_MICROSOFT)) { + mimetype = mimetype.replaceFirst(ResourceType.MIME_TYPE_MICROSOFT, ""); + } iconLocation = contentTypeImageService.getContentTypeImage(mimetype); } } @@ -123,6 +126,9 @@ public String getIconClass(ContentEntity entity) { if (entity instanceof ContentResource) { String mimetype = ((ContentResource) entity).getContentType(); if (mimetype != null && !"".equals(mimetype.trim())) { + if(mimetype.startsWith(ResourceType.MIME_TYPE_MICROSOFT)) { + mimetype = mimetype.replaceFirst(ResourceType.MIME_TYPE_MICROSOFT, ""); + } iconClass = contentTypeImageService.getContentTypeImageClass(mimetype); } } @@ -145,6 +151,9 @@ public String getLocalizedHoverText(ContentEntity entity) { if (entity instanceof ContentResource) { String mimetype = ((ContentResource) entity).getContentType(); if (mimetype != null && !"".equals(mimetype.trim())) { + if(mimetype.startsWith(ResourceType.MIME_TYPE_MICROSOFT)) { + mimetype = mimetype.replaceFirst(ResourceType.MIME_TYPE_MICROSOFT, ""); + } hoverText = contentTypeImageService.getContentTypeDisplayName(mimetype); } } diff --git a/kernel/api/src/main/java/org/sakaiproject/content/api/ResourceType.java b/kernel/api/src/main/java/org/sakaiproject/content/api/ResourceType.java index b85f33d0a832..916570802963 100644 --- a/kernel/api/src/main/java/org/sakaiproject/content/api/ResourceType.java +++ b/kernel/api/src/main/java/org/sakaiproject/content/api/ResourceType.java @@ -40,6 +40,7 @@ public interface ResourceType public static final String MIME_TYPE_HTML = "text/html"; public static final String MIME_TYPE_METAOBJ = "application/x-osp"; public static final String MIME_TYPE_URL = "text/url"; + public static final String MIME_TYPE_MICROSOFT = "microsoft/"; public static final int EXPANDABLE_FOLDER_SIZE_LIMIT = 256; diff --git a/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessage.java b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessage.java new file mode 100644 index 000000000000..8c9f913bfa8b --- /dev/null +++ b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessage.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2023 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.messaging.api; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class MicrosoftMessage { + public enum Topic { + CREATE_ELEMENT, + DELETE_ELEMENT, + MODIFY_ELEMENT, + ADD_MEMBER_TO_AUTHZGROUP, + REMOVE_MEMBER_FROM_AUTHZGROUP, + TEAM_CREATION, + CHANGE_LISTEN_GROUP_EVENTS + } + + public enum Action { + CREATE, DELETE, ADD, REMOVE, REMOVE_ALL, ENABLE, DISABLE, UNPUBLISH; + } + public enum Type { + SITE, GROUP, TEAM; + } + + private Action action; + private Type type; + private String reference; + private String siteId; + private String groupId; + private String userId; + private boolean owner; + private int status; + @Builder.Default + private boolean force = false; + + //custom builder + public static class MicrosoftMessageBuilder { + private static Pattern sitePattern = Pattern.compile("^/site/([^/]+)$"); + private static Pattern groupPattern = Pattern.compile("^/site/([^/]+)/group/([^/]+)$"); + + //fill type, siteId and groupId based on reference + public MicrosoftMessageBuilder reference(String reference) { + this.reference = reference; + + Matcher matcher = sitePattern.matcher(reference); + if(matcher.find()) { + this.type = Type.SITE; + this.siteId = matcher.group(1); + } else { + matcher = groupPattern.matcher(reference); + if(matcher.find()) { + this.type = Type.GROUP; + this.siteId = matcher.group(1); + this.groupId = matcher.group(2); + } + } + return this; + } + } +} diff --git a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFolder.java b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessageListener.java similarity index 62% rename from cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFolder.java rename to kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessageListener.java index 2fe23d51e493..5bfd20b0e57c 100644 --- a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFolder.java +++ b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessageListener.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2003-2019 The Apereo Foundation + * Copyright (c) 2023 The Apereo Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sakaiproject.onedrive.model; +package org.sakaiproject.messaging.api; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +public interface MicrosoftMessageListener { -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -@Getter @Setter -@ToString -@JsonIgnoreProperties(ignoreUnknown = true) -public class OneDriveFolder { - public Integer childCount; + public void read(MicrosoftMessage msg); } + diff --git a/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessagingService.java b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessagingService.java new file mode 100644 index 000000000000..5d4b7bde2cb1 --- /dev/null +++ b/kernel/api/src/main/java/org/sakaiproject/messaging/api/MicrosoftMessagingService.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2023 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.messaging.api; + +public interface MicrosoftMessagingService { + + public void listen(MicrosoftMessage.Topic topic, MicrosoftMessageListener listener); + public void send(MicrosoftMessage.Topic topic, MicrosoftMessage msg); + +} diff --git a/kernel/component-manager/src/main/bundle/org/sakaiproject/config/kernel.properties b/kernel/component-manager/src/main/bundle/org/sakaiproject/config/kernel.properties index 9dcfed6c98f6..472d73966a24 100644 --- a/kernel/component-manager/src/main/bundle/org/sakaiproject/config/kernel.properties +++ b/kernel/component-manager/src/main/bundle/org/sakaiproject/config/kernel.properties @@ -282,8 +282,7 @@ realm.allowed..auth=annc.all.groups,annc.read,asn.read,asn.receive.notifications rwiki.update,signup.attend,signup.attend.all,signup.view,signup.view.all,site.viewRoster,site.visit,sitestats.view,\ usermembership.view,lessonbuilder.read -stealthTools@org.sakaiproject.tool.api.ActiveToolManager= - +stealthTools@org.sakaiproject.tool.api.ActiveToolManager=sakai.meetings,microsoft.mediagallery,microsoft.collaborativedocuments # These tools need to use server saving method as they are on older JSF versions. If upgraded to JSf 1.2+ please remove from this list # Podcasts Tool jsf.state_saving_method.sakai-podcasts=server diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/AuthzGroupServiceTest.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/AuthzGroupServiceTest.java index 63a229b03cb8..ebb5a5d3e4c3 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/AuthzGroupServiceTest.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/AuthzGroupServiceTest.java @@ -27,6 +27,7 @@ import org.sakaiproject.db.api.SqlService; import org.sakaiproject.entity.api.EntityManager; import org.sakaiproject.event.api.EventTrackingService; +import org.sakaiproject.messaging.api.MicrosoftMessagingService; import org.sakaiproject.time.api.TimeService; import org.sakaiproject.user.api.UserDirectoryService; import org.sakaiproject.tool.api.SessionManager; @@ -121,4 +122,9 @@ public Set getMaintainRoles() { public String getGroupReference(String siteId, String groupId) { return null; } + + @Override + protected MicrosoftMessagingService microsoftMessagingService() { + return null; + } } diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroup.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroup.java index 864be90ba9ea..a6c913385f77 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroup.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroup.java @@ -44,6 +44,8 @@ import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.entity.api.ResourcePropertiesEdit; +import org.sakaiproject.messaging.api.MicrosoftMessage; +import org.sakaiproject.site.api.SiteService; import org.sakaiproject.time.api.TimeService; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserDirectoryService; @@ -1048,6 +1050,16 @@ public void addMember(String user, String roleId, boolean active, boolean provid grant.active = active; grant.provided = provided; } + + + //send message to (ignite) MicrosoftMessagingService + this.baseAuthzGroupService.microsoftMessagingService().send(MicrosoftMessage.Topic.ADD_MEMBER_TO_AUTHZGROUP, MicrosoftMessage.builder() + .action(MicrosoftMessage.Action.ADD) + .reference(this.getId()) + .userId(user) + .owner(role.isAllowed(SiteService.SECURE_UPDATE_SITE)) + .build() + ); } /** @@ -1058,6 +1070,14 @@ public void removeMember(String user) if (m_lazy) baseAuthzGroupService.m_storage.completeGet(this); m_userGrants.remove(user); + + //send message to (ignite) MicrosoftMessagingService + this.baseAuthzGroupService.microsoftMessagingService().send(MicrosoftMessage.Topic.REMOVE_MEMBER_FROM_AUTHZGROUP, MicrosoftMessage.builder() + .action(MicrosoftMessage.Action.REMOVE) + .reference(this.getId()) + .userId(user) + .build() + ); } /** @@ -1079,6 +1099,13 @@ public void removeMembers() if (m_lazy) baseAuthzGroupService.m_storage.completeGet(this); m_userGrants.clear(); + + //send message to (ignite) MicrosoftMessagingService + this.baseAuthzGroupService.microsoftMessagingService().send(MicrosoftMessage.Topic.REMOVE_MEMBER_FROM_AUTHZGROUP, MicrosoftMessage.builder() + .action(MicrosoftMessage.Action.REMOVE_ALL) + .reference(this.getId()) + .build() + ); } /** diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroupService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroupService.java index e5fcb5f81d07..eea4832895e6 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroupService.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/authz/impl/BaseAuthzGroupService.java @@ -61,6 +61,7 @@ import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.javax.PagingPosition; +import org.sakaiproject.messaging.api.MicrosoftMessagingService; import org.sakaiproject.site.api.SiteService; import org.sakaiproject.time.api.TimeService; import org.sakaiproject.tool.api.SessionManager; @@ -281,6 +282,8 @@ public void setRoleProvider(RoleProvider provider) protected List authzGroupAdvisors; + protected abstract MicrosoftMessagingService microsoftMessagingService(); + protected SiteService siteService; public void setSiteService(SiteService siteService) { this.siteService = siteService; diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java index 00687c0949ee..085246a255ad 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java @@ -5947,13 +5947,13 @@ public void commitResource(ContentResourceEdit edit, int priority) throws OverQu boolean hasContentTypeAlready = hasContentType(edit.getId()); //use magic to fix mimetype - //Don't process for special TYPE_URL type + //Don't process for special TYPE_URL or MICROSOFT type String currentContentType = edit.getContentType(); m_useMimeMagic = m_serverConfigurationService.getBoolean("content.useMimeMagic", m_useMimeMagic); m_ignoreExtensions = Arrays.asList(ArrayUtils.nullToEmpty(m_serverConfigurationService.getStrings("content.mimeMagic.ignorecontent.extensions"))); m_ignoreMimeTypes = Arrays.asList(ArrayUtils.nullToEmpty(m_serverConfigurationService.getStrings("content.mimeMagic.ignorecontent.mimetypes"))); - if (m_useMimeMagic && DETECTOR != null && !ResourceProperties.TYPE_URL.equals(currentContentType) && !hasContentTypeAlready) { + if (m_useMimeMagic && DETECTOR != null && !ResourceProperties.TYPE_URL.equals(currentContentType) && !currentContentType.startsWith(ResourceType.MIME_TYPE_MICROSOFT) && !hasContentTypeAlready) { try ( TikaInputStream buff = TikaInputStream.get(edit.streamContent()); ) { @@ -6926,8 +6926,8 @@ protected void handleAccessResource(HttpServletRequest req, HttpServletResponse res.addHeader("Last-Modified", rfc1123Date.format(lastModTime)); } - // for url content type, encode a redirect to the body URL - if (contentType.equalsIgnoreCase(ResourceProperties.TYPE_URL)) + // for url or Microsoft content type, encode a redirect to the body URL + if (contentType.equalsIgnoreCase(ResourceProperties.TYPE_URL) || contentType.startsWith(ResourceType.MIME_TYPE_MICROSOFT)) { if (len < MAX_URL_LENGTH) { diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/MicrosoftMessagingServiceImpl.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/MicrosoftMessagingServiceImpl.java new file mode 100644 index 000000000000..fc555255f72c --- /dev/null +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/messaging/impl/MicrosoftMessagingServiceImpl.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2023 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.messaging.impl; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.annotation.Resource; + +import org.apache.ignite.IgniteMessaging; +import org.sakaiproject.ignite.EagerIgniteSpringBean; +import org.sakaiproject.messaging.api.MicrosoftMessage; +import org.sakaiproject.messaging.api.MicrosoftMessageListener; +import org.sakaiproject.messaging.api.MicrosoftMessagingService; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MicrosoftMessagingServiceImpl implements MicrosoftMessagingService { + + @Resource + private EagerIgniteSpringBean ignite; + + private IgniteMessaging messaging; + + private ExecutorService executor; + + public void init() { + log.info("Initializing Microsoft Messaging Service"); + + executor = Executors.newFixedThreadPool(20); + messaging = ignite.message(ignite.cluster().forLocal()); + } + + public void destroy() { + executor.shutdownNow(); + } + + public void listen(MicrosoftMessage.Topic topic, MicrosoftMessageListener listener) { + messaging.localListen(topic, (nodeId, message) -> { + executor.execute(() -> { + listener.read((MicrosoftMessage)message); + }); + return true; + }); + } + + public void send(MicrosoftMessage.Topic topic, MicrosoftMessage msg) { + try { + messaging.send(topic, msg); + } catch(Exception e) { + log.error("Error sending MicrosoftMessage"); + } + } + +} diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSite.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSite.java index 336b8bb0bf4a..af036580c612 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSite.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSite.java @@ -47,6 +47,8 @@ import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.entity.api.ResourcePropertiesEdit; +import org.sakaiproject.messaging.api.MicrosoftMessage; +import org.sakaiproject.messaging.api.MicrosoftMessagingService; import org.sakaiproject.site.api.Group; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SitePage; @@ -169,6 +171,7 @@ public class BaseSite implements Site protected AuthzGroup m_azg = null; private AuthzGroupService authzGroupService; + private MicrosoftMessagingService microsoftMessagingService; private FormattedText formattedText; /** * Set to true if we have changed our azg, so it need to be written back on @@ -577,6 +580,7 @@ private void setupServices(BaseSiteService siteService, SessionManager sessionMa } } this.authzGroupService = this.siteService.authzGroupService(); + this.microsoftMessagingService = this.siteService.microsoftMessagingService(); this.sessionManager = sessionManager; if (this.sessionManager == null) { this.sessionManager = (SessionManager) ComponentManager.get(SessionManager.class); @@ -1550,7 +1554,16 @@ public void setJoinerRole(String role) public void setPublished(boolean published) { m_published = published; - + + //send message to (ignite) MicrosoftMessagingService + if(!published) { + microsoftMessagingService.send(MicrosoftMessage.Topic.MODIFY_ELEMENT, MicrosoftMessage.builder() + .action(MicrosoftMessage.Action.UNPUBLISH) + .type(MicrosoftMessage.Type.SITE) + .siteId(this.getId()) + .build() + ); + } } /** @@ -1717,6 +1730,15 @@ public Group addGroup() { Group rv = new BaseGroup(siteService, this); m_groups.add(rv); + + //send message to (ignite) MicrosoftMessagingService + microsoftMessagingService.send(MicrosoftMessage.Topic.CREATE_ELEMENT, MicrosoftMessage.builder() + .action(MicrosoftMessage.Action.CREATE) + .type(MicrosoftMessage.Type.GROUP) + .siteId(this.getId()) + .groupId(rv.getId()) + .build() + ); return rv; } @@ -1747,6 +1769,15 @@ public void deleteGroup(Group group) throws AuthzRealmLockException // track so we can clean up related on commit m_deletedGroups.add(group); + + //send message to (ignite) MicrosoftMessagingService + microsoftMessagingService.send(MicrosoftMessage.Topic.DELETE_ELEMENT, MicrosoftMessage.builder() + .action(MicrosoftMessage.Action.DELETE) + .type(MicrosoftMessage.Type.GROUP) + .siteId(this.getId()) + .groupId(group.getId()) + .build() + ); } /** diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSiteService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSiteService.java index d8a96035ec32..b5a12f52ccde 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSiteService.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/site/impl/BaseSiteService.java @@ -85,6 +85,8 @@ import org.sakaiproject.javax.PagingPosition; import org.sakaiproject.memory.api.Cache; import org.sakaiproject.memory.api.MemoryService; +import org.sakaiproject.messaging.api.MicrosoftMessage; +import org.sakaiproject.messaging.api.MicrosoftMessagingService; import org.sakaiproject.site.api.AllowedJoinableAccount; import org.sakaiproject.site.api.Group; import org.sakaiproject.site.api.Site; @@ -478,6 +480,12 @@ public void setCacheCleanerMinutes(String time) * @return the NotificationService collaborator */ protected abstract NotificationService notificationService(); + + /** + * + * @return the MicrosoftMessagingService collaborator + */ + protected abstract MicrosoftMessagingService microsoftMessagingService(); /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy @@ -1345,6 +1353,14 @@ public Site addSite(String id, String type) throws IdInvalidException, IdUsedExc ((BaseSite) site).setEvent(SECURE_ADD_SITE); doSave((BaseSite) site, true); + + //send message to (ignite) MicrosoftMessagingService + microsoftMessagingService().send(MicrosoftMessage.Topic.CREATE_ELEMENT, MicrosoftMessage.builder() + .action(MicrosoftMessage.Action.CREATE) + .type(MicrosoftMessage.Type.SITE) + .siteId(id) + .build() + ); return site; } @@ -1432,6 +1448,14 @@ public Site addSite(String id, Site other, String realmTemplate) throws IdInvali ((BaseSite) site).setEvent(SECURE_ADD_SITE); doSave((BaseSite) site, true); + + //send message to (ignite) MicrosoftMessagingService + microsoftMessagingService().send(MicrosoftMessage.Topic.CREATE_ELEMENT, MicrosoftMessage.builder() + .action(MicrosoftMessage.Action.CREATE) + .type(MicrosoftMessage.Type.SITE) + .siteId(id) + .build() + ); return site; } @@ -1527,6 +1551,14 @@ public void removeSite(Site site, boolean isHardDelete) throws PermissionExcepti // Use the HardDelete interface to purge content from database if (isHardDelete) { hardDelete(site); + + //send message to (ignite) MicrosoftMessagingService + microsoftMessagingService().send(MicrosoftMessage.Topic.DELETE_ELEMENT, MicrosoftMessage.builder() + .action(MicrosoftMessage.Action.DELETE) + .type(MicrosoftMessage.Type.SITE) + .siteId(site.getId()) + .build() + ); } } diff --git a/kernel/kernel-impl/src/main/sql/mysql/sakai_site.sql b/kernel/kernel-impl/src/main/sql/mysql/sakai_site.sql index 5d7b440d1d66..c8aaf57cef6e 100644 --- a/kernel/kernel-impl/src/main/sql/mysql/sakai_site.sql +++ b/kernel/kernel-impl/src/main/sql/mysql/sakai_site.sql @@ -235,6 +235,8 @@ INSERT INTO SAKAI_SITE_PAGE VALUES('~admin-1120', '~admin', 'Preferences', '0', INSERT INTO SAKAI_SITE_TOOL VALUES('~admin-1125', '~admin-1120', '~admin', 'sakai.preferences', 1, 'Preferences', NULL ); INSERT INTO SAKAI_SITE_PAGE VALUES('~admin-1200', '~admin', 'User Membership', '0', 14, '0' ); INSERT INTO SAKAI_SITE_TOOL VALUES('~admin-1210', '~admin-1200', '~admin', 'sakai.usermembership', 1, 'User Membership', NULL ); +INSERT INTO SAKAI_SITE_PAGE VALUES('~admin-2000', '~admin', 'Microsoft Admin Tool', '0', 15, '0' ); +INSERT INTO SAKAI_SITE_TOOL VALUES('~admin-2000', '~admin-2000', '~admin', 'microsoft.admin', 1, 'Microsoft Admin Tool', NULL ); INSERT INTO SAKAI_SITE VALUES('!admin', 'Administration Workspace', null, null, 'Administration Workspace', null, null, null, 1, 0, 0, '', 'admin', 'admin', NOW(), NOW(), 0, 0, 0, 0, null); INSERT INTO SAKAI_SITE_PAGE VALUES('!admin-100', '!admin', 'Home', '0', 1, '0' ); @@ -299,6 +301,9 @@ INSERT INTO SAKAI_SITE_PAGE_PROPERTY VALUES('!admin', '!admin-1575', 'sitePage.c INSERT INTO SAKAI_SITE_PAGE VALUES('!admin-1590', '!admin', 'Academic Term Manager', '0', 22, '0' ); INSERT INTO SAKAI_SITE_TOOL VALUES('!admin-1590', '!admin-1590', '!admin', 'sakai.acadtermmanage', 1, 'Academic Term Manager', NULL ); INSERT INTO SAKAI_SITE_PAGE_PROPERTY VALUES('!admin', '!admin-1590', 'sitePage.customTitle', 'true'); +INSERT INTO SAKAI_SITE_PAGE VALUES('!admin-2000', '!admin', 'Microsoft Admin Tool', '0', 23, '0' ); +INSERT INTO SAKAI_SITE_TOOL VALUES('!admin-2000', '!admin-2000', '!admin', 'microsoft.admin', 1, 'Microsoft Admin Tool', NULL ); +INSERT INTO SAKAI_SITE_PAGE_PROPERTY VALUES('!admin', '!admin-2000', 'sitePage.customTitle', 'true'); INSERT INTO SAKAI_SITE_USER VALUES('!admin', 'admin', -1); diff --git a/kernel/kernel-impl/src/main/webapp/WEB-INF/authz-components.xml b/kernel/kernel-impl/src/main/webapp/WEB-INF/authz-components.xml index 5b533a7c4b4a..3b17dca591d5 100644 --- a/kernel/kernel-impl/src/main/webapp/WEB-INF/authz-components.xml +++ b/kernel/kernel-impl/src/main/webapp/WEB-INF/authz-components.xml @@ -23,6 +23,7 @@ + ${auto.ddl} diff --git a/kernel/kernel-impl/src/main/webapp/WEB-INF/messaging-components.xml b/kernel/kernel-impl/src/main/webapp/WEB-INF/messaging-components.xml index eed305555861..6bcdf673cedb 100644 --- a/kernel/kernel-impl/src/main/webapp/WEB-INF/messaging-components.xml +++ b/kernel/kernel-impl/src/main/webapp/WEB-INF/messaging-components.xml @@ -35,4 +35,9 @@ + + + diff --git a/kernel/kernel-impl/src/main/webapp/WEB-INF/site-components.xml b/kernel/kernel-impl/src/main/webapp/WEB-INF/site-components.xml index a36f922eae31..2d85f2d57461 100644 --- a/kernel/kernel-impl/src/main/webapp/WEB-INF/site-components.xml +++ b/kernel/kernel-impl/src/main/webapp/WEB-INF/site-components.xml @@ -29,6 +29,7 @@ + ${auto.ddl} diff --git a/kernel/kernel-impl/src/test/java/org/sakaiproject/authz/impl/test/ContreteAuthzGroupService.java b/kernel/kernel-impl/src/test/java/org/sakaiproject/authz/impl/test/ContreteAuthzGroupService.java index 2d8fc9fdf1bb..26d4310e1849 100644 --- a/kernel/kernel-impl/src/test/java/org/sakaiproject/authz/impl/test/ContreteAuthzGroupService.java +++ b/kernel/kernel-impl/src/test/java/org/sakaiproject/authz/impl/test/ContreteAuthzGroupService.java @@ -21,6 +21,7 @@ import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.entity.api.EntityManager; import org.sakaiproject.event.api.EventTrackingService; +import org.sakaiproject.messaging.api.MicrosoftMessagingService; import org.sakaiproject.time.api.TimeService; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.user.api.UserDirectoryService; @@ -82,4 +83,10 @@ protected Storage newStorage() { return null; } + @Override + protected MicrosoftMessagingService microsoftMessagingService() { + // TODO Auto-generated method stub + return null; + } + } diff --git a/kernel/kernel-impl/src/test/java/org/sakaiproject/site/impl/SiteServiceTest.java b/kernel/kernel-impl/src/test/java/org/sakaiproject/site/impl/SiteServiceTest.java index 349baad997a3..7c5cf43c0ed1 100644 --- a/kernel/kernel-impl/src/test/java/org/sakaiproject/site/impl/SiteServiceTest.java +++ b/kernel/kernel-impl/src/test/java/org/sakaiproject/site/impl/SiteServiceTest.java @@ -36,6 +36,7 @@ import org.sakaiproject.id.api.IdManager; import org.sakaiproject.javax.PagingPosition; import org.sakaiproject.memory.api.MemoryService; +import org.sakaiproject.messaging.api.MicrosoftMessagingService; import org.sakaiproject.site.api.Site; import org.sakaiproject.thread_local.api.ThreadLocalManager; import org.sakaiproject.time.api.TimeService; @@ -188,4 +189,10 @@ protected NotificationService notificationService() { // TODO Auto-generated method stub return null; } + + @Override + protected MicrosoftMessagingService microsoftMessagingService() { + // TODO Auto-generated method stub + return null; + } } diff --git a/library/src/skins/default/src/sass/base/_icons.scss b/library/src/skins/default/src/sass/base/_icons.scss index 3a706d0ee2ed..3177d9b50ed5 100644 --- a/library/src/skins/default/src/sass/base/_icons.scss +++ b/library/src/skins/default/src/sass/base/_icons.scss @@ -102,6 +102,9 @@ $fa-font-path: "./fonts"; sakai-users : bi-people, sakai-web-168 : bi-globe, sakai-plus: bi-plus-square, + microsoft-admin : bi-microsoft, + microsoft-mediagallery : bi-collection-play, + microsoft-collaborativedocuments : bi-files, // other tools alerts : bi-bell, kaltura-admin : bi-gear, diff --git a/library/src/skins/default/src/sass/modules/tool/microsoft-admin/_microsoft-admin.scss b/library/src/skins/default/src/sass/modules/tool/microsoft-admin/_microsoft-admin.scss new file mode 100644 index 000000000000..667e7879e1b2 --- /dev/null +++ b/library/src/skins/default/src/sass/modules/tool/microsoft-admin/_microsoft-admin.scss @@ -0,0 +1,250 @@ +.#{$namespace}microsoft-admin { + + // General styles ------------------------ + $background-color_1: var(--sakai-color-gray--lighter-7); + + .microsoft-search{ + font-family:Arial, FontAwesome; + margin-bottom: 15px; + } + + div.page-header { + .header-title { + margin: revert; + } + } + + .text-bold { + font-weight: 700; + } + + div.table-row { + display: flex; + align-items: center; + border: 1px solid #ddd; + } + + .table-space { + >div { + padding: 10px; + } + } + + div.index-background { + background-color: $background-color_1; + } + + a.my-info-class { + text-decoration: none; + } + + input[type="checkbox"] { + margin: 0 !important; + } + + .popover{ + width: 250px; + } + + .flex-column{ + display:flex; + flex-direction: column; + } + + .property-filter{ + display: flex; + align-items: center; + justify-content: flex-end; + gap: 5px; + } + + + // Index styles ----------------- + .rotate90 { + transform: rotate(90deg); + transition: transform 0.3s ease-in-out; + } + + .no-h-padding { + padding-right: 0; + padding-left: 0; + } + + .utility-gap { + gap: 15px; + } + + .utility-container { + div { + justify-content: center; + } + } + + div.search { + justify-content: flex-start; + } + + .site-title-margin { + margin-left: 8px; + } + + .input-search { + margin: 0 !important; + } + + + // Synch Row Styles --------------------- + .container-flex { + display: flex; + align-items: center; + justify-content: center; + } + + .group-table { + padding: 1em 2.2em; + border: solid #ddd; + border-width: 0px 1px 1px; + >div { + >div { + border: 1px solid #ddd; + } + &:nth-child(odd) { + background-color: $background-color_1; + } + } + } + + .multi-button { + display: flex; + justify-content: space-evenly; + align-items: center; + } + + .run-button-disabled { + color: lightgrey; + } + + + // Auto Config styles ------------------------- + .align-center { + display: flex; + align-items: center; + } + + .mb-15 { + margin-bottom: 15px; + } + + #progress-container { + .progress { + padding: 0px; + margin: auto; + } + .fa-exclamation-circle { + color: red; + } + } + + // Edit Group styles ---------------------------------------- + .row-list { + >div { + &:nth-child(even) { + >div { + background-color: $background-color_1; + } + } + } + } + + i.fa.fa-solid.fa-trash { + font-size: 20px; + } + + .modal-dialog { + width: 700px; + } + + .col-title { + margin-bottom: 10px; + } + + + // New Site styles ------------------------------------------ + .microsoft-block{ + border: 1px solid black; + + ul.list-unstyled{ + overflow-y: auto; + height: 262px; + } + } + + .fieldset-collapse { + border: 2px solid #8A8A8A; + position: relative; + margin-bottom: 15px; + padding: 1em; + } + + .arrow-container { + margin-top: 40px; + margin-bottom: 20px; + font-size: 30px; + } + + .synchContainer { + display: flex; + } + + .dates-container { + display: flex; + flex-direction: column; + margin-top: 10px; + padding: 0 30px; + + .date-element { + display: flex; + gap: 10px; + align-items: center; + margin-top: 10px; + + div { + min-width: 120px; + text-align: right; + font-weight: bold; + } + + input { + padding: 5px; + } + } + } + + + + + @media (max-width: 1200px) { + .synchContainer { + display: block; + } + } + + // Config styles ------------------------------------- + .config-fieldset{ + border: 2px solid rgba(138, 138, 138, 1); + padding: 1em; + position: relative; + + legend { + display: inline-block; + width: auto; + margin-bottom: 0; + padding: 0.5em; + border: 0 none; + } + } + + .form-child { + padding-left: 40px; + } +} diff --git a/library/src/skins/default/src/sass/modules/tool/microsoft-collaborativedocuments/_microsoft-collaborativedocuments.scss b/library/src/skins/default/src/sass/modules/tool/microsoft-collaborativedocuments/_microsoft-collaborativedocuments.scss new file mode 100644 index 000000000000..8723bb945254 --- /dev/null +++ b/library/src/skins/default/src/sass/modules/tool/microsoft-collaborativedocuments/_microsoft-collaborativedocuments.scss @@ -0,0 +1,150 @@ +.#{$namespace}microsoft-collaborativedocuments { + + $color-hover: var(--button-hover-text-color); + $color-active: var(--button-active-text-color); + + $background-color: var(--sakai-background-color-2); + $background-color-hover: var(--sakai-background-color-3); + $background-color-button-hover: var(--button-hover-background); + $background-color-button-active: var(--button-active-background); + + $shadow-button-hover: var(--button-hover-shadow); + $shadow-button-active: var(--button-active-shadow); + + $border-color-button: var(--button-border-color); + $border-color-hover: var(--button-hover-border-color); + $border-color-active: var(--button-active-border-color); + + #loading-container { + font-size: 22px; + } + + /* Accordion section */ + /* Section Title */ + .section-title { + .refresh-icon { + z-index: 3; + translate: -5rem; + &:hover, &:focus { + background-color: var(--button-hover-background); + border-color: var(--button-hover-background); + + } + &:active { + background-color: var(--button-active-background); + border-color: var(--button-active-background); + } + } + } + + /* Team/Folder Container */ + .breadcrumb-header { + border-radius: 4px; + padding: 8px 15px; + margin-bottom: 20px; + background-color: $background-color; + display: flex; + justify-content: flex-start; + align-items: center; + width: 100%; + position: relative; + .breadcrumb { + padding: 0 15px; + margin-left: 3px; + margin-bottom: 0; + } + .act { + margin: 0; + >button { + margin: 0; + } + } + .dropdown-menu { + margin: -5px 0 0 15px; + a { + text-decoration: none; + } + } + } + .table-row { + display: flex; + align-items: center; + border: 1px solid #ddd; + padding: 10px; + &:nth-of-type(even) { + background-color: $background-color; + } + &:hover, &:focus, &.focused-row { + background: $background-color-hover; + .dropdown-toggle { + display: block; + &:hover, &:focus { + color: $color-hover; + text-decoration: none; + background: $background-color-button-hover; + border-color: $border-color-hover; + box-shadow: $shadow-button-hover; + } + &:active { + outline: 0; + color: $color-active; + text-decoration: none; + background: $background-color-button-active; + border-color: $border-color-active; + box-shadow: $shadow-button-active; + } + } + } + .main-column { + display: flex; + justify-content: space-between; + } + .dropdown-toggle { + display: none; + /*border: 1px solid $border-color-button;*/ + border-radius: 4px; + padding: 0 5px; + text-decoration: none; + position: relative; + .dropdown-menu { + position: absolute; + margin-top: 5px; + } + } + a { + text-decoration: none; + } + } + form.dropzone { + margin-top: 10px; + } + + /* Login/Logout related items */ + .menu-container { + position: relative; + min-height: 40px; + display: flex; + align-items: flex-end; + margin-bottom: 10px; + margin-top: 20px; + .menu { + width: 100%; + } + .login-header { + position: absolute; + right: 0; + bottom: 7px; + .user-account { + font-size: smaller; + font-weight: bold; + } + } + } + + /* Special margin for small screens */ + @media (max-width: 800px) { + .menu-container { + margin-top: 4em; + } + } +} diff --git a/library/src/skins/default/src/sass/modules/tool/microsoft-mediagallery/_microsoft-mediagallery.scss b/library/src/skins/default/src/sass/modules/tool/microsoft-mediagallery/_microsoft-mediagallery.scss new file mode 100644 index 000000000000..c054af5cea37 --- /dev/null +++ b/library/src/skins/default/src/sass/modules/tool/microsoft-mediagallery/_microsoft-mediagallery.scss @@ -0,0 +1,217 @@ +.#{$namespace}microsoft-mediagallery { + + $color_info: #ff000090; + $color_info_hover: red; + $color_play: #007dff80; + $color_play_hover: #0000ff; + $color_thumbnail: grey; + $background-color_overlay: #00000033; + $color_folder: var(--sakai-primary-color-1); + + .container-fluid { + margin-left: 0; + padding-left: 0; + margin-right: 0; + padding-right: 0; + } + + #loading-container { + font-size: 22px; + } + + .menu { + margin-bottom: 10px; + } + + /* Login/Logout related items */ + .login-container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 200px; + gap: 10px; + } + + .logout-header { + display: flex; + justify-content: flex-end; + flex-wrap: wrap; + + .user-account { + font-size: smaller; + font-weight: bold; + } + } + + /* Container Header (tree view + sortby + search) */ + .container-header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 15px; + flex-wrap: wrap; + + .icon-toggle { + margin-left: 5px; + } + + form { + display: flex; + align-items: stretch; + column-gap: 10px; + + input { + height: 100%; + + /* Search */ + &.input-search { + font-family:Arial, FontAwesome; + max-width: 300px; + padding-left: 5px; + } + } + } + } + + + /* Accordion section */ + /* Section Title */ + .section-title { + .refresh-icon { + z-index: 3; + translate: -5rem; + &:hover, &:focus { + background-color: var(--button-hover-background); + border-color: var(--button-hover-background); + + } + &:active { + background-color: var(--button-active-background); + border-color: var(--button-active-background); + } + } + } + + /* Items (video/folder) in rows */ + .display-flex { + display: flex; + flex-wrap: wrap; + align-items: stretch; + + >.video-item { + display: flex; + flex-direction: column; + margin-bottom: 10px; + justify-content: flex-end; + } + } + + /* Video item */ + .video-item { + .video-content { + position: relative; + border: thin solid black; + + .info-icon { + position: absolute; + top: 5px; + right: 5px; + color: $color_info; + text-decoration: none; + font-size: 20px; + + &:hover { + color: $color_info_hover; + text-decoration: none; + cursor: pointer; + } + } + + .thumb-overlay { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: $background-color_overlay; + display: flex; + justify-content: center; + align-items: center; + font-size: 40px; + + .play-icon { + text-decoration: none; + color: $color_play; + + &:hover { + color: $color_play_hover; + cursor: pointer; + text-decoration: none; + } + } + } + + .thumbnail-container { + font-size: 60px; + color: $color_thumbnail; + } + } + } + + /* Folder Item */ + .folder-content { + font-size: 80px; + color: $color_folder; + + .folder-item { + color: $color_folder; + + &:hover { + scale: 1.2; + } + } + + } + + /* Inside a folder (shows breadcrumb bar) */ + .folder-row{ + .breadcrumb { + margin-top: -15px; + } + } + + /* Info */ + .info-container { + display: flex; + justify-content: center; + } + .item-container { + display: flex; + flex-direction: column; + } + .item-property { + display: flex; + margin-bottom: 5px; + + label { + // Override global margin on label + margin: unset; + margin-right: 5px; + } + + .thumbnail-container { + font-size: 60px; + } + } + + /* Special margin for small screens */ + @media (max-width: 800px) { + .container-view { + margin-top: 2em; + } + .logout-header { + margin-top: 2em; + } + } +} diff --git a/library/src/skins/default/src/sass/tool.scss b/library/src/skins/default/src/sass/tool.scss index 9fedf791ce54..565e9f14c5b9 100644 --- a/library/src/skins/default/src/sass/tool.scss +++ b/library/src/skins/default/src/sass/tool.scss @@ -168,6 +168,9 @@ $jumbotron-heading-font-size: $h4-font-size; @import "modules/tool/emailtemplates/emailtemplates"; @import "modules/tool/wiki/wiki"; @import "modules/tool/bulk-user-membership/bulk-user-membership"; +@import "modules/tool/microsoft-admin/microsoft-admin"; +@import "modules/tool/microsoft-mediagallery/microsoft-mediagallery"; +@import "modules/tool/microsoft-collaborativedocuments/microsoft-collaborativedocuments"; @import "base/rtl"; diff --git a/master/pom.xml b/master/pom.xml index a618fbfedcb9..f8abd73d32d8 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -1912,12 +1912,6 @@ ${sakai.version} provided
- - org.sakaiproject - sakai-onedrive-api - ${sakai.version} - provided - org.sakaiproject sakai-podcasts-api @@ -2104,6 +2098,12 @@ ${sakai.version} provided + + org.sakaiproject.microsoft + microsoft-api + ${sakai.version} + provided + diff --git a/meetings/README.md b/meetings/README.md new file mode 100644 index 000000000000..ac7097b6c3b4 --- /dev/null +++ b/meetings/README.md @@ -0,0 +1,111 @@ +# SAKAI - Online Meetings tool + +This is a tool for Sakai dedicated to the creation, management and use of virtual meeting rooms based on different online video conferencing providers. Instructors can schedule new meetings for their students on a site or group basis. Students can see a list of their scheduled meetings for each site and access them directly by clicking on them. Only members of the organization can access these meetings, unless the organizer allows guest access. + +#### Features + +- Instructors can schedule new meetings for their students. +- Instructors can control access to meetings by site or group. +- Students can access a list of their scheduled meetings. +- Search for scheduled meetings by name. +- Meetings can be added as events to the site calendar. +- Instructors can notify all meeting participants by e-mail. +- Only members of the organization can access Microsoft Teams meetings. +- Only the organizer can invite external users to access the Microsoft Teams meeting. +- Simple, fast and responsive interface. + +## Current supported providers +- Microsoft Teams + +## Prerequisites +You need: +- A Sakai 22.x instance or higher. +- For Microsoft Teams integration: + - A Microsoft Azure Active Directory application. + - Azure Active Directory users must have the same email in Sakai to be identified as members of the organization. + +## Microsoft Teams +### Azure AD configuration +You must create a new application in the _App Registrations_ section of the Azure Active Directory portal by clicking on the _New Registration_ button. + +![App registrations](../microsoft-integration/docs/images/1.png "App registration") + +You can enter a name and select the supported account types. The _Single tenant_ option is marked by default. + +![Registering new app](../microsoft-integrationdocs/images/2.png "Registering new app") + +To grant **Meetings tool** access to your registered Azure application, you will need a **client secret**. To obtain this, you can access the _Certificates & secrets_ section within the configuration page of your registered Azure application. + +![Client secret](../microsoft-integrationdocs/images/3.png "Client secret") + +Once the app is created, you need to configure the permissions for your registered Azure App in the _API Permissions_ section. To add a new permission you must click _Add a permission_, then select _Microsoft Graph_ and _Application Permissions_. + +![Permissions](../microsoft-integrationdocs/images/4.png "Permissions") + +The permissions to enable are defined in the following table: + +```sh +Directory.Read.All +Directory.ReadWrite.All +Group.Read.All +OnlineMeetings.ReadWrite.All +User.Read.All +-- Chat Messages +Chat.Read.All +ChatMessage.Read.All +-- Create link +Files.ReadWrite.All +Sites.ReadWrite.All +Sites.Manage.All +``` + +Then you must click on the _Grant admin consent_ button for your Azure directory. + +### Application access directives +In order for Sakai to manage meetings on its own, without user authentication by Microsoft, you need to set up an application user and its access policies. You can read the official Microsoft documentation on these steps: + +https://docs.microsoft.com/en-us/graph/cloud-communication-online-meeting-application-access-policy + +#### Summary + #Install Microsoft Teams Poweshell Module: https://learn.microsoft.com/en-us/microsoftteams/teams-powershell-install + + #Get version + $PSVersionTable.PSVersion + + #Install module + Install-Module -Name PowerShellGet -Force -AllowClobber + Install-Module -Name MicrosoftTeams -Force -AllowClobber + + #Enable script running + Set-ExecutionPolicy Unrestricted + + #Connect to Teams + Import-Module MicrosoftTeams + $userCredential = Get-Credential + Connect-MicrosoftTeams -Credential $userCredential + + #Create policy + New-CsApplicationAccessPolicy -Identity Test-policy -AppIds "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -Description "OnlineMeetings Access Policy" + + #Grant to specific user (sakai@nkrd.onmicrosoft.com) + Grant-CsApplicationAccessPolicy -PolicyName Test-policy -Identity "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + + #(Optional)Grant Global + Grant-CsApplicationAccessPolicy -PolicyName Test-policy -Global + +### Access to Protected APIs +~~To get Microsoft chat messages using application permissions, we need to request access to this protected API: https://learn.microsoft.com/en-us/graph/teams-protected-apis~~ +Request to protected APIs is no longer needed. Everything should work as long as these requirements are accomplished: https://learn.microsoft.com/en-us/graph/auth-v2-service?tabs=http + +## Global Microsoft README +Remember to set all Microsoft related configuration according to: [microsoft-integration/README.md](../microsoft-integration/README.md) + +## Future plans and Roadmap + +- Improve documentation. +- Improve Microsoft permissions for institutions using Microsoft accounts. +- Reuse the meeting card component in other tools like Lessons or create a meeting widget. +- Support other webconference providers like Zoom or BBB depending on funding or contributions. + +## Contact +If you have any questions please contact the devs at **Entornos de Formacion S.L.** at sakaigers@edf.global diff --git a/meetings/api/pom.xml b/meetings/api/pom.xml new file mode 100644 index 000000000000..4d295124fd08 --- /dev/null +++ b/meetings/api/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + + org.sakaiproject.meetings + meetings + 24-SNAPSHOT + ../pom.xml + + + meetings-api + org.sakaiproject.meetings + meetings-api + jar + + + shared + + + + + + org.hibernate + hibernate-core + + + com.fasterxml.jackson.core + jackson-annotations + + + org.sakaiproject.kernel + sakai-kernel-api + + + diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/MeetingService.java b/meetings/api/src/java/org/sakaiproject/meetings/api/MeetingService.java new file mode 100644 index 000000000000..4fd980ff2ba8 --- /dev/null +++ b/meetings/api/src/java/org/sakaiproject/meetings/api/MeetingService.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.api; + +import java.util.List; +import java.util.Optional; + +import org.sakaiproject.meetings.api.model.Meeting; + +public interface MeetingService { + + public Iterable getAllMeetings(); + public List getAllMeetingsFromSite(String siteId); + public List getUserMeetings(String userId, String siteId, List groupIds); + public Meeting createMeeting(Meeting meetingData); + public void updateMeeting(Meeting meetingData); + public void deleteMeetingById(String id); + public Optional getMeetingById(String id); + public Meeting getMeeting(String id); + public void removeSiteAndGroupAttendeesByMeetingId(String id); + public void setMeetingProperty(Meeting meeting, String property, String value); + public String getMeetingProperty(Meeting meeting, String property); + public void removeMeetingProperty(Meeting meeting, String property); + +} diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/AttendeeType.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/AttendeeType.java new file mode 100644 index 000000000000..8c89844c6feb --- /dev/null +++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/AttendeeType.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.api.model; + +public enum AttendeeType { + USER, + SITE, + GROUP +} diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/Meeting.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/Meeting.java new file mode 100644 index 000000000000..2af25cc37531 --- /dev/null +++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/Meeting.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.api.model; + +import java.time.Instant; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +import org.hibernate.annotations.GenericGenerator; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "meetings") +@AllArgsConstructor +@Data +@NoArgsConstructor +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") +public class Meeting { + + @Id + @Column(name = "meeting_id", length = 99, nullable = false) + @GeneratedValue(generator = "uuid") + @GenericGenerator(name = "uuid", strategy = "uuid2") + private String id; + + @Column(name = "meeting_title", length = 255, nullable = false) + private String title; + + @Lob + @Column(name = "meeting_description", length = 4000) + private String description; + + @Column(name = "meeting_site_id", length = 99) + private String siteId; + + @Column(name = "meeting_start_date") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]Z", timezone = "UTC") + private Instant startDate; + + @Column(name = "meeting_end_date") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]Z", timezone = "UTC") + private Instant endDate; + + @Column(name = "meeting_url", length = 255) + private String url; + + @Column(name = "meeting_owner_id", length = 99) + private String ownerId; + + @ManyToOne + @JoinColumn(name="meeting_provider_id") + private MeetingsProvider provider; + + @OneToMany(mappedBy="meeting", cascade = CascadeType.ALL) + private List attendees; + + /** + * Extract meeting ID from URL + * @return meetingId + */ + public String getMeetingId() { + String ret = null; + if(!"".equals(this.url)) { + Pattern teamPattern = Pattern.compile("^https://teams.microsoft.com/l/meetup-join/([^/]+)/.*$"); + Matcher matcher = teamPattern.matcher(this.url); + if(matcher.find()) { + ret = matcher.group(1); + } + } + return ret; + } +} diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingAttendee.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingAttendee.java new file mode 100644 index 000000000000..f73c0c09c601 --- /dev/null +++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingAttendee.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.api.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "meeting_attendees") +@AllArgsConstructor +@Data +@NoArgsConstructor +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") +public class MeetingAttendee { + + @Id + @Column(name = "attendee_id") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "meeting_attendee_sequence") + @SequenceGenerator(name = "meeting_attendee_sequence", sequenceName = "MEETING_ATTENDEE_S") + private Long id; + + @ManyToOne + @JoinColumn(name="attendee_meeting_id") + private Meeting meeting; + + @Column(name = "attendee_type") + private AttendeeType type; + + @Column(name = "attendee_object_id", length = 255) + private String objectId; + +} diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingProperty.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingProperty.java new file mode 100644 index 000000000000..a4d75abfa8cf --- /dev/null +++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingProperty.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.api.model; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "meeting_properties") +@AllArgsConstructor +@Data +@NoArgsConstructor +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") +public class MeetingProperty { + + @Id + @Column(name = "prop_id") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "meeting_property_sequence") + @SequenceGenerator(name = "meeting_property_sequence", sequenceName = "MEETING_PROPERTY_S") + private Long id; + + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name="prop_meeting_id") + private Meeting meeting; + + @Column(name = "prop_name", length = 255, nullable = false) + private String name; + + @Column(name = "prop_value", length = 255) + private String value; + +} diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingsProvider.java b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingsProvider.java new file mode 100644 index 000000000000..15da100b04ac --- /dev/null +++ b/meetings/api/src/java/org/sakaiproject/meetings/api/model/MeetingsProvider.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.api.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.annotations.GenericGenerator; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "meeting_providers") +@AllArgsConstructor +@Data +@NoArgsConstructor +@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") +public class MeetingsProvider { + + @Id + @Column(name = "provider_id", length = 99, nullable = false) + @GeneratedValue(generator = "uuid") + @GenericGenerator(name = "uuid", strategy = "uuid2") + private String id; + + @Column(name = "provider_name", length = 255, nullable = false) + private String name; + +} diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingAttendeeRepository.java b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingAttendeeRepository.java new file mode 100644 index 000000000000..e33e61251be4 --- /dev/null +++ b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingAttendeeRepository.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.api.persistence; + +import org.sakaiproject.meetings.api.model.MeetingAttendee; +import org.sakaiproject.serialization.SerializableRepository; + +public interface MeetingAttendeeRepository extends SerializableRepository { + + public void removeAttendeesByMeetingId(String meetingId); + public void removeSiteAndGroupAttendeesByMeetingId (String meetingId); + +} diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingPropertyRepository.java b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingPropertyRepository.java new file mode 100644 index 000000000000..6781ade00bfc --- /dev/null +++ b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingPropertyRepository.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.api.persistence; + +import java.util.Optional; + +import org.sakaiproject.meetings.api.model.MeetingProperty; +import org.sakaiproject.serialization.SerializableRepository; + +public interface MeetingPropertyRepository extends SerializableRepository { + + public Optional findFirstByMeetingIdAndName(String meetingId, String name); + public void deletePropertiesByMeetingId(String meetingId); + public void deletePropertyByMeetingIdAndName(String meetingId, String propertyName); + +} diff --git a/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingRepository.java b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingRepository.java new file mode 100644 index 000000000000..90bd47c3c4e4 --- /dev/null +++ b/meetings/api/src/java/org/sakaiproject/meetings/api/persistence/MeetingRepository.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.api.persistence; + +import java.util.List; +import java.util.Optional; + +import org.sakaiproject.meetings.api.model.Meeting; +import org.sakaiproject.serialization.SerializableRepository; + +public interface MeetingRepository extends SerializableRepository { + + public Optional findById(String id); + public Meeting findMeetingById(String id); + public void deleteById(String id); + public List getSiteMeetings(String siteId); + public List getMeetings(String userId, String siteId, List groupIds); + +} diff --git a/meetings/impl/pom.xml b/meetings/impl/pom.xml new file mode 100644 index 000000000000..96ef9bfcaa8f --- /dev/null +++ b/meetings/impl/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + + org.sakaiproject.meetings + meetings + 24-SNAPSHOT + ../pom.xml + + + meetings-impl + org.sakaiproject.meetings + meetings-impl + sakai-component + + + + 5.5.2 + 2.22.2 + + + + + org.sakaiproject.meetings + meetings-api + ${project.version} + provided + + + + + org.sakaiproject.kernel + sakai-kernel-api + + + + + org.springframework + spring-tx + + + org.springframework + spring-context + + + org.springframework + spring-orm + + + + + org.hibernate + hibernate-core + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + org.quartz-scheduler + quartz + + + org.apache.commons + commons-lang3 + + + + + org.springframework + spring-test + test + + + org.mockito + mockito-core + test + + + org.hsqldb + hsqldb + test + + + javax.servlet + javax.servlet-api + test + + + + + + ${basedir}/src/main/java + ${basedir}/src/test/java + + + ${basedir}/src/webapp + + + ${basedir}/src/test/resources + + + + diff --git a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java new file mode 100644 index 000000000000..278aadfa2e97 --- /dev/null +++ b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/MeetingServiceImpl.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.impl; + +import org.springframework.transaction.annotation.Transactional; +import org.sakaiproject.meetings.api.model.Meeting; +import org.sakaiproject.meetings.api.model.MeetingProperty; +import org.sakaiproject.meetings.api.persistence.MeetingAttendeeRepository; +import org.sakaiproject.meetings.api.persistence.MeetingPropertyRepository; +import org.sakaiproject.meetings.api.persistence.MeetingRepository; + +import java.util.List; +import java.util.Optional; + +import org.sakaiproject.meetings.api.MeetingService; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Transactional +public class MeetingServiceImpl implements MeetingService { + + @Setter + MeetingRepository meetingRepository; + + @Setter + MeetingPropertyRepository meetingPropertyRepository; + + @Setter + MeetingAttendeeRepository meetingAttendeeRepository; + + public void init() { + log.info("Initializing Meeting Service"); + } + + public Iterable getAllMeetings() { + return meetingRepository.findAll(); + } + + public List getAllMeetingsFromSite(String siteId) { + return meetingRepository.getSiteMeetings(siteId); + } + + public List getUserMeetings(String userId, String siteId, List groupIds) { + return meetingRepository.getMeetings(userId, siteId, groupIds); + } + + public Optional getMeetingById(String id) { + return meetingRepository.findById(id); + } + + public Meeting getMeeting(String id) { + return meetingRepository.findMeetingById(id); + } + + public Meeting createMeeting(Meeting meetingData) { + return meetingRepository.save(meetingData); + } + + public void updateMeeting(Meeting meetingData) { + meetingRepository.update(meetingData); + } + + public void deleteMeetingById(String id) { + meetingPropertyRepository.deletePropertiesByMeetingId(id); + meetingAttendeeRepository.removeAttendeesByMeetingId(id); + meetingRepository.deleteById(id); + } + + public void removeSiteAndGroupAttendeesByMeetingId(String id) { + meetingAttendeeRepository.removeSiteAndGroupAttendeesByMeetingId(id); + } + + public void setMeetingProperty(Meeting meeting, String property, String value) { + Optional optMeetingProp = meetingPropertyRepository.findFirstByMeetingIdAndName(meeting.getId(), property); + if (optMeetingProp.isPresent()) { + MeetingProperty meetingProp = optMeetingProp.get(); + meetingProp.setValue(value); + meetingPropertyRepository.update(meetingProp); + } else { + MeetingProperty meetingProp = new MeetingProperty(); + meetingProp.setMeeting(meeting); + meetingProp.setName(property); + meetingProp.setValue(value); + meetingPropertyRepository.save(meetingProp); + } + } + + public String getMeetingProperty(Meeting meeting, String property) { + String result = null; + Optional optMeetingProp = meetingPropertyRepository.findFirstByMeetingIdAndName(meeting.getId(), property); + if (optMeetingProp.isPresent()) { + MeetingProperty prop = optMeetingProp.get(); + result = prop.getValue(); + } + return result; + } + + public void removeMeetingProperty(Meeting meeting, String property) { + meetingPropertyRepository.deletePropertyByMeetingIdAndName(meeting.getId(), property); + } + +} diff --git a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImpl.java b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImpl.java new file mode 100644 index 000000000000..276aacd99796 --- /dev/null +++ b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImpl.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.impl.persistence; + +import org.hibernate.Session; +import org.sakaiproject.meetings.api.model.MeetingAttendee; +import org.sakaiproject.meetings.api.persistence.MeetingAttendeeRepository; +import org.sakaiproject.serialization.BasicSerializableRepository; + +public class MeetingAttendeeRepositoryImpl extends BasicSerializableRepository implements MeetingAttendeeRepository{ + + public Session getCurrentSession() { + return sessionFactory.getCurrentSession(); + } + + @Override + public void removeAttendeesByMeetingId(String meetingId) { + getCurrentSession().createQuery("delete from MeetingAttendee where meeting.id = :id").setParameter("id", meetingId).executeUpdate(); + } + + @Override + public void removeSiteAndGroupAttendeesByMeetingId (String meetingId) { + getCurrentSession().createQuery("delete from MeetingAttendee where meeting.id = :id and type in (1, 2)").setParameter("id", meetingId).executeUpdate(); + } + +} diff --git a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImpl.java b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImpl.java new file mode 100644 index 000000000000..7c338a918a09 --- /dev/null +++ b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImpl.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.impl.persistence; + +import java.util.Optional; + +import org.hibernate.Session; +import org.sakaiproject.meetings.api.model.MeetingProperty; +import org.sakaiproject.meetings.api.persistence.MeetingPropertyRepository; +import org.sakaiproject.serialization.BasicSerializableRepository; + +public class MeetingPropertyRepositoryImpl extends BasicSerializableRepository implements MeetingPropertyRepository{ + + public Session getCurrentSession() { + return sessionFactory.getCurrentSession(); + } + + @Override + public Optional findFirstByMeetingIdAndName(String meetingId, String name) { + MeetingProperty result = (MeetingProperty) getCurrentSession() + .createQuery("from MeetingProperty where meeting.id = :id and name = :name") + .setParameter("id", meetingId) + .setParameter("name", name).uniqueResult(); + return Optional.ofNullable(result); + } + + @Override + public void deletePropertiesByMeetingId(String meetingId) { + getCurrentSession().createQuery("delete from MeetingProperty where meeting.id = :id").setParameter("id", meetingId).executeUpdate(); + } + + @Override + public void deletePropertyByMeetingIdAndName(String meetingId, String propertyName) { + getCurrentSession().createQuery("delete from MeetingProperty where meeting.id = :id and name = :name") + .setParameter("id", meetingId) + .setParameter("name", propertyName) + .executeUpdate(); + } + +} diff --git a/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImpl.java b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImpl.java new file mode 100644 index 000000000000..6fe3f307a495 --- /dev/null +++ b/meetings/impl/src/main/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImpl.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.impl.persistence; + +import org.sakaiproject.meetings.api.persistence.MeetingRepository; +import org.sakaiproject.serialization.BasicSerializableRepository; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Optional; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.hibernate.Session; +import org.hibernate.criterion.Restrictions; +import org.sakaiproject.meetings.api.model.AttendeeType; +import org.sakaiproject.meetings.api.model.Meeting; +import org.sakaiproject.meetings.api.model.MeetingAttendee; + +public class MeetingRepositoryImpl extends BasicSerializableRepository implements MeetingRepository { + + public Session getCurrentSession() { + return sessionFactory.getCurrentSession(); + } + + public Optional findById(String id) { + Meeting meeting = (Meeting) startCriteriaQuery().add(Restrictions.eq("id", id)).uniqueResult(); + return Optional.ofNullable(meeting); + } + + public Meeting findMeetingById(String id) { + return (Meeting) startCriteriaQuery().add(Restrictions.eq("id", id)).uniqueResult(); + } + + @Override + public void deleteById(String id) { + getCurrentSession().createQuery("delete from Meeting where id = :id").setParameter("id", id).executeUpdate(); + } + + @Override + public List getSiteMeetings(String siteId) { + CriteriaBuilder criteriaBuilder = getCurrentSession().getCriteriaBuilder(); + CriteriaQuery query = criteriaBuilder.createQuery(Meeting.class); + Root root = query.from(Meeting.class); + Predicate siteRestriction = criteriaBuilder.equal(root.get("siteId"), siteId); + query.select(root).where(siteRestriction).distinct(true); + return getCurrentSession().createQuery(query).getResultList(); + } + + @Override + public List getMeetings(String userId, String siteId, List groupIds) { + CriteriaBuilder criteriaBuilder = getCurrentSession().getCriteriaBuilder(); + CriteriaQuery query = criteriaBuilder.createQuery(Meeting.class); + Root root = query.from(Meeting.class); + Join joinAttendees = root.join("attendees"); + Predicate orClause = criteriaBuilder.disjunction(); + if (userId != null) { + Predicate userRestriction = criteriaBuilder.and( + criteriaBuilder.equal(joinAttendees.get("type"), AttendeeType.USER), + criteriaBuilder.equal(joinAttendees.get("objectId"), userId)); + orClause.getExpressions().add(userRestriction); + } + if (siteId != null) { + Predicate siteRestriction = criteriaBuilder.and( + criteriaBuilder.equal(joinAttendees.get("type"), AttendeeType.SITE), + criteriaBuilder.equal(joinAttendees.get("objectId"), siteId)); + orClause.getExpressions().add(siteRestriction); + } + if (!CollectionUtils.isEmpty(groupIds)) { + Predicate groupRestriction = criteriaBuilder.and( + criteriaBuilder.equal(joinAttendees.get("type"), AttendeeType.GROUP), + joinAttendees.get("objectId").in(groupIds)); + orClause.getExpressions().add(groupRestriction); + } + Predicate where = criteriaBuilder.and(criteriaBuilder.equal(root.get("siteId"), siteId), orClause); + query.select(root).where(where).distinct(true); + return getCurrentSession().createQuery(query).getResultList(); + } + +} diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingServiceImplTest.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingServiceImplTest.java new file mode 100644 index 000000000000..301da6718078 --- /dev/null +++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingServiceImplTest.java @@ -0,0 +1,252 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sakaiproject.meetings.api.MeetingService; +import org.sakaiproject.meetings.api.model.AttendeeType; +import org.sakaiproject.meetings.api.model.Meeting; +import org.sakaiproject.meetings.api.model.MeetingAttendee; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {MeetingsImplTestConfiguration.class}) +public class MeetingServiceImplTest extends AbstractTransactionalJUnit4SpringContextTests { + + @Autowired private MeetingService meetingService; + + @Before + public void setUp() { + // Meeting "Test 0" - SITE1 - Perms: GROUP1 + Meeting data = new Meeting(); + data.setTitle("Test 0"); + data.setDescription("Test 0"); + data.setSiteId("site1"); + // Attendees + List attendees = new ArrayList(); + // Attendee 1 - GROUP + MeetingAttendee attendee = new MeetingAttendee(); + attendee.setObjectId("groupId1"); + attendee.setType(AttendeeType.GROUP); + attendees.add(attendee); + attendee.setMeeting(data); + // Create meeting 1 + data.setAttendees(attendees); + data = meetingService.createMeeting(data); + + // Meeting "Test 1" - SITE1 - Perms: USERID1, GROUP1, GROUP2 + data = new Meeting(); + data.setTitle("Test 1"); + data.setDescription("Test 1"); + data.setSiteId("site1"); + // Attendees + attendees = new ArrayList(); + // Attendee 1 - USER + attendee = new MeetingAttendee(); + attendee.setObjectId("userId1"); + attendee.setType(AttendeeType.USER); + attendees.add(attendee); + attendee.setMeeting(data); + // Attendee 2 - GROUP + attendee = new MeetingAttendee(); + attendee.setObjectId("groupId1"); + attendee.setType(AttendeeType.GROUP); + attendees.add(attendee); + attendee.setMeeting(data); + // Attendee 3 - GROUP + attendee = new MeetingAttendee(); + attendee.setObjectId("groupId2"); + attendee.setType(AttendeeType.GROUP); + attendees.add(attendee); + attendee.setMeeting(data); + // Create meeting 1 + data.setAttendees(attendees); + data = meetingService.createMeeting(data); + + // Meeting "Test 2" - SITE2 - Perms: USERID2, SITE2 + data = new Meeting(); + data.setTitle("Test 2"); + data.setDescription("Test 2"); + data.setSiteId("site2"); + // Attendees + attendees = new ArrayList(); + // Attendee 1 - USER + attendee = new MeetingAttendee(); + attendee.setObjectId("userId2"); + attendee.setType(AttendeeType.USER); + attendees.add(attendee); + attendee.setMeeting(data); + // Attendee 2 - SITE + attendee = new MeetingAttendee(); + attendee.setObjectId("site2"); + attendee.setType(AttendeeType.SITE); + attendees.add(attendee); + attendee.setMeeting(data); + // Create meeting 2 + data.setAttendees(attendees); + data = meetingService.createMeeting(data); + + // Meeting "Test 3" - SITE2 - Perms: USERID2 + data = new Meeting(); + data.setTitle("Test 3"); + data.setDescription("Test 3"); + data.setSiteId("site3"); + // Attendees + attendees = new ArrayList(); + // Attendee 1 - USER + attendee = new MeetingAttendee(); + attendee.setObjectId("userId2"); + attendee.setType(AttendeeType.USER); + attendees.add(attendee); + attendee.setMeeting(data); + // Create meeting 3 + data.setAttendees(attendees); + data = meetingService.createMeeting(data); + } + + @Test + public void createAndUpdateMeetingTest() { + Meeting data = new Meeting(); + data.setTitle("Test"); + data.setDescription("Test"); + data.setSiteId("site"); + data = meetingService.createMeeting(data); + Assert.assertNotNull(data.getId()); + data.setTitle("Modified"); + meetingService.updateMeeting(data); + Meeting data2 = meetingService.getMeeting(data.getId()); + Assert.assertTrue("Modified".equals(data2.getTitle())); + } + + @Test + public void createLongDescriptionMeetingTest() { + Meeting data = new Meeting(); + data.setTitle("Test Long"); + data.setDescription(RandomStringUtils.randomAlphabetic(4000)); + data.setSiteId("site"); + Meeting ret = meetingService.createMeeting(data); + Assert.assertNotNull(ret.getId()); + + Meeting test = meetingService.getMeeting(ret.getId()); + Assert.assertNotNull(test.getId()); + + Assert.assertThrows(Exception.class, + ()->{ + Meeting data2 = new Meeting(); + data2.setTitle("Test Long 2"); + data2.setDescription(RandomStringUtils.randomAlphabetic(4001)); + data2.setSiteId("site"); + + Meeting ret2 = meetingService.createMeeting(data2); + Assert.assertNotNull(ret.getId()); + + Meeting test2 = meetingService.getMeeting(ret2.getId()); + //should never reach this point + Assert.assertNull(test2.getId()); + }); + } + + @Test + public void testGetOptionalMeetingByIdIsNotPresent() { + Optional optMeeting = meetingService.getMeetingById("nonExistentId"); + Assert.assertFalse(optMeeting.isPresent()); + } + + @Test + public void testGetOptionalMeetingByIdIsPresent() { + Meeting data = new Meeting(); + data.setTitle("Test"); + data.setDescription("Test"); + data.setSiteId("site"); + data = meetingService.createMeeting(data); + Optional optMeeting = meetingService.getMeetingById(data.getId()); + Assert.assertTrue(optMeeting.isPresent()); + } + + @Test + public void getAllMeetings() { + Iterable meetings = meetingService.getAllMeetings(); + List list = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(meetings.iterator(), Spliterator.ORDERED), false) + .collect(Collectors.toList()); + Assert.assertTrue(list.size() == 4); + } + + @Test + public void getAllMeetingsFromSite() { + List list = meetingService.getAllMeetingsFromSite("site2"); + Assert.assertTrue(list.size() == 1); + } + + @Test + public void getUserMeetingsUserPermission() { + List list = meetingService.getUserMeetings("userId2", "site3", null); + Assert.assertTrue(list.size() == 1); + } + + @Test + public void getUserMeetingsSitePermission() { + List list = meetingService.getUserMeetings(null, "site2", null); + Assert.assertTrue(list.size() == 1); + } + + @Test + public void getUserMeetingsGroupPermission() { + List list = meetingService.getUserMeetings(null, "site1", Arrays.asList("groupId1")); + Assert.assertTrue(list.size() == 2); + list = meetingService.getUserMeetings(null, "site1", Arrays.asList("groupId2")); + Assert.assertTrue(list.size() == 1); + } + + @Test + public void deleteMeetingById() { + List list = meetingService.getAllMeetingsFromSite("site2"); + Meeting meeting = list.get(0); + String idMeeting = meeting.getId(); + meetingService.deleteMeetingById(idMeeting); + Optional optMeeting = meetingService.getMeetingById(idMeeting); + Assert.assertFalse(optMeeting.isPresent()); + } + + @Test + public void setGetAndRemoveMeetingProperty() { + List list = meetingService.getAllMeetingsFromSite("site2"); + Meeting meeting = list.get(0); + meetingService.setMeetingProperty(meeting, "property", "value"); + String value = meetingService.getMeetingProperty(meeting, "property"); + Assert.assertTrue("value".equals(value)); + meetingService.removeMeetingProperty(meeting, "property"); + value = meetingService.getMeetingProperty(meeting, "property"); + Assert.assertNull(value); + } + +} diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingsImplTestConfiguration.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingsImplTestConfiguration.java new file mode 100644 index 000000000000..0139907a1743 --- /dev/null +++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/MeetingsImplTestConfiguration.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.impl; + +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.util.Properties; +import javax.annotation.Resource; +import javax.sql.DataSource; + +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.id.factory.internal.MutableIdentifierGeneratorFactoryInitiator; +import org.hibernate.SessionFactory; +import org.hsqldb.jdbcDriver; +import org.sakaiproject.hibernate.AssignableUUIDGenerator; +import org.sakaiproject.springframework.orm.hibernate.AdditionalHibernateMappings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.context.annotation.PropertySource; +import org.springframework.core.env.Environment; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.hibernate5.HibernateTransactionManager; +import org.springframework.orm.hibernate5.LocalSessionFactoryBuilder; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.support.TransactionTemplate; + +@Configuration +@EnableTransactionManagement +@ImportResource("classpath:/WEB-INF/components.xml") +@PropertySource("classpath:/hibernate.properties") +public class MeetingsImplTestConfiguration { + + @Autowired + private Environment environment; + + @Resource(name = "org.sakaiproject.springframework.orm.hibernate.impl.AdditionalHibernateMappings.meetingstool") + private AdditionalHibernateMappings hibernateMappings; + + @Bean(name = "org.sakaiproject.springframework.orm.hibernate.GlobalSessionFactory") + public SessionFactory sessionFactory() throws IOException { + DataSource dataSource = dataSource(); + LocalSessionFactoryBuilder sfb = new LocalSessionFactoryBuilder(dataSource); + StandardServiceRegistryBuilder srb = sfb.getStandardServiceRegistryBuilder(); + srb.applySetting(org.hibernate.cfg.Environment.DATASOURCE, dataSource); + srb.applySettings(hibernateProperties()); + StandardServiceRegistry sr = srb.build(); + sr.getService(MutableIdentifierGeneratorFactoryInitiator.INSTANCE.getServiceInitiated()) + .register("uuid2", AssignableUUIDGenerator.class); + hibernateMappings.processAdditionalMappings(sfb); + return sfb.buildSessionFactory(sr); + } + + @Bean(name = "javax.sql.DataSource") + public DataSource dataSource() { + DriverManagerDataSource db = new DriverManagerDataSource(); + db.setDriverClassName(environment.getProperty(org.hibernate.cfg.Environment.DRIVER, jdbcDriver.class.getName())); + db.setUrl(environment.getProperty(org.hibernate.cfg.Environment.URL, "jdbc:hsqldb:mem:test")); + db.setUsername(environment.getProperty(org.hibernate.cfg.Environment.USER, "sa")); + db.setPassword(environment.getProperty(org.hibernate.cfg.Environment.PASS, "")); + return db; + } + + @Bean + public Properties hibernateProperties() { + return new Properties() { + { + setProperty(org.hibernate.cfg.Environment.DIALECT, environment.getProperty(org.hibernate.cfg.Environment.DIALECT, HSQLDialect.class.getName())); + setProperty(org.hibernate.cfg.Environment.HBM2DDL_AUTO, environment.getProperty(org.hibernate.cfg.Environment.HBM2DDL_AUTO)); + setProperty(org.hibernate.cfg.Environment.ENABLE_LAZY_LOAD_NO_TRANS, environment.getProperty(org.hibernate.cfg.Environment.ENABLE_LAZY_LOAD_NO_TRANS, "true")); + setProperty(org.hibernate.cfg.Environment.USE_SECOND_LEVEL_CACHE, environment.getProperty(org.hibernate.cfg.Environment.USE_SECOND_LEVEL_CACHE)); + setProperty(org.hibernate.cfg.Environment.CURRENT_SESSION_CONTEXT_CLASS, environment.getProperty(org.hibernate.cfg.Environment.CURRENT_SESSION_CONTEXT_CLASS)); + } + }; + } + + @Bean(name = "org.sakaiproject.springframework.orm.hibernate.GlobalTransactionManager") + public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) { + HibernateTransactionManager txManager = new HibernateTransactionManager(); + txManager.setSessionFactory(sessionFactory); + return txManager; + } + + @Bean(name = "org.springproject.transaction.support.TransactionTemplate") + public TransactionTemplate transactionTemplate() { + return mock(TransactionTemplate.class); + } + +} diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImplTest.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImplTest.java new file mode 100644 index 000000000000..476f0319c7f9 --- /dev/null +++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingAttendeeRepositoryImplTest.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.impl.persistence; + +import java.util.ArrayList; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sakaiproject.meetings.api.model.AttendeeType; +import org.sakaiproject.meetings.api.model.Meeting; +import org.sakaiproject.meetings.api.model.MeetingAttendee; +import org.sakaiproject.meetings.api.model.MeetingProperty; +import org.sakaiproject.meetings.api.persistence.MeetingAttendeeRepository; +import org.sakaiproject.meetings.api.persistence.MeetingRepository; +import org.sakaiproject.meetings.impl.MeetingsImplTestConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {MeetingsImplTestConfiguration.class}) +public class MeetingAttendeeRepositoryImplTest { + + @Autowired private MeetingRepository meetingRepository; + @Autowired private MeetingAttendeeRepository meetingAttendeeRepository; + + private Meeting meeting; + + @Before + public void setUp() { + Meeting data = new Meeting(); + data.setTitle("Test 0"); + data.setDescription("Test 0"); + data.setSiteId("site"); + List attendees = new ArrayList(); + MeetingAttendee attendee = new MeetingAttendee(); + attendee.setObjectId("userId"); + attendee.setType(AttendeeType.USER); + attendees.add(attendee); + attendee.setMeeting(data); + attendee = new MeetingAttendee(); + attendee.setObjectId("groupId"); + attendee.setType(AttendeeType.GROUP); + attendees.add(attendee); + attendee.setMeeting(data); + data.setAttendees(attendees); + this.meeting = meetingRepository.save(data); + } + + @Test + public void removeAttendeesByMeetingId() { + meetingAttendeeRepository.removeAttendeesByMeetingId(meeting.getId()); + Meeting meeting = meetingRepository.findMeetingById(this.meeting.getId()); + Assert.assertEquals(meeting.getAttendees().size(), 0); + } + + @Test + public void removeSiteAndGroupAttendeesByMeetingId () { + meetingAttendeeRepository.removeSiteAndGroupAttendeesByMeetingId(meeting.getId()); + Meeting meeting = meetingRepository.findMeetingById(this.meeting.getId()); + Assert.assertEquals(meeting.getAttendees().size(), 1); + } + +} diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImplTest.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImplTest.java new file mode 100644 index 000000000000..4ff163774c8b --- /dev/null +++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingPropertyRepositoryImplTest.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.impl.persistence; + +import java.util.Optional; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sakaiproject.meetings.api.model.Meeting; +import org.sakaiproject.meetings.api.model.MeetingProperty; +import org.sakaiproject.meetings.api.persistence.MeetingPropertyRepository; +import org.sakaiproject.meetings.api.persistence.MeetingRepository; +import org.sakaiproject.meetings.impl.MeetingsImplTestConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {MeetingsImplTestConfiguration.class}) +public class MeetingPropertyRepositoryImplTest { + + @Autowired private MeetingRepository meetingRepository; + @Autowired private MeetingPropertyRepository meetingPropertyRepository; + + private Meeting meeting; + + @Before + public void setUp() { + Meeting meeting = new Meeting(); + meeting.setTitle("Test 0"); + meeting.setDescription("Test 0"); + meeting.setSiteId("site1"); + meeting = meetingRepository.save(meeting); + MeetingProperty property = new MeetingProperty(); + property.setName("property"); + property.setValue("value"); + property.setMeeting(meeting); + meetingPropertyRepository.save(property); + this.meeting = meeting; + } + + @Test + public void findFirstByMeetingIdAndName() { + Optional opt = meetingPropertyRepository.findFirstByMeetingIdAndName(meeting.getId(), "property"); + Assert.assertTrue(opt.isPresent()); + MeetingProperty prop = opt.get(); + Assert.assertEquals(prop.getValue(), "value"); + } + + @Test + public void deletePropertiesByMeetingId() { + meetingPropertyRepository.deletePropertiesByMeetingId(meeting.getId()); + Optional opt = meetingPropertyRepository.findFirstByMeetingIdAndName(meeting.getId(), "property"); + Assert.assertFalse(opt.isPresent()); + } + +} diff --git a/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImplTest.java b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImplTest.java new file mode 100644 index 000000000000..3e82460c2c9a --- /dev/null +++ b/meetings/impl/src/test/java/org/sakaiproject/meetings/impl/persistence/MeetingRepositoryImplTest.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.impl.persistence; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sakaiproject.meetings.api.model.AttendeeType; +import org.sakaiproject.meetings.api.model.Meeting; +import org.sakaiproject.meetings.api.model.MeetingAttendee; +import org.sakaiproject.meetings.api.persistence.MeetingRepository; +import org.sakaiproject.meetings.impl.MeetingsImplTestConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {MeetingsImplTestConfiguration.class}) +public class MeetingRepositoryImplTest { + + @Autowired private MeetingRepository meetingRepository; + + private Meeting meeting1; + + @Before + public void setUp() { + // Meeting "Test 0" - SITE3 - Perms: GROUP1 + Meeting data = new Meeting(); + data.setTitle("Test 0"); + data.setDescription("Test 0"); + data.setSiteId("site3"); + // Attendees + List attendees = new ArrayList(); + // Attendee 1 - GROUP + MeetingAttendee attendee = new MeetingAttendee(); + attendee.setObjectId("groupId1"); + attendee.setType(AttendeeType.GROUP); + attendees.add(attendee); + attendee.setMeeting(data); + // Create meeting + data.setAttendees(attendees); + this.meeting1 = meetingRepository.save(data); + + // Meeting "Test 1" - SITE4 - Perms: USER1 + data = new Meeting(); + data.setTitle("Test 1"); + data.setDescription("Test 1"); + data.setSiteId("site4"); + // Attendees + attendees = new ArrayList(); + // Attendee 1 - USER + attendee = new MeetingAttendee(); + attendee.setObjectId("userId"); + attendee.setType(AttendeeType.USER); + attendees.add(attendee); + attendee.setMeeting(data); + // Create meeting + data.setAttendees(attendees); + meetingRepository.save(data); + } + + @Test + public void findById() { + Optional meeting = meetingRepository.findById(meeting1.getId()); + Assert.assertTrue(meeting.isPresent()); + } + + @Test + public void findMeetingById() { + Meeting meeting = meetingRepository.findMeetingById(meeting1.getId()); + Assert.assertNotNull(meeting); + } + + @Test + public void deleteById() { + Meeting meeting = meetingRepository.findMeetingById(meeting1.getId()); + meetingRepository.delete(meeting); + meeting = meetingRepository.findMeetingById(meeting1.getId()); + Assert.assertNull(meeting); + } + + @Test + public void getSiteMeetings() { + List meetings = meetingRepository.getSiteMeetings("site3"); + Assert.assertTrue(meetings.size() > 0); + } + + @Test + public void getMeetingsByUser() { + List meetings = meetingRepository.getMeetings("userId", "site3", null); + Assert.assertTrue(meetings.size() == 0); + meetings = meetingRepository.getMeetings("userId", "site4", null); + Assert.assertTrue(meetings.size() > 0); + } + +} diff --git a/meetings/impl/src/test/resources/hibernate.properties b/meetings/impl/src/test/resources/hibernate.properties new file mode 100644 index 000000000000..a1cb68123384 --- /dev/null +++ b/meetings/impl/src/test/resources/hibernate.properties @@ -0,0 +1,14 @@ +# Base Hibernate settings +hibernate.show_sql=false +hibernate.hbm2ddl.auto=create-only +hibernate.enable_lazy_load_no_trans=true +hibernate.cache.use_second_level_cache=false +hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext + +# Connection definition to the HSQLDB database +hibernate.connection.driver_class=org.hsqldb.jdbcDriver +hibernate.connection.url=jdbc:hsqldb:mem:test +hibernate.dialect=org.hibernate.dialect.HSQLDialect +hibernate.connection.username=sa +hibernate.connection.password= + diff --git a/meetings/impl/src/webapp/WEB-INF/components.xml b/meetings/impl/src/webapp/WEB-INF/components.xml new file mode 100644 index 000000000000..7162c7b4163f --- /dev/null +++ b/meetings/impl/src/webapp/WEB-INF/components.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + org.sakaiproject.meetings.api.model.Meeting + org.sakaiproject.meetings.api.model.MeetingProperty + org.sakaiproject.meetings.api.model.MeetingAttendee + org.sakaiproject.meetings.api.model.MeetingsProvider + + + + + + + + + + + + + + + + + + + + + diff --git a/meetings/pom.xml b/meetings/pom.xml new file mode 100644 index 000000000000..51df36f92bad --- /dev/null +++ b/meetings/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + org.sakaiproject + master + 24-SNAPSHOT + ../master/pom.xml + + + meetings + org.sakaiproject.meetings + meetings + pom + + + api + impl + tool + ui + + + diff --git a/cloud-storage/onedrive/tool/pom.xml b/meetings/tool/pom.xml similarity index 66% rename from cloud-storage/onedrive/tool/pom.xml rename to meetings/tool/pom.xml index 7946847877ae..e3d42e5091bc 100644 --- a/cloud-storage/onedrive/tool/pom.xml +++ b/meetings/tool/pom.xml @@ -4,22 +4,46 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.sakaiproject.onedrive - sakai-onedrive + org.sakaiproject.meetings + meetings 24-SNAPSHOT ../pom.xml - org.sakaiproject.onedrive - sakai-onedrive-tool - Sakai OneDrive Integration - TOOL + org.sakaiproject.meetings + meetings-tool war - - true - true - - + + org.sakaiproject.meetings + meetings-api + ${project.version} + provided + + + + + org.sakaiproject.kernel + sakai-kernel-api + provided + + + org.sakaiproject.kernel + sakai-component-manager + provided + + + org.sakaiproject.kernel + sakai-kernel-util + + + org.sakaiproject.microsoft + microsoft-api + ${project.version} + provided + + + org.springframework spring-core @@ -37,6 +61,7 @@ javax.inject javax.inject + org.thymeleaf @@ -46,44 +71,47 @@ org.thymeleaf thymeleaf-spring5 - - javax.servlet - javax.servlet-api + com.fasterxml.jackson.core + jackson-core - - org.sakaiproject.onedrive - sakai-onedrive-api + com.fasterxml.jackson.core + jackson-annotations - org.sakaiproject.kernel - sakai-kernel-api - - - org.sakaiproject.kernel - sakai-component-manager + com.fasterxml.jackson.core + jackson-databind + - org.sakaiproject.kernel - sakai-kernel-util + javax.servlet + javax.servlet-api + + org.apache.commons commons-lang3 + + + + org.springframework + spring-test + test + + + org.mockito + mockito-core + test + src/main/java - - ${basedir}/src/main/java - - **/*.xml - - ${basedir}/src/main/resources diff --git a/cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/ThymeleafConfig.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/ThymeleafConfig.java similarity index 88% rename from cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/ThymeleafConfig.java rename to meetings/tool/src/main/java/org/sakaiproject/meetings/ThymeleafConfig.java index cd9244e1c6a4..253a2fd140b2 100644 --- a/cloud-storage/onedrive/tool/src/main/java/org/sakaiproject/onedrive/ThymeleafConfig.java +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/ThymeleafConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2003-2019 The Apereo Foundation + * Copyright (c) 2024 The Apereo Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sakaiproject.onedrive; +package org.sakaiproject.meetings; + +import java.nio.charset.StandardCharsets; import org.sakaiproject.util.ResourceLoaderMessageSource; import org.springframework.context.ApplicationContext; @@ -24,8 +26,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; -import org.thymeleaf.TemplateEngine; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.thymeleaf.spring5.ISpringTemplateEngine; import org.thymeleaf.spring5.SpringTemplateEngine; import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; @@ -35,9 +36,8 @@ @Configuration @EnableWebMvc -@ComponentScan("org.sakaiproject.onedrive") -public class ThymeleafConfig extends WebMvcConfigurerAdapter implements ApplicationContextAware { - private static final String UTF8 = "UTF-8"; +@ComponentScan("org.sakaiproject.meetings") +public class ThymeleafConfig implements WebMvcConfigurer, ApplicationContextAware { private ApplicationContext applicationContext; @@ -57,7 +57,7 @@ public MessageSource messageSource() { public ViewResolver viewResolver() { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setTemplateEngine(templateEngine()); - viewResolver.setCharacterEncoding(UTF8); + viewResolver.setCharacterEncoding(StandardCharsets.UTF_8.name()); return viewResolver; } @@ -77,4 +77,5 @@ private ITemplateResolver templateResolver() { templateResolver.setTemplateMode(TemplateMode.HTML); return templateResolver; } + } diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/WebAppConfiguration.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/WebAppConfiguration.java new file mode 100644 index 000000000000..f69e57b7ff1a --- /dev/null +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/WebAppConfiguration.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings; + +import java.util.EnumSet; + +import javax.servlet.DispatcherType; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration.Dynamic; + +import org.sakaiproject.util.RequestFilter; +import org.sakaiproject.util.SakaiContextLoaderListener; +import org.sakaiproject.util.ToolListener; +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +public class WebAppConfiguration implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); + rootContext.setServletContext(servletContext); + rootContext.register(ThymeleafConfig.class); + + servletContext.addListener(new ToolListener()); + servletContext.addListener(new SakaiContextLoaderListener(rootContext)); + + servletContext.addFilter("sakai.request", RequestFilter.class) + .addMappingForUrlPatterns( + EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE), + true, + "/*"); + + Dynamic servlet = servletContext.addServlet("sakai.meetings", new DispatcherServlet(rootContext)); + servlet.addMapping("/"); + servlet.setLoadOnStartup(1); + } + +} diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/GlobalController.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/GlobalController.java new file mode 100644 index 000000000000..0a5cbc57b333 --- /dev/null +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/GlobalController.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2023 Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sakaiproject.meetings.controller; + +import javax.servlet.http.HttpServletRequest; + +import org.sakaiproject.exception.IdUnusedException; +import org.sakaiproject.meetings.exceptions.MeetingsException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + + +/** + * GlobalController + * + * This controller is used to handle exceptions + * + */ +@ControllerAdvice +public class GlobalController { + + @ExceptionHandler(value = {MeetingsException.class, IdUnusedException.class}) + public ResponseEntity handleMeetingsError(HttpServletRequest req, Exception ex) { + if (ex instanceof MeetingsException) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ex.getMessage()); + } + if (ex instanceof IdUnusedException) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(ex.getMessage()); + } + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(ex.getMessage()); + } +} diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MainController.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MainController.java new file mode 100644 index 000000000000..b62fa11eaadb --- /dev/null +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MainController.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import lombok.extern.slf4j.Slf4j; + + +/** + * MainController + * + * This is the controller used by Spring MVC to handle requests + * + */ +@Slf4j +@Controller +public class MainController { + + private final String INDEX_TEMPLATE = "index"; + + @GetMapping(value = {"/", "/index"}) + public String index(Model model) { + return INDEX_TEMPLATE; + } + +} diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MeetingsController.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MeetingsController.java new file mode 100644 index 000000000000..757058b2ebec --- /dev/null +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/MeetingsController.java @@ -0,0 +1,700 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.controller; + +import java.text.MessageFormat; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.sakaiproject.authz.api.Member; +import org.sakaiproject.exception.IdUnusedException; +import org.sakaiproject.meetings.api.MeetingService; +import org.sakaiproject.meetings.api.model.AttendeeType; +import org.sakaiproject.meetings.api.model.Meeting; +import org.sakaiproject.meetings.api.model.MeetingAttendee; +import org.sakaiproject.meetings.controller.data.GroupData; +import org.sakaiproject.meetings.controller.data.MeetingData; +import org.sakaiproject.meetings.controller.data.NotificationType; +import org.sakaiproject.meetings.controller.data.ParticipantData; +import org.sakaiproject.meetings.exceptions.MeetingsException; +import org.sakaiproject.microsoft.api.MicrosoftCommonService; +import org.sakaiproject.microsoft.api.MicrosoftSynchronizationService; +import org.sakaiproject.microsoft.api.SakaiProxy; +import org.sakaiproject.microsoft.api.data.MeetingRecordingData; +import org.sakaiproject.microsoft.api.data.SakaiCalendarEvent; +import org.sakaiproject.microsoft.api.data.TeamsMeetingData; +import org.sakaiproject.microsoft.api.exceptions.MicrosoftCredentialsException; +import org.sakaiproject.site.api.Group; +import org.sakaiproject.site.api.Site; +import org.sakaiproject.user.api.User; +import org.sakaiproject.util.ResourceLoader; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import lombok.extern.slf4j.Slf4j; + + +/** + * MainController + * + * This is the controller used by Spring MVC to handle requests + * + */ +@SuppressWarnings("deprecation") +@Slf4j +@RestController +public class MeetingsController { + + /** Resource bundle using current language locale */ + private static ResourceLoader rb = new ResourceLoader("Messages"); + + @Autowired + private MeetingService meetingService; + + @Autowired + private MicrosoftCommonService microsoftCommonService; + + @Autowired + private MicrosoftSynchronizationService microsoftSynchronizationService; + + @Autowired + private SakaiProxy sakaiProxy; + + private static final String MS_TEAMS = "microsoft_teams"; + private static final String ONLINE_MEETING_ID = "onlineMeetingId"; + private static final String ORGANIZER_USER = "organizerUser"; + private static final String CALENDAR_EVENT_ID = "calendarEventId"; + private static final String MEETING_EVENT_TYPE = "Meeting"; + private static final String NOTIF_SUBJECT = "notification.subject"; + private static final String NOTIF_CONTENT = "notification.content"; + private static final String SMTP_FROM = "smtpFrom@org.sakaiproject.email.api.EmailService"; + private static final String NO_REPLY = "no-reply@"; + + /** + * Check if there's an user logged + * @return + * @throws MeetingsException + */ + private void checkSakaiSession() throws MeetingsException { + if (StringUtils.isBlank(sakaiProxy.getCurrentUserId())) { + throw new MeetingsException("Unable to get current user"); + } + } + + /** + * Retrieves current user permission to edit meetings + * @return + */ + @GetMapping(value = "/meetings/user/editperms/site/{siteId}", produces = MediaType.APPLICATION_JSON_VALUE) + public boolean canUpdateSite(@PathVariable String siteId) { + boolean result = false; + try { + Site site = sakaiProxy.getSite(siteId); + + result = sakaiProxy.canUpdateSite(site.getReference(), sakaiProxy.getCurrentUserId()); + } catch (Exception e) { + log.error("Error retrieving user permissions", e); + result = false; + } + return result; + } + + /** + * 'Update site' permissions check + * @param siteId + * @return + * @throws MeetingsException + */ + private void checkUpdatePermissions(String siteId) throws MeetingsException { + boolean result = canUpdateSite(siteId); + if (!result) { + throw new MeetingsException("User doesn't have permissions to update this site."); + } + } + + /** + * + * @param siteId + * @throws MeetingsException + */ + private void checkCurrentUserInSite(String siteId) throws MeetingsException { + if (!sakaiProxy.canCurrentUserAccessSite(siteId)) { + throw new MeetingsException("Current user has not permissions to see information from site " + siteId); + } + } + + /** + * Check current user permission to see a meeting + * @param meetingId + * @throws MeetingsException + */ + private void checkCurrentUserInMeeting(String meetingId) throws MeetingsException { + if (sakaiProxy.isAdmin()) { + return; + } + List result = new ArrayList<>(); + try { + Meeting meeting = meetingService.getMeeting(meetingId); + if(canUpdateSite(meeting.getSiteId())) { + return; + } + + Site site = sakaiProxy.getSite(meeting.getSiteId()); + String userId = sakaiProxy.getCurrentUserId(); + String siteId = site.getId(); + List groups = new ArrayList<>(); + groups.addAll(site.getGroupsWithMember(userId)); + site.getGroupsWithMember(userId); + List groupIds = groups.stream().map(e->e.getId()).collect(Collectors.toList()); + List meetingList = meetingService.getUserMeetings(userId, siteId, groupIds); + result = meetingList.stream().filter(item -> meetingId.equals(item.getId())).collect(Collectors.toList()); + } catch(Exception e) {} + + if (result.size() == 0) { + throw new MeetingsException("Current user does not have permission to see this meeting."); + } + } + + /** + * Method to evaluate if there is a calendar tool added to a site + * @param siteId + * @return + * @throws MeetingsException + */ + @GetMapping(value = "/meetings/site/{siteId}/existcalendar", produces = MediaType.APPLICATION_JSON_VALUE) + private boolean isThereAnyCalendarForSite(@PathVariable String siteId) throws MeetingsException { + checkCurrentUserInSite(siteId); + + String calReference = "/calendar/calendar/" + siteId + "/main"; + return sakaiProxy.existsCalendar(calReference); + } + + /** + * Retrieves the groups list from a site + * @param siteId + * @return + * @throws MeetingsException + */ + @GetMapping(value = "/meetings/site/{siteId}/groups", produces = MediaType.APPLICATION_JSON_VALUE) + public Iterable getSiteGroups(@PathVariable String siteId) throws MeetingsException { + checkCurrentUserInSite(siteId); + List siteGroups = new ArrayList<>(); + Site site = sakaiProxy.getSite(siteId); + if(site != null) { + Collection groups = site.getGroups(); + groups.stream().forEach(group -> { + GroupData data = new GroupData(); + data.setGroupId(group.getId()); + data.setGroupName(group.getTitle()); + siteGroups.add(data); + }); + } else { + log.error("Error retrieving groups"); + throw new MeetingsException("Error retrieving groups"); + } + return siteGroups; + } + + /** + * Method to retrieve the list of participants in a meeting + * @param meetingId + * @return + * @throws MeetingsException + */ + @GetMapping(value = "/meeting/{meetingId}/participants", produces = MediaType.APPLICATION_JSON_VALUE) + public List getParticipants(@PathVariable String meetingId) throws MeetingsException { + checkCurrentUserInMeeting(meetingId); + final List participants = new ArrayList<>(); + Optional optMeeting = meetingService.getMeetingById(meetingId); + if (optMeeting.isPresent()) { + Meeting meeting = optMeeting.get(); + checkCurrentUserInSite(meeting.getSiteId()); + Site site = sakaiProxy.getSite(meeting.getSiteId()); + if(site != null) { + for (MeetingAttendee attendee : meeting.getAttendees()) { + switch (attendee.getType()) { + case USER: + User user = sakaiProxy.getUser(attendee.getObjectId()); + if(user != null) { + ParticipantData participant = new ParticipantData(); + participant.setUserid(user.getId()); + participant.setName(user.getDisplayName()); + participants.add(participant); + } else { + log.error("Error retrieving participants"); + } + break; + + case SITE: + site.getMembers().stream() + .sorted((o1, o2) -> o1.getUserId().compareTo(o2.getUserId())) + .forEach(member -> { + ParticipantData siteParticipant = new ParticipantData(); + User siteUser = sakaiProxy.getUser(member.getUserId()); + if(siteUser != null) { + siteParticipant.setUserid(siteUser.getId()); + siteParticipant.setName(siteUser.getDisplayName()); + participants.add(siteParticipant); + } else { + log.error("Error retrieving participants (SITE): userId={}", member.getUserId()); + } + }); + break; + + case GROUP: + site.getMembersInGroups(Collections.singleton(attendee.getObjectId())) + .stream().forEach(userId -> { + ParticipantData groupParticipant = new ParticipantData(); + User groupUser = sakaiProxy.getUser(userId); + if(groupUser != null) { + groupParticipant.setUserid(groupUser.getId()); + groupParticipant.setName(groupUser.getDisplayName()); + participants.add(groupParticipant); + } else { + log.error("Error retrieving participants (GROUP): userId={}", userId); + } + }); + break; + default: break; + } + } + return participants.stream().distinct().collect(Collectors.toList()); + } else { + log.error("Error retrieving participants"); + throw new MeetingsException("Error retrieving participants"); + } + } + return participants; + } + + /** + * Retrieves all current user meetings + * @return + * @throws MeetingsException + */ + @GetMapping(value = "/meetings/site/{siteId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Iterable getSiteMeetings(@PathVariable String siteId) throws MeetingsException { + checkCurrentUserInSite(siteId); + // Retrieve meetings for which the user has permission + String userId = sakaiProxy.getCurrentUserId(); + List meetingList = null; + if (sakaiProxy.isAdmin() || canUpdateSite(siteId)) { + meetingList = meetingService.getAllMeetingsFromSite(siteId); + } else { + try { + Site site = sakaiProxy.getSite(siteId); + List groups = new ArrayList<>(); + groups.addAll(site.getGroupsWithMember(userId)); + site.getGroupsWithMember(userId); + List groupIds = groups.stream().map(e->e.getId()).collect(Collectors.toList()); + meetingList = meetingService.getUserMeetings(userId, siteId, groupIds); + } catch (Exception e) { + log.error("Error while retrieving group list on Meetings", e); + } + } + // Compose the data to send to the frontend + List data = new ArrayList<>(); + meetingList.stream().forEach(meeting -> { + MeetingData item = new MeetingData(); + BeanUtils.copyProperties(meeting, item); + item.setStartDate(meeting.getStartDate().toString()); + item.setEndDate(meeting.getEndDate().toString()); + data.add(item); + }); + + return data; + } + + @GetMapping(value = "/meeting/{meetingId}", produces = MediaType.APPLICATION_JSON_VALUE) + public MeetingData getMeeting(@PathVariable String meetingId) throws MeetingsException { + checkCurrentUserInMeeting(meetingId); + Optional optMeeting = meetingService.getMeetingById(meetingId); + if (optMeeting.isPresent()) { + final MeetingData data = new MeetingData(); + Meeting meeting = optMeeting.get(); + BeanUtils.copyProperties(meeting, data); + data.setStartDate(meeting.getStartDate().toString()); + data.setEndDate(meeting.getEndDate().toString()); + List meetingGroupIds = new ArrayList(); + meeting.getAttendees().stream().forEach(attendee -> { + switch (attendee.getType()) { + case SITE: + data.setParticipantOption(attendee.getType()); + break; + case GROUP: + data.setParticipantOption(attendee.getType()); + meetingGroupIds.add(attendee.getObjectId()); + break; + default: break; + } + }); + data.setGroupSelection(meetingGroupIds); + String calendarEventId = meetingService.getMeetingProperty(meeting, CALENDAR_EVENT_ID); + data.setSaveToCalendar(StringUtils.isNotBlank(calendarEventId)); + data.setParticipants(getParticipants(meetingId)); + return data; + } + return null; + } + + + /** + * Method to save a new meeting + * @param data + * @return + * @throws MeetingsException + */ + @PostMapping(value = "/meeting", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public Meeting createMeeting(@RequestBody MeetingData data) throws MeetingsException { + checkUpdatePermissions(data.getSiteId()); + Meeting meeting = null; + User user = sakaiProxy.getCurrentUser(); + try { + // Meeting info + meeting = new Meeting(); + BeanUtils.copyProperties(data, meeting); + meeting.setStartDate(Instant.parse(data.getStartDate())); + meeting.setEndDate(Instant.parse(data.getEndDate())); + meeting.setOwnerId(user.getId()); + // Online meeting creation with the selected provider + String onlineMeetingId = null; + String onlineMeetingUrl = null; + if (MS_TEAMS.equals(data.getProvider())) { + TeamsMeetingData meetingTeams = microsoftCommonService.createOnlineMeeting(user.getEmail(), meeting.getTitle(), meeting.getStartDate(), meeting.getEndDate()); + onlineMeetingUrl = meetingTeams.getJoinUrl(); + onlineMeetingId = meetingTeams.getId(); + } + meeting.setUrl(onlineMeetingUrl); + // Participants + MeetingAttendee attendee = new MeetingAttendee(); + List meetingAttendees = new ArrayList(); + attendee.setType(AttendeeType.USER); + attendee.setObjectId(user.getId()); + meetingAttendees.add(attendee); + attendee.setMeeting(meeting); + switch (data.getParticipantOption()) { + case SITE: + attendee = new MeetingAttendee(); + attendee.setType(AttendeeType.SITE); + attendee.setObjectId(data.getSiteId()); + meetingAttendees.add(attendee); + attendee.setMeeting(meeting); + break; + case GROUP: + for (String groupId : data.getGroupSelection()) { + attendee = new MeetingAttendee(); + attendee.setType(AttendeeType.GROUP); + attendee.setObjectId(groupId); + meetingAttendees.add(attendee); + attendee.setMeeting(meeting); + } + break; + default: + break; + } + meeting.setAttendees(meetingAttendees); + // Meeting creation + meeting = meetingService.createMeeting(meeting); + // Properties + meetingService.setMeetingProperty(meeting, ORGANIZER_USER, user.getEmail()); + meetingService.setMeetingProperty(meeting, ONLINE_MEETING_ID, onlineMeetingId); + // Calendar events + if (data.isSaveToCalendar() && isThereAnyCalendarForSite(data.getSiteId()) + && StringUtils.isNotBlank(data.getStartDate()) && StringUtils.isNotBlank(data.getEndDate())) { + this.saveToCalendar(meeting); + } + // Notifications + this.sendNotification(meeting, data.getNotificationType()); + } catch (DateTimeParseException e) { + log.error("Could not parse Meetings start date string '{}' or end time string '{}'", meeting.getStartDate(), meeting.getEndDate()); + throw new MeetingsException(e.getLocalizedMessage()); + } catch (IdUnusedException e) { + log.error("Error retrieving site when sending notifications.", e); + throw new MeetingsException(e.getLocalizedMessage()); + } catch (MicrosoftCredentialsException e) { + log.error("Error creating meeting", e); + throw new MeetingsException(e.getLocalizedMessage()); + } + return meeting; + } + + /** + * Method to update an existing meeting + * @param data + * @param meetingId + * @return + * @throws MeetingsException + */ + @PutMapping(value = "/meeting/{meetingId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public Meeting updateMeeting(@RequestBody MeetingData data, @PathVariable String meetingId) throws MeetingsException { + checkUpdatePermissions(data.getSiteId()); + checkCurrentUserInMeeting(meetingId); + Meeting meeting = null; + try { + // Remove site and group attendees + meetingService.removeSiteAndGroupAttendeesByMeetingId(meetingId); + meeting = meetingService.getMeeting(meetingId); + + // Meeting info + meeting.setTitle(data.getTitle()); + meeting.setDescription(data.getDescription()); + meeting.setStartDate(Instant.parse(data.getStartDate())); + meeting.setEndDate(Instant.parse(data.getEndDate())); + + if (MS_TEAMS.equals(data.getProvider())) { + String organizerEmail = meetingService.getMeetingProperty(meeting, ORGANIZER_USER); + String onlineMeetingId = meetingService.getMeetingProperty(meeting, ONLINE_MEETING_ID); + if(StringUtils.isNotBlank(onlineMeetingId)) { + microsoftCommonService.updateOnlineMeeting(organizerEmail, onlineMeetingId, meeting.getTitle(), meeting.getStartDate(), meeting.getEndDate()); + } + } + + // Participants + MeetingAttendee attendee = null; + switch (data.getParticipantOption()) { + case SITE: + attendee = new MeetingAttendee(); + attendee.setType(AttendeeType.SITE); + attendee.setObjectId(data.getSiteId()); + meeting.getAttendees().add(attendee); + attendee.setMeeting(meeting); + break; + case GROUP: + for (String groupId : data.getGroupSelection()) { + attendee = new MeetingAttendee(); + attendee.setType(AttendeeType.GROUP); + attendee.setObjectId(groupId); + meeting.getAttendees().add(attendee); + attendee.setMeeting(meeting); + } + break; + default: + break; + } + // Update meeting + meetingService.updateMeeting(meeting); + + // Calendar events + if (data.isSaveToCalendar() && isThereAnyCalendarForSite(data.getSiteId()) + && StringUtils.isNotBlank(data.getStartDate()) && StringUtils.isNotBlank(data.getEndDate())) { + this.saveToCalendar(meeting); + } + if(!data.isSaveToCalendar() && StringUtils.isNotBlank(meetingService.getMeetingProperty(meeting, CALENDAR_EVENT_ID))) { + removeFromCalendar(meetingId); + meetingService.removeMeetingProperty(meeting, CALENDAR_EVENT_ID); + } + + // Notifications + this.sendNotification(meeting, data.getNotificationType()); + } catch (DateTimeParseException e) { + log.error("Could not parse Meetings start date string '{}' or end time string '{}'", meeting.getStartDate(), meeting.getEndDate()); + throw new MeetingsException(e.getLocalizedMessage()); + } catch (IdUnusedException e) { + log.error("Error retrieving site when sending notifications.", e); + throw new MeetingsException(e.getLocalizedMessage()); + } catch (MicrosoftCredentialsException e) { + log.error("Error updating meeting", e); + throw new MeetingsException(e.getLocalizedMessage()); + } catch (Exception e) { + log.error("Error updating meeting", e); + throw new MeetingsException(e.getLocalizedMessage()); + } + return meeting; + } + + /** + * Method to remove an existing meeting + * @param meetingId + * @throws MeetingsException + */ + @DeleteMapping(value = "/meeting/{meetingId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public void deleteMeeting(@PathVariable String meetingId) throws MeetingsException { + checkCurrentUserInMeeting(meetingId); + Meeting meeting = meetingService.getMeeting(meetingId); + checkUpdatePermissions(meeting.getSiteId()); + try { + this.removeFromCalendar(meetingId); + meetingService.deleteMeetingById(meetingId); + } catch (Exception e) { + log.error("Error deleting meeting", e); + throw new MeetingsException(e.getLocalizedMessage()); + } + } + + /** + * Get i18n bundle + * @param bundle + * @param locale + * @return + * @throws MeetingsException + */ + @GetMapping(value = "/i18n/{locale}/{bundle}", produces = MediaType.APPLICATION_JSON_VALUE) + public String getI18nProperties(@PathVariable String bundle, @PathVariable String locale) throws MeetingsException { + checkSakaiSession(); + StringBuilder i18n = new StringBuilder(); + if (StringUtils.isNotBlank(bundle) && StringUtils.isNotBlank(locale)) { + ResourceLoader rbundle = new ResourceLoader(bundle); + if (rbundle != null) { + rbundle.setContextLocale(Locale.forLanguageTag(locale)); + rbundle.forEach((k, v) -> i18n.append(k).append("=").append(v).append("\n")); + } + } + return i18n.toString(); + } + + /** + * Returns true if MS Teams is set up in Sakai properties + * @return + * @throws MeetingsException + */ + @GetMapping(value = "/meetings/teams/status", produces = MediaType.APPLICATION_JSON_VALUE) + public boolean isMicrosofTeamsConfigured() throws MeetingsException { + checkSakaiSession(); + try { + microsoftCommonService.checkConnection(); + return true; + } catch (MicrosoftCredentialsException e) {} + return false; + } + + /** + * Method to send notifications to users about meetings, by level of priority + * @param meeting + * @param type + * @throws IdUnusedException + * @throws AddressException + */ + private void sendNotification(Meeting meeting, NotificationType type) throws IdUnusedException { + if (type == NotificationType.ALL && meeting != null) { + Site site = sakaiProxy.getSite(meeting.getSiteId()); + if(site == null) { + throw new IdUnusedException(meeting.getSiteId()); + } + String from = sakaiProxy.getString(SMTP_FROM, NO_REPLY + sakaiProxy.getServerName()); + String subject = MessageFormat.format(rb.getString(NOTIF_SUBJECT), meeting.getTitle(), site.getTitle()); + String content = MessageFormat.format(rb.getString(NOTIF_CONTENT), meeting.getTitle(), site.getTitle()); + Set members = site.getMembers(); + List participantEmails = members.stream().map(member -> { + String email = null; + User user = sakaiProxy.getUser(member.getUserId()); + if(user != null) { + email = user.getEmail(); + } else { + log.warn("Member {} does not exist as a user.", member.getUserId()); + } + return email; + }).collect(Collectors.toList()); + sakaiProxy.sendMail(from, participantEmails, subject, content); + } + } + + /** + * Method to save a meeting as an event of the Sakai calendar + * @param meeting + * @throws MeetingsException + */ + private void saveToCalendar(Meeting meeting) { + try { + String calReference = "/calendar/calendar/" + meeting.getSiteId() + "/main"; + String calendarEventId = meetingService.getMeetingProperty(meeting, CALENDAR_EVENT_ID); + long init = meeting.getStartDate().toEpochMilli(); + long duration = meeting.getEndDate().toEpochMilli() - init; + + List groups = new ArrayList(); + Site site = sakaiProxy.getSite(meeting.getSiteId()); + meeting.getAttendees().stream() + .filter(attendee -> AttendeeType.GROUP.equals(attendee.getType())) + .forEach(attendee -> { + groups.add(site.getGroup(attendee.getObjectId())); + }); + + String retId = sakaiProxy.saveCalendar(SakaiCalendarEvent.builder() + .calendarReference(calReference) + .eventId(calendarEventId) + .init(init) + .duration(duration) + .title(meeting.getTitle()) + .description(meeting.getDescription()) + .type(MEETING_EVENT_TYPE) + .groups(groups) + .build() + ); + if(retId != null) { + meetingService.setMeetingProperty(meeting, CALENDAR_EVENT_ID, retId); + } + } catch (Exception e) { + log.error("Error saving calendar"); + } + } + + + /** + * Method to remove a calendar event based on a meeting + * @param meetingId + * @throws MeetingsException + */ + private void removeFromCalendar(String meetingId) throws MeetingsException { + try { + Optional optMeeting = meetingService.getMeetingById(meetingId); + if (optMeeting.isPresent()) { + Meeting meeting = optMeeting.get(); + String calendarEventId = meetingService.getMeetingProperty(meeting, CALENDAR_EVENT_ID); + if (StringUtils.isNotBlank(calendarEventId)) { + sakaiProxy.removeFromCalendar(meeting.getSiteId(), calendarEventId); + } + } + } catch (Exception e) { + throw new MeetingsException(e.getLocalizedMessage()); + } + } + + // ------------------------------- RECORDINGS ----------------------------------------------- + @GetMapping(value = "/meeting/{meetingId}/recordings", produces = MediaType.APPLICATION_JSON_VALUE) + public List getMeetingRecordings(@PathVariable String meetingId, @RequestParam(defaultValue = "false") Boolean force) throws MeetingsException { + checkCurrentUserInMeeting(meetingId); + try { + Meeting meeting = meetingService.getMeeting(meetingId); + List teamIdsList = microsoftSynchronizationService.getSiteSynchronizationsBySite(meeting.getSiteId()).stream() + .map(ss -> ss.getTeamId()) + .collect(Collectors.toList()); + return microsoftCommonService.getOnlineMeetingRecordings(meeting.getMeetingId(), teamIdsList, force); + } catch (MicrosoftCredentialsException e) { + log.error("Error getting meeting recordings", e); + throw new MeetingsException(e.getLocalizedMessage()); + } catch (Exception e) { + log.error("Error getting meeting recordings", e); + throw new MeetingsException(e.getLocalizedMessage()); + } + } +} diff --git a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFile.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/GroupData.java similarity index 63% rename from cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFile.java rename to meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/GroupData.java index 37bd8ae1aca6..26e9bb8ac597 100644 --- a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveFile.java +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/GroupData.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2003-2019 The Apereo Foundation + * Copyright (c) 2024 The Apereo Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sakaiproject.onedrive.model; +package org.sakaiproject.meetings.controller.data; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.io.Serializable; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; +import lombok.Data; -@Getter @Setter -@ToString -@JsonIgnoreProperties(ignoreUnknown = true) -public class OneDriveFile { +@Data +public class GroupData implements Serializable { + + private static final long serialVersionUID = 1L; + + private String groupId; + private String groupName; - public String mimeType; } diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/MeetingData.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/MeetingData.java new file mode 100644 index 000000000000..52db322d5e25 --- /dev/null +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/MeetingData.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.controller.data; + +import java.io.Serializable; +import java.util.List; + +import org.sakaiproject.meetings.api.model.AttendeeType; + +import lombok.Data; + +@Data +public class MeetingData implements Serializable { + + private static final long serialVersionUID = 3284276542110972341L; + + private String id; + private String title; + private String contextTitle; + private String description; + private String siteId; + private String startDate; + private String endDate; + private String url; + private String ownerId; + private boolean saveToCalendar; + private NotificationType notificationType; + private Integer live; + private String provider; + private AttendeeType participantOption; + private List groupSelection; + private List participants; + +} diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/NotificationType.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/NotificationType.java new file mode 100644 index 000000000000..712e024d051e --- /dev/null +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/NotificationType.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.controller.data; + +public enum NotificationType { + NONE, + ALL +} diff --git a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveParent.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/ParticipantData.java similarity index 57% rename from cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveParent.java rename to meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/ParticipantData.java index d7bb2c092779..0ef42e710681 100644 --- a/cloud-storage/onedrive/api/src/java/org/sakaiproject/onedrive/model/OneDriveParent.java +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/controller/data/ParticipantData.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2003-2019 The Apereo Foundation + * Copyright (c) 2024 The Apereo Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sakaiproject.onedrive.model; +package org.sakaiproject.meetings.controller.data; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; +import lombok.Data; -@Getter @Setter -@ToString -@JsonIgnoreProperties(ignoreUnknown = true) -public class OneDriveParent { +@Data +public class ParticipantData implements Serializable { - @JsonProperty(value = "id") - public String parentId; + private static final long serialVersionUID = 1L; + + private String userid; + private String text; + private String name; + } diff --git a/meetings/tool/src/main/java/org/sakaiproject/meetings/exceptions/MeetingsException.java b/meetings/tool/src/main/java/org/sakaiproject/meetings/exceptions/MeetingsException.java new file mode 100644 index 000000000000..906b057c68e3 --- /dev/null +++ b/meetings/tool/src/main/java/org/sakaiproject/meetings/exceptions/MeetingsException.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2024 The Apereo Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://opensource.org/licenses/ecl2 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sakaiproject.meetings.exceptions; + +public class MeetingsException extends Exception { + + private static final long serialVersionUID = 1L; + + public MeetingsException(String errorMessage) { + super(errorMessage); + } + +} diff --git a/meetings/tool/src/main/resources/Messages.properties b/meetings/tool/src/main/resources/Messages.properties new file mode 100644 index 000000000000..e10ea95c0e54 --- /dev/null +++ b/meetings/tool/src/main/resources/Messages.properties @@ -0,0 +1,2 @@ +notification.subject=A new meeting \u0027{0}\u0027 has been published in the site \u0027{1}\u0027 +notification.content=You have been invited to participate in the meeting {0}. diff --git a/meetings/tool/src/main/resources/Messages_ca.properties b/meetings/tool/src/main/resources/Messages_ca.properties new file mode 100644 index 000000000000..4ef311404641 --- /dev/null +++ b/meetings/tool/src/main/resources/Messages_ca.properties @@ -0,0 +1,2 @@ +notification.subject=S\u2019ha publicat una nova reuni\u00F3 \u0027{0}\u0027 a l\u2019espai {1} +notification.content=Ha sigut convidat a participar en la reuni\u00F3 {0}. \ No newline at end of file diff --git a/meetings/tool/src/main/resources/Messages_es.properties b/meetings/tool/src/main/resources/Messages_es.properties new file mode 100644 index 000000000000..704c8b3bdd78 --- /dev/null +++ b/meetings/tool/src/main/resources/Messages_es.properties @@ -0,0 +1,2 @@ +notification.subject=Se ha publicado una nueva reuni\u00F3n \u0027{0}\u0027 en el sitio {1} +notification.content=Ha sido invitado a participar en la reuni\u00F3n {0}. diff --git a/meetings/tool/src/main/resources/Messages_eu.properties b/meetings/tool/src/main/resources/Messages_eu.properties new file mode 100644 index 000000000000..e10ea95c0e54 --- /dev/null +++ b/meetings/tool/src/main/resources/Messages_eu.properties @@ -0,0 +1,2 @@ +notification.subject=A new meeting \u0027{0}\u0027 has been published in the site \u0027{1}\u0027 +notification.content=You have been invited to participate in the meeting {0}. diff --git a/meetings/tool/src/main/resources/card.properties b/meetings/tool/src/main/resources/card.properties new file mode 100644 index 000000000000..608174f3c085 --- /dev/null +++ b/meetings/tool/src/main/resources/card.properties @@ -0,0 +1,20 @@ +decription_link_text=View Description +decription_modal_title=Description +delete_modal_cancel=No, cancel +delete_modal_confirm=Yes, delete +delete_modal_message=Are you sure you want to delete the meeting {0}? +delete_modal_title=Delete meeting +menu_name=Options +status=Status +status_text_live=live +status_text_starts=starts +status_text_unknown=unknown status +status_text_waiting=waiting for start +edit_action=Edit +delete_action=Delete +get_link_action=Get Link +message_link_copied=Link copied to clipboard +check_recordings_action=Check recordings +availableParticipants=Available participants: +and_x_more=and {} more. +join_action=Join Meeting diff --git a/meetings/tool/src/main/resources/card_ca.properties b/meetings/tool/src/main/resources/card_ca.properties new file mode 100644 index 000000000000..9d7abbe31a2a --- /dev/null +++ b/meetings/tool/src/main/resources/card_ca.properties @@ -0,0 +1,20 @@ +decription_link_text=Veure descripci\u00F3 +decription_modal_title=Descripci\u00F3 +delete_modal_cancel=No, cancel\u00b7lar +delete_modal_confirm=S\u00ed, eliminar +delete_modal_message=Est\u00E0 segur que desitja eliminar la reuni\u00F3 \u0022{0}\u0022? +delete_modal_title=Eliminar reuni\u00F3 +menu_name=Opcions +status=Estat +status_text_live=en directe +status_text_starts=comen\u00E7a +status_text_unknown=estat descononegut +status_text_waiting=esperant per a comen\u00E7ar +edit_action=Editar +delete_action=Eliminar +get_link_action=Obtindre Enlla\u00E7 +message_link_copied=Enlla\u00E7 copiat al portapapers +check_recordings_action=Comprovar gravacions +availableParticipants=Participants disponibles: +and_x_more=i {} m\u00E9s. +join_action=Afegir-se a la reuni\u00F3 diff --git a/meetings/tool/src/main/resources/card_es.properties b/meetings/tool/src/main/resources/card_es.properties new file mode 100644 index 000000000000..880906c30cd5 --- /dev/null +++ b/meetings/tool/src/main/resources/card_es.properties @@ -0,0 +1,20 @@ +decription_link_text=Ver descripci\u00F3n +decription_modal_title=Descripci\u00F3n +delete_modal_cancel=No, cancelar +delete_modal_confirm=S\u00ed, eliminar +delete_modal_message=¿Est\u00e1 seguro de que desea eliminar la reuni\u00F3n \u0022{0}\u0022? +delete_modal_title=Eliminar reuni\u00F3n +menu_name=Opciones +status=Estado +status_text_live=en directo +status_text_starts=empieza +status_text_unknown=estado desconocido +status_text_waiting=esperando para comenzar +edit_action=Editar +delete_action=Eliminar +get_link_action=Obtener Enlace +message_link_copied=Enlace copiado al portapapeles +check_recordings_action=Comprobar grabaciones +availableParticipants=Participantes disponibles: +and_x_more=y {} m\u00e1s. +join_action=Unirse a la reuni\u00F3n diff --git a/meetings/tool/src/main/resources/card_eu.properties b/meetings/tool/src/main/resources/card_eu.properties new file mode 100644 index 000000000000..75337e412686 --- /dev/null +++ b/meetings/tool/src/main/resources/card_eu.properties @@ -0,0 +1,20 @@ +decription_link_text=Ikusi deskribapena +decription_modal_title=Deskribapena +delete_modal_cancel=Ez, utzi +delete_modal_confirm=Bai, ezabatu +delete_modal_message=Ziur zaude ezabatu nahi duzula bilera hau, {0}? +delete_modal_title=Ezabatu bilera +menu_name=Aukerak +status=Egoera +status_text_live= zuzenean +status_text_starts= hasita +status_text_unknown= egoera ezezaguna +status_text_waiting=itxaroten hasteko +edit_action=Editatu +delete_action=Ezabatu +get_link_action=Esteka lortu +message_link_copied= Esteka kopiatuta arbelan +check_recordings_action=Egiaztatu grabazioak +availableParticipants=Dauden partaideak: +and_x_more= eta {} gehiago. +join_action=Join Meeting diff --git a/meetings/tool/src/main/resources/create-meeting.properties b/meetings/tool/src/main/resources/create-meeting.properties new file mode 100644 index 000000000000..4a63413121ad --- /dev/null +++ b/meetings/tool/src/main/resources/create-meeting.properties @@ -0,0 +1,26 @@ +add_to_calendar=Add meeting to calendar +all_participants_notification=All - Send notification to all participants +all_participants=All Participants from Site +cancel=Cancel +close_date=Close Date +error_create_meeting_500=There was a problem creating the meeting +error_updating_meeting_500=There was a problem updating the meeting +error_create_meeting_unknown=Unknown error when trying to create meeting. Response code: +error_video_conferencing_config=The meetings tool requires at least one provider like Microsoft Teams or Zoom, please contact your service administrator or vendor. +group_participants=Participants from selected group(s) +info_no_groups=Group selection is disabled, because this Site has no groups. +meeting_description=Description +meeting_title=Title +ms_teams=Microsoft Teams +no_notification=None - No notification +notifications=Notifications +open_date=Open Date +participants_selection=Participants selection +save=Save +section_availability=3. Availability +section_meeting_information=1. Meeting Information +section_notifications=4. Notifications +section_participants=2. Participants +select_groups=Select Group(s) +validation_close_date=End date must be later then start date +video_conferencing_service=Video conferencing service diff --git a/meetings/tool/src/main/resources/create-meeting_ca.properties b/meetings/tool/src/main/resources/create-meeting_ca.properties new file mode 100644 index 000000000000..de6b118fe6d1 --- /dev/null +++ b/meetings/tool/src/main/resources/create-meeting_ca.properties @@ -0,0 +1,26 @@ +add_to_calendar=Afegir reuni\u00F3 al calendari +all_participants_notification=Tots - Enviar notificaci\u00F3 a tots els participants +all_participants=Tots els participants de l\u2019espai +cancel=Cancel\u00b7la +close_date=Data de tancament +error_create_meeting_500=Va haver-hi un problema al crear la reuni\u00F3 +error_update_meeting_500=Va haver-hi un problema al editar la reuni\u00F3 +error_create_meeting_unknown=Error desconegut al intentar crear la reuni\u00F3. C\u00F3di de resposta: +error_video_conferencing_config=L\u2019eina necessita un prove\u00EFdor com Microsoft Meetings o Zoom. Per favor contacta amb l\u2019administrador o el prove\u00EFdor del servei. +group_participants=Participants dels grups seleccionats +info_no_groups=La selecci\u00F3 de grup est\u00E0 deshabilitada pel fet que aquest espai no t\u00E9 grups. +meeting_description=Descripci\u00F3 +meeting_title=T\u00edtol +ms_teams=Microsoft Teams +no_notification=No - Sense notificaci\u00F3 +notifications=Notificacions +open_date=Data d\u2019inici +participants_selection=Selecci\u00F3 de participants +save=Desa +section_availability=3. Disponibilitat +section_meeting_information=1. Dades de la reuni\u00F3 +section_notifications=4. Notificacions +section_participants=2. Participants +select_groups=Selecci\u00F3 de Grup(s) +validation_close_date=La data de tancamente ha de ser posterior a la data d\u2019inici +video_conferencing_service=Servei de v\u00EDdeo conferencia \ No newline at end of file diff --git a/meetings/tool/src/main/resources/create-meeting_es.properties b/meetings/tool/src/main/resources/create-meeting_es.properties new file mode 100644 index 000000000000..8e9ce7e8b018 --- /dev/null +++ b/meetings/tool/src/main/resources/create-meeting_es.properties @@ -0,0 +1,26 @@ +add_to_calendar=A\u00f1adir reuni\u00F3n al calendario +all_participants_notification=Todos - Enviar notificaci\u00F3n a todos los participantes +all_participants=Todos los participantes del sitio +cancel=Cancelar +close_date=Fecha de cierre +error_create_meeting_500=Hubo un problema al crear la reuni\u00F3n +error_update_meeting_500=Hubo un problema al editar la reuni\u00F3n +error_create_meeting_unknown=Error desconocido al intentar crear la reuni\u00F3n. C\u00F3digo de respuesta: +error_video_conferencing_config=La herramienta necesita un proveedor como Microsoft Meetings o Zoom. Por favor contacta con el administrador o el proveedor del servicio. +group_participants=Participantes de los grupos seleccionados +info_no_groups=La selecci\u00F3n de grupo est\u00E1 deshabilitada debido a que este sitio no tiene grupos. +meeting_description=Descripci\u00F3n +meeting_title=T\u00edtulo +ms_teams=Microsoft Teams +no_notification=No - Sin notificaci\u00F3n +notifications=Notificaciones +open_date=Fecha de inicio +participants_selection=Selecci\u00F3n de participantes +save=Guardar +section_availability=3. Disponibilidad +section_meeting_information=1. Datos de la reuni\u00F3n +section_notifications=4. Notificaciones +section_participants=2. Participantes +select_groups=Selecci\u00F3n de Grupo(s) +validation_close_date=La fecha de cierre debe ser posterior a la fecha de inicio +video_conferencing_service=Servicio de video conferencia diff --git a/meetings/tool/src/main/resources/create-meeting_eu.properties b/meetings/tool/src/main/resources/create-meeting_eu.properties new file mode 100644 index 000000000000..6ae68214bc98 --- /dev/null +++ b/meetings/tool/src/main/resources/create-meeting_eu.properties @@ -0,0 +1,26 @@ +add_to_calendar=Gehitu bilera egutegian +all_participants_notification= Guztiak - Bidali jakinarazpena partaide guztiei +all_participants= Guneko partaide guztiak +cancel= Utzi +close_date= Bukatzeko data +error_create_meeting_500= Bilera sortzean arazo bat egon da +error_updating_meeting_500= Akats bat egon da bilera eguneratzean +error_create_meeting_unknown= Akats ezezagun bat egon da bilera bat sortzean. Erantzun-kodea: +error_video_conferencing_config= Bileren tresnak, gutxienez, Microsoft Teams edo Zoom bezalako hornitzaile bat behar du. Kontaktatu behar duzu zure zerbitzu-administratzailea edo hornitzailea. +group_participants= Hautatutako taldeetako partaideak +info_no_groups= Taldeen hautaketa desgaituta dago gune honek ez baitu talderik. +meeting_description= Deskribapena +meeting_title= Izenburua +ms_teams= Microsofteko taldeak +no_notification= Bat ere ez - Jakinarazpenik gabe +notifications= Jakinarazpenak +open_date= Hasteko data +participants_selection= Partaideen hautaketa +save= Gorde +section_availability= 3. Erabilgarritasuna +section_meeting_information= 1. Bilerari buruzko informazioa +section_notifications= 4. Jakinarazpenak +section_participants= 2. Partaideak +select_groups= Hautatu taldeak +validation_close_date= Bukatzeko datak hasteko dataren ondorengoa izan behar du +video_conferencing_service= Bideokonferentzia zerbitzua diff --git a/meetings/tool/src/main/resources/main.properties b/meetings/tool/src/main/resources/main.properties new file mode 100644 index 000000000000..51169f7e99f9 --- /dev/null +++ b/meetings/tool/src/main/resources/main.properties @@ -0,0 +1,10 @@ +create_new_meeting=Create new meeting +error_load_meetings=Failed to load meetings +error_load_permissions=Failed to load permissions +future=Future +no_meetings_scheduled=There are no meetings scheduled for this site. +no_results=No results for this search. +past=Past +search_results=Search results +search=Search for meetings +today=Today diff --git a/meetings/tool/src/main/resources/main_ca.properties b/meetings/tool/src/main/resources/main_ca.properties new file mode 100644 index 000000000000..ccba98820ab9 --- /dev/null +++ b/meetings/tool/src/main/resources/main_ca.properties @@ -0,0 +1,11 @@ +create_new_meeting=Crea nova reuni\u00F3 +error_load_meetings=Es va produir un error en carregar les reunions +error_load_permissions=Es va produir un error en recuperar els permisos +future=Pr\u00F2ximes +no_meetings_scheduled=No hi ha reunions programades per a aquest espai. +no_results=No s\u2019han trobat resultats per a aquesta cerca. +past=Anteriors +search_results=Resultats de cerca +search=Cerca de reunions +today=Hui + diff --git a/meetings/tool/src/main/resources/main_es.properties b/meetings/tool/src/main/resources/main_es.properties new file mode 100644 index 000000000000..6a273e077eb4 --- /dev/null +++ b/meetings/tool/src/main/resources/main_es.properties @@ -0,0 +1,10 @@ +create_new_meeting=Crear nueva reuni\u00F3n +error_load_meetings=Se produjo un error al cargar las reuniones +error_load_permissions=Se produjo un error al recuperar los permisos +future=Pr\u00F3ximas +no_meetings_scheduled=No hay reuniones programadas para este sitio. +no_results=No se han encontrado resultados para esta b\u00fasqueda. +past=Anteriores +search_results=Resultados de b\u00fasqueda +search=B\u00fasqueda de reuniones +today=Hoy diff --git a/meetings/tool/src/main/resources/main_eu.properties b/meetings/tool/src/main/resources/main_eu.properties new file mode 100644 index 000000000000..389baa08ae42 --- /dev/null +++ b/meetings/tool/src/main/resources/main_eu.properties @@ -0,0 +1,10 @@ +create_new_meeting= Sortu bilera +error_load_meetings= Ezin izan dira bilerak kargatu +error_load_permissions= Ezin izan dira baimenak kargatu +future= etorkizuna +no_meetings_scheduled= Ez dago bilerarik programatuta gune honetarako. +no_results= Ez dago emaitzarik bilaketa honetarako. +past= Iragana +search_results= Bilaketaren emaitzak +search= Bilatu bilerak +today = gaur diff --git a/meetings/tool/src/main/resources/meeting-recordings.properties b/meetings/tool/src/main/resources/meeting-recordings.properties new file mode 100644 index 000000000000..00c49a98ef18 --- /dev/null +++ b/meetings/tool/src/main/resources/meeting-recordings.properties @@ -0,0 +1,5 @@ +back=Back +error_load_recordings=Failed to load recordings +no_recordings_found=No recordings found +meeting_title=Recordings from: {} +refresh=Refresh \ No newline at end of file diff --git a/meetings/tool/src/main/resources/meeting-recordings_ca.properties b/meetings/tool/src/main/resources/meeting-recordings_ca.properties new file mode 100644 index 000000000000..ae85b38a2dc6 --- /dev/null +++ b/meetings/tool/src/main/resources/meeting-recordings_ca.properties @@ -0,0 +1,5 @@ +back=Tornar +error_load_recordings=Es va produir un error en carregar les gravacions +no_recordings_found=No s\u2019han trobat gravacions +meeting_title=Gravacions de: {} +refresh=Recarregar \ No newline at end of file diff --git a/meetings/tool/src/main/resources/meeting-recordings_es.properties b/meetings/tool/src/main/resources/meeting-recordings_es.properties new file mode 100644 index 000000000000..c5747ca943a1 --- /dev/null +++ b/meetings/tool/src/main/resources/meeting-recordings_es.properties @@ -0,0 +1,5 @@ +back=Volver +error_load_recordings=Se produjo un error al cargar las grabaciones +no_recordings_found=No se han encontrado grabaciones +meeting_title=Grabaciones de: {} +refresh=Recargar \ No newline at end of file diff --git a/meetings/tool/src/main/resources/meeting-recordings_eu.properties b/meetings/tool/src/main/resources/meeting-recordings_eu.properties new file mode 100644 index 000000000000..00c49a98ef18 --- /dev/null +++ b/meetings/tool/src/main/resources/meeting-recordings_eu.properties @@ -0,0 +1,5 @@ +back=Back +error_load_recordings=Failed to load recordings +no_recordings_found=No recordings found +meeting_title=Recordings from: {} +refresh=Refresh \ No newline at end of file diff --git a/meetings/tool/src/main/resources/validations.properties b/meetings/tool/src/main/resources/validations.properties new file mode 100644 index 000000000000..75ec771186a8 --- /dev/null +++ b/meetings/tool/src/main/resources/validations.properties @@ -0,0 +1,3 @@ +empty_field=This field is required to be filled out +empty_checkbox=This checkbox is required to be checked +maxlenght_field=Value exceeds maximum allowed size \ No newline at end of file diff --git a/meetings/tool/src/main/resources/validations_ca.properties b/meetings/tool/src/main/resources/validations_ca.properties new file mode 100644 index 000000000000..e151fad2e7a7 --- /dev/null +++ b/meetings/tool/src/main/resources/validations_ca.properties @@ -0,0 +1,3 @@ +empty_field=Aquest camp no pot ser buit +empty_checkbox=Aquesta casella de selecci\u00F3 ha d\u2019estar marcat +maxlenght_field=El valor supera la grand\u00E0ria m\u00E0xima permesa \ No newline at end of file diff --git a/meetings/tool/src/main/resources/validations_es.properties b/meetings/tool/src/main/resources/validations_es.properties new file mode 100644 index 000000000000..1d438f1ca624 --- /dev/null +++ b/meetings/tool/src/main/resources/validations_es.properties @@ -0,0 +1,3 @@ +empty_field=Este campo no puede ser vac\u00edo +empty_checkbox=Este checkbox debe estar marcado +maxlenght_field=El valor supera el tama\u00F1o m\u00E1ximo permitido \ No newline at end of file diff --git a/meetings/tool/src/main/resources/validations_eu.properties b/meetings/tool/src/main/resources/validations_eu.properties new file mode 100644 index 000000000000..da3d9cb6b03d --- /dev/null +++ b/meetings/tool/src/main/resources/validations_eu.properties @@ -0,0 +1,3 @@ +empty_field= Eremu hau nahitaez bete behar da +empty_checkbox= Egiaztapen-lauki honek markatuta egon behar du +maxlenght_field= Balioak gainditzen du baimendutako gehieneko tamaina diff --git a/meetings/tool/src/main/webapp/WEB-INF/templates/index.html b/meetings/tool/src/main/webapp/WEB-INF/templates/index.html new file mode 100644 index 000000000000..307dfe3ed1d8 --- /dev/null +++ b/meetings/tool/src/main/webapp/WEB-INF/templates/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/meetings/tool/src/main/webapp/WEB-INF/tools/sakai.meetings.xml b/meetings/tool/src/main/webapp/WEB-INF/tools/sakai.meetings.xml new file mode 100644 index 000000000000..846e029ac8f8 --- /dev/null +++ b/meetings/tool/src/main/webapp/WEB-INF/tools/sakai.meetings.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/meetings/tool/src/main/webapp/WEB-INF/web.xml b/meetings/tool/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..e3969e59e453 --- /dev/null +++ b/meetings/tool/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,7 @@ + + + + diff --git a/meetings/ui/pom.xml b/meetings/ui/pom.xml new file mode 100644 index 000000000000..1f0d143b2c02 --- /dev/null +++ b/meetings/ui/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + org.sakaiproject.meetings + meetings + 24-SNAPSHOT + ../pom.xml + + org.sakaiproject.meetings + meetings-ui + war + + + 1.12.1 + 9.5.1 + v18.16.0 + 3.1.0 + + + + + + javax.servlet + javax.servlet-api + + + + + + + + + org.apache.maven.plugins + maven-war-plugin + + ${basedir}/target/webapp + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + + ${basedir}/src/main/frontend/node_modules + + + ${basedir}/target/webapp + + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + + install node and npm + process-resources + + install-node-and-npm + + + + npm install + + npm + + + install + + process-resources + + + npm run build + + npm + + + run build + + process-resources + + + + src/main/frontend + src/main/frontend + ${frontend-maven-plugin.npmVersion} + ${frontend-maven-plugin.nodeVersion} + + + + + + diff --git a/meetings/ui/src/main/frontend/README.md b/meetings/ui/src/main/frontend/README.md new file mode 100644 index 000000000000..c0793a82398e --- /dev/null +++ b/meetings/ui/src/main/frontend/README.md @@ -0,0 +1,7 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + diff --git a/meetings/ui/src/main/frontend/package-lock.json b/meetings/ui/src/main/frontend/package-lock.json new file mode 100644 index 000000000000..a78030db97a6 --- /dev/null +++ b/meetings/ui/src/main/frontend/package-lock.json @@ -0,0 +1,1030 @@ +{ + "name": "meetings-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meetings-ui", + "version": "1.0.0", + "dependencies": { + "@popperjs/core": "^2.11.7", + "bootstrap": "^5.2.3", + "dayjs": "^1.10.8", + "pinia": "^2.0.36", + "string.prototype.format": "^1.0.0", + "uuid": "^9.0.0", + "vue": "^3.2.47", + "vue-router": "^4.1.6" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.2.1", + "sass": "^1.62.1", + "vite": "^4.3.5" + } + }, + "node_modules/@babel/parser": { + "version": "7.21.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", + "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", + "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", + "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", + "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", + "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", + "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", + "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", + "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", + "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", + "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", + "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", + "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", + "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", + "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", + "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", + "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", + "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", + "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", + "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", + "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", + "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", + "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", + "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", + "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.1.tgz", + "integrity": "sha512-ZTZjzo7bmxTRTkb8GSTwkPOYDIP7pwuyV+RV53c9PYUouwcbkIZIvWvNWlX2b1dYZqtOv7D6iUAnJLVNGcLrSw==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz", + "integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==", + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/shared": "3.2.47", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz", + "integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==", + "dependencies": { + "@vue/compiler-core": "3.2.47", + "@vue/shared": "3.2.47" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz", + "integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==", + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.47", + "@vue/compiler-dom": "3.2.47", + "@vue/compiler-ssr": "3.2.47", + "@vue/reactivity-transform": "3.2.47", + "@vue/shared": "3.2.47", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7", + "postcss": "^8.1.10", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz", + "integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==", + "dependencies": { + "@vue/compiler-dom": "3.2.47", + "@vue/shared": "3.2.47" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "node_modules/@vue/reactivity": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz", + "integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==", + "dependencies": { + "@vue/shared": "3.2.47" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz", + "integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==", + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.47", + "@vue/shared": "3.2.47", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.47.tgz", + "integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==", + "dependencies": { + "@vue/reactivity": "3.2.47", + "@vue/shared": "3.2.47" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz", + "integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==", + "dependencies": { + "@vue/runtime-core": "3.2.47", + "@vue/shared": "3.2.47", + "csstype": "^2.6.8" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.47.tgz", + "integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==", + "dependencies": { + "@vue/compiler-ssr": "3.2.47", + "@vue/shared": "3.2.47" + }, + "peerDependencies": { + "vue": "3.2.47" + } + }, + "node_modules/@vue/shared": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz", + "integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bootstrap": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", + "integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.6" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + }, + "node_modules/dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + }, + "node_modules/esbuild": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", + "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.18", + "@esbuild/android-arm64": "0.17.18", + "@esbuild/android-x64": "0.17.18", + "@esbuild/darwin-arm64": "0.17.18", + "@esbuild/darwin-x64": "0.17.18", + "@esbuild/freebsd-arm64": "0.17.18", + "@esbuild/freebsd-x64": "0.17.18", + "@esbuild/linux-arm": "0.17.18", + "@esbuild/linux-arm64": "0.17.18", + "@esbuild/linux-ia32": "0.17.18", + "@esbuild/linux-loong64": "0.17.18", + "@esbuild/linux-mips64el": "0.17.18", + "@esbuild/linux-ppc64": "0.17.18", + "@esbuild/linux-riscv64": "0.17.18", + "@esbuild/linux-s390x": "0.17.18", + "@esbuild/linux-x64": "0.17.18", + "@esbuild/netbsd-x64": "0.17.18", + "@esbuild/openbsd-x64": "0.17.18", + "@esbuild/sunos-x64": "0.17.18", + "@esbuild/win32-arm64": "0.17.18", + "@esbuild/win32-ia32": "0.17.18", + "@esbuild/win32-x64": "0.17.18" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/immutable": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.36.tgz", + "integrity": "sha512-4UKApwjlmJH+VuHKgA+zQMddcCb3ezYnyewQ9NVrsDqZ/j9dMv5+rh+1r48whKNdpFkZAWVxhBp5ewYaYX9JcQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.1.tgz", + "integrity": "sha512-rt+yuCtXvscYot9SQQj3WKZJVSriPNqVkpVBNEHPzSgBv7QIYzsS410VqVgvx8f9AAPgjg+XPKvmV3vOqqkJQQ==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rollup": { + "version": "3.21.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.6.tgz", + "integrity": "sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/sass": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", + "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead" + }, + "node_modules/string.prototype.format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.format/-/string.prototype.format-1.0.0.tgz", + "integrity": "sha512-3LSqA7GUH5LcA0QtBvMkcNE5xmd3BYeexrRWS12c+V85mkNksvji7RDfFd9X9Awnj+p3rkS2Y4sP0b1C4H5+NQ==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.2.47", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.47.tgz", + "integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==", + "dependencies": { + "@vue/compiler-dom": "3.2.47", + "@vue/compiler-sfc": "3.2.47", + "@vue/runtime-dom": "3.2.47", + "@vue/server-renderer": "3.2.47", + "@vue/shared": "3.2.47" + } + }, + "node_modules/vue-router": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz", + "integrity": "sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ==", + "dependencies": { + "@vue/devtools-api": "^6.4.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + } + } +} diff --git a/meetings/ui/src/main/frontend/package.json b/meetings/ui/src/main/frontend/package.json new file mode 100644 index 000000000000..3efea419ff19 --- /dev/null +++ b/meetings/ui/src/main/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "meetings-ui", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build --base=/meetings-ui/ --outDir ../../../target/webapp/", + "preview": "vite preview" + }, + "dependencies": { + "@popperjs/core": "^2.11.7", + "bootstrap": "^5.2.3", + "dayjs": "^1.10.8", + "string.prototype.format": "^1.0.0", + "uuid": "^9.0.0", + "vue": "^3.2.47", + "vue-router": "^4.1.6", + "pinia": "^2.0.36" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.2.1", + "sass": "^1.62.1", + "vite": "^4.3.5" + } +} diff --git a/meetings/ui/src/main/frontend/public/favicon.ico b/meetings/ui/src/main/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/meetings/ui/src/main/frontend/src/App.vue b/meetings/ui/src/main/frontend/src/App.vue new file mode 100644 index 000000000000..6833dd36cd15 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/App.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-accordion-item.vue b/meetings/ui/src/main/frontend/src/components/sakai-accordion-item.vue new file mode 100644 index 000000000000..1e1320ccaf38 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-accordion-item.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-accordion.vue b/meetings/ui/src/main/frontend/src/components/sakai-accordion.vue new file mode 100644 index 000000000000..c6492793f024 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-accordion.vue @@ -0,0 +1,24 @@ + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-avatar-list.vue b/meetings/ui/src/main/frontend/src/components/sakai-avatar-list.vue new file mode 100644 index 000000000000..52c589c84d68 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-avatar-list.vue @@ -0,0 +1,66 @@ + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-avatar.vue b/meetings/ui/src/main/frontend/src/components/sakai-avatar.vue new file mode 100644 index 000000000000..52a5e74d9f6b --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-avatar.vue @@ -0,0 +1,128 @@ + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-button.vue b/meetings/ui/src/main/frontend/src/components/sakai-button.vue new file mode 100644 index 000000000000..0185a8508cd7 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-button.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-dropdown-button.vue b/meetings/ui/src/main/frontend/src/components/sakai-dropdown-button.vue new file mode 100644 index 000000000000..32caaceb87e4 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-dropdown-button.vue @@ -0,0 +1,109 @@ + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-dropdown.vue b/meetings/ui/src/main/frontend/src/components/sakai-dropdown.vue new file mode 100644 index 000000000000..a11066386454 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-dropdown.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-icon.vue b/meetings/ui/src/main/frontend/src/components/sakai-icon.vue new file mode 100644 index 000000000000..dfef55953b28 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-icon.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-input-labelled.vue b/meetings/ui/src/main/frontend/src/components/sakai-input-labelled.vue new file mode 100644 index 000000000000..72d3d470ea8b --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-input-labelled.vue @@ -0,0 +1,124 @@ + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-input.vue b/meetings/ui/src/main/frontend/src/components/sakai-input.vue new file mode 100644 index 000000000000..b020175e3be1 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-input.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue b/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue new file mode 100644 index 000000000000..07c8887afe5e --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-meeting-card.vue @@ -0,0 +1,418 @@ + + + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-modal.vue b/meetings/ui/src/main/frontend/src/components/sakai-modal.vue new file mode 100644 index 000000000000..46f2a037c178 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-modal.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-radio-group.vue b/meetings/ui/src/main/frontend/src/components/sakai-radio-group.vue new file mode 100644 index 000000000000..7aed41f38a33 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-radio-group.vue @@ -0,0 +1,63 @@ + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-recording.vue b/meetings/ui/src/main/frontend/src/components/sakai-recording.vue new file mode 100644 index 000000000000..90208c49d5f5 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-recording.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-select.vue b/meetings/ui/src/main/frontend/src/components/sakai-select.vue new file mode 100644 index 000000000000..773bd4b8fedf --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-select.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-selected-participants.vue b/meetings/ui/src/main/frontend/src/components/sakai-selected-participants.vue new file mode 100644 index 000000000000..290e30f4c631 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-selected-participants.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/meetings/ui/src/main/frontend/src/components/sakai-textarea.vue b/meetings/ui/src/main/frontend/src/components/sakai-textarea.vue new file mode 100644 index 000000000000..ec0cc4949747 --- /dev/null +++ b/meetings/ui/src/main/frontend/src/components/sakai-textarea.vue @@ -0,0 +1,100 @@ +