From 65212c538ba65ad51df5aaa0a8480a3acf20b51f Mon Sep 17 00:00:00 2001 From: laurentdavid Date: Wed, 24 Nov 2021 12:22:08 +0000 Subject: [PATCH] MDL-70658 mod_bigbluebutton: Include version 2021091407 into core This commit includes a copy of the bigbluebuttonbn activity module from: - branch: tags/v2.5-alpha.2^0 - commit: --- lib/classes/plugin_manager.php | 2 +- mod/bigbluebuttonbn/amd/build/actions.min.js | 2 + .../amd/build/actions.min.js.map | 1 + .../amd/build/event_dispatcher.min.js | 2 + .../amd/build/event_dispatcher.min.js.map | 1 + mod/bigbluebuttonbn/amd/build/events.min.js | 2 + .../amd/build/events.min.js.map | 1 + mod/bigbluebuttonbn/amd/build/index.min.js | 2 + .../amd/build/index.min.js.map | 1 + mod/bigbluebuttonbn/amd/build/modform.min.js | 2 + .../amd/build/modform.min.js.map | 1 + .../amd/build/recordings.min.js | 2 + .../amd/build/recordings.min.js.map | 1 + .../amd/build/repository.min.js | 2 + .../amd/build/repository.min.js.map | 1 + mod/bigbluebuttonbn/amd/build/rooms.min.js | 2 + .../amd/build/rooms.min.js.map | 1 + .../amd/build/roomupdater.min.js | 2 + .../amd/build/roomupdater.min.js.map | 1 + mod/bigbluebuttonbn/amd/src/actions.js | 66 + .../amd/src/event_dispatcher.js | 82 ++ mod/bigbluebuttonbn/amd/src/events.js | 74 + mod/bigbluebuttonbn/amd/src/index.js | 31 + mod/bigbluebuttonbn/amd/src/modform.js | 367 +++++ mod/bigbluebuttonbn/amd/src/recordings.js | 416 ++++++ mod/bigbluebuttonbn/amd/src/repository.js | 139 ++ mod/bigbluebuttonbn/amd/src/rooms.js | 83 ++ mod/bigbluebuttonbn/amd/src/roomupdater.js | 109 ++ ...up_bigbluebuttonbn_activity_task.class.php | 81 ++ .../backup_bigbluebuttonbn_stepslib.php | 93 ++ ...re_bigbluebuttonbn_activity_task.class.php | 108 ++ .../restore_bigbluebuttonbn_stepslib.php | 116 ++ mod/bigbluebuttonbn/bbb_broker.php | 70 + mod/bigbluebuttonbn/bbb_view.php | 163 +++ .../analytics/indicator/activity_base.php | 47 + .../analytics/indicator/cognitive_depth.php | 70 + .../analytics/indicator/social_breadth.php | 70 + mod/bigbluebuttonbn/classes/broker.php | 130 ++ .../classes/completion/custom_completion.php | 300 ++++ .../event/activity_management_viewed.php | 55 + .../classes/event/activity_viewed.php | 56 + mod/bigbluebuttonbn/classes/event/base.php | 133 ++ mod/bigbluebuttonbn/classes/event/events.php | 51 + .../classes/event/live_session_event.php | 57 + .../classes/event/meeting_created.php | 56 + .../classes/event/meeting_ended.php | 57 + .../classes/event/meeting_joined.php | 57 + .../classes/event/meeting_left.php | 57 + .../classes/event/recording_deleted.php | 56 + .../classes/event/recording_edited.php | 56 + .../classes/event/recording_imported.php | 56 + .../classes/event/recording_protected.php | 55 + .../classes/event/recording_published.php | 56 + .../classes/event/recording_unprotected.php | 56 + .../classes/event/recording_unpublished.php | 56 + .../classes/event/recording_viewed.php | 56 + .../classes/external/can_join.php | 111 ++ .../classes/external/completion_validate.php | 115 ++ .../classes/external/end_meeting.php | 129 ++ .../get_bigbluebuttonbns_by_courses.php | 139 ++ .../classes/external/get_join_url.php | 115 ++ .../classes/external/get_recordings.php | 159 ++ .../external/get_recordings_to_import.php | 206 +++ .../classes/external/meeting_info.php | 137 ++ .../classes/external/update_recording.php | 141 ++ .../classes/external/view_bigbluebuttonbn.php | 114 ++ mod/bigbluebuttonbn/classes/instance.php | 1138 +++++++++++++++ .../recordings/recording_action.php | 130 ++ .../recordings/recording_data.php | 306 ++++ mod/bigbluebuttonbn/classes/local/config.php | 262 ++++ .../exceptions/bigbluebutton_exception.php | 40 + .../exceptions/meeting_join_exception.php | 38 + .../server_not_available_exception.php | 52 + .../classes/local/helpers/files.php | 320 ++++ .../classes/local/helpers/mod_helper.php | 243 ++++ .../classes/local/helpers/reset.php | 120 ++ .../classes/local/helpers/roles.php | 451 ++++++ .../local/proxy/bigbluebutton_proxy.php | 483 +++++++ .../classes/local/proxy/curl.php | 156 ++ .../classes/local/proxy/proxy_base.php | 152 ++ .../classes/local/proxy/recording_proxy.php | 342 +++++ mod/bigbluebuttonbn/classes/logger.php | 463 ++++++ mod/bigbluebuttonbn/classes/meeting.php | 526 +++++++ .../classes/output/import_view.php | 149 ++ mod/bigbluebuttonbn/classes/output/index.php | 208 +++ .../output/instance_updated_message.php | 76 + mod/bigbluebuttonbn/classes/output/mobile.php | 227 +++ .../output/recording_description_editable.php | 60 + .../classes/output/recording_editable.php | 137 ++ .../output/recording_name_editable.php | 61 + .../output/recording_row_actionbar.php | 193 +++ .../classes/output/recording_row_playback.php | 130 ++ .../classes/output/recording_row_preview.php | 92 ++ .../classes/output/recordings_session.php | 77 + .../classes/output/renderer.php | 110 ++ .../classes/output/view_page.php | 156 ++ mod/bigbluebuttonbn/classes/plugin.php | 68 + .../classes/privacy/provider.php | 316 ++++ mod/bigbluebuttonbn/classes/recording.php | 788 ++++++++++ .../classes/search/activity.php | 36 + mod/bigbluebuttonbn/classes/search/tags.php | 117 ++ .../classes/setting_validator.php | 273 ++++ mod/bigbluebuttonbn/classes/settings.php | 1284 +++++++++++++++++ .../classes/task/check_pending_recordings.php | 45 + .../classes/task/completion_update_state.php | 41 + .../classes/task/reset_recordings.php | 96 ++ .../send_instance_update_notification.php | 124 ++ .../classes/task/send_notification.php | 224 +++ .../send_recording_ready_notification.php | 83 ++ .../task/upgrade_imported_recordings.php | 55 + .../task/upgrade_recording_base_task.php | 160 ++ .../classes/task/upgrade_recordings.php | 50 + .../classes/test/testcase_helper_trait.php | 323 +++++ mod/bigbluebuttonbn/db/access.php | 160 ++ mod/bigbluebuttonbn/db/caches.php | 59 + mod/bigbluebuttonbn/db/install.xml | 104 ++ mod/bigbluebuttonbn/db/log.php | 43 + mod/bigbluebuttonbn/db/messages.php | 39 + mod/bigbluebuttonbn/db/mobile.php | 48 + mod/bigbluebuttonbn/db/services.php | 117 ++ mod/bigbluebuttonbn/db/tasks.php | 37 + mod/bigbluebuttonbn/db/upgrade.php | 478 ++++++ .../fonts/bigbluebutton-font.svg | 11 + .../fonts/bigbluebutton-font.ttf | Bin 0 -> 1544 bytes .../fonts/bigbluebutton-font.woff | Bin 0 -> 1064 bytes mod/bigbluebuttonbn/import_view.php | 66 + mod/bigbluebuttonbn/index.php | 59 + .../lang/en/bigbluebuttonbn.php | 616 ++++++++ mod/bigbluebuttonbn/lib.php | 656 +++++++++ .../mobileapp/mobile.notification.js | 39 + mod/bigbluebuttonbn/mod_form.php | 718 +++++++++ mod/bigbluebuttonbn/pix/flex_icons.php | 34 + .../pix/i/bigbluebutton-128.png | Bin 0 -> 7752 bytes .../pix/i/bigbluebutton-24.png | Bin 0 -> 1386 bytes .../pix/i/bigbluebutton-256.png | Bin 0 -> 18500 bytes .../pix/i/bigbluebutton-32.png | Bin 0 -> 1939 bytes .../pix/i/bigbluebutton-48.png | Bin 0 -> 3250 bytes .../pix/i/bigbluebutton-64.png | Bin 0 -> 4741 bytes .../pix/i/bigbluebutton-72.png | Bin 0 -> 5183 bytes .../pix/i/bigbluebutton-80.png | Bin 0 -> 5578 bytes .../pix/i/bigbluebutton-96.png | Bin 0 -> 6229 bytes mod/bigbluebuttonbn/pix/i/bigbluebutton.svg | 28 + mod/bigbluebuttonbn/pix/icon.gif | Bin 0 -> 1246 bytes mod/bigbluebuttonbn/pix/icon.png | Bin 0 -> 1393 bytes mod/bigbluebuttonbn/pix/icon.svg | 55 + mod/bigbluebuttonbn/settings.php | 32 + mod/bigbluebuttonbn/styles.css | 64 + .../templates/end_session_button.mustache | 41 + .../templates/import_view.mustache | 77 + .../instance_updated_message.mustache | 70 + .../templates/mobile_view_error.mustache | 34 + .../mobile_view_notification.mustache | 51 + .../mobile_view_page_ionic3.mustache | 62 + .../mobile_view_page_latest.mustache | 64 + .../templates/participant_form.mustache | 49 + .../templates/participant_form_add.mustache | 56 + .../participant_single_select.mustache | 120 ++ .../templates/recentactivity.mustache | 58 + .../recording_row_actionbar.mustache | 53 + .../templates/recording_row_playback.mustache | 52 + .../templates/recording_row_preview.mustache | 60 + .../templates/recordings_session.mustache | 51 + .../templates/recordings_table.mustache | 59 + .../templates/room_view.mustache | 98 ++ .../search_input_recordings.mustache | 62 + .../templates/view_page.mustache | 73 + .../tests/backup_restore_test.php | 196 +++ .../tests/behat/add_instance.feature | 64 + .../tests/behat/behat_mod_bigbluebuttonbn.php | 179 +++ .../tests/behat/end_meeting.feature | 102 ++ .../tests/behat/group_mode.feature | 77 + .../tests/behat/join_meeting.feature | 38 + .../tests/behat/meeting_roles.feature | 39 + .../tests/behat/recordings.feature | 84 ++ .../tests/behat/recordings_import.feature | 111 ++ .../tests/behat/start_meeting.feature | 113 ++ mod/bigbluebuttonbn/tests/completion_test.php | 218 +++ mod/bigbluebuttonbn/tests/coverage.php | 55 + .../tests/external/can_join_test.php | 123 ++ .../external/completion_validate_test.php | 154 ++ .../tests/external/end_meeting_test.php | 171 +++ .../get_bigbluebuttons_by_courses_test.php | 179 +++ .../tests/external/get_join_url_test.php | 234 +++ .../tests/external/get_recordings_test.php | 358 +++++ .../external/view_bigbluebuttonbn_test.php | 126 ++ .../tests/fixtures/bbpresentation.pptx | Bin 0 -> 40373 bytes .../behat_mod_bigbluebuttonbn_generator.php | 118 ++ mod/bigbluebuttonbn/tests/generator/lib.php | 374 +++++ mod/bigbluebuttonbn/tests/instance_test.php | 498 +++++++ mod/bigbluebuttonbn/tests/lib_test.php | 527 +++++++ .../recordings/recording_data_test.php | 65 + .../tests/local/helpers/files_test.php | 235 +++ .../local/helpers/mod_helper_trait_test.php | 189 +++ .../tests/local/helpers/reset_test.php | 128 ++ .../tests/local/helpers/roles_test.php | 94 ++ mod/bigbluebuttonbn/tests/logger_test.php | 72 + mod/bigbluebuttonbn/tests/meeting_test.php | 341 +++++ .../tests/privacy/provider_test.php | 291 ++++ mod/bigbluebuttonbn/tests/recording_test.php | 256 ++++ mod/bigbluebuttonbn/tests/search_test.php | 74 + ...send_instance_update_notification_test.php | 106 ++ .../tests/task/send_notification_test.php | 62 + ...send_recording_ready_notification_test.php | 194 +++ .../task/upgrade_recordings_imported_test.php | 123 ++ .../tests/task/upgrade_recordings_test.php | 124 ++ mod/bigbluebuttonbn/version.php | 32 + mod/bigbluebuttonbn/view.php | 101 ++ 207 files changed, 27294 insertions(+), 1 deletion(-) create mode 100644 mod/bigbluebuttonbn/amd/build/actions.min.js create mode 100644 mod/bigbluebuttonbn/amd/build/actions.min.js.map create mode 100644 mod/bigbluebuttonbn/amd/build/event_dispatcher.min.js create mode 100644 mod/bigbluebuttonbn/amd/build/event_dispatcher.min.js.map create mode 100644 mod/bigbluebuttonbn/amd/build/events.min.js create mode 100644 mod/bigbluebuttonbn/amd/build/events.min.js.map create mode 100644 mod/bigbluebuttonbn/amd/build/index.min.js create mode 100644 mod/bigbluebuttonbn/amd/build/index.min.js.map create mode 100644 mod/bigbluebuttonbn/amd/build/modform.min.js create mode 100644 mod/bigbluebuttonbn/amd/build/modform.min.js.map create mode 100644 mod/bigbluebuttonbn/amd/build/recordings.min.js create mode 100644 mod/bigbluebuttonbn/amd/build/recordings.min.js.map create mode 100644 mod/bigbluebuttonbn/amd/build/repository.min.js create mode 100644 mod/bigbluebuttonbn/amd/build/repository.min.js.map create mode 100644 mod/bigbluebuttonbn/amd/build/rooms.min.js create mode 100644 mod/bigbluebuttonbn/amd/build/rooms.min.js.map create mode 100644 mod/bigbluebuttonbn/amd/build/roomupdater.min.js create mode 100644 mod/bigbluebuttonbn/amd/build/roomupdater.min.js.map create mode 100644 mod/bigbluebuttonbn/amd/src/actions.js create mode 100644 mod/bigbluebuttonbn/amd/src/event_dispatcher.js create mode 100644 mod/bigbluebuttonbn/amd/src/events.js create mode 100644 mod/bigbluebuttonbn/amd/src/index.js create mode 100644 mod/bigbluebuttonbn/amd/src/modform.js create mode 100644 mod/bigbluebuttonbn/amd/src/recordings.js create mode 100644 mod/bigbluebuttonbn/amd/src/repository.js create mode 100644 mod/bigbluebuttonbn/amd/src/rooms.js create mode 100644 mod/bigbluebuttonbn/amd/src/roomupdater.js create mode 100644 mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_activity_task.class.php create mode 100644 mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php create mode 100644 mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_activity_task.class.php create mode 100644 mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php create mode 100644 mod/bigbluebuttonbn/bbb_broker.php create mode 100644 mod/bigbluebuttonbn/bbb_view.php create mode 100644 mod/bigbluebuttonbn/classes/analytics/indicator/activity_base.php create mode 100644 mod/bigbluebuttonbn/classes/analytics/indicator/cognitive_depth.php create mode 100644 mod/bigbluebuttonbn/classes/analytics/indicator/social_breadth.php create mode 100644 mod/bigbluebuttonbn/classes/broker.php create mode 100644 mod/bigbluebuttonbn/classes/completion/custom_completion.php create mode 100644 mod/bigbluebuttonbn/classes/event/activity_management_viewed.php create mode 100644 mod/bigbluebuttonbn/classes/event/activity_viewed.php create mode 100644 mod/bigbluebuttonbn/classes/event/base.php create mode 100644 mod/bigbluebuttonbn/classes/event/events.php create mode 100644 mod/bigbluebuttonbn/classes/event/live_session_event.php create mode 100644 mod/bigbluebuttonbn/classes/event/meeting_created.php create mode 100644 mod/bigbluebuttonbn/classes/event/meeting_ended.php create mode 100644 mod/bigbluebuttonbn/classes/event/meeting_joined.php create mode 100644 mod/bigbluebuttonbn/classes/event/meeting_left.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_deleted.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_edited.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_imported.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_protected.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_published.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_unprotected.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_unpublished.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_viewed.php create mode 100644 mod/bigbluebuttonbn/classes/external/can_join.php create mode 100644 mod/bigbluebuttonbn/classes/external/completion_validate.php create mode 100644 mod/bigbluebuttonbn/classes/external/end_meeting.php create mode 100644 mod/bigbluebuttonbn/classes/external/get_bigbluebuttonbns_by_courses.php create mode 100644 mod/bigbluebuttonbn/classes/external/get_join_url.php create mode 100644 mod/bigbluebuttonbn/classes/external/get_recordings.php create mode 100644 mod/bigbluebuttonbn/classes/external/get_recordings_to_import.php create mode 100644 mod/bigbluebuttonbn/classes/external/meeting_info.php create mode 100644 mod/bigbluebuttonbn/classes/external/update_recording.php create mode 100644 mod/bigbluebuttonbn/classes/external/view_bigbluebuttonbn.php create mode 100644 mod/bigbluebuttonbn/classes/instance.php create mode 100644 mod/bigbluebuttonbn/classes/local/bigbluebutton/recordings/recording_action.php create mode 100644 mod/bigbluebuttonbn/classes/local/bigbluebutton/recordings/recording_data.php create mode 100644 mod/bigbluebuttonbn/classes/local/config.php create mode 100644 mod/bigbluebuttonbn/classes/local/exceptions/bigbluebutton_exception.php create mode 100644 mod/bigbluebuttonbn/classes/local/exceptions/meeting_join_exception.php create mode 100644 mod/bigbluebuttonbn/classes/local/exceptions/server_not_available_exception.php create mode 100644 mod/bigbluebuttonbn/classes/local/helpers/files.php create mode 100644 mod/bigbluebuttonbn/classes/local/helpers/mod_helper.php create mode 100644 mod/bigbluebuttonbn/classes/local/helpers/reset.php create mode 100644 mod/bigbluebuttonbn/classes/local/helpers/roles.php create mode 100644 mod/bigbluebuttonbn/classes/local/proxy/bigbluebutton_proxy.php create mode 100644 mod/bigbluebuttonbn/classes/local/proxy/curl.php create mode 100644 mod/bigbluebuttonbn/classes/local/proxy/proxy_base.php create mode 100644 mod/bigbluebuttonbn/classes/local/proxy/recording_proxy.php create mode 100644 mod/bigbluebuttonbn/classes/logger.php create mode 100644 mod/bigbluebuttonbn/classes/meeting.php create mode 100644 mod/bigbluebuttonbn/classes/output/import_view.php create mode 100644 mod/bigbluebuttonbn/classes/output/index.php create mode 100644 mod/bigbluebuttonbn/classes/output/instance_updated_message.php create mode 100644 mod/bigbluebuttonbn/classes/output/mobile.php create mode 100644 mod/bigbluebuttonbn/classes/output/recording_description_editable.php create mode 100644 mod/bigbluebuttonbn/classes/output/recording_editable.php create mode 100644 mod/bigbluebuttonbn/classes/output/recording_name_editable.php create mode 100644 mod/bigbluebuttonbn/classes/output/recording_row_actionbar.php create mode 100644 mod/bigbluebuttonbn/classes/output/recording_row_playback.php create mode 100644 mod/bigbluebuttonbn/classes/output/recording_row_preview.php create mode 100644 mod/bigbluebuttonbn/classes/output/recordings_session.php create mode 100644 mod/bigbluebuttonbn/classes/output/renderer.php create mode 100644 mod/bigbluebuttonbn/classes/output/view_page.php create mode 100644 mod/bigbluebuttonbn/classes/plugin.php create mode 100644 mod/bigbluebuttonbn/classes/privacy/provider.php create mode 100644 mod/bigbluebuttonbn/classes/recording.php create mode 100644 mod/bigbluebuttonbn/classes/search/activity.php create mode 100644 mod/bigbluebuttonbn/classes/search/tags.php create mode 100644 mod/bigbluebuttonbn/classes/setting_validator.php create mode 100644 mod/bigbluebuttonbn/classes/settings.php create mode 100644 mod/bigbluebuttonbn/classes/task/check_pending_recordings.php create mode 100644 mod/bigbluebuttonbn/classes/task/completion_update_state.php create mode 100644 mod/bigbluebuttonbn/classes/task/reset_recordings.php create mode 100644 mod/bigbluebuttonbn/classes/task/send_instance_update_notification.php create mode 100644 mod/bigbluebuttonbn/classes/task/send_notification.php create mode 100644 mod/bigbluebuttonbn/classes/task/send_recording_ready_notification.php create mode 100644 mod/bigbluebuttonbn/classes/task/upgrade_imported_recordings.php create mode 100644 mod/bigbluebuttonbn/classes/task/upgrade_recording_base_task.php create mode 100644 mod/bigbluebuttonbn/classes/task/upgrade_recordings.php create mode 100644 mod/bigbluebuttonbn/classes/test/testcase_helper_trait.php create mode 100644 mod/bigbluebuttonbn/db/access.php create mode 100644 mod/bigbluebuttonbn/db/caches.php create mode 100644 mod/bigbluebuttonbn/db/install.xml create mode 100644 mod/bigbluebuttonbn/db/log.php create mode 100644 mod/bigbluebuttonbn/db/messages.php create mode 100644 mod/bigbluebuttonbn/db/mobile.php create mode 100644 mod/bigbluebuttonbn/db/services.php create mode 100644 mod/bigbluebuttonbn/db/tasks.php create mode 100644 mod/bigbluebuttonbn/db/upgrade.php create mode 100644 mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg create mode 100644 mod/bigbluebuttonbn/fonts/bigbluebutton-font.ttf create mode 100644 mod/bigbluebuttonbn/fonts/bigbluebutton-font.woff create mode 100644 mod/bigbluebuttonbn/import_view.php create mode 100644 mod/bigbluebuttonbn/index.php create mode 100644 mod/bigbluebuttonbn/lang/en/bigbluebuttonbn.php create mode 100644 mod/bigbluebuttonbn/lib.php create mode 100644 mod/bigbluebuttonbn/mobileapp/mobile.notification.js create mode 100644 mod/bigbluebuttonbn/mod_form.php create mode 100644 mod/bigbluebuttonbn/pix/flex_icons.php create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-128.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-24.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-256.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-32.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-48.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-64.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-72.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-80.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-96.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton.svg create mode 100644 mod/bigbluebuttonbn/pix/icon.gif create mode 100644 mod/bigbluebuttonbn/pix/icon.png create mode 100644 mod/bigbluebuttonbn/pix/icon.svg create mode 100644 mod/bigbluebuttonbn/settings.php create mode 100644 mod/bigbluebuttonbn/styles.css create mode 100644 mod/bigbluebuttonbn/templates/end_session_button.mustache create mode 100644 mod/bigbluebuttonbn/templates/import_view.mustache create mode 100644 mod/bigbluebuttonbn/templates/instance_updated_message.mustache create mode 100644 mod/bigbluebuttonbn/templates/mobile_view_error.mustache create mode 100644 mod/bigbluebuttonbn/templates/mobile_view_notification.mustache create mode 100644 mod/bigbluebuttonbn/templates/mobile_view_page_ionic3.mustache create mode 100644 mod/bigbluebuttonbn/templates/mobile_view_page_latest.mustache create mode 100644 mod/bigbluebuttonbn/templates/participant_form.mustache create mode 100644 mod/bigbluebuttonbn/templates/participant_form_add.mustache create mode 100644 mod/bigbluebuttonbn/templates/participant_single_select.mustache create mode 100644 mod/bigbluebuttonbn/templates/recentactivity.mustache create mode 100644 mod/bigbluebuttonbn/templates/recording_row_actionbar.mustache create mode 100644 mod/bigbluebuttonbn/templates/recording_row_playback.mustache create mode 100644 mod/bigbluebuttonbn/templates/recording_row_preview.mustache create mode 100644 mod/bigbluebuttonbn/templates/recordings_session.mustache create mode 100644 mod/bigbluebuttonbn/templates/recordings_table.mustache create mode 100644 mod/bigbluebuttonbn/templates/room_view.mustache create mode 100644 mod/bigbluebuttonbn/templates/search_input_recordings.mustache create mode 100644 mod/bigbluebuttonbn/templates/view_page.mustache create mode 100644 mod/bigbluebuttonbn/tests/backup_restore_test.php create mode 100644 mod/bigbluebuttonbn/tests/behat/add_instance.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/behat_mod_bigbluebuttonbn.php create mode 100644 mod/bigbluebuttonbn/tests/behat/end_meeting.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/group_mode.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/join_meeting.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/meeting_roles.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/recordings.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/recordings_import.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/start_meeting.feature create mode 100644 mod/bigbluebuttonbn/tests/completion_test.php create mode 100644 mod/bigbluebuttonbn/tests/coverage.php create mode 100644 mod/bigbluebuttonbn/tests/external/can_join_test.php create mode 100644 mod/bigbluebuttonbn/tests/external/completion_validate_test.php create mode 100644 mod/bigbluebuttonbn/tests/external/end_meeting_test.php create mode 100644 mod/bigbluebuttonbn/tests/external/get_bigbluebuttons_by_courses_test.php create mode 100644 mod/bigbluebuttonbn/tests/external/get_join_url_test.php create mode 100644 mod/bigbluebuttonbn/tests/external/get_recordings_test.php create mode 100644 mod/bigbluebuttonbn/tests/external/view_bigbluebuttonbn_test.php create mode 100644 mod/bigbluebuttonbn/tests/fixtures/bbpresentation.pptx create mode 100644 mod/bigbluebuttonbn/tests/generator/behat_mod_bigbluebuttonbn_generator.php create mode 100644 mod/bigbluebuttonbn/tests/generator/lib.php create mode 100644 mod/bigbluebuttonbn/tests/instance_test.php create mode 100644 mod/bigbluebuttonbn/tests/lib_test.php create mode 100644 mod/bigbluebuttonbn/tests/local/bigbluebutton/recordings/recording_data_test.php create mode 100644 mod/bigbluebuttonbn/tests/local/helpers/files_test.php create mode 100644 mod/bigbluebuttonbn/tests/local/helpers/mod_helper_trait_test.php create mode 100644 mod/bigbluebuttonbn/tests/local/helpers/reset_test.php create mode 100644 mod/bigbluebuttonbn/tests/local/helpers/roles_test.php create mode 100644 mod/bigbluebuttonbn/tests/logger_test.php create mode 100644 mod/bigbluebuttonbn/tests/meeting_test.php create mode 100644 mod/bigbluebuttonbn/tests/privacy/provider_test.php create mode 100644 mod/bigbluebuttonbn/tests/recording_test.php create mode 100644 mod/bigbluebuttonbn/tests/search_test.php create mode 100644 mod/bigbluebuttonbn/tests/task/send_instance_update_notification_test.php create mode 100644 mod/bigbluebuttonbn/tests/task/send_notification_test.php create mode 100644 mod/bigbluebuttonbn/tests/task/send_recording_ready_notification_test.php create mode 100644 mod/bigbluebuttonbn/tests/task/upgrade_recordings_imported_test.php create mode 100644 mod/bigbluebuttonbn/tests/task/upgrade_recordings_test.php create mode 100644 mod/bigbluebuttonbn/version.php create mode 100644 mod/bigbluebuttonbn/view.php diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index dd74d02af8d..f6ebe21afe8 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1929,7 +1929,7 @@ class core_plugin_manager { ), 'mod' => array( - 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder', + 'assign', 'assignment', 'bigbluebuttonbn', 'book', 'chat', 'choice', 'data', 'feedback', 'folder', 'forum', 'glossary', 'h5pactivity', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop' ), diff --git a/mod/bigbluebuttonbn/amd/build/actions.min.js b/mod/bigbluebuttonbn/amd/build/actions.min.js new file mode 100644 index 00000000000..240a413aeb0 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/actions.min.js @@ -0,0 +1,2 @@ +define ("mod_bigbluebuttonbn/actions",["./repository","core/notification","./events","core/str"],function(a,b,c,d){"use strict";var f=function(a,c,d){return new Promise(function(e){(0,b.saveCancel)(a,c,d,e)})},g=function(){document.addEventListener("click",function(g){var e=g.target.closest(".bbb-btn-action[data-action=\"end\"]");if(!e){return}g.preventDefault();var h=e.dataset.bbbId,i=e.dataset.groupId?e.dataset.groupId:0;f((0,d.get_string)("end_session_confirm_title","mod_bigbluebuttonbn"),(0,d.get_string)("end_session_confirm","mod_bigbluebuttonbn"),(0,d.get_string)("yes","moodle")).then(function(){return(0,a.endMeeting)(h,i)}).then(function(){(0,c.notifySessionEnded)(h,i)}).catch(b.exception)})},h=!1;if(!h){g();h=!0}}); +//# sourceMappingURL=actions.min.js.map diff --git a/mod/bigbluebuttonbn/amd/build/actions.min.js.map b/mod/bigbluebuttonbn/amd/build/actions.min.js.map new file mode 100644 index 00000000000..dd4123a693b --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/actions.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/actions.js"],"names":["confirmedPromise","title","question","saveLabel","Promise","resolve","registerEventListeners","document","addEventListener","e","actionButton","target","closest","preventDefault","bbbId","dataset","groupId","then","catch","displayException","listening"],"mappings":"mIA8BMA,CAAAA,CAAgB,CAAG,SAACC,CAAD,CAAQC,CAAR,CAAkBC,CAAlB,QAAgC,IAAIC,CAAAA,OAAJ,CAAY,SAAAC,CAAO,CAAI,CAC5E,iBAAWJ,CAAX,CAAkBC,CAAlB,CAA4BC,CAA5B,CAAuCE,CAAvC,CACH,CAFwD,CAAhC,C,CAInBC,CAAsB,CAAG,UAAM,CACjCC,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAC,CAAI,CACpC,GAAMC,CAAAA,CAAY,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiB,sCAAjB,CAArB,CACA,GAAI,CAACF,CAAL,CAAmB,CACf,MACH,CAEDD,CAAC,CAACI,cAAF,GANoC,GAQ9BC,CAAAA,CAAK,CAAGJ,CAAY,CAACK,OAAb,CAAqBD,KARC,CAS9BE,CAAO,CAAGN,CAAY,CAACK,OAAb,CAAqBC,OAArB,CAA+BN,CAAY,CAACK,OAAb,CAAqBC,OAApD,CAA8D,CAT1C,CAWpChB,CAAgB,CACZ,iBAAU,2BAAV,CAAuC,qBAAvC,CADY,CAEZ,iBAAU,qBAAV,CAAiC,qBAAjC,CAFY,CAGZ,iBAAU,KAAV,CAAiB,QAAjB,CAHY,CAAhB,CAKCiB,IALD,CAKM,iBAAM,iBAAkBH,CAAlB,CAAyBE,CAAzB,CAAN,CALN,EAMCC,IAND,CAMM,UAAM,CACR,yBAAmBH,CAAnB,CAA0BE,CAA1B,CAGH,CAVD,EAWCE,KAXD,CAWOC,WAXP,CAYH,CAvBD,CAwBH,C,CAEGC,CAAS,G,CACb,GAAI,CAACA,CAAL,CAAgB,CACZd,CAAsB,GACtBc,CAAS,GACZ,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * JS actions.\n *\n * @module mod_bigbluebuttonbn/actions\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {endMeeting as requestEndMeeting} from './repository';\nimport {\n exception as displayException,\n saveCancel,\n} from 'core/notification';\nimport {notifySessionEnded} from './events';\nimport {get_string as getString} from 'core/str';\n\nconst confirmedPromise = (title, question, saveLabel) => new Promise(resolve => {\n saveCancel(title, question, saveLabel, resolve);\n});\n\nconst registerEventListeners = () => {\n document.addEventListener('click', e => {\n const actionButton = e.target.closest('.bbb-btn-action[data-action=\"end\"]');\n if (!actionButton) {\n return;\n }\n\n e.preventDefault();\n\n const bbbId = actionButton.dataset.bbbId;\n const groupId = actionButton.dataset.groupId ? actionButton.dataset.groupId : 0;\n\n confirmedPromise(\n getString('end_session_confirm_title', 'mod_bigbluebuttonbn'),\n getString('end_session_confirm', 'mod_bigbluebuttonbn'),\n getString('yes', 'moodle')\n )\n .then(() => requestEndMeeting(bbbId, groupId))\n .then(() => {\n notifySessionEnded(bbbId, groupId);\n\n return;\n })\n .catch(displayException);\n });\n};\n\nlet listening = false;\nif (!listening) {\n registerEventListeners();\n listening = true;\n}\n"],"file":"actions.min.js"} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/amd/build/event_dispatcher.min.js b/mod/bigbluebuttonbn/amd/build/event_dispatcher.min.js new file mode 100644 index 00000000000..9b7d489a34a --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/event_dispatcher.min.js @@ -0,0 +1,2 @@ +define ("mod_bigbluebuttonbn/event_dispatcher",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.dispatchEvent=void 0;a.dispatchEvent=function dispatchEvent(a){var b=1.\n\n/**\n * An Event dispatcher used to dispatch Native JS CustomEvent objects with custom default properties.\n *\n * @module core/event_dispatcher\n * @copyright 2021 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\n\n/**\n * Dispatch an event as a CustomEvent on the specified container.\n * By default events are bubbled, and cancelable.\n *\n * The eventName should typically by sourced using a constant. See the supplied examples.\n *\n * Note: This function uses native events. Any additional details are passed to the function in event.detail.\n *\n * This function mimics the behaviour of EventTarget.dispatchEvent but bubbles by default.\n *\n * @method dispatchEvent\n * @param {String} eventName The name of the event\n * @param {Object} detail Any additional details to pass into the eveent\n * @param {HTMLElement} container The point at which to dispatch the event\n * @param {Object} options\n * @param {Boolean} options.bubbles Whether to bubble up the DOM\n * @param {Boolean} options.cancelable Whether preventDefault() can be called\n * @param {Boolean} options.composed Whether the event can bubble across the ShadowDOM bounadry\n * @returns {CustomEvent}\n *\n * @example Using a native CustomEvent to indicate that some example data was displayed.\n * // mod/example/amd/src/events.js\n *\n * import {dispatchEvent} from 'core/event_dispatcher';\n *\n * export const eventTypes = {\n * exampleDataDisplayed: 'mod_example/exampleDataDisplayed',\n * };\n *\n * export const notifyExampleDisplayed = someArgument => dispatchEvent(eventTypes.exampleDataDisplayed, {\n * someArgument,\n * }, document, {\n * cancelable: false,\n * });\n */\nexport const dispatchEvent = (\n eventName,\n detail = {},\n container = document,\n {\n bubbles = true,\n cancelable = false,\n composed = false,\n } = {}\n) => {\n const customEvent = new CustomEvent(\n eventName,\n {\n bubbles,\n cancelable,\n composed,\n detail,\n }\n );\n\n container.dispatchEvent(customEvent);\n\n return customEvent;\n};\n"],"file":"event_dispatcher.min.js"} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/amd/build/events.min.js b/mod/bigbluebuttonbn/amd/build/events.min.js new file mode 100644 index 00000000000..fb8798c4c01 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/events.min.js @@ -0,0 +1,2 @@ +define ("mod_bigbluebuttonbn/events",["exports","./event_dispatcher"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.notifyCurrentSessionEnded=a.notifySessionEnded=a.eventTypes=void 0;var c={sessionEnded:"mod_bigbluebuttonbn/sessionEnded",currentSessionEnded:"mod_bigbluebuttonbn/currentSessionEnded"};a.eventTypes=c;a.notifySessionEnded=function notifySessionEnded(a,d){return(0,b.dispatchEvent)(c.sessionEnded,{bbbId:a,groupId:d})};a.notifyCurrentSessionEnded=function notifyCurrentSessionEnded(a){return(0,b.dispatchEvent)(c.currentSessionEnded,{},a)}}); +//# sourceMappingURL=events.min.js.map diff --git a/mod/bigbluebuttonbn/amd/build/events.min.js.map b/mod/bigbluebuttonbn/amd/build/events.min.js.map new file mode 100644 index 00000000000..019aa8f3f49 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/events.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/events.js"],"names":["eventTypes","sessionEnded","currentSessionEnded","notifySessionEnded","bbbId","groupId","notifyCurrentSessionEnded","container"],"mappings":"uNA2BO,GAAMA,CAAAA,CAAU,CAAG,CAUtBC,YAAY,CAAE,kCAVQ,CAmBtBC,mBAAmB,CAAE,yCAnBC,CAAnB,C,oCA8B2B,QAArBC,CAAAA,kBAAqB,CAACC,CAAD,CAAQC,CAAR,QAAoB,oBAAcL,CAAU,CAACC,YAAzB,CAAuC,CACzFG,KAAK,CAALA,CADyF,CAEzFC,OAAO,CAAPA,CAFyF,CAAvC,CAApB,C,6BAYO,QAA5BC,CAAAA,yBAA4B,CAAAC,CAAS,QAAI,oBAClDP,CAAU,CAACE,mBADuC,CAElD,EAFkD,CAGlDK,CAHkD,CAAJ,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Events for the mod_bigbluebuttonbn plugin.\n *\n * @module mod_bigbluebuttonbn/events\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// TODO For master this can be changed to `core/event_dispatcher` and the local copy removed.\n// For 3.11 and earlier this line should stay.\nimport {dispatchEvent} from './event_dispatcher';\n\nexport const eventTypes = {\n /**\n * Fired when a session has been ended.\n *\n * @event mod_bigbluebuttonbn/sessionEnded\n * @type CustomEvent\n * @property {object} detail\n * @property {number} detail.bbbId\n * @property {number} detail.groupId\n */\n sessionEnded: 'mod_bigbluebuttonbn/sessionEnded',\n\n /**\n * Fired when the current session has been ended.\n *\n * @event mod_bigbluebuttonbn/currentSessionEnded\n * @type CustomEvent\n * @property {object} detail\n */\n currentSessionEnded: 'mod_bigbluebuttonbn/currentSessionEnded',\n};\n\n/**\n * Trigger the sessionEnded event.\n *\n * @param {number} bbbId\n * @param {number} groupId\n * @returns {CustomEvent}\n * @fires event:mod_bigbluebuttonbn/sessionEnded\n */\nexport const notifySessionEnded = (bbbId, groupId) => dispatchEvent(eventTypes.sessionEnded, {\n bbbId,\n groupId,\n});\n\n/**\n * Trigger the currentSessionEnded event.\n *\n * @param {Element} container\n * @returns {CustomEvent}\n * @fires event:mod_bigbluebuttonbn/currentSessionEnded\n */\nexport const notifyCurrentSessionEnded = container => dispatchEvent(\n eventTypes.currentSessionEnded,\n {},\n container\n);\n"],"file":"events.min.js"} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/amd/build/index.min.js b/mod/bigbluebuttonbn/amd/build/index.min.js new file mode 100644 index 00000000000..fafc1dd9baf --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/index.min.js @@ -0,0 +1,2 @@ +define ("mod_bigbluebuttonbn/index",["exports","./actions","./events"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;a.init=function init(){document.addEventListener(c.eventTypes.sessionEnded,function(){window.location.reload()})}}); +//# sourceMappingURL=index.min.js.map diff --git a/mod/bigbluebuttonbn/amd/build/index.min.js.map b/mod/bigbluebuttonbn/amd/build/index.min.js.map new file mode 100644 index 00000000000..7559123f51a --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/index.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/index.js"],"names":["init","document","addEventListener","eventTypes","sessionEnded","window","location","reload"],"mappings":"0KA0BoB,QAAPA,CAAAA,IAAO,EAAM,CACtBC,QAAQ,CAACC,gBAAT,CAA0BC,aAAWC,YAArC,CAAmD,UAAM,CACrDC,MAAM,CAACC,QAAP,CAAgBC,MAAhB,EACH,CAFD,CAGH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Index page for the mod_bigbluebuttonbn plugin.\n *\n * @module mod_bigbluebuttonbn/index\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport './actions';\nimport {eventTypes} from './events';\n\nexport const init = () => {\n document.addEventListener(eventTypes.sessionEnded, () => {\n window.location.reload();\n });\n};\n"],"file":"index.min.js"} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/amd/build/modform.min.js b/mod/bigbluebuttonbn/amd/build/modform.min.js new file mode 100644 index 00000000000..fbf03fafe87 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/modform.min.js @@ -0,0 +1,2 @@ +define ("mod_bigbluebuttonbn/modform",["exports","core/str","core/notification","core/templates"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}function f(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}var h={instanceTypeSelection:function instanceTypeSelection(){return document.querySelector("select#id_type")},instanceTypeProfiles:function instanceTypeProfiles(){return document.querySelector("[data-profile-types]")},participantData:function participantData(){return document.querySelector("[data-participant-data]")},participantList:function participantList(){return document.getElementsByName("participants")[0]},participantTable:function participantTable(){return document.getElementById("participant_list_table")},participantSelectionType:function participantSelectionType(){return document.getElementsByName("bigbluebuttonbn_participant_selection_type")[0]},participantSelection:function participantSelection(){return document.getElementsByName("bigbluebuttonbn_participant_selection")[0]},participantAddButton:function participantAddButton(){return document.getElementsByName("bigbluebuttonbn_participant_selection_add")[0]}};a.init=function init(a){var b=h.instanceTypeSelection(),c=JSON.parse(h.instanceTypeProfiles().dataset.profileTypes),d=a.instanceTypeDefault;if(null!==b&&-1!==b.selectedIndex){d=b.options[b.selectedIndex].value}var f=function(a,b){var d=c[a].features;return-1!==d.indexOf(b)};i(d,f);b.addEventListener("change",function(a){i(a.target.value,f)});h.participantSelectionType().addEventListener("change",function(a){var b=a.target;q(b)});h.participantAddButton().addEventListener("click",function(a){a.stopPropagation();a.preventDefault();p()});j()};var i=function(a,b){var c=b(a,"all"),d=function(a,b){var c=document.querySelector("#"+a);if(!c){return}if(b){c.style.display="block";return}c.style.display="none"},e=function(a,b){var c=document.querySelector("#"+a);if(!c){return}var d=c.closest("div").closest("div");if(b){d.style.display="block";return}d.style.display="none"},f=function(a,b){var c=document.querySelector("#fgroup_id_"+a);if(!c){return}if(b){c.classList.remove("hidden");return}c.classList.add("hidden")};d("id_room",c||b(a,"showroom"));e("id_record",c||b(a,"showroom"));d("id_recordings",c||b(a,"showrecordings"));e("id_recordings_imported",c||b(a,"showrecordings"));d("id_preuploadpresentation",c||b(a,"preuploadpresentation"));d("id_permissions",c||b(a,"permissions"));d("id_schedule",c||b(a,"schedule"));d("id_modstandardelshdr",c||b(a,"modstandardelshdr"));d("id_availabilityconditionsheader",c||b(a,"availabilityconditionsheader"));d("id_tagshdr",c||b(a,"tagshdr"));d("id_competenciessection",c||b(a,"competenciessection"));f("completionattendancegroup",c||b(a,"completionattendance"));f("completionengagementgroup",c||b(a,"completionengagement"))},j=function(){var a=JSON.parse(h.participantData().dataset.participantData),b=m();b.forEach(function(b){var c=b.selectiontype,d=b.selectionid,e=b.role;if("all"===b.selectiontype||"undefined"!=typeof a[b.selectiontype].children[b.selectionid]){k(c,d,e,!0).then()}})},k=function(){var a=g(regeneratorRuntime.mark(function a(e,f,g,i){var j,k,l,m,p,q,r,s,t,u,v;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:j=JSON.parse(h.participantData().dataset.participantData);a.next=3;return(0,b.get_string)("mod_form_field_participant_bbb_role_viewer","mod_bigbluebuttonbn");case 3:k=a.sent;a.next=6;return(0,b.get_string)("mod_form_field_participant_bbb_role_moderator","mod_bigbluebuttonbn");case 6:l=a.sent;m={viewer:{id:"viewer",label:k},moderator:{id:"moderator",label:l}};m[g].isselected=!0;a.prev=9;p=document.querySelector("#participant_list_table tbody");q={selectiontypevalue:e,selectionvalue:f,participanttype:j[e].name,participantvalue:"all"!==e?j[e].children[f].name:null,roles:Object.values(m),canRemove:i};a.next=14;return d.default.renderForPromise("mod_bigbluebuttonbn/participant_form_add",q);case 14:r=a.sent;s=r.html;t=r.js;u=d.default.appendNodeContents(p,s,t)[0];u.querySelector(".participant-select").addEventListener("change",function(){o(e,f)});v=u.querySelector(".remove-button");if(v){v.addEventListener("click",function(){n(e,f)})}a.next=26;break;case 23:a.prev=23;a.t0=a["catch"](9);c.default.exception(a.t0);case 26:case"end":return a.stop();}}},a,null,[[9,23]])}));return function(){return a.apply(this,arguments)}}(),l=function(a){var b=h.participantList();b.value=JSON.stringify(a)},m=function(){var a=h.participantList().value;if(a){return JSON.parse(a)}return[]},n=function(a,b){for(var c=m(),d="participant_list_tr_"+a+"-"+b,e=h.participantTable(),f=""===b?null:b,g=0;g.\n\n/**\n * JS for the mod_form page on mod_bigbluebuttonbn plugin.\n *\n * @module mod_bigbluebuttonbn/mod_form\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {get_string as getString} from 'core/str';\nimport Notification from 'core/notification';\nimport Templates from \"core/templates\";\n\n/**\n * Get all selectors in one place.\n *\n */\nconst ELEMENT_SELECTOR = {\n instanceTypeSelection: () => document.querySelector('select#id_type'),\n instanceTypeProfiles: () => document.querySelector('[data-profile-types]'),\n participantData: () => document.querySelector('[data-participant-data]'),\n participantList: () => document.getElementsByName('participants')[0],\n participantTable: () => document.getElementById('participant_list_table'),\n participantSelectionType: () => document.getElementsByName('bigbluebuttonbn_participant_selection_type')[0],\n participantSelection: () => document.getElementsByName('bigbluebuttonbn_participant_selection')[0],\n participantAddButton: () => document.getElementsByName('bigbluebuttonbn_participant_selection_add')[0],\n};\n/**\n * Initialise the moodle form code.\n *\n * This will help hide or show items depending on the selection of the instance type.\n *\n * @method init\n * @param {object} info\n */\nexport const init = (info) => {\n const selectedType = ELEMENT_SELECTOR.instanceTypeSelection();\n const instanceTypeProfiles = JSON.parse(ELEMENT_SELECTOR.instanceTypeProfiles().dataset.profileTypes);\n\n let profileType = info.instanceTypeDefault;\n if (selectedType !== null && selectedType.selectedIndex !== -1) {\n profileType = selectedType.options[selectedType.selectedIndex].value;\n }\n\n const isFeatureEnabled = (profileType, feature) => {\n const features = instanceTypeProfiles[profileType].features;\n return (features.indexOf(feature) !== -1);\n };\n applyInstanceTypeProfile(profileType, isFeatureEnabled);\n\n // Change form visible fields depending on the selection.\n selectedType.addEventListener('change', (e) => {\n applyInstanceTypeProfile(e.target.value, isFeatureEnabled);\n });\n\n ELEMENT_SELECTOR.participantSelectionType().addEventListener('change', (e) => {\n const currentTypeSelect = e.target;\n updateSelectionFromType(currentTypeSelect);\n });\n\n ELEMENT_SELECTOR.participantAddButton().addEventListener('click', (e) => {\n e.stopPropagation();\n e.preventDefault();\n participantAddFromCurrentSelection();\n });\n\n participantListInit();\n};\n\n/**\n * Show or hide form element depending on the selected profile\n *\n * @param {string} profileType\n * @param {function} isFeatureEnabled\n */\nconst applyInstanceTypeProfile = (profileType, isFeatureEnabled) => {\n let showAll = isFeatureEnabled(profileType, 'all');\n const showFieldset = (id, show) => {\n // Show room settings validation.\n const node = document.querySelector('#' + id);\n if (!node) {\n return;\n }\n if (show) {\n node.style.display = 'block';\n return;\n }\n node.style.display = 'none';\n };\n const showInput = (id, show) => {\n // Show room settings validation.\n const node = document.querySelector('#' + id);\n if (!node) {\n return;\n }\n var ancestor = node.closest('div').closest('div');\n if (show) {\n ancestor.style.display = 'block';\n return;\n }\n ancestor.style.display = 'none';\n };\n const showFormGroup = (id, show) => {\n // Show room settings validation.\n const node = document.querySelector('#fgroup_id_' + id);\n if (!node) {\n return;\n }\n if (show) {\n node.classList.remove('hidden');\n return;\n }\n node.classList.add('hidden');\n };\n // Show room settings validation.\n showFieldset('id_room', showAll ||\n isFeatureEnabled(profileType, 'showroom'));\n showInput('id_record', showAll ||\n isFeatureEnabled(profileType, 'showroom'));\n // Show recordings settings validation.\n showFieldset('id_recordings', showAll ||\n isFeatureEnabled(profileType, 'showrecordings'));\n // Show recordings imported settings validation.\n showInput('id_recordings_imported', showAll ||\n isFeatureEnabled(profileType, 'showrecordings'));\n // Preuploadpresentation feature validation.\n showFieldset('id_preuploadpresentation', showAll ||\n isFeatureEnabled(profileType, 'preuploadpresentation'));\n // Participants feature validation.\n showFieldset('id_permissions', showAll ||\n isFeatureEnabled(profileType, 'permissions'));\n // Schedule feature validation.\n showFieldset('id_schedule', showAll ||\n isFeatureEnabled(profileType, 'schedule'));\n // Common module settings validation.\n showFieldset('id_modstandardelshdr', showAll ||\n isFeatureEnabled(profileType, 'modstandardelshdr'));\n // Restrict access validation.\n showFieldset('id_availabilityconditionsheader', showAll ||\n isFeatureEnabled(profileType, 'availabilityconditionsheader'));\n // Tags validation.\n showFieldset('id_tagshdr', showAll || isFeatureEnabled(profileType, 'tagshdr'));\n // Competencies validation.\n showFieldset('id_competenciessection', showAll ||\n isFeatureEnabled(profileType, 'competenciessection'));\n // Completion validation.\n showFormGroup('completionattendancegroup', showAll ||\n isFeatureEnabled(profileType, 'completionattendance'));\n // Completion validation.\n showFormGroup('completionengagementgroup', showAll ||\n isFeatureEnabled(profileType, 'completionengagement'));\n};\n\n/**\n * Init the participant list\n */\nconst participantListInit = () => {\n const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData);\n const participantList = getParticipantList();\n participantList.forEach(participant => {\n const selectionTypeValue = participant.selectiontype;\n const selectionValue = participant.selectionid;\n const selectionRole = participant.role;\n if (participant.selectiontype === 'all' ||\n typeof participantData[participant.selectiontype].children[participant.selectionid] !== 'undefined') {\n // Add it to the form, but don't add the delete button if it is the first item.\n participantAddToForm(selectionTypeValue, selectionValue, selectionRole, true).then();\n }\n });\n};\n\n/**\n * Add rows to the participant list dependingon the current selection.\n *\n * @param {string} selectionTypeValue\n * @param {string} selectionValue\n * @param {string} selectedRole\n * @param {boolean} canRemove\n * @returns {Promise}\n */\nconst participantAddToForm = async(selectionTypeValue, selectionValue, selectedRole, canRemove) => {\n const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData);\n const sviewer = await getString('mod_form_field_participant_bbb_role_viewer', 'mod_bigbluebuttonbn');\n const smoderator = await getString('mod_form_field_participant_bbb_role_moderator', 'mod_bigbluebuttonbn');\n let roles = {\n viewer: {'id': 'viewer', label: sviewer},\n moderator: {'id': 'moderator', label: smoderator}\n };\n roles[selectedRole].isselected = true;\n try {\n const listTable = document.querySelector('#participant_list_table tbody');\n const templateContext = {\n 'selectiontypevalue': selectionTypeValue,\n 'selectionvalue': selectionValue,\n 'participanttype': participantData[selectionTypeValue].name,\n 'participantvalue':\n (selectionTypeValue !== 'all') ?\n participantData[selectionTypeValue].children[selectionValue].name : null,\n 'roles': Object.values(roles),\n 'canRemove': canRemove\n };\n const {html, js} = await Templates.renderForPromise('mod_bigbluebuttonbn/participant_form_add', templateContext);\n const newNode = Templates.appendNodeContents(listTable, html, js)[0];\n newNode.querySelector('.participant-select').addEventListener('change', () => {\n participantListRoleUpdate(selectionTypeValue, selectionValue);\n });\n // Now add the callbacks: participantListRoleUpdate() and participantRemove().\n const removeNode = newNode.querySelector('.remove-button');\n if (removeNode) {\n removeNode\n .addEventListener('click', () => {\n participantRemove(selectionTypeValue, selectionValue);\n });\n }\n\n } catch (e) {\n Notification.exception(e);\n }\n};\n/*\n\n */\n\n/**\n * Update the related form element with the list value.\n *\n * @param {object} list\n */\nconst participantListUpdate = (list) => {\n const participantList = ELEMENT_SELECTOR.participantList();\n participantList.value = JSON.stringify(list);\n};\n\n/**\n *\n * @returns {any}\n */\nconst getParticipantList = () => {\n const participantListValue = ELEMENT_SELECTOR.participantList().value;\n if (participantListValue) {\n return JSON.parse(participantListValue);\n }\n return [];\n};\n\n/**\n * Remove participant both in the table/form and in the form element.\n *\n * @param {string} selectionTypeValue\n * @param {string} selectionValue\n */\nconst participantRemove = (selectionTypeValue, selectionValue) => {\n const pList = getParticipantList();\n const id = 'participant_list_tr_' + selectionTypeValue + '-' + selectionValue;\n const participantListTable = ELEMENT_SELECTOR.participantTable();\n const selectionid = (selectionValue === '' ? null : selectionValue);\n for (let i = 0; i < pList.length; i++) {\n if (pList[i].selectiontype === selectionTypeValue &&\n pList[i].selectionid === selectionid) {\n pList.splice(i, 1);\n }\n }\n // Remove from the form.\n for (let i = 0; i < participantListTable.rows.length; i++) {\n if (participantListTable.rows[i].id === id) {\n participantListTable.deleteRow(i);\n }\n }\n // Update value in the form.\n participantListUpdate(pList);\n};\n\n/**\n * Role update\n *\n * @param {string} type\n * @param {string} id\n */\nconst participantListRoleUpdate = (type, id) => {\n // Update in memory.\n const participantListRoleSelection = document.querySelector(`#participant_list_tr_${type}-${id} .participant-select`);\n const pList = getParticipantList();\n\n for (var i = 0; i < pList.length; i++) {\n if (pList[i].selectiontype === type &&\n pList[i].selectionid === (id === '' ? null : parseInt(id))) {\n pList[i].role = participantListRoleSelection.value;\n }\n }\n // Update in the form.\n participantListUpdate(pList);\n};\n\n/**\n * Add participant from the currently selected options\n */\nconst participantAddFromCurrentSelection = () => {\n let selectionType = ELEMENT_SELECTOR.participantSelectionType();\n let selection = ELEMENT_SELECTOR.participantSelection();\n const pList = getParticipantList();\n // Lookup to see if it has been added already.\n for (var i = 0; i < pList.length; i++) {\n if (pList[i].selectiontype === selectionType.value &&\n pList[i].selectionid === selection.value) {\n return;\n }\n }\n pList.push({\n \"selectiontype\": selectionType.value,\n \"selectionid\": selection.value,\n \"role\": \"viewer\"\n });\n // Add it to the form.\n participantAddToForm(selectionType.value, selection.value, 'viewer', true).then();\n // Update in the form.\n participantListUpdate(pList);\n};\n\n/**\n * Update selectable options when changing types\n *\n * @param {HTMLNode} currentTypeSelect\n */\nconst updateSelectionFromType = (currentTypeSelect) => {\n const createNewOption = (selectItem, label, value) => {\n const option = document.createElement('option');\n option.text = label;\n option.value = value;\n\n selectItem.add(option);\n };\n\n const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData);\n // Clear all selection items.\n const participantSelect = ELEMENT_SELECTOR.participantSelection();\n while (participantSelect.firstChild) {\n participantSelect.removeChild(participantSelect.firstChild);\n }\n // Add options depending on the selection.\n if (currentTypeSelect.selectedIndex !== -1) {\n const options = Object.values(participantData[currentTypeSelect.value].children);\n options.forEach(option => {\n createNewOption(participantSelect, option.name, option.id);\n });\n\n if (currentTypeSelect.value === 'all') {\n createNewOption(participantSelect, '---------------', 'all');\n currentTypeSelect.disabled = true;\n } else {\n currentTypeSelect.disabled = false;\n }\n }\n};\n"],"file":"modform.min.js"} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/amd/build/recordings.min.js b/mod/bigbluebuttonbn/amd/build/recordings.min.js new file mode 100644 index 00000000000..a085484ac49 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/recordings.min.js @@ -0,0 +1,2 @@ +function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("mod_bigbluebuttonbn/recordings",["exports","./repository","core/notification","core/str","core/loadingicon","core/modal_factory","core/modal_events","core/pending"],function(a,b,c,d,f,g,h,i){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=l(b);d=l(d);g=j(g);h=j(h);i=j(i);function j(a){return a&&a.__esModule?a:{default:a}}function k(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;k=function(){return a};return a}function l(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=k();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function m(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function n(a){for(var b=1,c;ba.length)b=a.length;for(var c=0,d=Array(b);c.*?".concat(c,".*?"),"i");i.set("data",b.filter({asList:!0},function(a){var b=a.get("recording");if(b&&d.test(b)){return!0}var c=a.get("description");return c&&d.test(c)}))},o=function(a){var c=function(a,b){var c=a.closest("[data-".concat(b,"]"));if(c){return c.dataset[b]}return null},e=a.dataset,f={bigbluebuttonbnid:k,recordingid:c(a,"recordingid"),additionaloptions:c(a,"additionaloptions"),action:e.action};if(!f.additionaloptions){f.additionaloptions={}}if("import"===e.action){var i=c(a,"source-instance-id"),j=c(a,"source-course-id");if(!f.additionaloptions){f.additionaloptions={}}f.additionaloptions.sourceid=i?i:0;f.additionaloptions.bbbcourseid=j?j:0}f.additionaloptions=JSON.stringify(f.additionaloptions);if("1"===a.dataset.requireConfirmation){return new Promise(function(a){return g.default.create({title:d.get_string("confirm"),body:p(f),type:g.default.types.SAVE_CANCEL}).then(function(){var b=q(regeneratorRuntime.mark(function b(c){return regeneratorRuntime.wrap(function(b){while(1){switch(b.prev=b.next){case 0:b.t0=c;b.next=3;return d.get_string("ok","moodle");case 3:b.t1=b.sent;b.t0.setSaveButtonText.call(b.t0,b.t1);c.getRoot().on(h.default.save,function(){a(!0)});c.getRoot().on(h.default.hidden,function(){c.destroy();a(!1)});c.show();return b.abrupt("return",c);case 9:case"end":return b.stop();}}},b)}));return function(){return b.apply(this,arguments)}}()).catch(Notification.exception)}).then(function(a){return a?b.updateRecording(f):function(){return null}})}else{return b.updateRecording(f)}},p=function(){var a=q(regeneratorRuntime.mark(function a(b){var c,e,f,g,h,i,j;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:f=document.querySelector("#playbacks-".concat(b.recordingid));a.next=3;return d.get_string("true"===f.dataset.imported?"view_recording_link":"view_recording","bigbluebuttonbn");case 3:g=a.sent;a.next=6;return d.get_string("view_recording_".concat(b.action,"_confirmation"),"bigbluebuttonbn",g);case 6:h=a.sent;if(!("import"===b.action)){a.next=9;break}return a.abrupt("return",h);case 9:i=null===(c=document.querySelector("a#recording-".concat(b.action,"-").concat(b.recordingid)))||void 0===c?void 0:null===(e=c.dataset)||void 0===e?void 0:e.links;if(!(!i||0===i)){a.next=12;break}return a.abrupt("return",h);case 12:a.next=14;return d.get_string(1===i?"view_recording_".concat(b.action,"_confirmation_warning_p"):"view_recording_".concat(b.action,"_confirmation_warning_s"),"bigbluebuttonbn",i);case 14:j=a.sent;return a.abrupt("return",j+"\n\n"+h);case 16:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}(),r=function(a){var b=a.target.closest("[data-action=\"play\"]");if(b){a.preventDefault();var e=window.open("","_blank");e.opener=null;e.location.href=b.href;return}var d=a.target.closest("a[data-action]");if(d&&!d.classList.contains("disabled")){a.preventDefault();var g=(0,f.addIconToContainerWithPromise)(i.get("boundingBox").getDOMNode());o(d).then(m).catch(c.exception).then(g.resolve).catch()}},s=function(a){a.preventDefault();var b=a.target.closest("div[role=search]"),c=b.querySelector("input[name=search]");n(c.value)};return{filterByText:n,refreshTableData:m,registerEventListeners:function registerEventListeners(){var a=i.get("boundingBox").getDOMNode();a.addEventListener("click",r);var b=document.querySelector(e);if(b){var c=document.querySelector(e+" button");c.addEventListener("click",s)}}}},F=function(a,b,c){if(!c){return Promise.resolve()}if(!c.status){return Promise.resolve()}var d=c.tabledata,e=new i.default("mod_bigbluebuttonbn/recordings/setupDatatable");return Promise.all([z(d.locale),y()]).then(function(a){var b=r(a,2),c=b[0],d=b[1];c.Intl.add("datatable-paginator",c.config.lang,n({},d));return c}).then(function(a){var b=B(c);a.RecordsPaginatorView=Y.Base.create("my-paginator-view",a.DataTable.Paginator.View,[],{_modelChange:function _modelChange(a){var b=a.changed,c=b&&b.totalItems;if(c){this._updateControlsUI(a.target.get("page"))}}});var e=new a.DataTable({paginatorView:"RecordsPaginatorView",width:"1195px",columns:d.columns,data:b,rowsPerPage:10,paginatorLocation:["header","footer"],autoSync:!0});return e}).then(function(c){c.render(a);var d=E(a,b,c),e=d.registerEventListeners;e();return c}).then(function(a){e.resolve();return a})};a.init=function init(a,b){D(a).then(function(c){return F(a,b,c)}).catch(c.exception)}}); +//# sourceMappingURL=recordings.min.js.map diff --git a/mod/bigbluebuttonbn/amd/build/recordings.min.js.map b/mod/bigbluebuttonbn/amd/build/recordings.min.js.map new file mode 100644 index 00000000000..0807e18ba55 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/recordings.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/recordings.js"],"names":["stringList","getStringsForYui","stringMap","map","key","component","then","first","prev","next","last","goToLabel","goToAction","perPage","showAll","catch","getYuiInstance","lang","Promise","resolve","YUI","use","Y","formatDates","locale","dateList","row","date","Date","toLocaleDateString","weekday","year","month","day","getFormattedData","response","recordingData","tabledata","rowData","JSON","parse","data","getTableNode","tableSelector","document","querySelector","fetchRecordingData","tableNode","dataset","importMode","repository","fetchRecordingsToImport","bbbid","bbbSourceInstanceId","bbbSourceCourseId","tools","groupId","fetchRecordings","getDataTableFunctions","tableId","searchFormId","dataTable","updateTableFromResponse","status","get","reset","set","currentFilter","filterByText","refreshTableData","value","dataModel","escapedRegex","replace","rsearch","RegExp","filter","asList","item","name","test","description","requestAction","element","getDataFromAction","dataType","dataElement","closest","elementData","payload","bigbluebuttonbnid","recordingid","additionaloptions","action","bbbsourceid","bbbcourseid","sourceid","stringify","requireConfirmation","ModalFactory","create","title","Str","get_string","body","recordingConfirmationMessage","type","types","SAVE_CANCEL","modal","setSaveButtonText","getRoot","on","ModalEvents","save","hidden","destroy","show","Notification","exception","proceed","updateRecording","playbackElement","imported","recordingType","confirmation","associatedLinkCount","links","confirmationWarning","processAction","e","popoutLink","target","preventDefault","videoPlayer","window","open","opener","location","href","clickedLink","classList","contains","iconPromise","getDOMNode","displayException","processSearchSubmission","parentNode","searchInput","registerEventListeners","boundingBox","addEventListener","searchForm","searchButton","setupDatatable","pendingPromise","Pending","all","yuiInstance","strings","Intl","add","config","tableData","RecordsPaginatorView","Base","DataTable","Paginator","View","_modelChange","changed","totalItems","_updateControlsUI","paginatorView","width","columns","rowsPerPage","paginatorLocation","autoSync","render","init"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * JS for the recordings page on mod_bigbluebuttonbn plugin.\n *\n * @module mod_bigbluebuttonbn/recordings\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as repository from './repository';\nimport {exception as displayException} from 'core/notification';\nimport {get_strings as getStrings} from 'core/str';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport * as Str from 'core/str';\nimport Pending from 'core/pending';\n\nconst stringList = [\n 'view_recording_yui_first',\n 'view_recording_yui_prev',\n 'view_recording_yui_next',\n 'view_recording_yui_last',\n 'view_recording_yui_page',\n 'view_recording_yui_go',\n 'view_recording_yui_rows',\n 'view_recording_yui_show_all',\n];\n\nconst getStringsForYui = () => {\n const stringMap = stringList.map(key => {\n return {\n key,\n component: 'bigbluebuttonbn',\n };\n });\n\n return getStrings(stringMap)\n .then(([first, prev, next, last, goToLabel, goToAction, perPage, showAll]) => {\n return {\n first,\n prev,\n next,\n last,\n goToLabel,\n goToAction,\n perPage,\n showAll,\n };\n })\n .catch();\n};\n\nconst getYuiInstance = lang => new Promise(resolve => {\n // eslint-disable-next-line\n YUI({\n lang,\n }).use('intl', 'datatable', 'datatable-sort', 'datatable-paginator', 'datatype-number', Y => {\n resolve(Y);\n });\n});\n\n/**\n * Format the supplied date per the specified locale.\n *\n * @param {string} locale\n * @param {array} dateList\n * @returns {array}\n */\nconst formatDates = (locale, dateList) => dateList.map(row => {\n const date = new Date(row.date);\n row.date = date.toLocaleDateString(locale, {\n weekday: 'long',\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n });\n\n return row;\n});\n\n/**\n * Format response data for the table.\n *\n * @param {string} response JSON-encoded table data\n * @returns {array}\n */\nconst getFormattedData = response => {\n const recordingData = response.tabledata;\n const rowData = JSON.parse(recordingData.data);\n\n return formatDates(recordingData.locale, rowData);\n};\n\nconst getTableNode = tableSelector => document.querySelector(tableSelector);\n\nconst fetchRecordingData = tableSelector => {\n const tableNode = getTableNode(tableSelector);\n\n if (tableNode.dataset.importMode) {\n return repository.fetchRecordingsToImport(\n tableNode.dataset.bbbid,\n tableNode.dataset.bbbSourceInstanceId,\n tableNode.dataset.bbbSourceCourseId,\n tableNode.dataset.tools,\n tableNode.dataset.groupId\n );\n } else {\n return repository.fetchRecordings(\n tableNode.dataset.bbbid,\n tableNode.dataset.tools,\n tableNode.dataset.groupId\n );\n }\n};\n\n/**\n * Fetch the data table functinos for the specified table.\n *\n * @param {String} tableId in which we will display the table\n * @param {String} searchFormId The Id of the relate.\n * @param {Object} dataTable\n * @returns {Object}\n * @private\n */\nconst getDataTableFunctions = (tableId, searchFormId, dataTable) => {\n const tableNode = getTableNode(tableId);\n const bbbid = tableNode.dataset.bbbid;\n\n const updateTableFromResponse = response => {\n if (!response || !response.status) {\n // There was no output at all.\n return;\n }\n\n dataTable.get('data').reset(getFormattedData(response));\n dataTable.set(\n 'currentData',\n dataTable.get('data')\n );\n\n const currentFilter = dataTable.get('currentFilter');\n if (currentFilter) {\n filterByText(currentFilter);\n }\n };\n\n const refreshTableData = () => fetchRecordingData(tableId).then(updateTableFromResponse);\n\n const filterByText = value => {\n const dataModel = dataTable.get('currentData');\n dataTable.set('currentFilter', value);\n\n const escapedRegex = value.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g, \"\\\\$&\");\n const rsearch = new RegExp(`.*?${escapedRegex}.*?`, 'i');\n\n dataTable.set('data', dataModel.filter({asList: true}, item => {\n const name = item.get('recording');\n if (name && rsearch.test(name)) {\n return true;\n }\n\n const description = item.get('description');\n return description && rsearch.test(description);\n }));\n };\n\n const requestAction = (element) => {\n const getDataFromAction = (element, dataType) => {\n const dataElement = element.closest(`[data-${dataType}]`);\n if (dataElement) {\n return dataElement.dataset[dataType];\n }\n\n return null;\n };\n\n const elementData = element.dataset;\n const payload = {\n bigbluebuttonbnid: bbbid,\n recordingid: getDataFromAction(element, 'recordingid'),\n additionaloptions: getDataFromAction(element, 'additionaloptions'),\n action: elementData.action,\n };\n // Slight change for import, for additional options.\n if (!payload.additionaloptions) {\n payload.additionaloptions = {};\n }\n if (elementData.action === 'import') {\n const bbbsourceid = getDataFromAction(element, 'source-instance-id');\n const bbbcourseid = getDataFromAction(element, 'source-course-id');\n if (!payload.additionaloptions) {\n payload.additionaloptions = {};\n }\n payload.additionaloptions.sourceid = bbbsourceid ? bbbsourceid : 0;\n payload.additionaloptions.bbbcourseid = bbbcourseid ? bbbcourseid : 0;\n }\n // Now additional options should be a json string.\n payload.additionaloptions = JSON.stringify(payload.additionaloptions);\n if (element.dataset.requireConfirmation === \"1\") {\n // Create the confirmation dialogue.\n return new Promise((resolve) =>\n ModalFactory.create({\n title: Str.get_string('confirm'),\n body: recordingConfirmationMessage(payload),\n type: ModalFactory.types.SAVE_CANCEL\n }).then(async(modal) => {\n modal.setSaveButtonText(await Str.get_string('ok', 'moodle'));\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, () => {\n resolve(true);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n resolve(false);\n });\n\n modal.show();\n\n return modal;\n }).catch(Notification.exception)\n ).then((proceed) =>\n proceed ? repository.updateRecording(payload) : () => null\n );\n } else {\n return repository.updateRecording(payload);\n }\n };\n\n const recordingConfirmationMessage = async(data) => {\n\n const playbackElement = document.querySelector(`#playbacks-${data.recordingid}`);\n const recordingType = await Str.get_string(\n playbackElement.dataset.imported === 'true' ? 'view_recording_link' : 'view_recording',\n 'bigbluebuttonbn'\n );\n\n const confirmation = await Str.get_string(`view_recording_${data.action}_confirmation`, 'bigbluebuttonbn', recordingType);\n\n if (data.action === 'import') {\n return confirmation;\n }\n\n // If it has associated links imported in a different course/activity, show that in confirmation dialog.\n const associatedLinkCount = document.querySelector(`a#recording-${data.action}-${data.recordingid}`)?.dataset?.links;\n if (!associatedLinkCount || associatedLinkCount === 0) {\n return confirmation;\n }\n\n const confirmationWarning = await Str.get_string(\n associatedLinkCount === 1\n ? `view_recording_${data.action}_confirmation_warning_p`\n : `view_recording_${data.action}_confirmation_warning_s`,\n 'bigbluebuttonbn',\n associatedLinkCount\n );\n\n return confirmationWarning + '\\n\\n' + confirmation;\n };\n\n /**\n * Process an action event.\n *\n * @param {Event} e\n */\n const processAction = e => {\n const popoutLink = e.target.closest('[data-action=\"play\"]');\n if (popoutLink) {\n e.preventDefault();\n\n const videoPlayer = window.open('', '_blank');\n videoPlayer.opener = null;\n videoPlayer.location.href = popoutLink.href;\n // TODO send a recording viewed event when this event will be implemented.\n return;\n }\n\n // Fetch any clicked anchor.\n const clickedLink = e.target.closest('a[data-action]');\n if (clickedLink && !clickedLink.classList.contains('disabled')) {\n e.preventDefault();\n\n // Create a spinning icon on the table.\n const iconPromise = addIconToContainerWithPromise(dataTable.get('boundingBox').getDOMNode());\n\n requestAction(clickedLink)\n .then(refreshTableData)\n .catch(displayException)\n .then(iconPromise.resolve)\n .catch();\n }\n };\n\n const processSearchSubmission = e => {\n // Prevent the default action.\n e.preventDefault();\n const parentNode = e.target.closest('div[role=search]');\n const searchInput = parentNode.querySelector('input[name=search]');\n filterByText(searchInput.value);\n };\n\n const registerEventListeners = () => {\n // Add event listeners to the table boundingBox.\n const boundingBox = dataTable.get('boundingBox').getDOMNode();\n boundingBox.addEventListener('click', processAction);\n\n // Setup the search from handlers.\n const searchForm = document.querySelector(searchFormId);\n if (searchForm) {\n const searchButton = document.querySelector(searchFormId + ' button');\n searchButton.addEventListener('click', processSearchSubmission);\n }\n };\n\n return {\n filterByText,\n refreshTableData,\n registerEventListeners,\n };\n};\n\n/**\n * Setup the data table for the specified BBB instance.\n *\n * @param {String} tableId in which we will display the table\n * @param {String} searchFormId The Id of the relate.\n * @param {object} response The response from the data request\n * @returns {Promise}\n */\nconst setupDatatable = (tableId, searchFormId, response) => {\n if (!response) {\n return Promise.resolve();\n }\n\n if (!response.status) {\n // Something failed. Continue to show the plain output.\n return Promise.resolve();\n }\n\n const recordingData = response.tabledata;\n\n const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings/setupDatatable');\n return Promise.all([getYuiInstance(recordingData.locale), getStringsForYui()])\n .then(([yuiInstance, strings]) => {\n // Add the fetched strings to the YUI Instance.\n yuiInstance.Intl.add('datatable-paginator', yuiInstance.config.lang, {...strings});\n\n return yuiInstance;\n })\n .then(yuiInstance => {\n const tableData = getFormattedData(response);\n yuiInstance.RecordsPaginatorView = Y.Base.create('my-paginator-view',yuiInstance.DataTable.Paginator.View, [],{\n _modelChange : function (e) {\n var changed = e.changed,\n totalItems = (changed && changed.totalItems);\n if (totalItems) {\n this._updateControlsUI(e.target.get('page'));\n }\n }\n });\n const dataTable = new yuiInstance.DataTable({\n paginatorView: \"RecordsPaginatorView\",\n width: \"1195px\",\n columns: recordingData.columns,\n data: tableData,\n rowsPerPage: 10,\n paginatorLocation: ['header', 'footer'],\n autoSync: true\n });\n return dataTable;\n })\n .then(dataTable => {\n dataTable.render(tableId);\n const {registerEventListeners} = getDataTableFunctions(\n tableId,\n searchFormId,\n dataTable);\n registerEventListeners();\n return dataTable;\n })\n .then(dataTable => {\n pendingPromise.resolve();\n return dataTable;\n });\n};\n\n/**\n * Initialise recordings code.\n *\n * @method init\n * @param {String} tableId in which we will display the table\n * @param {String} searchFormId The Id of the relate.\n */\nexport const init = (tableId, searchFormId) => {\n fetchRecordingData(tableId)\n .then(response => setupDatatable(tableId, searchFormId, response))\n .catch(displayException);\n};\n"],"file":"recordings.min.js"} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/amd/build/repository.min.js b/mod/bigbluebuttonbn/amd/build/repository.min.js new file mode 100644 index 00000000000..59b3cf83c93 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/repository.min.js @@ -0,0 +1,2 @@ +define ("mod_bigbluebuttonbn/repository",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.getMeetingInfo=a.completionValidate=a.endMeeting=a.updateRecording=a.fetchRecordingsToImport=a.fetchRecordings=void 0;a.fetchRecordings=function fetchRecordings(a,c,d){var e={bigbluebuttonbnid:a,tools:c};if(d){e.groupid=d}return(0,b.call)([{methodname:"mod_bigbluebuttonbn_get_recordings",args:e}])[0]};a.fetchRecordingsToImport=function fetchRecordingsToImport(a,c,d,e,f){var g={destinationinstanceid:a,sourcebigbluebuttonbnid:c,sourcecourseid:d,tools:e};if(f){g.groupid=f}return(0,b.call)([{methodname:"mod_bigbluebuttonbn_get_recordings_to_import",args:g}])[0]};a.updateRecording=function updateRecording(a){return(0,b.call)([{methodname:"mod_bigbluebuttonbn_update_recording",args:a}])[0]};a.endMeeting=function endMeeting(a,c){return(0,b.call)([{methodname:"mod_bigbluebuttonbn_end_meeting",args:{bigbluebuttonbnid:a,groupid:c}}])[0]};a.completionValidate=function completionValidate(a){return(0,b.call)([{methodname:"mod_bigbluebuttonbn_completion_validate",args:a}])[0]};a.getMeetingInfo=function getMeetingInfo(a,c){var d=2.\n\n/**\n * Repository to perform WS calls for mod_bigbluebuttonbn.\n *\n * @module mod_bigbluebuttonbn/repository\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\n\n/**\n * Fetch the list of recordings from the server.\n *\n * @param {Number} bigbluebuttonbnid The instance ID\n * @param {String} tools the set of tools to display\n * @param {number} groupid\n * @returns {Promise}\n */\nexport const fetchRecordings = (bigbluebuttonbnid, tools, groupid) => {\n const args = {\n bigbluebuttonbnid,\n tools,\n };\n\n if (groupid) {\n args.groupid = groupid;\n }\n\n return fetchMany([{methodname: 'mod_bigbluebuttonbn_get_recordings', args}])[0];\n};\n\n/**\n * Fetch the list of recordings from the server that can be imported.\n *\n * @param {Number} destinationinstanceid The destination instance ID\n * @param {Number} sourcebigbluebuttonbnid The original instance ID\n * @param {Number} sourcecourseid The destination instance ID\n * @param {String} tools the set of tools to display\n * @param {number} groupid\n * @returns {Promise}\n */\nexport const fetchRecordingsToImport = (\n destinationinstanceid,\n sourcebigbluebuttonbnid,\n sourcecourseid,\n tools,\n groupid\n) => {\n const args = {\n destinationinstanceid,\n sourcebigbluebuttonbnid,\n sourcecourseid,\n tools,\n };\n\n if (groupid) {\n args.groupid = groupid;\n }\n\n return fetchMany([{methodname: 'mod_bigbluebuttonbn_get_recordings_to_import', args}])[0];\n};\n\n/**\n * Perform an update on a single recording.\n *\n * @param {object} args The instance ID\n * @returns {Promise}\n */\nexport const updateRecording = args => fetchMany([\n {\n methodname: 'mod_bigbluebuttonbn_update_recording',\n args,\n }\n])[0];\n\n/**\n * End the Meeting\n *\n * @param {number} bigbluebuttonbnid\n * @param {number} groupid\n * @returns {Promise}\n */\nexport const endMeeting = (bigbluebuttonbnid, groupid) => fetchMany([\n {\n methodname: 'mod_bigbluebuttonbn_end_meeting',\n args: {\n bigbluebuttonbnid,\n groupid\n },\n }\n])[0];\n\n/**\n * Validate completion.\n *\n * @param {object} args\n * @returns {Promise}\n */\nexport const completionValidate = args => fetchMany([\n {\n methodname: 'mod_bigbluebuttonbn_completion_validate',\n args,\n }\n])[0];\n\n\n/**\n * Fetch meeting info for the specified meeting.\n *\n * @param {number} bigbluebuttonbnid\n * @param {number} groupid\n * @param {boolean} [updatecache=false]\n * @returns {Promise}\n */\nexport const getMeetingInfo = (bigbluebuttonbnid, groupid, updatecache = false) => fetchMany([\n {\n methodname: 'mod_bigbluebuttonbn_meeting_info',\n args: {\n bigbluebuttonbnid,\n groupid,\n updatecache,\n },\n }\n])[0];\n"],"file":"repository.min.js"} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/amd/build/rooms.min.js b/mod/bigbluebuttonbn/amd/build/rooms.min.js new file mode 100644 index 00000000000..ecf5d6e59d8 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/rooms.min.js @@ -0,0 +1,2 @@ +function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("mod_bigbluebuttonbn/rooms",["exports","./actions","./repository","./roomupdater","core/notification","./events"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.setupWindowAutoClose=a.init=void 0;c=h(c);d=h(d);function g(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;g=function(){return a};return a}function h(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=g();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}a.init=function init(a){var b=document.querySelector("a[href*=completion_validate]");if(b){b.addEventListener("click",function(){c.completionValidate(a).catch(e.exception)})}document.addEventListener("click",function(a){var b=a.target.closest("[data-action=\"join\"]");if(b){d.start();window.open(b.href,"bigbluebutton_conference");a.preventDefault()}});document.addEventListener(f.eventTypes.sessionEnded,function(){d.stop();d.updateRoom();(0,e.fetchNotifications)()});window.addEventListener(f.eventTypes.currentSessionEnded,function(){d.stop();d.updateRoom();(0,e.fetchNotifications)()})};var i=function(){window.opener.setTimeout(function(){d.updateRoom(!0)},5e3);window.removeEventListener("onbeforeunload",i)};a.setupWindowAutoClose=function setupWindowAutoClose(){(0,f.notifyCurrentSessionEnded)(window.opener);window.addEventListener("onbeforeunload",i);window.close()}}); +//# sourceMappingURL=rooms.min.js.map diff --git a/mod/bigbluebuttonbn/amd/build/rooms.min.js.map b/mod/bigbluebuttonbn/amd/build/rooms.min.js.map new file mode 100644 index 00000000000..16661a7ca92 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/rooms.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/rooms.js"],"names":["init","bigbluebuttonbnid","completionElement","document","querySelector","addEventListener","repository","completionValidate","catch","displayException","e","joinButton","target","closest","roomUpdater","start","window","open","href","preventDefault","eventTypes","sessionEnded","stop","updateRoom","currentSessionEnded","autoclose","opener","setTimeout","removeEventListener","setupWindowAutoClose","close"],"mappings":"shBAwBA,OACA,O,6iBAQoB,QAAPA,CAAAA,IAAO,CAACC,CAAD,CAAuB,CACvC,GAAMC,CAAAA,CAAiB,CAAGC,QAAQ,CAACC,aAAT,CAAuB,8BAAvB,CAA1B,CACA,GAAIF,CAAJ,CAAuB,CACnBA,CAAiB,CAACG,gBAAlB,CAAmC,OAAnC,CAA4C,UAAM,CAC9CC,CAAU,CAACC,kBAAX,CAA8BN,CAA9B,EAAiDO,KAAjD,CAAuDC,WAAvD,CACH,CAFD,CAGH,CAEDN,QAAQ,CAACE,gBAAT,CAA0B,OAA1B,CAAmC,SAAAK,CAAC,CAAI,CACpC,GAAMC,CAAAA,CAAU,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiB,wBAAjB,CAAnB,CACA,GAAIF,CAAJ,CAAgB,CACZG,CAAW,CAACC,KAAZ,GACAC,MAAM,CAACC,IAAP,CAAYN,CAAU,CAACO,IAAvB,CAA6B,0BAA7B,EAEAR,CAAC,CAACS,cAAF,EACH,CACJ,CARD,EAUAhB,QAAQ,CAACE,gBAAT,CAA0Be,aAAWC,YAArC,CAAmD,UAAM,CACrDP,CAAW,CAACQ,IAAZ,GACAR,CAAW,CAACS,UAAZ,GACA,0BACH,CAJD,EAMAP,MAAM,CAACX,gBAAP,CAAwBe,aAAWI,mBAAnC,CAAwD,UAAM,CAC1DV,CAAW,CAACQ,IAAZ,GACAR,CAAW,CAACS,UAAZ,GACA,0BACH,CAJD,CAKH,C,IAKKE,CAAAA,CAAS,CAAG,UAAM,CACpBT,MAAM,CAACU,MAAP,CAAcC,UAAd,CAAyB,UAAM,CAC3Bb,CAAW,CAACS,UAAZ,IACH,CAFD,CAEG,GAFH,EAGAP,MAAM,CAACY,mBAAP,CAA2B,gBAA3B,CAA6CH,CAA7C,CACH,C,wBAKmC,QAAvBI,CAAAA,oBAAuB,EAAM,CACtC,gCAA0Bb,MAAM,CAACU,MAAjC,EACAV,MAAM,CAACX,gBAAP,CAAwB,gBAAxB,CAA0CoB,CAA1C,EAEAT,MAAM,CAACc,KAAP,EACH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * JS actions for the rooms page for mod_bigbluebuttonbn.\n *\n * @module mod_bigbluebuttonbn/rooms\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport './actions';\nimport * as repository from './repository';\nimport * as roomUpdater from './roomupdater';\nimport {\n exception as displayException,\n fetchNotifications,\n} from 'core/notification';\n\nimport {eventTypes, notifyCurrentSessionEnded} from './events';\n\nexport const init = (bigbluebuttonbnid) => {\n const completionElement = document.querySelector('a[href*=completion_validate]');\n if (completionElement) {\n completionElement.addEventListener(\"click\", () => {\n repository.completionValidate(bigbluebuttonbnid).catch(displayException);\n });\n }\n\n document.addEventListener('click', e => {\n const joinButton = e.target.closest('[data-action=\"join\"]');\n if (joinButton) {\n roomUpdater.start();\n window.open(joinButton.href, 'bigbluebutton_conference');\n\n e.preventDefault();\n }\n });\n\n document.addEventListener(eventTypes.sessionEnded, () => {\n roomUpdater.stop();\n roomUpdater.updateRoom();\n fetchNotifications();\n });\n\n window.addEventListener(eventTypes.currentSessionEnded, () => {\n roomUpdater.stop();\n roomUpdater.updateRoom();\n fetchNotifications();\n });\n};\n\n/**\n * Handle autoclosing of the window.\n */\nconst autoclose = () => {\n window.opener.setTimeout(() => {\n roomUpdater.updateRoom(true);\n }, 5000);\n window.removeEventListener('onbeforeunload', autoclose);\n};\n\n/**\n * Auto close child windows when clicking the End meeting button.\n */\nexport const setupWindowAutoClose = () => {\n notifyCurrentSessionEnded(window.opener);\n window.addEventListener('onbeforeunload', autoclose);\n\n window.close(); // This does not work as scripts can only close windows that are opened by themselves.\n};\n"],"file":"rooms.min.js"} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/amd/build/roomupdater.min.js b/mod/bigbluebuttonbn/amd/build/roomupdater.min.js new file mode 100644 index 00000000000..e3333f64eb3 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/build/roomupdater.min.js @@ -0,0 +1,2 @@ +define ("mod_bigbluebuttonbn/roomupdater",["exports","core/templates","core/notification","./repository"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.updateRoom=a.stop=a.start=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var e=0,f=1,g=null,h=!1,i=function(){e=0;f=1};a.start=function start(){h=!0;g=setTimeout(function(){return k()},1000)};var j=function(){h=!1;if(g){clearInterval(g);g=null}i()};a.stop=j;var k=function(){if(!h){return}if(0==e%f){l(!0).then(function(){if(f>=10){f=1}else{f++}}).catch().then(function(){g=setTimeout(function(){return k()},1000)}).catch()}},l=function(){var a=0.\n\n/**\n * JS room updater.\n *\n * @module mod_bigbluebuttonbn/roomupdater\n * @copyright 2021 Blindside Networks Inc\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from \"core/templates\";\nimport {exception as displayException} from 'core/notification';\nimport {getMeetingInfo} from './repository';\n\nconst timeout = 1000;\nconst maxFactor = 10;\n\nlet updateCount = 0;\nlet updateFactor = 1;\nlet timerReference = null;\nlet timerRunning = false;\n\nconst resetValues = () => {\n updateCount = 0;\n updateFactor = 1;\n};\n\n/**\n * Start the information poller.\n */\nexport const start = () => {\n timerRunning = true;\n timerReference = setTimeout(() => poll(), timeout);\n};\n\n/**\n * Stop the room updater.\n */\nexport const stop = () => {\n timerRunning = false;\n if (timerReference) {\n clearInterval(timerReference);\n timerReference = null;\n }\n\n resetValues();\n};\n\nconst poll = () => {\n if (!timerRunning) {\n // The poller has been stopped.\n return;\n }\n if ((updateCount % updateFactor) === 0) {\n updateRoom(true)\n .then(() => {\n if (updateFactor >= maxFactor) {\n updateFactor = 1;\n } else {\n updateFactor++;\n }\n\n return;\n\n })\n .catch()\n .then(() => {\n timerReference = setTimeout(() => poll(), timeout);\n return;\n })\n .catch();\n }\n};\n\n/**\n * Update the room information.\n *\n * @param {boolean} [updatecache=false]\n * @returns {Promise}\n */\nexport const updateRoom = (updatecache = false) => {\n const bbbRoomViewElement = document.getElementById('bbb-room-view');\n const bbbId = bbbRoomViewElement.dataset.bbbId;\n const groupId = bbbRoomViewElement.dataset.groupId;\n return getMeetingInfo(bbbId, groupId, updatecache)\n .then(data => {\n // Just make sure we have the right information for the template.\n data.haspresentations = false;\n if (data.presentations && data.presentations.length) {\n data.haspresentations = true;\n }\n return Templates.renderForPromise('mod_bigbluebuttonbn/room_view', data);\n })\n .then(({html, js}) => Templates.replaceNodeContents(bbbRoomViewElement, html, js))\n .catch(displayException);\n};\n"],"file":"roomupdater.min.js"} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/amd/src/actions.js b/mod/bigbluebuttonbn/amd/src/actions.js new file mode 100644 index 00000000000..dc0aa27674d --- /dev/null +++ b/mod/bigbluebuttonbn/amd/src/actions.js @@ -0,0 +1,66 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . +/** + * JS actions. + * + * @module mod_bigbluebuttonbn/actions + * @copyright 2021 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {endMeeting as requestEndMeeting} from './repository'; +import { + exception as displayException, + saveCancel, +} from 'core/notification'; +import {notifySessionEnded} from './events'; +import {get_string as getString} from 'core/str'; + +const confirmedPromise = (title, question, saveLabel) => new Promise(resolve => { + saveCancel(title, question, saveLabel, resolve); +}); + +const registerEventListeners = () => { + document.addEventListener('click', e => { + const actionButton = e.target.closest('.bbb-btn-action[data-action="end"]'); + if (!actionButton) { + return; + } + + e.preventDefault(); + + const bbbId = actionButton.dataset.bbbId; + const groupId = actionButton.dataset.groupId ? actionButton.dataset.groupId : 0; + + confirmedPromise( + getString('end_session_confirm_title', 'mod_bigbluebuttonbn'), + getString('end_session_confirm', 'mod_bigbluebuttonbn'), + getString('yes', 'moodle') + ) + .then(() => requestEndMeeting(bbbId, groupId)) + .then(() => { + notifySessionEnded(bbbId, groupId); + + return; + }) + .catch(displayException); + }); +}; + +let listening = false; +if (!listening) { + registerEventListeners(); + listening = true; +} diff --git a/mod/bigbluebuttonbn/amd/src/event_dispatcher.js b/mod/bigbluebuttonbn/amd/src/event_dispatcher.js new file mode 100644 index 00000000000..92667d3e9fd --- /dev/null +++ b/mod/bigbluebuttonbn/amd/src/event_dispatcher.js @@ -0,0 +1,82 @@ +// This file is part of Moodle - http://moodle.org/ // +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * An Event dispatcher used to dispatch Native JS CustomEvent objects with custom default properties. + * + * @module core/event_dispatcher + * @copyright 2021 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 4.0 + */ + +/** + * Dispatch an event as a CustomEvent on the specified container. + * By default events are bubbled, and cancelable. + * + * The eventName should typically by sourced using a constant. See the supplied examples. + * + * Note: This function uses native events. Any additional details are passed to the function in event.detail. + * + * This function mimics the behaviour of EventTarget.dispatchEvent but bubbles by default. + * + * @method dispatchEvent + * @param {String} eventName The name of the event + * @param {Object} detail Any additional details to pass into the eveent + * @param {HTMLElement} container The point at which to dispatch the event + * @param {Object} options + * @param {Boolean} options.bubbles Whether to bubble up the DOM + * @param {Boolean} options.cancelable Whether preventDefault() can be called + * @param {Boolean} options.composed Whether the event can bubble across the ShadowDOM bounadry + * @returns {CustomEvent} + * + * @example Using a native CustomEvent to indicate that some example data was displayed. + * // mod/example/amd/src/events.js + * + * import {dispatchEvent} from 'core/event_dispatcher'; + * + * export const eventTypes = { + * exampleDataDisplayed: 'mod_example/exampleDataDisplayed', + * }; + * + * export const notifyExampleDisplayed = someArgument => dispatchEvent(eventTypes.exampleDataDisplayed, { + * someArgument, + * }, document, { + * cancelable: false, + * }); + */ +export const dispatchEvent = ( + eventName, + detail = {}, + container = document, + { + bubbles = true, + cancelable = false, + composed = false, + } = {} +) => { + const customEvent = new CustomEvent( + eventName, + { + bubbles, + cancelable, + composed, + detail, + } + ); + + container.dispatchEvent(customEvent); + + return customEvent; +}; diff --git a/mod/bigbluebuttonbn/amd/src/events.js b/mod/bigbluebuttonbn/amd/src/events.js new file mode 100644 index 00000000000..a370bb10d20 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/src/events.js @@ -0,0 +1,74 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Events for the mod_bigbluebuttonbn plugin. + * + * @module mod_bigbluebuttonbn/events + * @copyright 2021 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// TODO For master this can be changed to `core/event_dispatcher` and the local copy removed. +// For 3.11 and earlier this line should stay. +import {dispatchEvent} from './event_dispatcher'; + +export const eventTypes = { + /** + * Fired when a session has been ended. + * + * @event mod_bigbluebuttonbn/sessionEnded + * @type CustomEvent + * @property {object} detail + * @property {number} detail.bbbId + * @property {number} detail.groupId + */ + sessionEnded: 'mod_bigbluebuttonbn/sessionEnded', + + /** + * Fired when the current session has been ended. + * + * @event mod_bigbluebuttonbn/currentSessionEnded + * @type CustomEvent + * @property {object} detail + */ + currentSessionEnded: 'mod_bigbluebuttonbn/currentSessionEnded', +}; + +/** + * Trigger the sessionEnded event. + * + * @param {number} bbbId + * @param {number} groupId + * @returns {CustomEvent} + * @fires event:mod_bigbluebuttonbn/sessionEnded + */ +export const notifySessionEnded = (bbbId, groupId) => dispatchEvent(eventTypes.sessionEnded, { + bbbId, + groupId, +}); + +/** + * Trigger the currentSessionEnded event. + * + * @param {Element} container + * @returns {CustomEvent} + * @fires event:mod_bigbluebuttonbn/currentSessionEnded + */ +export const notifyCurrentSessionEnded = container => dispatchEvent( + eventTypes.currentSessionEnded, + {}, + container +); diff --git a/mod/bigbluebuttonbn/amd/src/index.js b/mod/bigbluebuttonbn/amd/src/index.js new file mode 100644 index 00000000000..04409915ba0 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/src/index.js @@ -0,0 +1,31 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Index page for the mod_bigbluebuttonbn plugin. + * + * @module mod_bigbluebuttonbn/index + * @copyright 2021 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import './actions'; +import {eventTypes} from './events'; + +export const init = () => { + document.addEventListener(eventTypes.sessionEnded, () => { + window.location.reload(); + }); +}; diff --git a/mod/bigbluebuttonbn/amd/src/modform.js b/mod/bigbluebuttonbn/amd/src/modform.js new file mode 100644 index 00000000000..e048f463acb --- /dev/null +++ b/mod/bigbluebuttonbn/amd/src/modform.js @@ -0,0 +1,367 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * JS for the mod_form page on mod_bigbluebuttonbn plugin. + * + * @module mod_bigbluebuttonbn/mod_form + * @copyright 2021 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {get_string as getString} from 'core/str'; +import Notification from 'core/notification'; +import Templates from "core/templates"; + +/** + * Get all selectors in one place. + * + */ +const ELEMENT_SELECTOR = { + instanceTypeSelection: () => document.querySelector('select#id_type'), + instanceTypeProfiles: () => document.querySelector('[data-profile-types]'), + participantData: () => document.querySelector('[data-participant-data]'), + participantList: () => document.getElementsByName('participants')[0], + participantTable: () => document.getElementById('participant_list_table'), + participantSelectionType: () => document.getElementsByName('bigbluebuttonbn_participant_selection_type')[0], + participantSelection: () => document.getElementsByName('bigbluebuttonbn_participant_selection')[0], + participantAddButton: () => document.getElementsByName('bigbluebuttonbn_participant_selection_add')[0], +}; +/** + * Initialise the moodle form code. + * + * This will help hide or show items depending on the selection of the instance type. + * + * @method init + * @param {object} info + */ +export const init = (info) => { + const selectedType = ELEMENT_SELECTOR.instanceTypeSelection(); + const instanceTypeProfiles = JSON.parse(ELEMENT_SELECTOR.instanceTypeProfiles().dataset.profileTypes); + + let profileType = info.instanceTypeDefault; + if (selectedType !== null && selectedType.selectedIndex !== -1) { + profileType = selectedType.options[selectedType.selectedIndex].value; + } + + const isFeatureEnabled = (profileType, feature) => { + const features = instanceTypeProfiles[profileType].features; + return (features.indexOf(feature) !== -1); + }; + applyInstanceTypeProfile(profileType, isFeatureEnabled); + + // Change form visible fields depending on the selection. + selectedType.addEventListener('change', (e) => { + applyInstanceTypeProfile(e.target.value, isFeatureEnabled); + }); + + ELEMENT_SELECTOR.participantSelectionType().addEventListener('change', (e) => { + const currentTypeSelect = e.target; + updateSelectionFromType(currentTypeSelect); + }); + + ELEMENT_SELECTOR.participantAddButton().addEventListener('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + participantAddFromCurrentSelection(); + }); + + participantListInit(); +}; + +/** + * Show or hide form element depending on the selected profile + * + * @param {string} profileType + * @param {function} isFeatureEnabled + */ +const applyInstanceTypeProfile = (profileType, isFeatureEnabled) => { + let showAll = isFeatureEnabled(profileType, 'all'); + const showFieldset = (id, show) => { + // Show room settings validation. + const node = document.querySelector('#' + id); + if (!node) { + return; + } + if (show) { + node.style.display = 'block'; + return; + } + node.style.display = 'none'; + }; + const showInput = (id, show) => { + // Show room settings validation. + const node = document.querySelector('#' + id); + if (!node) { + return; + } + var ancestor = node.closest('div').closest('div'); + if (show) { + ancestor.style.display = 'block'; + return; + } + ancestor.style.display = 'none'; + }; + const showFormGroup = (id, show) => { + // Show room settings validation. + const node = document.querySelector('#fgroup_id_' + id); + if (!node) { + return; + } + if (show) { + node.classList.remove('hidden'); + return; + } + node.classList.add('hidden'); + }; + // Show room settings validation. + showFieldset('id_room', showAll || + isFeatureEnabled(profileType, 'showroom')); + showInput('id_record', showAll || + isFeatureEnabled(profileType, 'showroom')); + // Show recordings settings validation. + showFieldset('id_recordings', showAll || + isFeatureEnabled(profileType, 'showrecordings')); + // Show recordings imported settings validation. + showInput('id_recordings_imported', showAll || + isFeatureEnabled(profileType, 'showrecordings')); + // Preuploadpresentation feature validation. + showFieldset('id_preuploadpresentation', showAll || + isFeatureEnabled(profileType, 'preuploadpresentation')); + // Participants feature validation. + showFieldset('id_permissions', showAll || + isFeatureEnabled(profileType, 'permissions')); + // Schedule feature validation. + showFieldset('id_schedule', showAll || + isFeatureEnabled(profileType, 'schedule')); + // Common module settings validation. + showFieldset('id_modstandardelshdr', showAll || + isFeatureEnabled(profileType, 'modstandardelshdr')); + // Restrict access validation. + showFieldset('id_availabilityconditionsheader', showAll || + isFeatureEnabled(profileType, 'availabilityconditionsheader')); + // Tags validation. + showFieldset('id_tagshdr', showAll || isFeatureEnabled(profileType, 'tagshdr')); + // Competencies validation. + showFieldset('id_competenciessection', showAll || + isFeatureEnabled(profileType, 'competenciessection')); + // Completion validation. + showFormGroup('completionattendancegroup', showAll || + isFeatureEnabled(profileType, 'completionattendance')); + // Completion validation. + showFormGroup('completionengagementgroup', showAll || + isFeatureEnabled(profileType, 'completionengagement')); +}; + +/** + * Init the participant list + */ +const participantListInit = () => { + const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData); + const participantList = getParticipantList(); + participantList.forEach(participant => { + const selectionTypeValue = participant.selectiontype; + const selectionValue = participant.selectionid; + const selectionRole = participant.role; + if (participant.selectiontype === 'all' || + typeof participantData[participant.selectiontype].children[participant.selectionid] !== 'undefined') { + // Add it to the form, but don't add the delete button if it is the first item. + participantAddToForm(selectionTypeValue, selectionValue, selectionRole, true).then(); + } + }); +}; + +/** + * Add rows to the participant list dependingon the current selection. + * + * @param {string} selectionTypeValue + * @param {string} selectionValue + * @param {string} selectedRole + * @param {boolean} canRemove + * @returns {Promise} + */ +const participantAddToForm = async(selectionTypeValue, selectionValue, selectedRole, canRemove) => { + const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData); + const sviewer = await getString('mod_form_field_participant_bbb_role_viewer', 'mod_bigbluebuttonbn'); + const smoderator = await getString('mod_form_field_participant_bbb_role_moderator', 'mod_bigbluebuttonbn'); + let roles = { + viewer: {'id': 'viewer', label: sviewer}, + moderator: {'id': 'moderator', label: smoderator} + }; + roles[selectedRole].isselected = true; + try { + const listTable = document.querySelector('#participant_list_table tbody'); + const templateContext = { + 'selectiontypevalue': selectionTypeValue, + 'selectionvalue': selectionValue, + 'participanttype': participantData[selectionTypeValue].name, + 'participantvalue': + (selectionTypeValue !== 'all') ? + participantData[selectionTypeValue].children[selectionValue].name : null, + 'roles': Object.values(roles), + 'canRemove': canRemove + }; + const {html, js} = await Templates.renderForPromise('mod_bigbluebuttonbn/participant_form_add', templateContext); + const newNode = Templates.appendNodeContents(listTable, html, js)[0]; + newNode.querySelector('.participant-select').addEventListener('change', () => { + participantListRoleUpdate(selectionTypeValue, selectionValue); + }); + // Now add the callbacks: participantListRoleUpdate() and participantRemove(). + const removeNode = newNode.querySelector('.remove-button'); + if (removeNode) { + removeNode + .addEventListener('click', () => { + participantRemove(selectionTypeValue, selectionValue); + }); + } + + } catch (e) { + Notification.exception(e); + } +}; +/* + + */ + +/** + * Update the related form element with the list value. + * + * @param {object} list + */ +const participantListUpdate = (list) => { + const participantList = ELEMENT_SELECTOR.participantList(); + participantList.value = JSON.stringify(list); +}; + +/** + * + * @returns {any} + */ +const getParticipantList = () => { + const participantListValue = ELEMENT_SELECTOR.participantList().value; + if (participantListValue) { + return JSON.parse(participantListValue); + } + return []; +}; + +/** + * Remove participant both in the table/form and in the form element. + * + * @param {string} selectionTypeValue + * @param {string} selectionValue + */ +const participantRemove = (selectionTypeValue, selectionValue) => { + const pList = getParticipantList(); + const id = 'participant_list_tr_' + selectionTypeValue + '-' + selectionValue; + const participantListTable = ELEMENT_SELECTOR.participantTable(); + const selectionid = (selectionValue === '' ? null : selectionValue); + for (let i = 0; i < pList.length; i++) { + if (pList[i].selectiontype === selectionTypeValue && + pList[i].selectionid === selectionid) { + pList.splice(i, 1); + } + } + // Remove from the form. + for (let i = 0; i < participantListTable.rows.length; i++) { + if (participantListTable.rows[i].id === id) { + participantListTable.deleteRow(i); + } + } + // Update value in the form. + participantListUpdate(pList); +}; + +/** + * Role update + * + * @param {string} type + * @param {string} id + */ +const participantListRoleUpdate = (type, id) => { + // Update in memory. + const participantListRoleSelection = document.querySelector(`#participant_list_tr_${type}-${id} .participant-select`); + const pList = getParticipantList(); + + for (var i = 0; i < pList.length; i++) { + if (pList[i].selectiontype === type && + pList[i].selectionid === (id === '' ? null : parseInt(id))) { + pList[i].role = participantListRoleSelection.value; + } + } + // Update in the form. + participantListUpdate(pList); +}; + +/** + * Add participant from the currently selected options + */ +const participantAddFromCurrentSelection = () => { + let selectionType = ELEMENT_SELECTOR.participantSelectionType(); + let selection = ELEMENT_SELECTOR.participantSelection(); + const pList = getParticipantList(); + // Lookup to see if it has been added already. + for (var i = 0; i < pList.length; i++) { + if (pList[i].selectiontype === selectionType.value && + pList[i].selectionid === selection.value) { + return; + } + } + pList.push({ + "selectiontype": selectionType.value, + "selectionid": selection.value, + "role": "viewer" + }); + // Add it to the form. + participantAddToForm(selectionType.value, selection.value, 'viewer', true).then(); + // Update in the form. + participantListUpdate(pList); +}; + +/** + * Update selectable options when changing types + * + * @param {HTMLNode} currentTypeSelect + */ +const updateSelectionFromType = (currentTypeSelect) => { + const createNewOption = (selectItem, label, value) => { + const option = document.createElement('option'); + option.text = label; + option.value = value; + + selectItem.add(option); + }; + + const participantData = JSON.parse(ELEMENT_SELECTOR.participantData().dataset.participantData); + // Clear all selection items. + const participantSelect = ELEMENT_SELECTOR.participantSelection(); + while (participantSelect.firstChild) { + participantSelect.removeChild(participantSelect.firstChild); + } + // Add options depending on the selection. + if (currentTypeSelect.selectedIndex !== -1) { + const options = Object.values(participantData[currentTypeSelect.value].children); + options.forEach(option => { + createNewOption(participantSelect, option.name, option.id); + }); + + if (currentTypeSelect.value === 'all') { + createNewOption(participantSelect, '---------------', 'all'); + currentTypeSelect.disabled = true; + } else { + currentTypeSelect.disabled = false; + } + } +}; diff --git a/mod/bigbluebuttonbn/amd/src/recordings.js b/mod/bigbluebuttonbn/amd/src/recordings.js new file mode 100644 index 00000000000..c3cb769482a --- /dev/null +++ b/mod/bigbluebuttonbn/amd/src/recordings.js @@ -0,0 +1,416 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * JS for the recordings page on mod_bigbluebuttonbn plugin. + * + * @module mod_bigbluebuttonbn/recordings + * @copyright 2021 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import * as repository from './repository'; +import {exception as displayException} from 'core/notification'; +import {get_strings as getStrings} from 'core/str'; +import {addIconToContainerWithPromise} from 'core/loadingicon'; +import ModalFactory from 'core/modal_factory'; +import ModalEvents from 'core/modal_events'; +import * as Str from 'core/str'; +import Pending from 'core/pending'; + +const stringList = [ + 'view_recording_yui_first', + 'view_recording_yui_prev', + 'view_recording_yui_next', + 'view_recording_yui_last', + 'view_recording_yui_page', + 'view_recording_yui_go', + 'view_recording_yui_rows', + 'view_recording_yui_show_all', +]; + +const getStringsForYui = () => { + const stringMap = stringList.map(key => { + return { + key, + component: 'bigbluebuttonbn', + }; + }); + + return getStrings(stringMap) + .then(([first, prev, next, last, goToLabel, goToAction, perPage, showAll]) => { + return { + first, + prev, + next, + last, + goToLabel, + goToAction, + perPage, + showAll, + }; + }) + .catch(); +}; + +const getYuiInstance = lang => new Promise(resolve => { + // eslint-disable-next-line + YUI({ + lang, + }).use('intl', 'datatable', 'datatable-sort', 'datatable-paginator', 'datatype-number', Y => { + resolve(Y); + }); +}); + +/** + * Format the supplied date per the specified locale. + * + * @param {string} locale + * @param {array} dateList + * @returns {array} + */ +const formatDates = (locale, dateList) => dateList.map(row => { + const date = new Date(row.date); + row.date = date.toLocaleDateString(locale, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + + return row; +}); + +/** + * Format response data for the table. + * + * @param {string} response JSON-encoded table data + * @returns {array} + */ +const getFormattedData = response => { + const recordingData = response.tabledata; + const rowData = JSON.parse(recordingData.data); + + return formatDates(recordingData.locale, rowData); +}; + +const getTableNode = tableSelector => document.querySelector(tableSelector); + +const fetchRecordingData = tableSelector => { + const tableNode = getTableNode(tableSelector); + + if (tableNode.dataset.importMode) { + return repository.fetchRecordingsToImport( + tableNode.dataset.bbbid, + tableNode.dataset.bbbSourceInstanceId, + tableNode.dataset.bbbSourceCourseId, + tableNode.dataset.tools, + tableNode.dataset.groupId + ); + } else { + return repository.fetchRecordings( + tableNode.dataset.bbbid, + tableNode.dataset.tools, + tableNode.dataset.groupId + ); + } +}; + +/** + * Fetch the data table functinos for the specified table. + * + * @param {String} tableId in which we will display the table + * @param {String} searchFormId The Id of the relate. + * @param {Object} dataTable + * @returns {Object} + * @private + */ +const getDataTableFunctions = (tableId, searchFormId, dataTable) => { + const tableNode = getTableNode(tableId); + const bbbid = tableNode.dataset.bbbid; + + const updateTableFromResponse = response => { + if (!response || !response.status) { + // There was no output at all. + return; + } + + dataTable.get('data').reset(getFormattedData(response)); + dataTable.set( + 'currentData', + dataTable.get('data') + ); + + const currentFilter = dataTable.get('currentFilter'); + if (currentFilter) { + filterByText(currentFilter); + } + }; + + const refreshTableData = () => fetchRecordingData(tableId).then(updateTableFromResponse); + + const filterByText = value => { + const dataModel = dataTable.get('currentData'); + dataTable.set('currentFilter', value); + + const escapedRegex = value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + const rsearch = new RegExp(`.*?${escapedRegex}.*?`, 'i'); + + dataTable.set('data', dataModel.filter({asList: true}, item => { + const name = item.get('recording'); + if (name && rsearch.test(name)) { + return true; + } + + const description = item.get('description'); + return description && rsearch.test(description); + })); + }; + + const requestAction = (element) => { + const getDataFromAction = (element, dataType) => { + const dataElement = element.closest(`[data-${dataType}]`); + if (dataElement) { + return dataElement.dataset[dataType]; + } + + return null; + }; + + const elementData = element.dataset; + const payload = { + bigbluebuttonbnid: bbbid, + recordingid: getDataFromAction(element, 'recordingid'), + additionaloptions: getDataFromAction(element, 'additionaloptions'), + action: elementData.action, + }; + // Slight change for import, for additional options. + if (!payload.additionaloptions) { + payload.additionaloptions = {}; + } + if (elementData.action === 'import') { + const bbbsourceid = getDataFromAction(element, 'source-instance-id'); + const bbbcourseid = getDataFromAction(element, 'source-course-id'); + if (!payload.additionaloptions) { + payload.additionaloptions = {}; + } + payload.additionaloptions.sourceid = bbbsourceid ? bbbsourceid : 0; + payload.additionaloptions.bbbcourseid = bbbcourseid ? bbbcourseid : 0; + } + // Now additional options should be a json string. + payload.additionaloptions = JSON.stringify(payload.additionaloptions); + if (element.dataset.requireConfirmation === "1") { + // Create the confirmation dialogue. + return new Promise((resolve) => + ModalFactory.create({ + title: Str.get_string('confirm'), + body: recordingConfirmationMessage(payload), + type: ModalFactory.types.SAVE_CANCEL + }).then(async(modal) => { + modal.setSaveButtonText(await Str.get_string('ok', 'moodle')); + + // Handle save event. + modal.getRoot().on(ModalEvents.save, () => { + resolve(true); + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, () => { + // Destroy when hidden. + modal.destroy(); + resolve(false); + }); + + modal.show(); + + return modal; + }).catch(Notification.exception) + ).then((proceed) => + proceed ? repository.updateRecording(payload) : () => null + ); + } else { + return repository.updateRecording(payload); + } + }; + + const recordingConfirmationMessage = async(data) => { + + const playbackElement = document.querySelector(`#playbacks-${data.recordingid}`); + const recordingType = await Str.get_string( + playbackElement.dataset.imported === 'true' ? 'view_recording_link' : 'view_recording', + 'bigbluebuttonbn' + ); + + const confirmation = await Str.get_string(`view_recording_${data.action}_confirmation`, 'bigbluebuttonbn', recordingType); + + if (data.action === 'import') { + return confirmation; + } + + // If it has associated links imported in a different course/activity, show that in confirmation dialog. + const associatedLinkCount = document.querySelector(`a#recording-${data.action}-${data.recordingid}`)?.dataset?.links; + if (!associatedLinkCount || associatedLinkCount === 0) { + return confirmation; + } + + const confirmationWarning = await Str.get_string( + associatedLinkCount === 1 + ? `view_recording_${data.action}_confirmation_warning_p` + : `view_recording_${data.action}_confirmation_warning_s`, + 'bigbluebuttonbn', + associatedLinkCount + ); + + return confirmationWarning + '\n\n' + confirmation; + }; + + /** + * Process an action event. + * + * @param {Event} e + */ + const processAction = e => { + const popoutLink = e.target.closest('[data-action="play"]'); + if (popoutLink) { + e.preventDefault(); + + const videoPlayer = window.open('', '_blank'); + videoPlayer.opener = null; + videoPlayer.location.href = popoutLink.href; + // TODO send a recording viewed event when this event will be implemented. + return; + } + + // Fetch any clicked anchor. + const clickedLink = e.target.closest('a[data-action]'); + if (clickedLink && !clickedLink.classList.contains('disabled')) { + e.preventDefault(); + + // Create a spinning icon on the table. + const iconPromise = addIconToContainerWithPromise(dataTable.get('boundingBox').getDOMNode()); + + requestAction(clickedLink) + .then(refreshTableData) + .catch(displayException) + .then(iconPromise.resolve) + .catch(); + } + }; + + const processSearchSubmission = e => { + // Prevent the default action. + e.preventDefault(); + const parentNode = e.target.closest('div[role=search]'); + const searchInput = parentNode.querySelector('input[name=search]'); + filterByText(searchInput.value); + }; + + const registerEventListeners = () => { + // Add event listeners to the table boundingBox. + const boundingBox = dataTable.get('boundingBox').getDOMNode(); + boundingBox.addEventListener('click', processAction); + + // Setup the search from handlers. + const searchForm = document.querySelector(searchFormId); + if (searchForm) { + const searchButton = document.querySelector(searchFormId + ' button'); + searchButton.addEventListener('click', processSearchSubmission); + } + }; + + return { + filterByText, + refreshTableData, + registerEventListeners, + }; +}; + +/** + * Setup the data table for the specified BBB instance. + * + * @param {String} tableId in which we will display the table + * @param {String} searchFormId The Id of the relate. + * @param {object} response The response from the data request + * @returns {Promise} + */ +const setupDatatable = (tableId, searchFormId, response) => { + if (!response) { + return Promise.resolve(); + } + + if (!response.status) { + // Something failed. Continue to show the plain output. + return Promise.resolve(); + } + + const recordingData = response.tabledata; + + const pendingPromise = new Pending('mod_bigbluebuttonbn/recordings/setupDatatable'); + return Promise.all([getYuiInstance(recordingData.locale), getStringsForYui()]) + .then(([yuiInstance, strings]) => { + // Add the fetched strings to the YUI Instance. + yuiInstance.Intl.add('datatable-paginator', yuiInstance.config.lang, {...strings}); + + return yuiInstance; + }) + .then(yuiInstance => { + const tableData = getFormattedData(response); + yuiInstance.RecordsPaginatorView = Y.Base.create('my-paginator-view',yuiInstance.DataTable.Paginator.View, [],{ + _modelChange : function (e) { + var changed = e.changed, + totalItems = (changed && changed.totalItems); + if (totalItems) { + this._updateControlsUI(e.target.get('page')); + } + } + }); + const dataTable = new yuiInstance.DataTable({ + paginatorView: "RecordsPaginatorView", + width: "1195px", + columns: recordingData.columns, + data: tableData, + rowsPerPage: 10, + paginatorLocation: ['header', 'footer'], + autoSync: true + }); + return dataTable; + }) + .then(dataTable => { + dataTable.render(tableId); + const {registerEventListeners} = getDataTableFunctions( + tableId, + searchFormId, + dataTable); + registerEventListeners(); + return dataTable; + }) + .then(dataTable => { + pendingPromise.resolve(); + return dataTable; + }); +}; + +/** + * Initialise recordings code. + * + * @method init + * @param {String} tableId in which we will display the table + * @param {String} searchFormId The Id of the relate. + */ +export const init = (tableId, searchFormId) => { + fetchRecordingData(tableId) + .then(response => setupDatatable(tableId, searchFormId, response)) + .catch(displayException); +}; diff --git a/mod/bigbluebuttonbn/amd/src/repository.js b/mod/bigbluebuttonbn/amd/src/repository.js new file mode 100644 index 00000000000..9906f433c29 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/src/repository.js @@ -0,0 +1,139 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Repository to perform WS calls for mod_bigbluebuttonbn. + * + * @module mod_bigbluebuttonbn/repository + * @copyright 2021 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {call as fetchMany} from 'core/ajax'; + +/** + * Fetch the list of recordings from the server. + * + * @param {Number} bigbluebuttonbnid The instance ID + * @param {String} tools the set of tools to display + * @param {number} groupid + * @returns {Promise} + */ +export const fetchRecordings = (bigbluebuttonbnid, tools, groupid) => { + const args = { + bigbluebuttonbnid, + tools, + }; + + if (groupid) { + args.groupid = groupid; + } + + return fetchMany([{methodname: 'mod_bigbluebuttonbn_get_recordings', args}])[0]; +}; + +/** + * Fetch the list of recordings from the server that can be imported. + * + * @param {Number} destinationinstanceid The destination instance ID + * @param {Number} sourcebigbluebuttonbnid The original instance ID + * @param {Number} sourcecourseid The destination instance ID + * @param {String} tools the set of tools to display + * @param {number} groupid + * @returns {Promise} + */ +export const fetchRecordingsToImport = ( + destinationinstanceid, + sourcebigbluebuttonbnid, + sourcecourseid, + tools, + groupid +) => { + const args = { + destinationinstanceid, + sourcebigbluebuttonbnid, + sourcecourseid, + tools, + }; + + if (groupid) { + args.groupid = groupid; + } + + return fetchMany([{methodname: 'mod_bigbluebuttonbn_get_recordings_to_import', args}])[0]; +}; + +/** + * Perform an update on a single recording. + * + * @param {object} args The instance ID + * @returns {Promise} + */ +export const updateRecording = args => fetchMany([ + { + methodname: 'mod_bigbluebuttonbn_update_recording', + args, + } +])[0]; + +/** + * End the Meeting + * + * @param {number} bigbluebuttonbnid + * @param {number} groupid + * @returns {Promise} + */ +export const endMeeting = (bigbluebuttonbnid, groupid) => fetchMany([ + { + methodname: 'mod_bigbluebuttonbn_end_meeting', + args: { + bigbluebuttonbnid, + groupid + }, + } +])[0]; + +/** + * Validate completion. + * + * @param {object} args + * @returns {Promise} + */ +export const completionValidate = args => fetchMany([ + { + methodname: 'mod_bigbluebuttonbn_completion_validate', + args, + } +])[0]; + + +/** + * Fetch meeting info for the specified meeting. + * + * @param {number} bigbluebuttonbnid + * @param {number} groupid + * @param {boolean} [updatecache=false] + * @returns {Promise} + */ +export const getMeetingInfo = (bigbluebuttonbnid, groupid, updatecache = false) => fetchMany([ + { + methodname: 'mod_bigbluebuttonbn_meeting_info', + args: { + bigbluebuttonbnid, + groupid, + updatecache, + }, + } +])[0]; diff --git a/mod/bigbluebuttonbn/amd/src/rooms.js b/mod/bigbluebuttonbn/amd/src/rooms.js new file mode 100644 index 00000000000..0216be9b677 --- /dev/null +++ b/mod/bigbluebuttonbn/amd/src/rooms.js @@ -0,0 +1,83 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * JS actions for the rooms page for mod_bigbluebuttonbn. + * + * @module mod_bigbluebuttonbn/rooms + * @copyright 2021 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import './actions'; +import * as repository from './repository'; +import * as roomUpdater from './roomupdater'; +import { + exception as displayException, + fetchNotifications, +} from 'core/notification'; + +import {eventTypes, notifyCurrentSessionEnded} from './events'; + +export const init = (bigbluebuttonbnid) => { + const completionElement = document.querySelector('a[href*=completion_validate]'); + if (completionElement) { + completionElement.addEventListener("click", () => { + repository.completionValidate(bigbluebuttonbnid).catch(displayException); + }); + } + + document.addEventListener('click', e => { + const joinButton = e.target.closest('[data-action="join"]'); + if (joinButton) { + roomUpdater.start(); + window.open(joinButton.href, 'bigbluebutton_conference'); + + e.preventDefault(); + } + }); + + document.addEventListener(eventTypes.sessionEnded, () => { + roomUpdater.stop(); + roomUpdater.updateRoom(); + fetchNotifications(); + }); + + window.addEventListener(eventTypes.currentSessionEnded, () => { + roomUpdater.stop(); + roomUpdater.updateRoom(); + fetchNotifications(); + }); +}; + +/** + * Handle autoclosing of the window. + */ +const autoclose = () => { + window.opener.setTimeout(() => { + roomUpdater.updateRoom(true); + }, 5000); + window.removeEventListener('onbeforeunload', autoclose); +}; + +/** + * Auto close child windows when clicking the End meeting button. + */ +export const setupWindowAutoClose = () => { + notifyCurrentSessionEnded(window.opener); + window.addEventListener('onbeforeunload', autoclose); + + window.close(); // This does not work as scripts can only close windows that are opened by themselves. +}; diff --git a/mod/bigbluebuttonbn/amd/src/roomupdater.js b/mod/bigbluebuttonbn/amd/src/roomupdater.js new file mode 100644 index 00000000000..78e84983ebd --- /dev/null +++ b/mod/bigbluebuttonbn/amd/src/roomupdater.js @@ -0,0 +1,109 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * JS room updater. + * + * @module mod_bigbluebuttonbn/roomupdater + * @copyright 2021 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Templates from "core/templates"; +import {exception as displayException} from 'core/notification'; +import {getMeetingInfo} from './repository'; + +const timeout = 1000; +const maxFactor = 10; + +let updateCount = 0; +let updateFactor = 1; +let timerReference = null; +let timerRunning = false; + +const resetValues = () => { + updateCount = 0; + updateFactor = 1; +}; + +/** + * Start the information poller. + */ +export const start = () => { + timerRunning = true; + timerReference = setTimeout(() => poll(), timeout); +}; + +/** + * Stop the room updater. + */ +export const stop = () => { + timerRunning = false; + if (timerReference) { + clearInterval(timerReference); + timerReference = null; + } + + resetValues(); +}; + +const poll = () => { + if (!timerRunning) { + // The poller has been stopped. + return; + } + if ((updateCount % updateFactor) === 0) { + updateRoom(true) + .then(() => { + if (updateFactor >= maxFactor) { + updateFactor = 1; + } else { + updateFactor++; + } + + return; + + }) + .catch() + .then(() => { + timerReference = setTimeout(() => poll(), timeout); + return; + }) + .catch(); + } +}; + +/** + * Update the room information. + * + * @param {boolean} [updatecache=false] + * @returns {Promise} + */ +export const updateRoom = (updatecache = false) => { + const bbbRoomViewElement = document.getElementById('bbb-room-view'); + const bbbId = bbbRoomViewElement.dataset.bbbId; + const groupId = bbbRoomViewElement.dataset.groupId; + return getMeetingInfo(bbbId, groupId, updatecache) + .then(data => { + // Just make sure we have the right information for the template. + data.haspresentations = false; + if (data.presentations && data.presentations.length) { + data.haspresentations = true; + } + return Templates.renderForPromise('mod_bigbluebuttonbn/room_view', data); + }) + .then(({html, js}) => Templates.replaceNodeContents(bbbRoomViewElement, html, js)) + .catch(displayException); +}; diff --git a/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_activity_task.class.php b/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_activity_task.class.php new file mode 100644 index 00000000000..d0ed5f31386 --- /dev/null +++ b/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_activity_task.class.php @@ -0,0 +1,81 @@ +. + +/** + * Class for backup BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; +global $CFG; +require_once($CFG->dirroot.'/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php'); + +/** + * Backup task that provides all the settings and steps to perform one complete backup of the activity. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_bigbluebuttonbn_activity_task extends backup_activity_task +{ + /** + * Define (add) particular settings this activity can have. + * + * @return void + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have. + * + * @return void + */ + protected function define_my_steps() { + // Choice only has one structure step. + $this->add_step(new backup_bigbluebuttonbn_activity_structure_step('bigbluebuttonbn_structure', 'bigbluebuttonbn.xml')); + } + + /** + * Code the transformations to perform in the activity in order to get transportable (encoded) links. + * + * @param string $content + * + * @return string + */ + public static function encode_content_links($content) { + global $CFG; + + $base = preg_quote($CFG->wwwroot.'/mod/bigbluebuttonbn', '#'); + + // Link to the list of bigbluebuttonbns. + $pattern = '#('.$base."\/index.php\?id\=)([0-9]+)#"; + $content = preg_replace($pattern, '$@BIGBLUEBUTTONBNINDEX*$2@$', $content); + + // Link to bigbluebuttonbn view by moduleid. + $pattern = '#('.$base."\/view.php\?id\=)([0-9]+)#"; + $content = preg_replace($pattern, '$@BIGBLUEBUTTONBNVIEWBYID*$2@$', $content); + + return $content; + } +} diff --git a/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php b/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php new file mode 100644 index 00000000000..c9e3ee4baa8 --- /dev/null +++ b/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php @@ -0,0 +1,93 @@ +. + +/** + * Class for the structure used for backup BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Define all the backup steps that will be used by the backup_bigbluebuttonbn_activity_task. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_bigbluebuttonbn_activity_structure_step extends backup_activity_structure_step { + /** + * Define the complete bigbluebuttonbn structure for backup, with file and id annotations. + * + * @return object + */ + protected function define_structure() { + + // To know if we are including userinfo. + $userinfo = $this->get_setting_value('userinfo'); + + // Define each element separated. + $bigbluebuttonbn = new backup_nested_element('bigbluebuttonbn', ['id'], [ + 'type', 'course', 'name', 'intro', 'introformat', 'meetingid', + 'moderatorpass', 'viewerpass', 'wait', 'record', 'recordallfromstart', + 'recordhidebutton', 'welcome', 'voicebridge', 'openingtime', 'closingtime', 'timecreated', + 'timemodified', 'presentation', 'participants', 'userlimit', + 'recordings_html', 'recordings_deleted', 'recordings_imported', 'recordings_preview', + 'clienttype', 'muteonstart', 'completionattendance', + 'completionengagementchats', 'completionengagementtalks', 'completionengagementraisehand', + 'completionengagementpollvotes', 'completionengagementemojis']); + + $logs = new backup_nested_element('logs'); + + $log = new backup_nested_element('log', ['id'], [ + 'courseid', 'bigbluebuttonbnid', 'userid', 'timecreated', 'meetingid', 'log', 'meta']); + + $recordings = new backup_nested_element('recordings'); + + $recording = new backup_nested_element('recording', ['id'], [ + 'courseid', 'bigbluebuttonbnid', 'groupid', 'recordingid', 'headlesss', 'imported', 'status', 'importeddata', + 'timecreated']); + + // Build the tree. + $bigbluebuttonbn->add_child($logs); + $logs->add_child($log); + $bigbluebuttonbn->add_child($recordings); + $recordings->add_child($recording); + + // Define sources. + $bigbluebuttonbn->set_source_table('bigbluebuttonbn', ['id' => backup::VAR_ACTIVITYID]); + + // This source definition only happen if we are including user info. + if ($userinfo) { + $log->set_source_table('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => backup::VAR_PARENTID]); + $recording->set_source_table('bigbluebuttonbn_recordings', ['bigbluebuttonbnid' => backup::VAR_PARENTID]); + } + + // Define id annotations. + $log->annotate_ids('user', 'userid'); + + // Define file annotations. + $bigbluebuttonbn->annotate_files('mod_bigbluebuttonbn', 'intro', null); + + // Return the root element (bigbluebuttonbn), wrapped into standard activity structure. + return $this->prepare_activity_structure($bigbluebuttonbn); + } +} diff --git a/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_activity_task.class.php b/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_activity_task.class.php new file mode 100644 index 00000000000..145c34c99ef --- /dev/null +++ b/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_activity_task.class.php @@ -0,0 +1,108 @@ +. + +/** + * Class for restore BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php'); + +/** + * Restore task that provides all the settings and steps to perform one complete restore of the activity. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_bigbluebuttonbn_activity_task extends restore_activity_task +{ + /** + * Define (add) particular settings this activity can have. + * + * @return void + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have. + * + * @return void + */ + protected function define_my_steps() { + // BigBlueButtonBN only has one structure step. + $this->add_step(new restore_bigbluebuttonbn_activity_structure_step('bigbluebuttonbn_structure', 'bigbluebuttonbn.xml')); + } + + /** + * Define the contents in the activity that must be processed by the link decoder. + * + * @return array + */ + public static function define_decode_contents() { + $contents = []; + $contents[] = new restore_decode_content('bigbluebuttonbn', ['intro'], 'bigbluebuttonbn'); + $contents[] = new restore_decode_content('bigbluebuttonbn_logs', ['log'], 'bigbluebuttonbn_logs'); + $contents[] = new restore_decode_content('bigbluebuttonbn_recordings', ['importeddata'], 'bigbluebuttonbn_recordings'); + return $contents; + } + + /** + * Define the decoding rules for links belonging to the activity to be executed by the link decoder. + * + * @return array + */ + public static function define_decode_rules() { + $rules = []; + $rules[] = new restore_decode_rule('BIGBLUEBUTTONBNVIEWBYID', '/mod/bigbluebuttonbn/view.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('BIGBLUEBUTTONBNINDEX', '/mod/bigbluebuttonbn/index.php?id=$1', 'course'); + return $rules; + } + + /** + * Define the restoring rules for logs belonging to the activity to be executed by the link decoder. + * + * @return array + */ + public static function define_restore_log_rules() { + $rules = []; + $rules[] = new restore_log_rule('bigbluebuttonbn', 'add', 'view.php?id={course_module}', '{bigbluebuttonbn}'); + $rules[] = new restore_log_rule('bigbluebuttonbn', 'update', 'view.php?id={course_module}', '{bigbluebuttonbn}'); + $rules[] = new restore_log_rule('bigbluebuttonbn', 'view', 'view.php?id={course_module}', '{bigbluebuttonbn}'); + $rules[] = new restore_log_rule('bigbluebuttonbn', 'report', 'report.php?id={course_module}', '{bigbluebuttonbn}'); + return $rules; + } + + /** + * Define the restoring rules for course associated to the activity to be executed by the link decoder. + * + * @return array + */ + public static function define_restore_log_rules_for_course() { + $rules = []; + $rules[] = new restore_log_rule('bigbluebuttonbn', 'view all', 'index.php?id={course}', null); + return $rules; + } +} diff --git a/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php b/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php new file mode 100644 index 00000000000..0fcce5585f9 --- /dev/null +++ b/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php @@ -0,0 +1,116 @@ +. + +/** + * Class for the structure used for restore BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Define all the restore steps that will be used by the restore_url_activity_task. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_bigbluebuttonbn_activity_structure_step extends restore_activity_structure_step { + /** + * Structure step to restore one bigbluebuttonbn activity. + * + * @return array + */ + protected function define_structure() { + $paths = []; + $paths[] = new restore_path_element('bigbluebuttonbn', '/activity/bigbluebuttonbn'); + $paths[] = new restore_path_element('bigbluebuttonbn_logs', '/activity/bigbluebuttonbn/logs/log'); + $paths[] = new restore_path_element('bigbluebuttonbn_recordings', '/activity/bigbluebuttonbn/recordings/recording'); + // Return the paths wrapped into standard activity structure. + return $this->prepare_activity_structure($paths); + } + + /** + * Process a bigbluebuttonbn restore. + * + * @param array $data The data in object form + * @return void + */ + protected function process_bigbluebuttonbn(array $data) { + global $DB; + $data = (object) $data; + $data->course = $this->get_courseid(); + $data->timemodified = $this->apply_date_offset($data->timemodified); + // Insert the bigbluebuttonbn record. + $newitemid = $DB->insert_record('bigbluebuttonbn', $data); + // Immediately after inserting "activity" record, call this. + $this->apply_activity_instance($newitemid); + } + + /** + * Process a bigbluebuttonbn_logs restore (additional table). + * + * @param array $data The data in object form + * @return void + */ + protected function process_bigbluebuttonbn_logs(array $data) { + global $DB; + $data = (object) $data; + // Apply modifications. + $data->courseid = $this->get_mappingid('course', $data->courseid); + $data->bigbluebuttonbnid = $this->get_new_parentid('bigbluebuttonbn'); + $data->userid = $this->get_mappingid('user', $data->userid); + $data->timecreated = $this->apply_date_offset($data->timecreated); + // Insert the bigbluebuttonbn_logs record. + $newitemid = $DB->insert_record('bigbluebuttonbn_logs', $data); + // Immediately after inserting associated record, call this. + $this->set_mapping('bigbluebuttonbn_logs', $data->id, $newitemid); + } + + /** + * Process a bigbluebuttonbn_recordings restore (additional table). + * + * @param array $data The data in object form + * @return void + */ + protected function process_bigbluebuttonbn_recordings(array $data) { + global $DB; + $data = (object) $data; + // Apply modifications. + $data->courseid = $this->get_mappingid('course', $data->courseid); + $data->bigbluebuttonbnid = $this->get_new_parentid('bigbluebuttonbn'); + $data->timecreated = $this->apply_date_offset($data->timecreated); + // Insert the bigbluebuttonbn_recordings record. + $newitemid = $DB->insert_record('bigbluebuttonbn_recordings', $data); + // Immediately after inserting associated record, call this. + $this->set_mapping('bigbluebuttonbn_recordings', $data->id, $newitemid); + } + + /** + * Actions to be executed after the restore is completed + * + * @return array + */ + protected function after_execute() { + // Add bigbluebuttonbn related files, no need to match by itemname (just internally handled context). + $this->add_related_files('mod_bigbluebuttonbn', 'intro', null); + } +} diff --git a/mod/bigbluebuttonbn/bbb_broker.php b/mod/bigbluebuttonbn/bbb_broker.php new file mode 100644 index 00000000000..550d696b420 --- /dev/null +++ b/mod/bigbluebuttonbn/bbb_broker.php @@ -0,0 +1,70 @@ +. + +/** + * Intermediator for handling requests from the BigBlueButton server. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ + +// We should not have any require login or MOODLE_INTERNAL Check in this file. +// phpcs:disable moodle.Files.MoodleInternal.MoodleInternalGlobalState,moodle.Files.RequireLogin.Missing +require(__DIR__ . '/../../config.php'); + +use mod_bigbluebuttonbn\broker; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\meeting; + +global $PAGE, $USER, $CFG, $SESSION, $DB; + +$params = $_REQUEST; + +$broker = new broker(); +$error = $broker->validate_parameters($params); +if (!empty($error)) { + header('HTTP/1.0 400 Bad Request. ' . $error); + return; +} + +$action = $params['action']; + +$instance = instance::get_from_instanceid($params['bigbluebuttonbn']); +if (empty($instance)) { + header('HTTP/1.0 410 Gone. The activity may have been deleted'); + return; +} + +$PAGE->set_context($instance->get_context()); + +try { + switch (strtolower($action)) { + case 'recording_ready': + broker::recording_ready($instance, $params); + return; + case 'meeting_events': + // When meeting_events callback is implemented by BigBlueButton, Moodle receives a POST request + // which is processed in the function using super globals. + meeting::meeting_events($instance); + return; + } + header("HTTP/1.0 400 Bad request. The action '{$action}' does not exist"); +} catch (Exception $e) { + header('HTTP/1.0 500 Internal Server Error. ' . $e->getMessage()); +} diff --git a/mod/bigbluebuttonbn/bbb_view.php b/mod/bigbluebuttonbn/bbb_view.php new file mode 100644 index 00000000000..e0a506c4cd1 --- /dev/null +++ b/mod/bigbluebuttonbn/bbb_view.php @@ -0,0 +1,163 @@ +. + +/** + * View for BigBlueButton interaction. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +use core\notification; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use mod_bigbluebuttonbn\logger; +use mod_bigbluebuttonbn\meeting; +use mod_bigbluebuttonbn\plugin; +use mod_bigbluebuttonbn\recording; + +require_once(dirname(dirname(dirname(__FILE__))) . '/config.php'); + +global $SESSION, $PAGE, $CFG, $DB, $USER, $OUTPUT; + +$action = required_param('action', PARAM_TEXT); +$id = optional_param('id', 0, PARAM_INT); +$bn = optional_param('bn', 0, PARAM_INT); +$rid = optional_param('rid', '', PARAM_TEXT); +$rtype = optional_param('rtype', 'presentation', PARAM_TEXT); +$errors = optional_param('errors', '', PARAM_TEXT); +$timeline = optional_param('timeline', 0, PARAM_INT); +$index = optional_param('index', 0, PARAM_INT); +$group = optional_param('group', -1, PARAM_INT); + +// Get the bbb instance from either the cmid (id), or the instanceid (bn). +if ($id) { + $instance = instance::get_from_cmid($id); +} else { + if ($bn) { + $instance = instance::get_from_instanceid($bn); + } +} + +if (!$instance) { + throw new moodle_exception('view_error_url_missing_parameters', plugin::COMPONENT); +} + +$cm = $instance->get_cm(); +$course = $instance->get_course(); +$bigbluebuttonbn = $instance->get_instance_data(); +$context = $instance->get_context(); + +require_login($course, true, $cm); + +// Note : this uses the group optional_param as a value to decide which groupid. +$groupid = groups_get_activity_group($cm, true) ?: null; +if ($groupid) { + $instance->set_group_id($groupid); +} + +// Print the page header. +$PAGE->set_context($context); +$PAGE->set_url('/mod/bigbluebuttonbn/bbb_view.php', ['id' => $cm->id, 'bigbluebuttonbn' => $bigbluebuttonbn->id]); +$PAGE->set_title(format_string($bigbluebuttonbn->name)); +$PAGE->set_cacheable(false); +$PAGE->set_heading($course->fullname); +$PAGE->blocks->show_only_fake_blocks(); + +switch (strtolower($action)) { + case 'logout': + if (isset($errors) && $errors != '') { + $errors = (array) json_decode(urldecode($errors)); + $msgerrors = ''; + foreach ($errors as $error) { + $msgerrors .= html_writer::tag('p', $error->{'message'}, ['class' => 'alert alert-danger']) . "\n"; + } + throw new moodle_exception('view_error_bigbluebutton', 'bigbluebuttonbn', + $CFG->wwwroot . '/mod/bigbluebuttonbn/view.php?id=' . $id, $msgerrors, $errors); + } + + if (empty($bigbluebuttonbn)) { + echo get_string('view_message_tab_close', 'bigbluebuttonbn'); + die(); + } + // Moodle event logger: Create an event for meeting left. + logger::log_meeting_left_event($instance); + + // Update the cache. + $meeting = new meeting($instance); + $meeting->update_cache(); + + // Check the origin page. + $select = "userid = ? AND log = ?"; + $params = [ + 'userid' => $USER->id, + 'log' => logger::EVENT_JOIN, + ]; + $accesses = $DB->get_records_select('bigbluebuttonbn_logs', $select, $params, 'id ASC', 'id, meta', 1); + $lastaccess = end($accesses); + if (!empty($lastaccess->meta)) { + $lastaccess = json_decode($lastaccess->meta); + // If the user acceded from Timeline it should be redirected to the Dashboard. + if (isset($lastaccess->origin) && $lastaccess->origin == logger::ORIGIN_TIMELINE) { + redirect($CFG->wwwroot . '/my/'); + } + } + break; + case 'join': + if (empty($bigbluebuttonbn)) { + throw new moodle_exception('view_error_unable_join', 'bigbluebuttonbn'); + break; + } + // Check the origin page. + $origin = logger::ORIGIN_BASE; + if ($timeline) { + $origin = logger::ORIGIN_TIMELINE; + } else if ($index) { + $origin = logger::ORIGIN_INDEX; + } + + try { + $url = meeting::join_meeting($instance, $origin); + redirect($url); + } catch (server_not_available_exception $e) { + bigbluebutton_proxy::handle_server_not_available($instance); + } + // We should never reach this point. + break; + + case 'play': + $recording = recording::get_record(['id' => $rid]); + if ($href = $recording->get_remote_playback_url($rtype)) { + logger::log_recording_played_event($instance, $rid); + redirect(urldecode($href)); + } else { + notification::add(get_string('recordingurlnotfound', 'mod_bigbluebuttonbn'), notification::ERROR); + redirect($instance->get_view_url()); + } + // We should never reach this point. + break; +} +// When we reach this point, we can close the tab or window where BBB was opened. +echo $OUTPUT->header(); +// Behat does not like when we close the Windows as it is expecting to locate +// on click part of the pages (bug with selenium raising an exception). So this is a workaround. +if (!defined('BEHAT_SITE_RUNNING')) { + $PAGE->requires->js_call_amd('mod_bigbluebuttonbn/rooms', 'setupWindowAutoClose'); +} +echo $OUTPUT->footer(); diff --git a/mod/bigbluebuttonbn/classes/analytics/indicator/activity_base.php b/mod/bigbluebuttonbn/classes/analytics/indicator/activity_base.php new file mode 100644 index 00000000000..970e7a263fe --- /dev/null +++ b/mod/bigbluebuttonbn/classes/analytics/indicator/activity_base.php @@ -0,0 +1,47 @@ +. + +/** + * Activity base class. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\analytics\indicator; +use core_analytics\local\indicator\community_of_inquiry_activity; + +/** + * Activity base class. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class activity_base extends community_of_inquiry_activity { + + /** + * No need to fetch grades for resources. + * + * @return bool + */ + public function feedback_check_grades() { + // BigBlueButtonBN's feedback is not contained in grades. + return false; + } +} diff --git a/mod/bigbluebuttonbn/classes/analytics/indicator/cognitive_depth.php b/mod/bigbluebuttonbn/classes/analytics/indicator/cognitive_depth.php new file mode 100644 index 00000000000..da86f86db54 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/analytics/indicator/cognitive_depth.php @@ -0,0 +1,70 @@ +. + +/** + * Cognitive depth indicator - BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\analytics\indicator; + +use cm_info; +use lang_string; + +/** + * Cognitive depth indicator - bigbluebuttonbn. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cognitive_depth extends activity_base { + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return lang_string + */ + public static function get_name(): lang_string { + return new lang_string('indicator:cognitivedepth', 'mod_bigbluebuttonbn'); + } + + /** + * Returns the indicator type. + * + * @return string + */ + public function get_indicator_type() { + return self::INDICATOR_COGNITIVE; + } + + /** + * Returns the cognitive depth level. + * + * @param cm_info $cm + * + * @return int + */ + public function get_cognitive_depth_level(cm_info $cm) { + return self::COGNITIVE_LEVEL_4; + } +} diff --git a/mod/bigbluebuttonbn/classes/analytics/indicator/social_breadth.php b/mod/bigbluebuttonbn/classes/analytics/indicator/social_breadth.php new file mode 100644 index 00000000000..51f93a177b5 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/analytics/indicator/social_breadth.php @@ -0,0 +1,70 @@ +. + +/** + * Social breadth indicator - BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\analytics\indicator; + +use cm_info; +use lang_string; + +/** + * Social breadth indicator - BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class social_breadth extends activity_base { + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return lang_string + */ + public static function get_name(): lang_string { + return new lang_string('indicator:socialbreadth', 'mod_bigbluebuttonbn'); + } + + /** + * Returns the indicator type. + * + * @return string + */ + public function get_indicator_type() { + return self::INDICATOR_SOCIAL; + } + + /** + * Returns the social breadth level. + * + * @param cm_info $cm + * + * @return int + */ + public function get_social_breadth_level(cm_info $cm) { + return self::SOCIAL_LEVEL_1; + } +} diff --git a/mod/bigbluebuttonbn/classes/broker.php b/mod/bigbluebuttonbn/classes/broker.php new file mode 100644 index 00000000000..4310be2e802 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/broker.php @@ -0,0 +1,130 @@ +. + +namespace mod_bigbluebuttonbn; + +use Exception; +use Firebase\JWT\JWT; +use mod_bigbluebuttonbn\local\config; + +/** + * The broker routines + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class broker { + + /** @var array List of required params */ + protected $requiredparams = [ + 'recording_ready' => [ + 'bigbluebuttonbn' => 'The BigBlueButtonBN instance ID must be specified.', + 'signed_parameters' => 'A JWT encoded string must be included as [signed_parameters].' + ], + 'meeting_events' => [ + 'bigbluebuttonbn' => 'The BigBlueButtonBN instance ID must be specified.' + ], + ]; + + /** + * Validate the supplied list of parameters, providing feedback about any missing or incorrect values. + * + * @param array $params + * @return null|string + */ + public function validate_parameters(array $params): ?string { + if (!isset($params['action']) || empty($params['action']) ) { + return 'Parameter ['.$params['action'].'] was not included'; + } + + $action = strtolower($params['action']); + if (!array_key_exists($action, $this->requiredparams)) { + return "Action {$params['action']} can not be performed."; + } + return $this->validate_parameters_message($params, $this->requiredparams[$action]); + } + + /** + * Check whether the specified parameter is valid. + * + * @param array $params + * @param array $requiredparams + * @return null|string + */ + protected static function validate_parameters_message(array $params, array $requiredparams): ?string { + foreach ($requiredparams as $param => $message) { + if (!array_key_exists($param, $params) || $params[$param] == '') { + return $message; + } + } + + // Everything is valid. + return null; + } + + /** + * Helper for responding when recording ready is performed. + * + * @param instance $instance + * @param array $params + */ + public static function recording_ready(instance $instance, array $params): void { + // Decodes the received JWT string. + try { + $decodedparameters = JWT::decode( + $params['signed_parameters'], + config::get('shared_secret'), + ['HS256'] + ); + } catch (Exception $e) { + $error = 'Caught exception: ' . $e->getMessage(); + header('HTTP/1.0 400 Bad Request. ' . $error); + return; + } + + // Validations. + if (!isset($decodedparameters->record_id)) { + header('HTTP/1.0 400 Bad request. Missing record_id parameter'); + return; + } + + $recording = recording::get_record(['recordingid' => $decodedparameters->record_id]); + if (!isset($recording)) { + header('HTTP/1.0 400 Bad request. Invalid record_id'); + return; + } + + // Sends the messages. + try { + // We make sure messages are sent only once. + if ($recording->get('status') != recording::RECORDING_STATUS_NOTIFIED) { + $task = new \mod_bigbluebuttonbn\task\send_recording_ready_notification(); + $task->set_instance_id($instance->get_instance_id()); + + \core\task\manager::queue_adhoc_task($task); + + $recording->set('status', recording::RECORDING_STATUS_NOTIFIED); + $recording->update(); + } + header('HTTP/1.0 202 Accepted'); + } catch (Exception $e) { + $error = 'Caught exception: ' . $e->getMessage(); + header('HTTP/1.0 503 Service Unavailable. ' . $error); + } + } +} diff --git a/mod/bigbluebuttonbn/classes/completion/custom_completion.php b/mod/bigbluebuttonbn/classes/completion/custom_completion.php new file mode 100644 index 00000000000..6d141ba0eb8 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/completion/custom_completion.php @@ -0,0 +1,300 @@ +. + +namespace mod_bigbluebuttonbn\completion; + +use core_completion\activity_custom_completion; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\logger; +use moodle_exception; +use stdClass; + +/** + * Class custom_completion + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +class custom_completion extends activity_custom_completion { + + /** + * Get current state + * + * @param string $rule + * @return int + */ + public function get_state(string $rule): int { + // Get instance details. + $instance = instance::get_from_cmid($this->cm->id); + + if (empty($instance)) { + throw new moodle_exception("Can't find bigbluebuttonbn instance {$this->cm->instance}"); + } + + // Default return value. + $value = COMPLETION_INCOMPLETE; + $filters = $rule != "completionview" ? [logger::EVENT_SUMMARY] : [logger::EVENT_JOIN, logger::EVENT_PLAYED]; + $logs = logger::get_user_completion_logs($instance, $this->userid, $filters); + + if (method_exists($this, "get_{$rule}_value")) { + $valuecount = $this->count_actions($logs, self::class . "::get_{$rule}_value"); + if ($valuecount) { + if (!is_null($instance->get_instance_var($rule))) { + if ($instance->get_instance_var($rule) <= $valuecount) { + $value = COMPLETION_COMPLETE; + } + } else { + // If there is at least a hit, we consider it as complete. + $value = $valuecount ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; + } + } + } + return $value; + } + + /** + * Compute current state from logs. + * + * @param array $logs + * @param callable $logvaluegetter + * @return int the number of hits on this particular rule + */ + protected function count_actions(array $logs, callable $logvaluegetter): int { + if (empty($logs)) { + // As completion by engagement with $rulename hand was required, the activity hasn't been completed. + return 0; + } + + $valuecount = 0; + foreach ($logs as $log) { + $valuecount += $logvaluegetter($log); + } + + return $valuecount; + } + + /** + * Fetch the list of custom completion rules that this module defines. + * + * @return array + */ + public static function get_defined_custom_rules(): array { + return [ + 'completionattendance', + 'completionengagementchats', + 'completionengagementtalks', + 'completionengagementraisehand', + 'completionengagementpollvotes', + 'completionengagementemojis', + 'completionview' // Completion view is now a customrule as it depends on the logs and not + // the view action itself. + ]; + } + + /** + * Returns an associative array of the descriptions of custom completion rules. + * + * @return array + */ + public function get_custom_rule_descriptions(): array { + $completionengagementchats = $this->cm->customdata['customcompletionrules']['completionengagementchats'] ?? 1; + $completionengagementtalks = $this->cm->customdata['customcompletionrules']['completionengagementtalks'] ?? 1; + $completionengagementraisehand = $this->cm->customdata['customcompletionrules']['completionengagementraisehand'] ?? 1; + $completionengagementpollvotes = $this->cm->customdata['customcompletionrules']['completionengagementpollvotes'] ?? 1; + $completionengagementemojis = $this->cm->customdata['customcompletionrules']['completionengagementemojis'] ?? 1; + $completionattendance = $this->cm->customdata['customcompletionrules']['completionattendance'] ?? 1; + return [ + 'completionengagementchats' => get_string('completionengagementchats_desc', 'mod_bigbluebuttonbn', + $completionengagementchats), + 'completionengagementtalks' => get_string('completionengagementtalks_desc', 'mod_bigbluebuttonbn', + $completionengagementtalks), + 'completionengagementraisehand' => get_string('completionengagementraisehand_desc', 'mod_bigbluebuttonbn', + $completionengagementraisehand), + 'completionengagementpollvotes' => get_string('completionengagementpollvotes_desc', 'mod_bigbluebuttonbn', + $completionengagementpollvotes), + 'completionengagementemojis' => get_string('completionengagementemojis_desc', 'mod_bigbluebuttonbn', + $completionengagementemojis), + 'completionattendance' => get_string('completionattendance_desc', 'mod_bigbluebuttonbn', + $completionattendance), + ]; + } + + /** + * Returns an array of all completion rules, in the order they should be displayed to users. + * + * @return array + */ + public function get_sort_order(): array { + return [ + 'completionview', + 'completionengagementchats', + 'completionengagementtalks', + 'completionengagementraisehand', + 'completionengagementpollvotes', + 'completionengagementemojis', + 'completionattendance', + ]; + } + + /** + * Get current state in a friendly version + * + * @param string $rule + * @return string + */ + public function get_printable_state(string $rule): string { + // Get instance details. + $instance = instance::get_from_cmid($this->cm->id); + + if (empty($instance)) { + throw new moodle_exception("Can't find bigbluebuttonbn instance {$this->cm->instance}"); + } + + $summary = ""; + $filters = $rule != "completionview" ? [logger::EVENT_SUMMARY] : [logger::EVENT_JOIN, logger::EVENT_PLAYED]; + $logs = logger::get_user_completion_logs($instance, $this->userid, $filters); + + if (method_exists($this, "get_{$rule}_value")) { + $summary = get_string( + $rule . '_event_desc', + 'mod_bigbluebuttonbn', + $this->count_actions($logs, self::class . "::get_{$rule}_value") + ); + } + return $summary; + } + + /** + * Get current state in a friendly version + * + * @param string $rule + * @return string + */ + public function get_last_log_timestamp(string $rule): string { + // Get instance details. + $instance = instance::get_from_cmid($this->cm->id); + + if (empty($instance)) { + throw new moodle_exception("Can't find bigbluebuttonbn instance {$this->cm->instance}"); + } + $filters = $rule != "completionview" ? [logger::EVENT_SUMMARY] : [logger::EVENT_JOIN, logger::EVENT_PLAYED]; + return logger::get_user_completion_logs_max_timestamp($instance, $this->userid, $filters); + } + + /** + * Fetches the list of custom completion rules that are being used by this activity module instance. + * + * @return array + */ + public function get_available_custom_rules(): array { + $availablerules = parent::get_available_custom_rules(); + $availablerules[] = 'completionview'; // Completion view is now a customrule. + return $availablerules; + } + + /** + * Get completion view value + * + * This will override the usual completion value (see COMPLETION_CUSTOM_MODULE_FLOW) + * + * @param stdClass $log + * @return int + */ + protected static function get_completionview_value(stdClass $log): int { + return $log->log == logger::EVENT_PLAYED || $log->log == logger::EVENT_JOIN; + } + + /** + * Get attendance summary value + * + * @param stdClass $log + * @return int + */ + protected static function get_completionattendance_value(stdClass $log): int { + $summary = json_decode($log->meta); + if ($summary && !empty($summary->data->duration)) { + return COMPLETION_COMPLETE; + } + return COMPLETION_INCOMPLETE; + } + + /** + * Get general completion engagement value + * + * @param stdClass $log + * @return int + */ + protected static function get_completionengagementchats_value(stdClass $log): int { + return self::get_completionengagement_value($log, 'chats'); + } + + /** + * Get general completion engagement value + * + * @param stdClass $log + * @return int + */ + protected static function get_completionengagementtalks_value(stdClass $log): int { + return self::get_completionengagement_value($log, 'talks'); + } + + /** + * Get general completion engagement value + * + * @param stdClass $log + * @return int + */ + protected static function get_completionengagementraisehand_value(stdClass $log): int { + return self::get_completionengagement_value($log, 'raisehand'); + } + + /** + * Get general completion engagement value + * + * @param stdClass $log + * @return int + */ + protected static function get_completionengagementpollvotes_value(stdClass $log): int { + return self::get_completionengagement_value($log, 'pollvotes'); + } + + /** + * Get general completion engagement value + * + * @param stdClass $log + * @return int + */ + protected static function get_completionengagementemojis_value(stdClass $log): int { + return self::get_completionengagement_value($log, 'emojis'); + } + + /** + * Get general completion engagement value + * + * @param stdClass $log + * @param string $type + * @return int + */ + protected static function get_completionengagement_value(stdClass $log, string $type): int { + $summary = json_decode($log->meta); + if ($summary && !empty($summary->data->engagement->$type)) { + return COMPLETION_COMPLETE; + } + return COMPLETION_INCOMPLETE; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/activity_management_viewed.php b/mod/bigbluebuttonbn/classes/event/activity_management_viewed.php new file mode 100644 index 00000000000..89c09b155dd --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/activity_management_viewed.php @@ -0,0 +1,55 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn activity management viewed event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010-2017 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later + */ +class activity_management_viewed extends base { + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' viewed the bigbluebuttonbn activity management page for ". + "the course module id '##contextinstanceid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return 'BigBlueButtonBN activity management viewed'; + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/activity_viewed.php b/mod/bigbluebuttonbn/classes/event/activity_viewed.php new file mode 100644 index 00000000000..a613292aa9e --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/activity_viewed.php @@ -0,0 +1,56 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn activity viewed event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity_viewed extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' viewed the bigbluebuttonbn activity " . + "with id '##objectid' for the course id '##courseid'."; + } + + /** + * Return event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_activity_viewed', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/base.php b/mod/bigbluebuttonbn/classes/event/base.php new file mode 100644 index 00000000000..7b012fc37f3 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/base.php @@ -0,0 +1,133 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn abstract base event class. Most mod_bigbluebuttonbn events can extend this class. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base extends \core\event\base { + + /** @var $bigbluebuttonbn */ + protected $bigbluebuttonbn; + + /** + * Description. + * + * @var string + */ + protected $description; + + /** + * Object Id Mapping. + * + * @var array + */ + protected static $objectidmapping = ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + + /** + * Legacy log data. + * + * @var array + */ + protected $legacylogdata; + + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + $this->data['crud'] = $crud; + $this->data['edulevel'] = $edulevel; + $this->data['objecttable'] = 'bigbluebuttonbn'; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $vars = [ + 'userid' => $this->userid, + 'courseid' => $this->courseid, + 'objectid' => $this->objectid, + 'contextinstanceid' => $this->contextinstanceid, + 'other' => $this->other + ]; + $string = $this->description; + foreach ($vars as $key => $value) { + $string = str_replace("##" . $key, $value, $string); + } + return $string; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/bigbluebuttonbn/view.php', ['id' => $this->contextinstanceid]); + } + + /** + * Sets the legacy event log data. + * + * @param string $action The current action + * @param string $info A detailed description of the change. But no more than 255 characters. + * @param string $url The url to the assign module instance. + */ + public function set_legacy_logdata($action = '', $info = '', $url = '') { + $fullurl = 'view.php?id=' . $this->contextinstanceid; + if ($url != '') { + $fullurl .= '&' . $url; + } + + $this->legacylogdata = [$this->courseid, 'bigbluebuttonbn', $action, $fullurl, $info, $this->contextinstanceid]; + } + + /** + * Return legacy data for add_to_log(). + * + * @return array + */ + protected function get_legacy_logdata() { + if (isset($this->legacylogdata)) { + return $this->legacylogdata; + } + + return null; + } + + /** + * Custom validation. + * + * @throws \coding_exception + */ + protected function validate_data() { + parent::validate_data(); + + if ($this->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Context level must be CONTEXT_MODULE.'); + } + } +} diff --git a/mod/bigbluebuttonbn/classes/event/events.php b/mod/bigbluebuttonbn/classes/event/events.php new file mode 100644 index 00000000000..22570514232 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/events.php @@ -0,0 +1,51 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn class for event name definition. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class events { + + /** + * Event name matcher. + * @var $events + */ + public static $events = [ + 'create' => 'activity_created', + 'view' => 'activity_viewed', + 'update' => 'activity_updated', + 'delete' => 'activity_deleted', + 'meeting_create' => 'meeting_created', + 'meeting_end' => 'meeting_ended', + 'meeting_join' => 'meeting_joined', + 'meeting_left' => 'meeting_left', + 'recording_delete' => 'recording_deleted', + 'recording_import' => 'recording_imported', + 'recording_protect' => 'recording_protected', + 'recording_publish' => 'recording_published', + 'recording_unprotect' => 'recording_unprotected', + 'recording_unpublish' => 'recording_unpublished', + 'recording_edit' => 'recording_edited', + 'recording_play' => 'recording_viewed', + 'live_session' => 'live_session' + ]; +} diff --git a/mod/bigbluebuttonbn/classes/event/live_session_event.php b/mod/bigbluebuttonbn/classes/event/live_session_event.php new file mode 100644 index 00000000000..cfeaea2dec8 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/live_session_event.php @@ -0,0 +1,57 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn live_session (Experimental: for being triggered when external events are received). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class live_session_event extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' triggered action ##other in a ". + "bigbluebutton meeting for the bigbluebuttonbn activity with id ". + "'##objectid' for the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_live_session', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/meeting_created.php b/mod/bigbluebuttonbn/classes/event/meeting_created.php new file mode 100644 index 00000000000..ac6b70267ce --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/meeting_created.php @@ -0,0 +1,56 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn meeting created event, triggered when the meeting is created before join. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting_created extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' created a bigbluebutton meeting for ". + "the bigbluebuttonbn activity with id '##objectid' for the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_meeting_created', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/meeting_ended.php b/mod/bigbluebuttonbn/classes/event/meeting_ended.php new file mode 100644 index 00000000000..6a6d1544ffc --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/meeting_ended.php @@ -0,0 +1,57 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn meeting ended event, triggered when the meeting is ended by the user. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting_ended extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "A bigbluebutton meeting for the bigbluebuttonbn activity with id ". + "'##objectid' for the course id '##courseid' has been forcibly ". + "ended by the user with id '##userid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_meeting_ended', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/meeting_joined.php b/mod/bigbluebuttonbn/classes/event/meeting_joined.php new file mode 100644 index 00000000000..3737a459b42 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/meeting_joined.php @@ -0,0 +1,57 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn meeting joined event, triggered when the user joins the session. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting_joined extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has joined a bigbluebutton meeting for ". + "the bigbluebuttonbn activity with id '##objectid' for the course id ". + "'##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_meeting_joined', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/meeting_left.php b/mod/bigbluebuttonbn/classes/event/meeting_left.php new file mode 100644 index 00000000000..8036a0b3092 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/meeting_left.php @@ -0,0 +1,57 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn meeting left event, triggered when the user lefts the meeting using the logout button. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting_left extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has left a bigbluebutton meeting for ". + "the bigbluebuttonbn activity with id '##objectid' for the course id ". + "'##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_meeting_left', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_deleted.php b/mod/bigbluebuttonbn/classes/event/recording_deleted.php new file mode 100644 index 00000000000..0f3ba9ca23f --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_deleted.php @@ -0,0 +1,56 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn recording deleted event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_deleted extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has deleted a recording with id ". + "'##other' from the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_deleted', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_edited.php b/mod/bigbluebuttonbn/classes/event/recording_edited.php new file mode 100644 index 00000000000..2caea232963 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_edited.php @@ -0,0 +1,56 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn recording edited event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_edited extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has edited a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_edited', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_imported.php b/mod/bigbluebuttonbn/classes/event/recording_imported.php new file mode 100644 index 00000000000..f66109b858e --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_imported.php @@ -0,0 +1,56 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn recording imported event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_imported extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has imported a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_imported', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_protected.php b/mod/bigbluebuttonbn/classes/event/recording_protected.php new file mode 100644 index 00000000000..267c097d70d --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_protected.php @@ -0,0 +1,55 @@ +. +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn recording protected event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_protected extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has protected a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_protected', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_published.php b/mod/bigbluebuttonbn/classes/event/recording_published.php new file mode 100644 index 00000000000..873620355cb --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_published.php @@ -0,0 +1,56 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn recording published event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_published extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has published a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_published', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_unprotected.php b/mod/bigbluebuttonbn/classes/event/recording_unprotected.php new file mode 100644 index 00000000000..1236520249d --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_unprotected.php @@ -0,0 +1,56 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn recording unprotected event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_unprotected extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has unprotected a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_unprotected', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_unpublished.php b/mod/bigbluebuttonbn/classes/event/recording_unpublished.php new file mode 100644 index 00000000000..c9f0d67ec8a --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_unpublished.php @@ -0,0 +1,56 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn recording unpublished event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_unpublished extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has unpublished a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_unpublished', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_viewed.php b/mod/bigbluebuttonbn/classes/event/recording_viewed.php new file mode 100644 index 00000000000..303aab0ef79 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_viewed.php @@ -0,0 +1,56 @@ +. + +namespace mod_bigbluebuttonbn\event; + +/** + * The mod_bigbluebuttonbn recording viewed event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_viewed extends base +{ + /** + * Init method. + * @param string $crud + * @param int $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has viewed a recording with id ". + "'##other' from the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_viewed', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return array + */ + public static function get_objectid_mapping() { + return ['db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn']; + } +} diff --git a/mod/bigbluebuttonbn/classes/external/can_join.php b/mod/bigbluebuttonbn/classes/external/can_join.php new file mode 100644 index 00000000000..b81fc4df57c --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/can_join.php @@ -0,0 +1,111 @@ +. + +namespace mod_bigbluebuttonbn\external; +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\meeting; +use restricted_context_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to check whether a user can join a meeting. + * + * This is mainly used by the mobile application. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class can_join extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'cmid' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED), + 'groupid' => new external_value(PARAM_INT, 'bigbluebuttonbn group id', VALUE_DEFAULT, 0), + ]); + } + + /** + * Updates a recording + * + * @param int $cmid the bigbluebuttonbn course module id + * @param null|int $groupid + * @return array (empty array for now) + * @throws \restricted_context_exception + */ + public static function execute( + int $cmid, + ?int $groupid = 0 + ): array { + // Validate the cmid ID. + [ + 'cmid' => $cmid, + 'groupid' => $groupid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'cmid' => $cmid, + 'groupid' => $groupid, + ]); + + $result = [ + 'can_join' => false, + 'cmid' => $cmid, + ]; + + $instance = instance::get_from_cmid($cmid); + if (empty($instance)) { + return $result; + } + // Validate the groupid. + if (!groups_group_visible($groupid, $instance->get_course(), $instance->get_cm())) { + throw new restricted_context_exception(); + } + $instance->set_group_id($groupid); + + self::validate_context($instance->get_context()); + + $meeting = new meeting($instance); + + $result['can_join'] = $meeting->can_join(); + + return $result; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + * @since Moodle 3.3 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'can_join' => new external_value(PARAM_BOOL, 'Can join session'), + 'cmid' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED), + ]); + } +} diff --git a/mod/bigbluebuttonbn/classes/external/completion_validate.php b/mod/bigbluebuttonbn/classes/external/completion_validate.php new file mode 100644 index 00000000000..599c531b0cd --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/completion_validate.php @@ -0,0 +1,115 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to validate completion. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class completion_validate extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), + ]); + } + + /** + * Mark activity as complete + * + * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id + * @return array (empty array for now) + */ + public static function execute( + int $bigbluebuttonbnid + ): array { + // Validate the bigbluebuttonbnid ID. + [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + ]); + $result = ['warnings' => []]; + // Fetch the session, features, and profile. + $instance = instance::get_from_instanceid($bigbluebuttonbnid); + if ($instance) { + $context = $instance->get_context(); + + // Validate that the user has access to this activity. + self::validate_context($context); + + // Get list with all the users enrolled in the course. + [$sort, $sqlparams] = users_order_by_sql('u'); + if (has_capability('moodle/course:update', $context)) { + $users = get_enrolled_users($context, 'mod/bigbluebuttonbn:view', 0, 'u.*', $sort); + foreach ($users as $user) { + // Enqueue a task for processing the completion. + bigbluebutton_proxy::enqueue_completion_event($instance->get_instance_data(), $user->id); + } + } else { + $result['warnings'][] = [ + 'item' => 'mod_bigbluebuttonbn', + 'itemid' => $instance->get_instance_id(), + 'warningcode' => 'nopermissions', + 'message' => get_string('nopermissions', 'error', 'completion_validate') + ]; + } + } else { + $result['warnings'][] = [ + 'item' => 'mod_bigbluebuttonbn', + 'itemid' => $bigbluebuttonbnid, + 'warningcode' => 'indexerrorbbtn', + 'message' => get_string('index_error_bbtn', 'mod_bigbluebuttonbn', $bigbluebuttonbnid) + ]; + } + // We might want to return a status here or some warnings. + return $result; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + * @since Moodle 3.0 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'warnings' => new \external_warnings() + ] + ); + } +} diff --git a/mod/bigbluebuttonbn/classes/external/end_meeting.php b/mod/bigbluebuttonbn/classes/external/end_meeting.php new file mode 100644 index 00000000000..ab7d4507add --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/end_meeting.php @@ -0,0 +1,129 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use core\notification; +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\bigbluebutton; +use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; +use mod_bigbluebuttonbn\logger; +use mod_bigbluebuttonbn\meeting; +use restricted_context_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to end a meeting. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class end_meeting extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), + 'groupid' => new external_value(PARAM_INT, 'bigbluebuttonbn group id', VALUE_DEFAULT, 0), + ]); + } + + /** + * Updates a recording + * + * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id + * @param int $groupid the groupid (either 0 or the groupid) + * @return array (empty array for now) + * @throws \restricted_context_exception + */ + public static function execute( + int $bigbluebuttonbnid, + int $groupid + ): array { + // Validate the bigbluebuttonbnid ID. + [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + 'groupid' => $groupid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + 'groupid' => $groupid, + ]); + + // Fetch the session, features, and profile. + $instance = instance::get_from_instanceid($bigbluebuttonbnid); + if (empty($instance)) { + throw new \moodle_exception('Unknown Instance'); + } + if (!groups_group_visible($groupid, $instance->get_course(), $instance->get_cm())) { + throw new restricted_context_exception(); + } + $instance->set_group_id($groupid); + $context = $instance->get_context(); + + // Validate that the user has access to this activity and to manage recordings. + self::validate_context($context); + + if (!$instance->user_can_end_meeting()) { + throw new restricted_context_exception(); + } + // Execute the end command. + $meeting = new meeting($instance); + try { + $meeting->end_meeting(); + } catch (bigbluebutton_exception $e) { + return [ + 'warnings' => [ + [ + 'item' => $instance->get_meeting_name(), + 'itemid' => $instance->get_instance_id(), + 'warningcode' => 'notFound', + 'message' => $e->getMessage() + ] + ] + ]; + } + logger::log_meeting_ended_event($instance); + // Update the cache. + $meeting->update_cache(); + notification::add(get_string('end_session_notification', 'mod_bigbluebuttonbn'), notification::INFO); + return []; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + * @since Moodle 3.0 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'warnings' => new \external_warnings() + ]); + } +} diff --git a/mod/bigbluebuttonbn/classes/external/get_bigbluebuttonbns_by_courses.php b/mod/bigbluebuttonbn/classes/external/get_bigbluebuttonbns_by_courses.php new file mode 100644 index 00000000000..8edce2e9c42 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/get_bigbluebuttonbns_by_courses.php @@ -0,0 +1,139 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use context_module; +use external_api; +use external_files; +use external_format_value; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_util; +use external_value; +use external_warnings; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to get activity per course + * + * This is mainly used by the mobile application. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_bigbluebuttonbns_by_courses extends external_api { + /** + * Describes the parameters for get_bigbluebuttonbns_by_courses. + * + * @return external_function_parameters + * @since Moodle 3.11 + */ + public static function execute_parameters() { + return new external_function_parameters([ + 'courseids' => new external_multiple_structure( + new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, [] + ), + ] + ); + } + + /** + * Returns a list of bigbluebuttonbns in a provided list of courses. + * If no list is provided all bigbluebuttonbns that the user can view will be returned. + * + * @param array $courseids course ids + * @return array of warnings and bigbluebuttonbns + * @since Moodle 3.11 + */ + public static function execute($courseids = []) { + global $USER; + $warnings = []; + $returnedbigbluebuttonbns = []; + + ['courseids' => $courseids] = self::validate_parameters(self::execute_parameters(), ['courseids' => $courseids]); + $mycourses = []; + if (empty($courseids)) { + $mycourses = enrol_get_my_courses(); + $courseids = array_keys($mycourses); + } + + // Ensure there are courseids to loop through. + if (!empty($courseids)) { + [$courses, $warnings] = external_util::validate_courses($courseids, $mycourses); + + // Get the bigbluebuttonbns in this course, this function checks users visibility permissions. + // We can avoid then additional validate_context calls. + $bigbluebuttonbns = get_all_instances_in_courses("bigbluebuttonbn", $courses, $USER->id); + foreach ($bigbluebuttonbns as $bigbluebuttonbn) { + $context = context_module::instance($bigbluebuttonbn->coursemodule); + // Entry to return. + $bigbluebuttonbn->name = external_format_string($bigbluebuttonbn->name, $context->id); + + [$bigbluebuttonbn->intro, $bigbluebuttonbn->introformat] = external_format_text($bigbluebuttonbn->intro, + $bigbluebuttonbn->introformat, $context->id, 'mod_bigbluebuttonbn', 'intro', null); + $bigbluebuttonbn->introfiles = external_util::get_area_files($context->id, + 'mod_bigbluebuttonbn', 'intro', false, false); + + $returnedbigbluebuttonbns[] = $bigbluebuttonbn; + } + } + + $result = [ + 'bigbluebuttonbns' => $returnedbigbluebuttonbns, + 'warnings' => $warnings + ]; + return $result; + } + + /** + * Describes the get_bigbluebuttonbns_by_courses return value. + * + * @return external_single_structure + * @since Moodle 3.11 + */ + public static function execute_returns() { + return new external_single_structure([ + 'bigbluebuttonbns' => new external_multiple_structure( + new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'Module id'), + 'coursemodule' => new external_value(PARAM_INT, 'Course module id'), + 'course' => new external_value(PARAM_INT, 'Course id'), + 'name' => new external_value(PARAM_RAW, 'Name'), + 'intro' => new external_value(PARAM_RAW, 'Description'), + 'meetingid' => new external_value(PARAM_RAW, 'Meeting id'), + 'introformat' => new external_format_value('intro', 'Summary format'), + 'introfiles' => new external_files('Files in the introduction text'), + 'timemodified' => new external_value(PARAM_INT, 'Last time the instance was modified'), + 'section' => new external_value(PARAM_INT, 'Course section id'), + 'visible' => new external_value(PARAM_INT, 'Module visibility'), + 'groupmode' => new external_value(PARAM_INT, 'Group mode'), + 'groupingid' => new external_value(PARAM_INT, 'Grouping id'), + ] + ) + ), + 'warnings' => new external_warnings(), + ] + ); + } +} diff --git a/mod/bigbluebuttonbn/classes/external/get_join_url.php b/mod/bigbluebuttonbn/classes/external/get_join_url.php new file mode 100644 index 00000000000..8a2b8fe49d4 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/get_join_url.php @@ -0,0 +1,115 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\exceptions\meeting_join_exception; +use mod_bigbluebuttonbn\meeting; +use restricted_context_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to create the meeting (if needed), check user limit, and return the join URL when we can join. + * + * This is mainly used by the mobile application. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_join_url extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'cmid' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED), + 'groupid' => new external_value(PARAM_INT, 'bigbluebuttonbn group id', VALUE_DEFAULT, 0), + ]); + } + + /** + * Updates a recording + * + * @param int $cmid the bigbluebuttonbn course module id + * @param null|int $groupid + * @return array (empty array for now) + */ + public static function execute( + int $cmid, + ?int $groupid = 0 + ): array { + // Validate the cmid ID. + [ + 'cmid' => $cmid, + 'groupid' => $groupid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'cmid' => $cmid, + 'groupid' => $groupid, + ]); + $result = ['warnings' => []]; + + $instance = instance::get_from_cmid($cmid); + if (empty($instance)) { + throw new \moodle_exception('nosuchinstance', 'mod_bigbluebuttonbn', null, + ['entity' => get_string('module', 'course'), 'id' => $cmid]); + } + // Validate the groupid. + if (!groups_group_visible($groupid, $instance->get_course(), $instance->get_cm())) { + throw new restricted_context_exception(); + } + $instance->set_group_id($groupid); + + self::validate_context($instance->get_context()); + + try { + $result['join_url'] = meeting::join_meeting($instance); + } catch (meeting_join_exception $e) { + $result['warnings'][] = [ + 'item' => 'mod_bigbluebuttonbn', + 'itemid' => $instance->get_instance_id(), + 'warningcode' => $e->errorcode, + 'message' => $e->getMessage() + ]; + } + return $result; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + * @since Moodle 3.3 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'join_url' => new external_value(PARAM_RAW, 'Can join session', VALUE_OPTIONAL), + 'warnings' => new \external_warnings() + ]); + } +} diff --git a/mod/bigbluebuttonbn/classes/external/get_recordings.php b/mod/bigbluebuttonbn/classes/external/get_recordings.php new file mode 100644 index 00000000000..b6b45df1902 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/get_recordings.php @@ -0,0 +1,159 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use external_warnings; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use restricted_context_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to fetch a list of recordings from the BBB service. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_recordings extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), + 'tools' => new external_value(PARAM_RAW, 'a set of enabled tools', VALUE_DEFAULT, + 'protect,unprotect,publish,unpublish,delete'), + 'groupid' => new external_value(PARAM_INT, 'Group ID', VALUE_DEFAULT, null), + ]); + } + + /** + * Get a list of recordings + * + * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id to which the recordings are referred. + * @param string|null $tools + * @param int|null $groupid + * @return array of warnings and status result + * @throws \webservice_access_exception + */ + public static function execute( + int $bigbluebuttonbnid = 0, + ?string $tools = 'protect,unprotect,publish,unpublish,delete', + ?int $groupid = null + ): array { + global $USER; + + $returnval = [ + 'status' => false, + 'warnings' => [], + ]; + + // Validate the bigbluebuttonbnid ID. + [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + 'tools' => $tools, + 'groupid' => $groupid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + 'tools' => $tools, + 'groupid' => $groupid, + ]); + + $tools = explode(',', $tools ?? 'protect,unprotect,publish,unpublish,delete'); + + // Fetch the session, features, and profile. + $instance = instance::get_from_instanceid($bigbluebuttonbnid); + if (!$instance) { + $returnval['warnings'][] = [ + 'item' => $bigbluebuttonbnid, + 'warningcode' => 'nosuchinstance', + 'message' => get_string('nosuchinstance', 'mod_bigbluebuttonbn', + (object) ['id' => $bigbluebuttonbnid, 'entity' => 'bigbluebuttonbn']) + ]; + } else { + $typeprofiles = bigbluebutton_proxy::get_instance_type_profiles(); + $profilefeature = $typeprofiles[$instance->get_type()]['features']; + $showrecordings = in_array('all', $profilefeature) || in_array('showrecordings', $profilefeature); + if ($showrecordings) { + $context = $instance->get_context(); + // Validate that the user has access to this activity. + self::validate_context($context); + if (!$instance->user_has_group_access($USER, $groupid)) { + new restricted_context_exception(); + } + if ($groupid) { + $instance->set_group_id($groupid); + } + $recordings = $instance->get_recordings([], $instance->get_instance_var('recordings_deleted')); + $tabledata = recording_data::get_recording_table($recordings, $tools, $instance); + + $returnval['tabledata'] = $tabledata; + $returnval['status'] = true; + } else { + $returnval['warnings'][] = [ + 'item' => $bigbluebuttonbnid, + 'warningcode' => 'instanceprofilewithoutrecordings', + 'message' => get_string('instanceprofilewithoutrecordings', 'mod_bigbluebuttonbn') + ]; + } + } + return $returnval; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + * @since Moodle 3.0 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'status' => new external_value(PARAM_BOOL, 'Whether the fetch was successful'), + 'tabledata' => new external_single_structure([ + 'activity' => new external_value(PARAM_ALPHANUMEXT), + 'ping_interval' => new external_value(PARAM_INT), + 'locale' => new external_value(PARAM_TEXT), + 'profile_features' => new external_multiple_structure(new external_value(PARAM_TEXT)), + 'columns' => new external_multiple_structure(new external_single_structure([ + 'key' => new external_value(PARAM_ALPHA), + 'label' => new external_value(PARAM_TEXT), + 'width' => new external_value(PARAM_ALPHANUMEXT), + // See https://datatables.net/reference/option/columns.type . + 'type' => new external_value(PARAM_ALPHANUMEXT, 'Column type', VALUE_OPTIONAL), + 'sortable' => new external_value(PARAM_BOOL, 'Whether this column is sortable', VALUE_OPTIONAL, false), + 'allowHTML' => new external_value(PARAM_BOOL, 'Whether this column contains HTML', VALUE_OPTIONAL, false), + ])), + 'data' => new external_value(PARAM_RAW), // For now it will be json encoded. + ], '', VALUE_OPTIONAL), + 'warnings' => new external_warnings() + ]); + } +} diff --git a/mod/bigbluebuttonbn/classes/external/get_recordings_to_import.php b/mod/bigbluebuttonbn/classes/external/get_recordings_to_import.php new file mode 100644 index 00000000000..73d79cb2127 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/get_recordings_to_import.php @@ -0,0 +1,206 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use external_warnings; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data; +use mod_bigbluebuttonbn\recording; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to fetch a list of recordings from the BBB service. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_recordings_to_import extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'destinationinstanceid' => new external_value( + PARAM_INT, + 'Id of the other BBB we target for importing recordings into. + The idea here is to remove already imported recordings.', + VALUE_REQUIRED + ), + 'sourcebigbluebuttonbnid' => new external_value(PARAM_INT, + 'bigbluebuttonbn instance id', + VALUE_DEFAULT, + 0), + 'sourcecourseid' => new external_value(PARAM_INT, + 'source courseid to filter by', + VALUE_DEFAULT, + 0), + 'tools' => new external_value(PARAM_RAW, 'a set of enabled tools', VALUE_DEFAULT, + 'protect,unprotect,publish,unpublish,delete'), + 'groupid' => new external_value(PARAM_INT, 'Group ID', VALUE_DEFAULT, null), + ]); + } + + /** + * Get a list of recordings + * + * @param int $destinationinstanceid the bigbluebuttonbn instance id where recordings have been already imported. + * @param int $sourcebigbluebuttonbnid the bigbluebuttonbn instance id to which the recordings are referred. + * @param int $sourcecourseid the source courseid to filter by + * @param string|null $tools + * @param int|null $groupid + * @return array of warnings and status result + * @throws \webservice_access_exception + */ + public static function execute( + int $destinationinstanceid, + ?int $sourcebigbluebuttonbnid = 0, + ?int $sourcecourseid = 0, + ?string $tools = 'protect,unprotect,publish,unpublish,delete', + ?int $groupid = null + ): array { + global $USER, $DB; + + $returnval = [ + 'status' => false, + 'warnings' => [], + ]; + + // Validate the sourcebigbluebuttonbnid ID. + [ + 'destinationinstanceid' => $destinationinstanceid, + 'sourcebigbluebuttonbnid' => $sourcebigbluebuttonbnid, + 'sourcecourseid' => $sourcecourseid, + 'tools' => $tools, + 'groupid' => $groupid + ] = self::validate_parameters(self::execute_parameters(), [ + 'destinationinstanceid' => $destinationinstanceid, + 'sourcebigbluebuttonbnid' => $sourcebigbluebuttonbnid, + 'sourcecourseid' => $sourcecourseid, + 'tools' => $tools, + 'groupid' => $groupid + ]); + + $tools = explode(',', $tools ?? 'protect,unprotect,publish,unpublish,delete'); + + // Fetch the session, features, and profile. + $sourceinstance = null; + $sourcecourse = null; + if ($sourcecourseid) { + $sourcecourse = $DB->get_record('course', ['id' => $sourcecourseid], '*', MUST_EXIST); + } + + if (!empty($sourcebigbluebuttonbnid)) { + $sourceinstance = instance::get_from_instanceid($sourcebigbluebuttonbnid); + if (!$sourceinstance) { + throw new \invalid_parameter_exception('Source Bigbluebutton Id is invalid'); + } + $sourcecourse = $sourceinstance->get_course(); + // Validate that the user has access to this activity. + self::validate_context($sourceinstance->get_context()); + } + $destinstance = instance::get_from_instanceid($destinationinstanceid); + // Validate that the user has access to this activity. + self::validate_context($destinstance->get_context()); + if (!$destinstance->user_has_group_access($USER, $groupid)) { + throw new \invalid_parameter_exception('Invalid group for this user ' . $groupid); + } + if ($groupid) { + $destinstance->set_group_id($groupid); + } + // Exclude itself from the list if in import mode. + $excludedids = [$destinstance->get_instance_id()]; + if ($sourceinstance) { + $recordings = $sourceinstance->get_recordings($excludedids); + } else { + // There is a course id or a 0, so we fetch all recording including deleted recordings from this course. + $recordings = recording::get_recordings_for_course( + $sourcecourseid, + $excludedids, + true, + false, + ($sourcecourseid == 0 || $sourcebigbluebuttonbnid == 0), + ($sourcecourseid == 0 || $sourcebigbluebuttonbnid == 0) + ); + } + + if ($destinationinstanceid) { + // Remove recording already imported in this specific activity. + $destinationinstance = instance::get_from_instanceid($destinationinstanceid); + $importedrecordings = recording::get_recordings_for_instance( + $destinationinstance, + true, + true + ); + // Unset from $recordings if recording is already imported. + // Recording $recordings are indexed by $id (moodle table column id). + foreach ($recordings as $index => $recording) { + foreach ($importedrecordings as $irecord) { + if ($irecord->get('recordingid') == $recording->get('recordingid')) { + unset($recordings[$index]); + } + } + } + } + $tabledata = recording_data::get_recording_table($recordings, $tools, $sourceinstance, $sourcecourseid); + $returnval['tabledata'] = $tabledata; + $returnval['status'] = true; + + return $returnval; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + * @since Moodle 3.0 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'status' => new external_value(PARAM_BOOL, 'Whether the fetch was successful'), + 'tabledata' => new external_single_structure([ + 'activity' => new external_value(PARAM_ALPHANUMEXT), + 'ping_interval' => new external_value(PARAM_INT), + 'locale' => new external_value(PARAM_TEXT), + 'profile_features' => new external_multiple_structure(new external_value(PARAM_TEXT)), + 'columns' => new external_multiple_structure(new external_single_structure([ + 'key' => new external_value(PARAM_ALPHA), + 'label' => new external_value(PARAM_TEXT), + 'width' => new external_value(PARAM_ALPHANUMEXT), + // See https://datatables.net/reference/option/columns.type . + 'type' => new external_value(PARAM_ALPHANUMEXT, 'Column type', VALUE_OPTIONAL), + 'sortable' => new external_value(PARAM_BOOL, 'Whether this column is sortable', VALUE_OPTIONAL, false), + 'allowHTML' => new external_value(PARAM_BOOL, 'Whether this column contains HTML', VALUE_OPTIONAL, false), + ])), + 'data' => new external_value(PARAM_RAW), // For now it will be json encoded. + ], '', VALUE_OPTIONAL), + 'warnings' => new external_warnings() + ]); + } +} diff --git a/mod/bigbluebuttonbn/classes/external/meeting_info.php b/mod/bigbluebuttonbn/classes/external/meeting_info.php new file mode 100644 index 00000000000..ffc18d0d96d --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/meeting_info.php @@ -0,0 +1,137 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use mod_bigbluebuttonbn\meeting; +use restricted_context_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to fetch meeting information. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting_info extends external_api { + + /** + * Returns description of method parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), + 'groupid' => new external_value(PARAM_INT, 'bigbluebuttonbn group id', VALUE_DEFAULT, 0), + 'updatecache' => new external_value(PARAM_BOOL, 'update cache ?', VALUE_DEFAULT, false), + ]); + } + + /** + * Fetch meeting information. + * + * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id + * @param int $groupid + * @param bool $updatecache + * @return array + */ + public static function execute( + int $bigbluebuttonbnid, + int $groupid, + bool $updatecache = false + ): array { + // Validate the bigbluebuttonbnid ID. + [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + 'groupid' => $groupid, + 'updatecache' => $updatecache, + ] = self::validate_parameters(self::execute_parameters(), [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + 'groupid' => $groupid, + 'updatecache' => $updatecache, + ]); + + // Fetch the session, features, and profile. + $instance = instance::get_from_instanceid($bigbluebuttonbnid); + $instance->set_group_id($groupid); + if (!groups_group_visible($groupid, $instance->get_course(), $instance->get_cm())) { + throw new restricted_context_exception(); + } + $context = $instance->get_context(); + + // Validate that the user has access to this activity and to manage recordings. + self::validate_context($context); + + // Check if the BBB server is working. + $serverversion = bigbluebutton_proxy::get_server_version(); + if ($serverversion === null) { + throw new \moodle_exception('general_error_no_answer', + bigbluebutton_proxy::get_server_not_available_message($instance)); + } + return (array) meeting::get_meeting_info_for_instance($instance, $updatecache); + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + * @since Moodle 3.0 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'cmid' => new external_value(PARAM_INT, 'CM id'), + 'userlimit' => new external_value(PARAM_INT, 'User limit'), + 'bigbluebuttonbnid' => new external_value(PARAM_RAW, 'bigbluebuttonbn instance id'), + 'meetingid' => new external_value(PARAM_RAW, 'Meeting id'), + 'openingtime' => new external_value(PARAM_INT, 'Opening time', VALUE_OPTIONAL), + 'closingtime' => new external_value(PARAM_INT, 'Closing time', VALUE_OPTIONAL), + 'statusrunning' => new external_value(PARAM_BOOL, 'Status running', VALUE_OPTIONAL), + 'statusclosed' => new external_value(PARAM_BOOL, 'Status closed', VALUE_OPTIONAL), + 'statusopen' => new external_value(PARAM_BOOL, 'Status open', VALUE_OPTIONAL), + 'statusmessage' => new external_value(PARAM_TEXT, 'Status message', VALUE_OPTIONAL), + 'startedat' => new external_value(PARAM_INT, 'Started at', VALUE_OPTIONAL), + 'moderatorcount' => new external_value(PARAM_INT, 'Moderator count', VALUE_OPTIONAL), + 'participantcount' => new external_value(PARAM_INT, 'Participant count', VALUE_OPTIONAL), + 'moderatorplural' => new external_value(PARAM_BOOL, 'Several moderators ?', VALUE_OPTIONAL), + 'participantplural' => new external_value(PARAM_BOOL, 'Several participants ?', VALUE_OPTIONAL), + 'canjoin' => new external_value(PARAM_BOOL, 'Can join'), + 'ismoderator' => new external_value(PARAM_BOOL, 'Is moderator'), + 'presentations' => new \external_multiple_structure( + new external_single_structure([ + 'url' => new external_value(PARAM_URL, 'presentation URL'), + 'iconname' => new external_value(PARAM_RAW, 'icon name'), + 'icondesc' => new external_value(PARAM_TEXT, 'icon text'), + 'name' => new external_value(PARAM_TEXT, 'presentation name'), + ]) + ), + 'joinurl' => new external_value(PARAM_URL, 'Join URL'), + ] + ); + } +} diff --git a/mod/bigbluebuttonbn/classes/external/update_recording.php b/mod/bigbluebuttonbn/classes/external/update_recording.php new file mode 100644 index 00000000000..f5bb2871c4f --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/update_recording.php @@ -0,0 +1,141 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use coding_exception; +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_action; +use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_helper; +use mod_bigbluebuttonbn\recording; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to update the details of one recording. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_recording extends external_api { + /** + * Updates a recording + * + * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id, either the same as the one set in the recording or a new + * instance to import the recording into. + * @param int $recordingid + * @param string $action + * @param string|null $additionaloptions + * @return array (empty array for now) + * @throws coding_exception + */ + public static function execute( + int $bigbluebuttonbnid, + int $recordingid, + string $action, + string $additionaloptions = null + ): array { + // Validate the bigbluebuttonbnid ID. + [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + 'recordingid' => $recordingid, + 'action' => $action, + 'additionaloptions' => $additionaloptions, + ] = self::validate_parameters(self::execute_parameters(), [ + 'bigbluebuttonbnid' => $bigbluebuttonbnid, + 'recordingid' => $recordingid, + 'action' => $action, + 'additionaloptions' => $additionaloptions, + ]); + + switch ($action) { + case 'delete': + case 'edit': + case 'protect': + case 'publish': + case 'unprotect': + case 'unpublish': + case 'import': + break; + default: + throw new coding_exception("Unknown action '{$action}'"); + } + $instance = instance::get_from_instanceid($bigbluebuttonbnid); + $recordingcontext = $instance->get_context(); + self::validate_context($recordingcontext); + require_capability('mod/bigbluebuttonbn:managerecordings', $recordingcontext); + require_capability("mod/bigbluebuttonbn:{$action}recordings", $recordingcontext); + + // Fetch the session, features, and profile. + $recording = new recording($recordingid); + // Check both the recording instance context and the bbb context. + $relatedinstance = instance::get_from_instanceid($recording->get('bigbluebuttonbnid')); + if ($relatedinstance) { + $recordingcontext = $relatedinstance->get_context(); + // Validate that the user has access to this activity and to manage recordings. + self::validate_context($recordingcontext); + require_capability('mod/bigbluebuttonbn:managerecordings', $recordingcontext); + require_capability("mod/bigbluebuttonbn:{$action}recordings", $recordingcontext); + } + $additionaloptionsobject = $additionaloptions ? json_decode($additionaloptions) : null; + // Specific action such as import, delete, publish, unpublish, edit,.... + if (method_exists(recording_action::class, "$action")) { + forward_static_call( + ['\mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_action', "$action"], + $recording, + $instance, + $additionaloptionsobject + ); + } + return []; + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'bigbluebuttonbnid' => new external_value( + PARAM_INT, + 'bigbluebuttonbn instance id, this might be a different one from the one set in recordingid in case of importing' + ), + 'recordingid' => new external_value(PARAM_INT, 'The moodle internal recording ID'), + 'action' => new external_value(PARAM_ALPHANUMEXT, 'The action to perform'), + 'additionaloptions' => new external_value(PARAM_RAW, 'Additional options', VALUE_REQUIRED, null), + ]); + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + * @since Moodle 3.0 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([]); + } +} diff --git a/mod/bigbluebuttonbn/classes/external/view_bigbluebuttonbn.php b/mod/bigbluebuttonbn/classes/external/view_bigbluebuttonbn.php new file mode 100644 index 00000000000..176bcd42ab2 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external/view_bigbluebuttonbn.php @@ -0,0 +1,114 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use context_module; +use external_api; +use external_description; +use external_function_parameters; +use external_single_structure; +use external_value; +use external_warnings; +use mod_bigbluebuttonbn\instance; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * External service to trigger the course module viewed event and update the module completion status + * + * This is mainly used by the mobile application. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class view_bigbluebuttonbn extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 3.0 + */ + public static function execute_parameters() { + return new external_function_parameters([ + 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id'), + ] + ); + } + + /** + * Trigger the course module viewed event and update the module completion status. + * + * @param int $instanceid the bigbluebuttonbn instance id + * @return array of warnings and status result + * @since Moodle 3.0 + */ + public static function execute($instanceid) { + global $CFG; + require_once($CFG->dirroot . "/mod/bigbluebuttonbn/lib.php"); + + ['bigbluebuttonbnid' => $instanceid] = self::validate_parameters( + self::execute_parameters(), + ['bigbluebuttonbnid' => $instanceid] + ); + + $instance = instance::get_from_instanceid($instanceid); + + if (empty($instance)) { + return [ + 'status' => false, + 'warnings' => [ + [ + 'item' => 'mod_bigbluebuttonbn', + 'itemid' => 0, + 'warningcode' => 'nosuchinstance', + 'message' => get_string('nosuchinstance', 'mod_bigbluebuttonbn', + (object) ['id' => $instanceid, 'entity' => 'bigbluebuttonbn']) + ] + ] + ]; + } + $context = context_module::instance($instance->get_cm_id()); + self::validate_context($context); + + require_capability('mod/bigbluebuttonbn:view', $context); + // Call the bigbluebuttonbn/lib API. + bigbluebuttonbn_view($instance->get_instance_data(), $instance->get_course(), $instance->get_cm(), $context); + return [ + 'status' => true, + 'warnings' => [] + ]; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 3.0 + */ + public static function execute_returns() { + return new external_single_structure([ + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'warnings' => new external_warnings() + ] + ); + } +} diff --git a/mod/bigbluebuttonbn/classes/instance.php b/mod/bigbluebuttonbn/classes/instance.php new file mode 100644 index 00000000000..c772c9954b7 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/instance.php @@ -0,0 +1,1138 @@ +. + +namespace mod_bigbluebuttonbn; + +use cm_info; +use context; +use context_course; +use context_module; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\local\helpers\files; +use mod_bigbluebuttonbn\local\helpers\roles; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use moodle_url; +use stdClass; + +/** + * Instance record for mod_bigbluebuttonbn. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class instance { + + /** @var int Defines an instance type that includes room and recordings */ + public const TYPE_ALL = 0; + + /** @var int Defines an instance type that includes only room */ + public const TYPE_ROOM_ONLY = 1; + + /** @var int Defines an instance type that includes only recordings */ + public const TYPE_RECORDING_ONLY = 2; + + /** @var cm_info The cm_info object relating to the instance */ + protected $cm; + + /** @var stdClass The course that the instance is in */ + protected $course; + + /** @var stdClass The instance data for the instance */ + protected $instancedata; + + /** @var context The current context */ + protected $context; + + /** @var array The list of participants */ + protected $participantlist; + + /** @var int The current groupid if set */ + protected $groupid; + + /** + * instance constructor. + * + * @param cm_info $cm + * @param stdClass $course + * @param stdClass $instancedata + * @param int|null $groupid + */ + public function __construct(cm_info $cm, stdClass $course, stdClass $instancedata, ?int $groupid = null) { + $this->cm = $cm; + $this->course = $course; + $this->instancedata = $instancedata; + $this->groupid = $groupid; + } + + /** + * Get a group instance of the specified instance. + * + * @param self $originalinstance + * @param int $groupid + * @return null|self + */ + public static function get_group_instance_from_instance(self $originalinstance, int $groupid): ?self { + return new self( + $originalinstance->get_cm(), + $originalinstance->get_course(), + $originalinstance->get_instance_data(), + $groupid + ); + } + + /** + * Get the instance information from an instance id. + * + * @param int $instanceid The id from the bigbluebuttonbn table + * @return null|self + */ + public static function get_from_instanceid(int $instanceid): ?self { + global $DB; + + $coursetable = new \core\dml\table('course', 'c', 'c'); + $courseselect = $coursetable->get_field_select(); + $coursefrom = $coursetable->get_from_sql(); + + $cmtable = new \core\dml\table('course_modules', 'cm', 'cm'); + $cmfrom = $cmtable->get_from_sql(); + + $bbbtable = new \core\dml\table('bigbluebuttonbn', 'bbb', 'bbb'); + $bbbselect = $bbbtable->get_field_select(); + $bbbfrom = $bbbtable->get_from_sql(); + + $sql = <<get_record_sql($sql, [ + 'modname' => 'bigbluebuttonbn', + 'instanceid' => $instanceid, + ]); + + if (empty($result)) { + return null; + } + + $course = $coursetable->extract_from_result($result); + $instancedata = $bbbtable->extract_from_result($result); + $cm = get_fast_modinfo($course)->instances['bigbluebuttonbn'][$instancedata->id]; + + return new self($cm, $course, $instancedata); + } + + /** + * Get the instance information from a cmid. + * + * @param int $cmid + * @return null|self + */ + public static function get_from_cmid(int $cmid): ?self { + global $DB; + + $coursetable = new \core\dml\table('course', 'c', 'c'); + $courseselect = $coursetable->get_field_select(); + $coursefrom = $coursetable->get_from_sql(); + + $cmtable = new \core\dml\table('course_modules', 'cm', 'cm'); + $cmfrom = $cmtable->get_from_sql(); + + $bbbtable = new \core\dml\table('bigbluebuttonbn', 'bbb', 'bbb'); + $bbbselect = $bbbtable->get_field_select(); + $bbbfrom = $bbbtable->get_from_sql(); + + $sql = <<get_record_sql($sql, [ + 'modname' => 'bigbluebuttonbn', + 'cmid' => $cmid, + ]); + + if (empty($result)) { + return null; + } + + $course = $coursetable->extract_from_result($result); + $instancedata = $bbbtable->extract_from_result($result); + $cm = get_fast_modinfo($course)->get_cm($cmid); + + return new self($cm, $course, $instancedata); + } + + /** + * Get the instance information from a meetingid. + * + * If a group is specified in the meetingid then this will also be set. + * + * @param string $meetingid + * @return null|self + */ + public static function get_from_meetingid(string $meetingid): ?self { + $matches = self::parse_meetingid($meetingid); + + $instance = self::get_from_instanceid($matches['instanceid']); + + if ($instance && array_key_exists('groupid', $matches)) { + $instance->set_group_id($matches['groupid']); + } + + return $instance; + } + + /** + * Parse a meetingID for key data. + * + * @param string $meetingid + * @return array + * @throws \moodle_exception + */ + public static function parse_meetingid(string $meetingid): array { + $result = preg_match( + '@(?P[^-]*)-(?P[^-]*)-(?P\d+)(\[(?P\d*)\])?@', + $meetingid, + $matches + ); + + if ($result !== 1) { + throw new \moodle_exception("The supplied meeting id '{$meetingid}' is invalid found."); + } + + return $matches; + } + + /** + * Get all instances in the specified course. + * + * @param int $courseid + * @return self[] + */ + public static function get_all_instances_in_course(int $courseid): array { + global $DB; + + $coursetable = new \core\dml\table('course', 'c', 'c'); + $courseselect = $coursetable->get_field_select(); + $coursefrom = $coursetable->get_from_sql(); + + $cmtable = new \core\dml\table('course_modules', 'cm', 'cm'); + $cmfrom = $cmtable->get_from_sql(); + + $bbbtable = new \core\dml\table('bigbluebuttonbn', 'bbb', 'bbb'); + $bbbselect = $bbbtable->get_field_select(); + $bbbfrom = $bbbtable->get_from_sql(); + + $sql = <<get_records_sql($sql, [ + 'modname' => 'bigbluebuttonbn', + 'courseid' => $courseid, + ]); + + $instances = []; + foreach ($results as $result) { + $course = $coursetable->extract_from_result($result); + $instancedata = $bbbtable->extract_from_result($result); + $cm = get_fast_modinfo($course)->get_cm($result->cmid); + $instances[$cm->id] = new self($cm, $course, $instancedata); + } + + return $instances; + } + + /** + * Set the current group id of the activity. + * + * @param int $groupid + */ + public function set_group_id(int $groupid): void { + $this->groupid = $groupid; + } + + /** + * Get the current groupid if set. + * + * @return null|int + */ + public function get_group_id(): ?int { + return empty($this->groupid) ? 0 : $this->groupid; + } + + /** + * Check whether this instance is configured to use a group. + * + * @return bool + */ + public function uses_groups(): bool { + return $this->groupid !== null; + } + + /** + * Get the group name for the current group, if a group has been set. + * + * @return null|string + */ + public function get_group_name(): ?string { + $groupid = $this->get_group_id(); + + if ($groupid === null) { + return null; + } + + if ($groupid == 0) { + return get_string('allparticipants'); + } + + return groups_get_group_name($groupid); + } + + /** + * Get the course object for the instance. + * + * @return stdClass + */ + public function get_course(): stdClass { + return $this->course; + } + + /** + * Get the course id of the course that the instance is in. + * + * @return int + */ + public function get_course_id(): int { + return $this->course->id; + } + + /** + * Get the cm_info object for the instance. + * + * @return cm_info + */ + public function get_cm(): cm_info { + return $this->cm; + } + + /** + * Get the id of the course module. + * + * @return int + */ + public function get_cm_id(): int { + return $this->get_cm()->id; + } + + /** + * Get the context. + * + * @return context_module + */ + public function get_context(): context_module { + if ($this->context === null) { + $this->context = context_module::instance($this->get_cm()->id); + } + + return $this->context; + } + + /** + * Get the context ID of the module context. + * + * @return int + */ + public function get_context_id(): int { + return $this->get_context()->id; + } + + /** + * Get the course context. + * + * @return context_course + */ + public function get_course_context(): context_course { + return $this->get_context()->get_course_context(); + } + + /** + * Get the big blue button instance data. + * + * @return stdClass + */ + public function get_instance_data(): stdClass { + return $this->instancedata; + } + + /** + * Get the instance id. + * + * @return int + */ + public function get_instance_id(): int { + return $this->instancedata->id; + } + + /** + * Helper to get an instance var. + * + * @param string $name + * @return string + */ + public function get_instance_var(string $name) { + $instance = $this->get_instance_data(); + if (property_exists($instance, $name)) { + return $instance->{$name}; + } + + return null; + } + + /** + * Get the meeting id for this meeting. + * + * @param null|int $groupid + * @return string + */ + public function get_meeting_id(?int $groupid = null): string { + $baseid = sprintf( + '%s-%s-%s', + $this->get_instance_var('meetingid'), + $this->get_course_id(), + $this->get_instance_var('id') + ); + + if ($groupid === null) { + $groupid = $this->get_group_id(); + } + + return sprintf('%s[%s]', $baseid, $groupid); + } + + /** + * Get the name of the meeting, considering any group if set. + * + * @return string + */ + public function get_meeting_name(): string { + $meetingname = $this->get_instance_var('name'); + + $groupname = $this->get_group_name(); + if ($groupname !== null) { + $meetingname .= " ({$groupname})"; + } + + return $meetingname; + } + + /** + * Get the meeting description with the pluginfile URLs optionally rewritten. + * + * @param bool $rewritepluginfileurls + * @return string + */ + public function get_meeting_description(bool $rewritepluginfileurls = false): string { + $description = $this->get_instance_var('intro'); + + if ($rewritepluginfileurls) { + $description = file_rewrite_pluginfile_urls( + $description, + 'pluginfile.php', + $this->get_context_id(), + 'mod_bigbluebuttonbn', + 'intro', + null + ); + } + + return $description; + } + + /** + * Get the meeting type if set. + * + * @return null|string + */ + public function get_type(): ?string { + return $this->get_instance_var('type'); + } + + /** + * Whether this instance is includes both a room, and recordings. + * + * @return bool + */ + public function is_type_room_and_recordings(): bool { + return $this->get_type() == self::TYPE_ALL; + } + + /** + * Whether this instance is one that only includes a room. + * + * @return bool + */ + public function is_type_room_only(): bool { + return $this->get_type() == self::TYPE_ROOM_ONLY; + } + + /** + * Whether this instance is one that only includes recordings. + * + * @return bool + */ + public function is_type_recordings_only(): bool { + return $this->get_type() == self::TYPE_RECORDING_ONLY; + } + + /** + * Get the participant list for the session. + * + * @return array + */ + public function get_participant_list(): array { + if ($this->participantlist === null) { + $this->participantlist = roles::get_participant_list( + $this->get_instance_data(), + $this->get_context() + ); + } + + return $this->participantlist; + } + + /** + * Get the user. + * + * @return stdClass + */ + public function get_user(): stdClass { + global $USER; + + return $USER; + } + + /** + * Get the id of the user. + * + * @return int + */ + public function get_user_id(): int { + $user = $this->get_user(); + + return $user->id; + } + + /** + * Get the fullname of the current user. + * + * @return string + */ + public function get_user_fullname(): string { + $user = $this->get_user(); + + return fullname($user); + } + + /** + * Whether the current user is an administrator. + * + * @return bool + */ + public function is_admin(): bool { + global $USER; + + return is_siteadmin($USER->id); + } + + /** + * Whether the user is a session moderator. + * + * @return bool + */ + public function is_moderator(): bool { + return roles::is_moderator( + $this->get_context(), + $this->get_participant_list() + ); + } + + /** + * Whether this user can join the conference. + * + * This checks the user right for access against capabilities and group membership + * + * @return bool + */ + public function can_join(): bool { + global $USER; + $groupid = $this->get_group_id(); + $context = $this->get_context(); + $inrightgroup = !$groupid || $this->user_has_group_access($USER, $groupid); + $hascapability = has_capability('moodle/category:manage', $context) + || (has_capability('mod/bigbluebuttonbn:join', $context) && $inrightgroup); + $canjoin = $this->get_type() != self::TYPE_RECORDING_ONLY && $hascapability; // Recording only cannot be joined ever. + return $canjoin; + } + + /** + * Whether this user can manage recordings. + * + * @return bool + */ + public function can_manage_recordings(): bool { + // Note: This will include site administrators. + // The has_capability() function returns truthy for admins unless otherwise directed. + return has_capability('mod/bigbluebuttonbn:managerecordings', $this->get_context()); + } + + /** + * Whether this user can publish/unpublish/protect/unprotect/delete recordings. + * + * @param string $action + * @return bool + */ + public function can_perform_on_recordings($action): bool { + // Note: This will include site administrators. + // The has_capability() function returns truthy for admins unless otherwise directed. + return has_capability("mod/bigbluebuttonbn:{$action}recordings", $this->get_context()); + } + + /** + * Get the configured user limit. + * + * @return int + */ + public function get_user_limit(): int { + if ((boolean) config::get('userlimit_editable')) { + return intval($this->get_instance_var('userlimit')); + } + + return intval((int) config::get('userlimit_default')); + } + + /** + * Check whether the user limit has been reached. + * + * @param int $currentusercount The user count to check + * @return bool + */ + public function has_user_limit_been_reached(int $currentusercount): bool { + $userlimit = $this->get_user_limit(); + if (empty($userlimit)) { + return false; + } + + return $currentusercount >= $userlimit; + } + + /** + * Check whether the current user counts towards the user limit. + * + * @return bool + */ + public function does_current_user_count_towards_user_limit(): bool { + if ($this->is_admin()) { + return false; + } + + if ($this->is_moderator()) { + return false; + } + + return true; + } + + /** + * Get the voice bridge details. + * + * @return null|int + */ + public function get_voice_bridge(): ?int { + $voicebridge = (int) $this->get_instance_var('voicebridge'); + if ($voicebridge > 0) { + return 70000 + $voicebridge; + } + + return null; + } + + /** + * Whether participants are muted on entry. + * + * @return bool + */ + public function get_mute_on_start(): bool { + return $this->get_instance_var('muteonstart'); + } + + /** + * Get the moderator password. + * + * @return string + */ + public function get_moderator_password(): string { + return $this->get_instance_var('moderatorpass'); + } + + /** + * Get the viewer password. + * + * @return string + */ + public function get_viewer_password(): string { + return $this->get_instance_var('viewerpass'); + } + + /** + * Get the appropriate password for the current user. + * + * @return string + */ + public function get_current_user_password(): string { + if ($this->is_admin() || $this->is_moderator()) { + return $this->get_moderator_password(); + } + + return $this->get_viewer_password(); + } + + /** + * Whether to show the recording button + * + * @return bool + */ + public function should_show_recording_button(): bool { + global $CFG; + + if (!empty($CFG->bigbluebuttonbn_recording_hide_button_editable)) { + return (bool) $this->get_instance_var('recordhidebutton'); + } + + return !$CFG->bigbluebuttonbn_recording_hide_button_default; + } + + /** + * Whether this instance is recorded. + * + * @return bool + */ + public function is_recorded(): bool { + return (bool) $this->get_instance_var('record'); + } + + /** + * Whether this instance can import recordings from another instance. + * + * @return bool + */ + public function can_import_recordings(): bool { + if ($this->can_manage_recordings()) { + return true; + } + + return $this->is_feature_enabled('importrecordings'); + } + + /** + * Whether this instance is recorded from the start. + * + * @return bool + */ + public function should_record_from_start(): bool { + if (!$this->is_recorded()) { + // This meeting is not recorded. + return false; + } + + return (bool) $this->get_instance_var('recordallfromstart'); + } + + /** + * Whether recording can be started and stopped. + * + * @return bool + */ + public function allow_recording_start_stop(): bool { + if (!$this->is_recorded()) { + // If the meeting is not configured for recordings, do not allow it to be recorded. + return false; + } + + return $this->should_show_recording_button(); + } + + /** + * Get the welcome message to display. + * + * @return string + */ + public function get_welcome_message(): string { + $welcomestring = $this->get_instance_var('welcome'); + if (empty($welcomestring)) { + $welcomestring = get_string('mod_form_field_welcome_default', 'bigbluebuttonbn'); + } + + $welcome = [$welcomestring]; + + if ($this->is_recorded()) { + if ($this->should_record_from_start()) { + $welcome[] = get_string('bbbrecordallfromstartwarning', 'bigbluebuttonbn'); + } else { + $welcome[] = get_string('bbbrecordwarning', 'bigbluebuttonbn'); + } + } + + return implode('

', $welcome); + } + + /** + * Get the presentation data for internal use. + * + * The URL returned for the presentation will be accessible through moodle with checks about user being logged in. + * + * @return array|null + */ + public function get_presentation(): ?array { + return $this->do_get_presentation_with_nonce(false); + } + + /** + * Get the presentation data for external API url. + * + * The URL returned for the presentation will be accessible publicly but once and with a specific URL. + * + * @return array|null + */ + public function get_presentation_for_bigbluebutton_upload(): ?array { + return $this->do_get_presentation_with_nonce(true); + } + + /** + * Generate Presentation URL. + * + * @param bool $withnonce The generated url will have a nonce included + * @return array|null + */ + protected function do_get_presentation_with_nonce(bool $withnonce): ?array { + if ($this->has_ended()) { + return files::get_presentation( + $this->get_context(), + $this->get_instance_var('presentation'), + null, + $withnonce + ); + } else if ($this->is_currently_open()) { + return files::get_presentation( + $this->get_context(), + $this->get_instance_var('presentation'), + $this->get_instance_id(), + $withnonce + ); + } else { + return []; + } + } + + /** + * Whether the current time is before the scheduled start time. + * + * @return bool + */ + public function before_start_time(): bool { + $openingtime = $this->get_instance_var('openingtime'); + if (empty($openingtime)) { + return false; + } + + return $openingtime >= time(); + } + + /** + * Whether the meeting time has passed. + * + * @return bool + */ + public function has_ended(): bool { + $closingtime = $this->get_instance_var('closingtime'); + if (empty($closingtime)) { + return false; + } + + return $closingtime < time(); + } + + /** + * Whether this session is currently open. + * + * @return bool + */ + public function is_currently_open(): bool { + if ($this->before_start_time()) { + return false; + } + + if ($this->has_ended()) { + return false; + } + + return true; + } + + /** + * Whether the user must wait to join the session. + * + * @return bool + */ + public function user_must_wait_to_join(): bool { + if ($this->is_admin() || $this->is_moderator()) { + return false; + } + + return (bool) $this->get_instance_var('wait'); + } + + /** + * Whether the user can force join in all cases + * + * @return bool + */ + public function user_can_force_join(): bool { + return $this->is_admin() || $this->is_moderator(); + } + + /** + * Whether the user can end a meeting + * + * @return bool + */ + public function user_can_end_meeting(): bool { + return $this->is_admin() || $this->is_moderator(); + } + + /** + * Get information about the origin. + * + * @return stdClass + */ + public function get_origin_data(): stdClass { + global $CFG; + + $parsedurl = parse_url($CFG->wwwroot); + return (object) [ + 'origin' => 'Moodle', + 'originVersion' => $CFG->release, + 'originServerName' => $parsedurl['host'], + 'originServerUrl' => $CFG->wwwroot, + 'originServerCommonName' => '', + 'originTag' => sprintf('moodle-mod_bigbluebuttonbn (%s)', get_config('mod_bigbluebuttonbn', 'version')), + ]; + } + + /** + * Whether this is a server belonging to blindside networks. + * + * @return bool + */ + public function is_blindside_network_server(): bool { + return bigbluebutton_proxy::is_bn_server(); + } + + /** + * Get the URL used to access the course that the instance is in. + * + * @return moodle_url + */ + public function get_course_url(): moodle_url { + return new moodle_url('/course/view.php', ['id' => $this->get_course_id()]); + } + + /** + * Get the URL used to view the instance as a user. + * + * @return moodle_url + */ + public function get_view_url(): moodle_url { + return new moodle_url('/mod/bigbluebuttonbn/view.php', [ + 'id' => $this->cm->id, + ]); + } + + /** + * Get the logout URL used to log out of the meeting. + * + * @return moodle_url + */ + public function get_logout_url(): moodle_url { + return new moodle_url('/mod/bigbluebuttonbn/bbb_view.php', [ + 'action' => 'logout', + 'id' => $this->cm->id, + ]); + } + + /** + * Get the URL that the remote server will use to notify that the recording is ready. + * + * @return moodle_url + */ + public function get_record_ready_url(): moodle_url { + return new moodle_url('/mod/bigbluebuttonbn/bbb_broker.php', [ + 'action' => 'recording_ready', + 'bigbluebuttonbn' => $this->instancedata->id, + ]); + } + + /** + * Get the URL that the remote server will use to notify of meeting events. + * + * @return moodle_url + */ + public function get_meeting_event_notification_url(): moodle_url { + return new moodle_url('/mod/bigbluebuttonbn/bbb_broker.php', [ + 'action' => 'meeting_events', + 'bigbluebuttonbn' => $this->instancedata->id, + ]); + } + + /** + * Get the URL used to join a meeting. + * + * @return moodle_url + */ + public function get_join_url(): moodle_url { + return new moodle_url('/mod/bigbluebuttonbn/bbb_view.php', [ + 'action' => 'join', + 'id' => $this->cm->id, + 'bn' => $this->instancedata->id, + ]); + } + + /** + * Get the URL used for the import page. + * + * @return moodle_url + */ + public function get_import_url(): moodle_url { + return new moodle_url('/mod/bigbluebuttonbn/import_view.php', [ + 'destbn' => $this->instancedata->id, + ]); + } + + /** + * Get the list of enabled features for this instance. + * + * @return array + */ + public function get_enabled_features(): array { + return config::get_enabled_features( + bigbluebutton_proxy::get_instance_type_profiles(), + $this->get_instance_var('type') ?? null + ); + } + + /** + * Check whetherthe named features is enabled. + * + * @param string $feature + * @return bool + */ + public function is_feature_enabled(string $feature): bool { + $features = $this->get_enabled_features(); + + return !empty($features[$feature]); + } + + /** + * Check if meeting is recorded. + * + * @return bool + */ + public function should_record() { + return (boolean) config::recordings_enabled() && $this->is_recorded(); + } + + /** + * Get recordings for this instance + * + * @param string[] $excludedid + * @param bool $viewdeleted view deleted recordings ? + * @return recording[] + */ + public function get_recordings(array $excludedid = [], $viewdeleted = false): array { + // Fetch the list of recordings depending on the status of the instance. + // show room is enabled for TYPE_ALL and TYPE_ROOM_ONLY. + if ($this->is_feature_enabled('showroom')) { + // Not in the import page. + return recording::get_recordings_for_instance( + $this, + $this->is_feature_enabled('importrecordings'), + $this->get_instance_var('recordings_imported'), + ); + } + // We show all recording from this course as this is TYPE_RECORDING. + return recording::get_recordings_for_course( + $this->get_course_id(), + $excludedid, + $this->is_feature_enabled('importrecordings'), + false, + $viewdeleted + ); + } + + /** + * Check if this is a valid group for this user/instance, + * + * + * @param stdClass $user + * @param int $groupid + * @return bool + */ + public function user_has_group_access($user, $groupid) { + $cm = $this->get_cm(); + $context = $this->get_context(); + // Then validate group. + $groupmode = groups_get_activity_groupmode($cm); + if ($groupmode && $groupid) { + $accessallgroups = has_capability('moodle/site:accessallgroups', $context); + if ($accessallgroups || $groupmode == VISIBLEGROUPS) { + $allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid); + } else { + $allowedgroups = groups_get_all_groups($cm->course, $user->id, $cm->groupingid); + } + if (!array_key_exists($groupid, $allowedgroups)) { + return false; + } + if (!groups_group_visible($groupid, $this->get_course(), $this->get_cm())) { + return false; + } + } + return true; + } +} diff --git a/mod/bigbluebuttonbn/classes/local/bigbluebutton/recordings/recording_action.php b/mod/bigbluebuttonbn/classes/local/bigbluebutton/recordings/recording_action.php new file mode 100644 index 00000000000..94297785628 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/bigbluebutton/recordings/recording_action.php @@ -0,0 +1,130 @@ +. + +namespace mod_bigbluebuttonbn\local\bigbluebutton\recordings; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\recording; + +/** + * Collection of helper methods for handling recordings actions in Moodle. + * + * Utility class for meeting actions + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_action { + /** + * Import recording + * + * @param recording $recording + * @param instance $targetinstance + */ + public static function import(recording $recording, instance $targetinstance) { + $recording->create_imported_recording($targetinstance); + } + + /** + * Helper for performing delete on recordings. + * + * @param recording $recording + */ + public static function delete(recording $recording) { + // As the recordingid was not identified as imported recording link, execute delete on a real recording. + // Step 1, delete imported links associated to the recording. + $recordingstodelete = recording::get_records(['recordingid' => $recording->get('recordingid'), + 'imported' => true]); + foreach ($recordingstodelete as $rec) { + $rec->delete(); + } + $recording->delete(); + } + + /** + * Helper for performing edit on recordings. + * + * @param recording $recording + */ + public static function edit(recording $recording) { + $recording->update(); + } + + /** + * Helper for performing unprotect on recordings. + * + * @param recording $recording + */ + public static function unprotect(recording $recording) { + if ($recording->get('imported')) { + /* Since the recording link is the one fetched from the BBB server, imported recordings can not be + * unprotected. There is no need to do anything else. + */ + throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'unprotect'); + } + $recording->set('protected', false); + $recording->update(); + } + + /** + * Helper for performing protect on recordings. + * + * @param recording $recording + */ + public static function protect(recording $recording) { + if ($recording->get('imported')) { + /* Since the recording link is the one fetched from the BBB server, imported recordings can not be + * protected. There is no need to do anything else. + */ + throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'protect'); + } + $recording->set('protected', true); + $recording->update(); + } + + /** + * Helper for performing unpublish on recordings. + * + * @param recording $recording + */ + public static function unpublish(recording $recording) { + if ($recording->get('imported')) { + /* Since the recording link is the one fetched from the BBB server, imported recordings can not be + * unpublished. There is no need to do anything else. + */ + throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'unpublish'); + } + $recording->set('published', false); + $recording->update(); + } + + /** + * Helper for performing publish on recordings. + * + * @param recording $recording + */ + public static function publish(recording $recording) { + if ($recording->get('imported')) { + /* Since the recording link is the one fetched from the BBB server, imported recordings can not be + * unpublished. There is no need to do anything else. + */ + throw new \moodle_exception('cannotperformaction', 'mod_bigblubuebuttobn', '', 'publish'); + } + $recording->set('published', true); + $recording->update(); + } +} diff --git a/mod/bigbluebuttonbn/classes/local/bigbluebutton/recordings/recording_data.php b/mod/bigbluebuttonbn/classes/local/bigbluebutton/recordings/recording_data.php new file mode 100644 index 00000000000..9cbe66922c3 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/bigbluebutton/recordings/recording_data.php @@ -0,0 +1,306 @@ +. + +namespace mod_bigbluebuttonbn\local\bigbluebutton\recordings; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\local\helpers\roles; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use mod_bigbluebuttonbn\output\recording_description_editable; +use mod_bigbluebuttonbn\output\recording_name_editable; +use mod_bigbluebuttonbn\output\recording_row_actionbar; +use mod_bigbluebuttonbn\output\recording_row_playback; +use mod_bigbluebuttonbn\output\recording_row_preview; +use mod_bigbluebuttonbn\recording; +use stdClass; + +/** + * The recordings_data. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent.david [at] call-learning [dt] fr) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class recording_data { + + /** + * Get the full recording table + * + * @param array $recordings + * @param array $tools + * @param instance|null $instance + * @param int $courseid + * @return array + * @throws \coding_exception + */ + public static function get_recording_table(array $recordings, array $tools, instance $instance = null, + int $courseid = 0): array { + $typeprofiles = bigbluebutton_proxy::get_instance_type_profiles(); + $typeprofile = empty($instance) ? $typeprofiles[0] : $typeprofiles[$instance->get_type()]; + $lang = get_string('locale', 'core_langconfig'); + $locale = substr($lang, 0, strpos($lang, '.')); + $tabledata = [ + 'activity' => empty($instance) ? '' : bigbluebutton_proxy::view_get_activity_status($instance), + 'ping_interval' => (int) config::get('waitformoderator_ping_interval') * 1000, + 'locale' => substr($locale, 0, strpos($locale, '_')), + 'profile_features' => $typeprofile['features'], + 'columns' => [], + 'data' => '', + ]; + $hascapabilityincourse = empty($instance) && roles::has_capability_in_course($courseid, + 'mod/bigbluebuttonbn:managerecordings'); + + $data = []; + + // Build table content. + foreach ($recordings as $recording) { + // Protected recordings is not a standard feature, remove actions when protected flag is not present. + $rowtools = $tools; + if (in_array('protect', $rowtools) && $recording->get('protected') === null) { + $rowtools = array_diff($rowtools, ['protect', 'unprotect']); + } + $rowdata = self::row($instance, $recording, $rowtools); + if (!empty($rowdata)) { + $data[] = $rowdata; + } + } + + $columns = [ + [ + 'key' => 'playback', + 'label' => get_string('view_recording_playback', 'bigbluebuttonbn'), + 'width' => '125px', + 'type' => 'html', + 'allowHTML' => true, + ], + [ + 'key' => 'recording', + 'label' => get_string('view_recording_name', 'bigbluebuttonbn'), + 'width' => '125px', + 'type' => 'html', + 'allowHTML' => true, + ], + [ + 'key' => 'description', + 'label' => get_string('view_recording_description', 'bigbluebuttonbn'), + 'sortable' => true, + 'width' => '250px', + 'type' => 'html', + 'allowHTML' => true, + ], + ]; + + // Initialize table headers. + $ispreviewenabled = !empty($instance) && self::preview_enabled($instance); + $ispreviewenabled = $ispreviewenabled || $hascapabilityincourse; + if ($ispreviewenabled) { + $columns[] = [ + 'key' => 'preview', + 'label' => get_string('view_recording_preview', 'bigbluebuttonbn'), + 'width' => '250px', + 'type' => 'html', + 'allowHTML' => true, + ]; + } + + $columns[] = [ + 'key' => 'date', + 'label' => get_string('view_recording_date', 'bigbluebuttonbn'), + 'sortable' => true, + 'width' => '225px', + 'type' => 'html', + 'allowHTML' => true, + ]; + $columns[] = [ + 'key' => 'duration', + 'label' => get_string('view_recording_duration', 'bigbluebuttonbn'), + 'width' => '50px', + 'allowHTML' => false, + 'sortable' => true, + ]; + // Either instance is empty and we must show the toolbar (with restricted content) or we check + // specific rights related to the instance. + $canmanagerecordings = !empty($instance) && $instance->can_manage_recordings(); + $canmanagerecordings = $canmanagerecordings || $hascapabilityincourse; + if ($canmanagerecordings) { + $columns[] = [ + 'key' => 'actionbar', + 'label' => get_string('view_recording_actionbar', 'bigbluebuttonbn'), + 'width' => '120px', + 'type' => 'html', + 'allowHTML' => true, + ]; + } + + $tabledata['columns'] = $columns; + $tabledata['data'] = json_encode($data); + + return $tabledata; + } + + /** + * Helper function builds a row for the data used by the recording table. + * + * TODO: replace this with templates whenever possible so we just + * return the data via the API. + * + * @param instance|null $instance $instance + * @param recording $rec a recording row + * @param array|null $tools + * @param int|null $courseid + * @return stdClass|null + */ + public static function row(?instance $instance, recording $rec, ?array $tools = null, ?int $courseid = 0): ?stdClass { + global $PAGE; + + $hascapabilityincourse = empty($instance) && roles::has_capability_in_course($courseid, + 'mod/bigbluebuttonbn:managerecordings'); + $renderer = $PAGE->get_renderer('mod_bigbluebuttonbn'); + foreach ($tools as $key => $tool) { + if ((!empty($instance) && !$instance->can_perform_on_recordings($tool)) + || (empty($instance) && !$hascapabilityincourse)) { + unset($tools[$key]); + } + } + if (!self::include_recording_table_row($instance, $rec)) { + return null; + } + $rowdata = new stdClass(); + + // Set recording_playback. + $recordingplayback = new recording_row_playback($rec, $instance); + $rowdata->playback = $renderer->render($recordingplayback); + + if (empty($instance)) { + // Set activity name. + $rowdata->recording = $rec->get('name'); + + // Set activity description. + $rowdata->description = $rec->get('description'); + } else { + // Set activity name. + $recordingname = new recording_name_editable($rec, $instance); + $rowdata->recording = $renderer->render_inplace_editable($recordingname); + // Set activity description. + $recordingdescription = new recording_description_editable($rec, $instance); + $rowdata->description = $renderer->render_inplace_editable($recordingdescription); + } + + if ((!empty($instance) && self::preview_enabled($instance)) || $hascapabilityincourse) { + // Set recording_preview. + $rowdata->preview = ''; + if ($rec->get('playbacks')) { + $rowpreview = new recording_row_preview($rec); + $rowdata->preview = $renderer->render($rowpreview); + } + } + // Set date. + $starttime = $rec->get('starttime'); + $rowdata->date = !is_null($starttime) ? floatval($starttime) : 0; + // Set duration. + $rowdata->duration = self::row_duration($rec); + // Set actionbar, if user is allowed to manage recordings. + if ((!empty($instance) && $instance->can_manage_recordings()) || $hascapabilityincourse) { + $actionbar = new recording_row_actionbar($rec, $tools); + $rowdata->actionbar = $renderer->render($actionbar); + } + return $rowdata; + } + + /** + * Helper function evaluates if recording preview should be included. + * + * @param instance $instance + * @return bool + */ + public static function preview_enabled(instance $instance): bool { + return $instance->get_instance_var('recordings_preview') == '1'; + } + + /** + * Helper function converts recording duration used in row for the data used by the recording table. + * + * @param recording $recording + * @return int + */ + protected static function row_duration(recording $recording): int { + $playbacks = $recording->get('playbacks'); + if (empty($playbacks)) { + return 0; + } + foreach ($playbacks as $playback) { + // Ignore restricted playbacks. + if (array_key_exists('restricted', $playback) && strtolower($playback['restricted']) == 'true') { + continue; + } + + // Take the length form the fist playback with an actual value. + if (!empty($playback['length'])) { + return intval($playback['length']); + } + } + return 0; + } + + /** + * Helper function to handle yet unknown recording types + * + * @param string $playbacktype : for now presentation, video, statistics, capture, notes, podcast + * @return string the matching language string or a capitalised version of the provided string + */ + public static function type_text(string $playbacktype): string { + // Check first if string exists, and if it does not, just default to the capitalised version of the string. + $text = ucwords($playbacktype); + $typestringid = 'view_recording_format_' . $playbacktype; + if (get_string_manager()->string_exists($typestringid, 'bigbluebuttonbn')) { + $text = get_string($typestringid, 'bigbluebuttonbn'); + } + return $text; + } + + /** + * Helper function evaluates if recording row should be included in the table. + * + * @param instance|null $instance + * @param recording $rec a bigbluebuttonbn_recordings row + * @return bool + */ + protected static function include_recording_table_row(?instance $instance, recording $rec): bool { + if (empty($instance)) { + return roles::has_capability_in_course($rec->get('courseid'), 'mod/bigbluebuttonbn:managerecordings'); + } + // Exclude unpublished recordings, only if user has no rights to manage them. + if (!$rec->get('published') && !$instance->can_manage_recordings()) { + return false; + } + // Imported recordings are always shown as long as they are published. + if ($rec->get('imported')) { + return true; + } + // Administrators and moderators are always allowed. + if ($instance->is_admin() || $instance->is_moderator()) { + return true; + } + // When groups are enabled, exclude those to which the user doesn't have access to. + if ($instance->uses_groups()) { + return intval($rec->get('groupid')) === intval($instance->get_group_id()); + } + return true; + } +} diff --git a/mod/bigbluebuttonbn/classes/local/config.php b/mod/bigbluebuttonbn/classes/local/config.php new file mode 100644 index 00000000000..bb957c00696 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/config.php @@ -0,0 +1,262 @@ +. + +namespace mod_bigbluebuttonbn\local; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\recording; + +/** + * Handles the global configuration based on config.php. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class config { + + /** @var string Default bigbluebutton server url */ + public const DEFAULT_SERVER_URL = 'http://test-install.blindsidenetworks.com/bigbluebutton/'; + + /** @var string Default bigbluebutton server shared secret */ + public const DEFAULT_SHARED_SECRET = '8cd8ef52e8e101574e400365b55e11a6'; + + /** + * Returns moodle version. + * + * @return string + */ + protected static function get_moodle_version_major() { + global $CFG; + $versionarray = explode('.', $CFG->version); + return $versionarray[0]; + } + + /** + * Returns configuration default values. + * + * @return array + */ + protected static function defaultvalues() { + return [ + 'server_url' => self::DEFAULT_SERVER_URL, + 'shared_secret' => self::DEFAULT_SHARED_SECRET, + 'voicebridge_editable' => false, + 'importrecordings_enabled' => false, + 'importrecordings_from_deleted_enabled' => false, + 'waitformoderator_default' => false, + 'waitformoderator_editable' => true, + 'waitformoderator_ping_interval' => '10', + 'waitformoderator_cache_ttl' => '60', + 'userlimit_default' => '0', + 'userlimit_editable' => false, + 'preuploadpresentation_enabled' => false, + 'sendnotifications_enabled' => false, + 'recordingready_enabled' => false, + 'recordingstatus_enabled' => false, + 'meetingevents_enabled' => false, + 'participant_moderator_default' => '0', + 'scheduled_pre_opening' => '10', + 'recordings_enabled' => true, + 'recordings_html_default' => false, + 'recordings_html_editable' => false, + 'recordings_deleted_default' => false, + 'recordings_deleted_editable' => false, + 'recordings_imported_default' => false, + 'recordings_imported_editable' => false, + 'recordings_preview_default' => true, + 'recordings_preview_editable' => false, + 'recordings_validate_url' => true, + 'recording_default' => true, + 'recording_editable' => true, + 'recording_refresh_period' => recording::RECORDING_REFRESH_DEFAULT_PERIOD, + 'recording_icons_enabled' => true, + 'recording_all_from_start_default' => false, + 'recording_all_from_start_editable' => false, + 'recording_hide_button_default' => false, + 'recording_hide_button_editable' => false, + 'general_warning_message' => '', + 'general_warning_roles' => 'editingteacher,teacher', + 'general_warning_box_type' => 'info', + 'general_warning_button_text' => '', + 'general_warning_button_href' => '', + 'general_warning_button_class' => '', + 'muteonstart_default' => false, + 'muteonstart_editable' => false, + 'disablecam_default' => false, + 'disablecam_editable' => true, + 'disablemic_default' => false, + 'disablemic_editable' => true, + 'disableprivatechat_default' => false, + 'disableprivatechat_editable' => true, + 'disablepublicchat_default' => false, + 'disablepublicchat_editable' => true, + 'disablenote_default' => false, + 'disablenote_editable' => true, + 'hideuserlist_default' => false, + 'hideuserlist_editable' => true, + 'lockedlayout_default' => false, + 'lockedlayout_editable' => true, + 'lockonjoin_default' => false, + 'lockonjoin_editable' => true, + 'lockonjoinconfigurable_default' => false, + 'lockonjoinconfigurable_editable' => true, + 'welcome_default' => '', + ]; + } + + /** + * Returns default value for an specific setting. + * + * @param string $setting + * @return string + */ + protected static function defaultvalue($setting) { + $defaultvalues = self::defaultvalues(); + if (!array_key_exists($setting, $defaultvalues)) { + return null; + } + return $defaultvalues[$setting]; + } + + /** + * Returns value for an specific setting. + * + * @param string $setting + * @return string + */ + public static function get($setting) { + global $CFG; + if (isset($CFG->bigbluebuttonbn[$setting])) { + return (string)$CFG->bigbluebuttonbn[$setting]; + } + if (isset($CFG->{'bigbluebuttonbn_'.$setting})) { + return (string)$CFG->{'bigbluebuttonbn_'.$setting}; + } + return self::defaultvalue($setting); + } + + /** + * Validates if recording settings are enabled. + * + * @return bool + */ + public static function recordings_enabled() { + return (boolean)self::get('recordings_enabled'); + } + + /** + * Validates if imported recording settings are enabled. + * + * @return bool + */ + public static function importrecordings_enabled() { + return (boolean)self::get('importrecordings_enabled'); + } + + /** + * Wraps current settings in an array. + * + * @return array + */ + public static function get_options() { + return [ + 'version_major' => self::get_moodle_version_major(), + 'voicebridge_editable' => self::get('voicebridge_editable'), + 'importrecordings_enabled' => self::get('importrecordings_enabled'), + 'importrecordings_from_deleted_enabled' => self::get('importrecordings_from_deleted_enabled'), + 'waitformoderator_default' => self::get('waitformoderator_default'), + 'waitformoderator_editable' => self::get('waitformoderator_editable'), + 'userlimit_default' => self::get('userlimit_default'), + 'userlimit_editable' => self::get('userlimit_editable'), + 'preuploadpresentation_enabled' => self::get('preuploadpresentation_enabled'), + 'sendnotifications_enabled' => self::get('sendnotifications_enabled'), + 'recordings_enabled' => self::get('recordings_enabled'), + 'meetingevents_enabled' => self::get('meetingevents_enabled'), + 'recordings_html_default' => self::get('recordings_html_default'), + 'recordings_html_editable' => self::get('recordings_html_editable'), + 'recordings_deleted_default' => self::get('recordings_deleted_default'), + 'recordings_deleted_editable' => self::get('recordings_deleted_editable'), + 'recordings_imported_default' => self::get('recordings_imported_default'), + 'recordings_imported_editable' => self::get('recordings_imported_editable'), + 'recordings_preview_default' => self::get('recordings_preview_default'), + 'recordings_preview_editable' => self::get('recordings_preview_editable'), + 'recordings_validate_url' => self::get('recordings_validate_url'), + 'recording_default' => self::get('recording_default'), + 'recording_editable' => self::get('recording_editable'), + 'recording_refresh_period' => self::get('recording_refresh_period'), + 'recording_icons_enabled' => self::get('recording_icons_enabled'), + 'recording_all_from_start_default' => self::get('recording_all_from_start_default'), + 'recording_all_from_start_editable' => self::get('recording_all_from_start_editable'), + 'recording_hide_button_default' => self::get('recording_hide_button_default'), + 'recording_hide_button_editable' => self::get('recording_hide_button_editable'), + 'general_warning_message' => self::get('general_warning_message'), + 'general_warning_box_type' => self::get('general_warning_box_type'), + 'general_warning_button_text' => self::get('general_warning_button_text'), + 'general_warning_button_href' => self::get('general_warning_button_href'), + 'general_warning_button_class' => self::get('general_warning_button_class'), + 'muteonstart_editable' => self::get('muteonstart_editable'), + 'muteonstart_default' => self::get('muteonstart_default'), + 'disablecam_editable' => self::get('disablecam_editable'), + 'disablecam_default' => self::get('disablecam_default'), + 'disablemic_editable' => self::get('disablemic_editable'), + 'disablemic_default' => self::get('disablemic_default'), + 'disableprivatechat_editable' => self::get('disableprivatechat_editable'), + 'disableprivatechat_default' => self::get('disableprivatechat_default'), + 'disablepublicchat_editable' => self::get('disablepublicchat_editable'), + 'disablepublicchat_default' => self::get('disablepublicchat_default'), + 'disablenote_editable' => self::get('disablenote_editable'), + 'disablenote_default' => self::get('disablenote_default'), + 'hideuserlist_editable' => self::get('hideuserlist_editable'), + 'hideuserlist_default' => self::get('hideuserlist_default'), + 'lockedlayout_editable' => self::get('lockedlayout_editable'), + 'lockedlayout_default' => self::get('lockedlayout_default'), + 'lockonjoin_editable' => self::get('lockonjoin_editable'), + 'lockonjoin_default' => self::get('lockonjoin_default'), + 'lockonjoinconfigurable_editable' => self::get('lockonjoinconfigurable_editable'), + 'lockonjoinconfigurable_default' => self::get('lockonjoinconfigurable_default'), + 'welcome_default' => self::get('welcome_default'), + ]; + } + + /** + * Helper function returns an array with enabled features for an specific profile type. + * + * @param array $typeprofiles + * @param string $type + * + * @return array + */ + public static function get_enabled_features($typeprofiles, $type = null) { + $enabledfeatures = []; + $features = $typeprofiles[instance::TYPE_ALL]['features']; + if (!is_null($type) && key_exists($type, $typeprofiles)) { + $features = $typeprofiles[$type]['features']; + } + $enabledfeatures['showroom'] = (in_array('all', $features) || in_array('showroom', $features)); + // Evaluates if recordings are enabled for the Moodle site. + $enabledfeatures['showrecordings'] = false; + if (self::recordings_enabled()) { + $enabledfeatures['showrecordings'] = (in_array('all', $features) || in_array('showrecordings', $features)); + } + $enabledfeatures['importrecordings'] = false; + if (self::importrecordings_enabled()) { + $enabledfeatures['importrecordings'] = (in_array('all', $features) || in_array('importrecordings', $features)); + } + return $enabledfeatures; + } +} diff --git a/mod/bigbluebuttonbn/classes/local/exceptions/bigbluebutton_exception.php b/mod/bigbluebuttonbn/classes/local/exceptions/bigbluebutton_exception.php new file mode 100644 index 00000000000..29a05013e3e --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/exceptions/bigbluebutton_exception.php @@ -0,0 +1,40 @@ +. + +namespace mod_bigbluebuttonbn\local\exceptions; + +use mod_bigbluebuttonbn\plugin; + +/** + * Class bigbluebutton_exception generic exception + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ +class bigbluebutton_exception extends \moodle_exception { + /** + * Constructor + * + * @param string $errorcode The name of the string from error.php to print + * @param mixed $additionalinfo Extra words and phrases that might be required in the error string + */ + public function __construct($errorcode, $additionalinfo = null) { + parent::__construct($errorcode, plugin::COMPONENT, '', $additionalinfo); + } + +} diff --git a/mod/bigbluebuttonbn/classes/local/exceptions/meeting_join_exception.php b/mod/bigbluebuttonbn/classes/local/exceptions/meeting_join_exception.php new file mode 100644 index 00000000000..60b9e896797 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/exceptions/meeting_join_exception.php @@ -0,0 +1,38 @@ +. + +namespace mod_bigbluebuttonbn\local\exceptions; + +use mod_bigbluebuttonbn\plugin; + +/** + * The mod_bigbluebuttonbn cannot join meeting exception. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ +class meeting_join_exception extends \moodle_exception { + /** + * Constructor + * + * @param string $errorcode The name of the string from error.php to print + */ + public function __construct($errorcode) { + parent::__construct($errorcode, plugin::COMPONENT); + } +} diff --git a/mod/bigbluebuttonbn/classes/local/exceptions/server_not_available_exception.php b/mod/bigbluebuttonbn/classes/local/exceptions/server_not_available_exception.php new file mode 100644 index 00000000000..dcdb0fecb98 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/exceptions/server_not_available_exception.php @@ -0,0 +1,52 @@ +. + +namespace mod_bigbluebuttonbn\local\exceptions; + +/** + * Class server_not_available_exception + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ +class server_not_available_exception extends \moodle_exception { + + /** + * Constructor + * + * @param string $errorcode The name of the string from error.php to print + * @param string $module name of module + * @param string $link The url where the user will be prompted to continue. If no url is provided the user will be directed to + * the site index page. + * @param mixed $a Extra words and phrases that might be required in the error string + * @param string $debuginfo optional debugging information + */ + public function __construct($errorcode, $module = '', $link = '', $a = null, $debuginfo = null) { + global $CFG; + $hasdebugdeveloper = ( + isset($CFG->debugdisplay) && + isset($CFG->debug) && + $CFG->debugdisplay && + $CFG->debug === DEBUG_DEVELOPER + ); + if ($hasdebugdeveloper && is_null($debuginfo)) { + $debuginfo = $this->getTraceAsString(); + } + parent::__construct($errorcode, $module, $link, $a, $debuginfo); + } +} diff --git a/mod/bigbluebuttonbn/classes/local/helpers/files.php b/mod/bigbluebuttonbn/classes/local/helpers/files.php new file mode 100644 index 00000000000..9cc37ecfba2 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/helpers/files.php @@ -0,0 +1,320 @@ +. + +/** + * The mod_bigbluebuttonbn files helper + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ + +namespace mod_bigbluebuttonbn\local\helpers; + +use cache; +use cache_store; +use context; +use context_module; +use context_system; +use mod_bigbluebuttonbn\instance; +use moodle_url; +use stdClass; + +/** + * Utility class for all files routines helper + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class files { + + /** + * Helper for validating pluginfile. + * + * @param stdClass $context context object + * @param string $filearea file area + * + * @return bool|null false if file not valid + */ + public static function pluginfile_valid(stdClass $context, string $filearea): ?bool { + + // Can be in context module or in context_system (if is the presentation by default). + if (!in_array($context->contextlevel, [CONTEXT_MODULE, CONTEXT_SYSTEM])) { + return false; + } + + if (!array_key_exists($filearea, self::get_file_areas())) { + return false; + } + + return true; + } + + /** + * Helper for getting pluginfile. + * + * @param stdClass $course course object + * @param \cm_info $cm course module object + * @param context $context context object + * @param string $filearea file area + * @param array $args extra arguments + * + * @return \stored_file|bool + */ + public static function pluginfile_file(stdClass $course, \cm_info $cm, context $context, string $filearea, array $args) { + $filename = self::get_plugin_filename($course, $cm, $context, $args); + if (!$filename) { + return false; + } + $fullpath = "/$context->id/mod_bigbluebuttonbn/$filearea/0/" . $filename; + $fs = get_file_storage(); + $file = $fs->get_file_by_hash(sha1($fullpath)); + if (!$file || $file->is_directory()) { + return false; + } + return $file; + } + + /** + * Get a full path to the file attached as a preuploaded presentation + * or if there is none, set the presentation field will be set to blank. + * + * @param stdClass $bigbluebuttonformdata BigBlueButtonBN form data + * Note that $bigbluebuttonformdata->presentation is the id of the filearea whereas the bbb instance table + * stores the file name/path + * @return string + */ + public static function save_media_file(stdClass &$bigbluebuttonformdata): string { + if (!isset($bigbluebuttonformdata->presentation) || $bigbluebuttonformdata->presentation == '') { + return ''; + } + $context = context_module::instance($bigbluebuttonformdata->coursemodule); + // Set the filestorage object. + $fs = get_file_storage(); + // Save the file if it exists that is currently in the draft area. + file_save_draft_area_files($bigbluebuttonformdata->presentation, $context->id, 'mod_bigbluebuttonbn', 'presentation', 0); + // Get the file if it exists. + $files = $fs->get_area_files( + $context->id, + 'mod_bigbluebuttonbn', + 'presentation', + 0, + 'itemid, filepath, filename', + false + ); + // Check that there is a file to process. + $filesrc = ''; + if (count($files) == 1) { + // Get the first (and only) file. + $file = reset($files); + $filesrc = '/' . $file->get_filename(); + } + return $filesrc; + } + + /** + * Helper return array containing the file descriptor for a preuploaded presentation. + * + * @param context $context + * @param string $presentation matching presentation file name + * @param int $id bigbluebutton instance id + * @param bool $withnonce add nonce to the url + * @return array|null the representation of the presentation as an associative array + */ + public static function get_presentation(context $context, string $presentation, $id = null, $withnonce = false): ?array { + global $CFG; + $fs = get_file_storage(); + $files = []; + if (empty($presentation)) { + if ($CFG->bigbluebuttonbn_preuploadpresentation_enabled) { + // Item has not presentation but presentation is enabled.. + // Check if exist some file by default in general mod setting ("presentationdefault"). + $files = $fs->get_area_files( + context_system::instance()->id, + 'mod_bigbluebuttonbn', + 'presentationdefault', + 0, + "filename", + false + ); + $id = null; // This is the general presentation/default so we will generate + // an id as if the activity was null. + } + } else { + $files = $fs->get_area_files( + $context->id, + 'mod_bigbluebuttonbn', + 'presentation', + false, + 'itemid, filepath, filename', + false + ); + } + + if (count($files) == 0) { + return null; // No presentation. + } + + $pnoncevalue = 0; + if ($withnonce) { + $nonceid = 0; + if (!is_null($id)) { + $instance = instance::get_from_instanceid($id); + $nonceid = $instance->get_instance_id(); + } + $pnoncevalue = self::generate_nonce($nonceid); + } + + $file = null; + foreach ($files as $f) { + if (basename($f->get_filename()) == basename($presentation)) { + $file = $f; + } + } + if (!$file && !empty($files)) { + $file = reset($files); + } + if (empty($file)) { + return null; // File was not found. + } + + // Note: $pnoncevalue is an int. + $url = moodle_url::make_pluginfile_url( + $file->get_contextid(), + $file->get_component(), + $file->get_filearea(), + $withnonce ? $pnoncevalue : null, // Hack: item id as a nonce. + $file->get_filepath(), + $file->get_filename() + ); + return [ + 'icondesc' => get_mimetype_description($file), + 'iconname' => file_file_icon($file, 24), + 'name' => $file->get_filename(), + 'url' => $url->out(false), + ]; + } + + /** + * Helper for getting pluginfile name. + * + * @param stdClass $course course object + * @param \cm_info $cm course module object + * @param context $context context object + * @param array $args extra arguments + * + * @return string|null + */ + public static function get_plugin_filename(stdClass $course, \cm_info $cm, context $context, array $args): ?string { + global $DB; + if ($context->contextlevel != CONTEXT_SYSTEM) { + // Plugin has a file to use as default in general setting. + // The difference with the standard bigbluebuttonbn_pluginfile_filename() are. + // - Context is system, so we don't need to check the cmid in this case. + // - The area is "presentationdefault_cache". + if (!$DB->get_record('bigbluebuttonbn', ['id' => $cm->instance])) { + return null; + } + } + // Plugin has a file to use as default in general setting. + // The difference with the standard bigbluebuttonbn_pluginfile_filename() are. + // - Context is system, so we don't need to check the cmid in this case. + // - The area is "presentationdefault_cache". + if (count($args) > 1) { + $id = 0; + if ($cm) { + $instance = instance::get_from_cmid($cm->id); + $id = $instance->get_instance_id(); + } + $actualnonce = self::get_nonce($id); + return ($args['0'] == $actualnonce) ? $args['1'] : null; + + } + require_course_login($course, true, $cm, true, true); + if (!has_capability('mod/bigbluebuttonbn:join', $context)) { + return null; + } + return implode('/', $args); + } + + /** + * Helper generates a salt used for the preuploaded presentation callback url. + * + * @param int $id + * @return int + * @throws \coding_exception + */ + protected static function get_nonce(int $id): int { + $cache = static::get_nonce_cache(); + $pnoncekey = sha1($id); + $existingnoncedata = $cache->get($pnoncekey); + if ($existingnoncedata) { + if ($existingnoncedata->counter > 0) { + $existingnoncedata->counter--; + $cache->set($pnoncekey, $existingnoncedata); + return $existingnoncedata->nonce; + } + } + // The item id was adapted for granting public access to the presentation once in order to allow BigBlueButton to gather + // the file once. + return static::generate_nonce($id); + } + + /** + * Generate a nonce and store it in the cache + * + * @param int $id + * @return int + */ + protected static function generate_nonce($id): int { + $cache = static::get_nonce_cache(); + $pnoncekey = sha1($id); + // The item id was adapted for granting public access to the presentation once in order to allow BigBlueButton to gather + // the file once. + $pnoncevalue = ((int) microtime()) + mt_rand(); + $cache->set($pnoncekey, (object) ['nonce' => $pnoncevalue, 'counter' => 2]); + return $pnoncevalue; + } + + /** + * Get cache for nonce + * + * @return \cache_application|\cache_session|cache_store + */ + private static function get_nonce_cache() { + return cache::make_from_params( + cache_store::MODE_APPLICATION, + 'mod_bigbluebuttonbn', + 'presentation_cache' + ); + } + + /** + * Returns an array of file areas. + * + * @return array a list of available file areas + * + */ + protected static function get_file_areas(): array { + $areas = []; + $areas['presentation'] = get_string('mod_form_block_presentation', 'bigbluebuttonbn'); + $areas['presentationdefault'] = get_string('mod_form_block_presentation_default', 'bigbluebuttonbn'); + return $areas; + } + +} diff --git a/mod/bigbluebuttonbn/classes/local/helpers/mod_helper.php b/mod/bigbluebuttonbn/classes/local/helpers/mod_helper.php new file mode 100644 index 00000000000..442f9b12fe9 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/helpers/mod_helper.php @@ -0,0 +1,243 @@ +. + +namespace mod_bigbluebuttonbn\local\helpers; + +use calendar_event; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\logger; +use mod_bigbluebuttonbn\plugin; +use mod_bigbluebuttonbn\task\send_instance_update_notification; +use stdClass; + +/** + * Utility class for all instance (module) routines helper. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ +class mod_helper { + + /** + * Runs any processes that must run before a bigbluebuttonbn insert/update. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data + **/ + public static function process_pre_save(stdClass $bigbluebuttonbn) { + self::process_pre_save_instance($bigbluebuttonbn); + self::process_pre_save_checkboxes($bigbluebuttonbn); + self::process_pre_save_common($bigbluebuttonbn); + $bigbluebuttonbn->participants = htmlspecialchars_decode($bigbluebuttonbn->participants); + } + + /** + * Runs process for defining the instance (insert/update). + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data + **/ + protected static function process_pre_save_instance(stdClass $bigbluebuttonbn): void { + $bigbluebuttonbn->timemodified = time(); + if ((integer) $bigbluebuttonbn->instance == 0) { + $bigbluebuttonbn->meetingid = 0; + $bigbluebuttonbn->timecreated = time(); + $bigbluebuttonbn->timemodified = 0; + // As it is a new activity, assign passwords. + $bigbluebuttonbn->moderatorpass = plugin::random_password(12); + $bigbluebuttonbn->viewerpass = plugin::random_password(12, $bigbluebuttonbn->moderatorpass); + } + } + + /** + * Runs process for assigning default value to checkboxes. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data + **/ + protected static function process_pre_save_checkboxes($bigbluebuttonbn) { + if (!isset($bigbluebuttonbn->wait)) { + $bigbluebuttonbn->wait = 0; + } + if (!isset($bigbluebuttonbn->record)) { + $bigbluebuttonbn->record = 0; + } + if (!isset($bigbluebuttonbn->recordallfromstart)) { + $bigbluebuttonbn->recordallfromstart = 0; + } + if (!isset($bigbluebuttonbn->recordhidebutton)) { + $bigbluebuttonbn->recordhidebutton = 0; + } + if (!isset($bigbluebuttonbn->recordings_html)) { + $bigbluebuttonbn->recordings_html = 0; + } + if (!isset($bigbluebuttonbn->recordings_deleted)) { + $bigbluebuttonbn->recordings_deleted = 0; + } + if (!isset($bigbluebuttonbn->recordings_imported)) { + $bigbluebuttonbn->recordings_imported = 0; + } + if (!isset($bigbluebuttonbn->recordings_preview)) { + $bigbluebuttonbn->recordings_preview = 0; + } + if (!isset($bigbluebuttonbn->muteonstart)) { + $bigbluebuttonbn->muteonstart = 0; + } + if (!isset($bigbluebuttonbn->disablecam)) { + $bigbluebuttonbn->disablecam = 0; + } + if (!isset($bigbluebuttonbn->disablemic)) { + $bigbluebuttonbn->disablemic = 0; + } + if (!isset($bigbluebuttonbn->disableprivatechat)) { + $bigbluebuttonbn->disableprivatechat = 0; + } + if (!isset($bigbluebuttonbn->disablepublicchat)) { + $bigbluebuttonbn->disablepublicchat = 0; + } + if (!isset($bigbluebuttonbn->disablenote)) { + $bigbluebuttonbn->disablenote = 0; + } + if (!isset($bigbluebuttonbn->hideuserlist)) { + $bigbluebuttonbn->hideuserlist = 0; + } + if (!isset($bigbluebuttonbn->lockedlayout)) { + $bigbluebuttonbn->lockedlayout = 0; + } + if (!isset($bigbluebuttonbn->lockonjoin)) { + $bigbluebuttonbn->lockonjoin = 0; + } + if (!isset($bigbluebuttonbn->lockonjoinconfigurable)) { + $bigbluebuttonbn->lockonjoinconfigurable = 0; + } + if (!isset($bigbluebuttonbn->recordings_validate_url)) { + $bigbluebuttonbn->recordings_validate_url = 1; + } + } + + /** + * Runs process for wipping common settings when 'recordings only'. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data + **/ + protected static function process_pre_save_common(stdClass $bigbluebuttonbn): void { + // Make sure common settings are removed when 'recordings only'. + if ($bigbluebuttonbn->type == instance::TYPE_RECORDING_ONLY) { + $bigbluebuttonbn->groupmode = 0; + $bigbluebuttonbn->groupingid = 0; + } + } + + /** + * Runs any processes that must be run after a bigbluebuttonbn insert/update. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data + **/ + public static function process_post_save(stdClass $bigbluebuttonbn): void { + if (isset($bigbluebuttonbn->notification) && $bigbluebuttonbn->notification) { + self::process_post_save_notification($bigbluebuttonbn); + } + self::process_post_save_event($bigbluebuttonbn); + self::process_post_save_completion($bigbluebuttonbn); + } + + /** + * Generates a message on insert/update which is sent to all users enrolled. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data + **/ + protected static function process_post_save_notification(stdClass $bigbluebuttonbn): void { + $task = new send_instance_update_notification(); + $task->set_instance_id($bigbluebuttonbn->id); + + if (isset($bigbluebuttonbn->add) && !empty($bigbluebuttonbn->add)) { + $task->set_update_type(send_instance_update_notification::TYPE_CREATED); + } else { + $task->set_update_type(send_instance_update_notification::TYPE_UPDATED); + } + + \core\task\manager::queue_adhoc_task($task); + } + + /** + * Generates an event after a bigbluebuttonbn insert/update. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data + **/ + protected static function process_post_save_event(stdClass $bigbluebuttonbn): void { + global $CFG, $DB; + + require_once($CFG->dirroot . '/calendar/lib.php'); + $eventid = $DB->get_field('event', 'id', [ + 'modulename' => 'bigbluebuttonbn', + 'instance' => $bigbluebuttonbn->id, + 'eventtype' => logger::EVENT_MEETING_START + ]); + + // Delete the event from calendar when/if openingtime is NOT set. + if (!isset($bigbluebuttonbn->openingtime) || !$bigbluebuttonbn->openingtime) { + if ($eventid) { + $calendarevent = calendar_event::load($eventid); + $calendarevent->delete(); + } + return; + } + + // Add event to the calendar as openingtime is set. + $event = (object) [ + 'eventtype' => logger::EVENT_MEETING_START, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'name' => get_string('calendarstarts', 'bigbluebuttonbn', $bigbluebuttonbn->name), + 'description' => format_module_intro('bigbluebuttonbn', $bigbluebuttonbn, $bigbluebuttonbn->coursemodule, false), + 'format' => FORMAT_HTML, + 'courseid' => $bigbluebuttonbn->course, + 'groupid' => 0, + 'userid' => 0, + 'modulename' => 'bigbluebuttonbn', + 'instance' => $bigbluebuttonbn->id, + 'timestart' => $bigbluebuttonbn->openingtime, + 'timeduration' => 0, + 'timesort' => $bigbluebuttonbn->openingtime, + 'visible' => instance_is_visible('bigbluebuttonbn', $bigbluebuttonbn), + 'priority' => null, + ]; + + // Update the event in calendar when/if eventid was found. + if ($eventid) { + $event->id = $eventid; + $calendarevent = calendar_event::load($eventid); + $calendarevent->update($event); + return; + } + calendar_event::create($event); + } + + /** + * Generates an event after a bigbluebuttonbn activity is completed. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN form data + **/ + protected static function process_post_save_completion(stdClass $bigbluebuttonbn): void { + if (empty($bigbluebuttonbn->completionexpected)) { + return; + } + \core_completion\api::update_completion_date_event( + $bigbluebuttonbn->coursemodule, + 'bigbluebuttonbn', + $bigbluebuttonbn->id, + $bigbluebuttonbn->completionexpected + ); + } +} diff --git a/mod/bigbluebuttonbn/classes/local/helpers/reset.php b/mod/bigbluebuttonbn/classes/local/helpers/reset.php new file mode 100644 index 00000000000..fb24b8a5e05 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/helpers/reset.php @@ -0,0 +1,120 @@ +. +/** + * The mod_bigbluebuttonbn resetting instance helper + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ + +namespace mod_bigbluebuttonbn\local\helpers; + +use context_module; +use core_tag_tag; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\recording; + +/** + * Utility class for resetting instance routines helper + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reset { + + /** + * Used by the reset_course_userdata for deleting recordings + * + * This will delete recordings in the database and not in the remote BBB server. + * + * @param int $courseid + */ + public static function reset_recordings(int $courseid): void { + // Criteria for search : courseid or bigbluebuttonbn=null or subset=false or includedeleted=true. + $recordings = recording::get_recordings_for_course( + $courseid, + [], // Exclude itself. + false, + true + ); + if ($recordings) { + // Remove all the recordings. + foreach ($recordings as $recording) { + $recording->delete(); + } + } + } + + /** + * Used by the reset_course_userdata for deleting tags linked to bigbluebuttonbn instances in the course. + * + * @param int $courseid + */ + public static function reset_tags(int $courseid): void { + global $DB; + // Remove all the tags linked to the room/activities in this course. + if ($bigbluebuttonbns = $DB->get_records('bigbluebuttonbn', ['course' => $courseid])) { + foreach ($bigbluebuttonbns as $bigbluebuttonbn) { + if (!$cm = get_coursemodule_from_instance('bigbluebuttonbn', $bigbluebuttonbn->id, $courseid)) { + continue; + } + $context = context_module::instance($cm->id); + core_tag_tag::delete_instances('mod_bigbluebuttonbn', null, $context->id); + } + } + } + + /** + * Used by the reset_course_userdata for deleting events linked to bigbluebuttonbn instances in the course. + * + * @param string $courseid + * @return bool status array + */ + public static function reset_events($courseid) { + global $DB; + // Remove all the events. + return $DB->delete_records('event', ['modulename' => 'bigbluebuttonbn', 'courseid' => $courseid]); + } + + /** + * Returns status used on every defined reset action. + * + * @param string $item + * @return array status array + */ + public static function reset_getstatus(string $item): array { + return ['component' => get_string('modulenameplural', 'bigbluebuttonbn'), + 'item' => get_string("removed{$item}", 'bigbluebuttonbn'), + 'error' => false]; + } + + /** + * Define items to be reset by course/reset.php + * + * @return array + */ + public static function reset_course_items(): array { + $items = ["events" => 0, "tags" => 0, "logs" => 0]; + // Include recordings only if enabled. + if ((boolean) config::recordings_enabled()) { + $items["recordings"] = 0; + } + return $items; + } +} diff --git a/mod/bigbluebuttonbn/classes/local/helpers/roles.php b/mod/bigbluebuttonbn/classes/local/helpers/roles.php new file mode 100644 index 00000000000..e57b9e41c4f --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/helpers/roles.php @@ -0,0 +1,451 @@ +. +/** + * The mod_bigbluebuttonbn roles helper + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ + +namespace mod_bigbluebuttonbn\local\helpers; + +use cache; +use cache_store; +use context; +use context_course; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use stdClass; + +/** + * Utility class for all roles routines helper + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class roles { + + /** @var int The bigbluebutton viewer role */ + public const ROLE_VIEWER = 'viewer'; + + /** @var string The bigbluebutton moderator role */ + public const ROLE_MODERATOR = 'moderator'; + + /** + * Returns user roles in a context. + * + * @param context $context + * @param int $userid + * + * @return array $userroles + */ + public static function get_user_roles(context $context, int $userid) { + global $DB; + $userroles = get_user_roles($context, $userid); + if ($userroles) { + $where = ''; + foreach ($userroles as $userrole) { + $where .= (empty($where) ? ' WHERE' : ' OR') . ' id=' . $userrole->roleid; + } + $userroles = $DB->get_records_sql('SELECT * FROM {role}' . $where); + } + return $userroles; + } + + /** + * Returns guest role wrapped in an array. + * + * @return array + */ + protected static function get_guest_role() { + $guestrole = get_guest_role(); + return [$guestrole->id => $guestrole]; + } + + /** + * Returns an array containing all the users in a context wrapped for html select element. + * + * @param context_course $context + * @param null $bbactivity + * @return array $users + */ + public static function get_users_array(context_course $context, $bbactivity = null) { + // CONTRIB-7972, check the group of current user and course group mode. + $groups = null; + $users = (array) get_enrolled_users($context, '', 0, 'u.*', null, 0, 0, true); + $course = get_course($context->instanceid); + $groupmode = groups_get_course_groupmode($course); + if ($bbactivity) { + list($bbcourse, $cm) = get_course_and_cm_from_instance($bbactivity->id, 'bigbluebuttonbn'); + $groupmode = groups_get_activity_groupmode($cm); + + } + if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) { + global $USER; + $groups = groups_get_all_groups($course->id, $USER->id); + $users = []; + foreach ($groups as $g) { + $users += (array) get_enrolled_users($context, '', $g->id, 'u.*', null, 0, 0, true); + } + } + return array_map( + function($u) { + return ['id' => $u->id, 'name' => fullname($u)]; + }, + $users); + } + + /** + * Can do some administration in this course, likely manage recordings + * + * @param int $courseid + * @param string $capability + */ + public static function has_capability_in_course(int $courseid, string $capability) { + global $DB; + if (empty($courseid) || $DB->record_exists('course', ['id' => $courseid])) { + return has_capability('moodle/site:config', \context_system::instance()); + } + + $coursecontext = context_course::instance($courseid); + return has_capability($capability, $coursecontext); + } + + /** + * Returns an array containing all the roles in a context. + * + * @param context|null $context $context + * @param bool|null $onlyviewableroles + * + * @return array $roles + */ + public static function get_roles(?context $context = null, ?bool $onlyviewableroles = true) { + global $CFG; + + if ($onlyviewableroles == true && $CFG->branch >= 35) { + $roles = (array) get_viewable_roles($context); + foreach ($roles as $key => $value) { + $roles[$key] = $value; + } + } else { + $roles = (array) role_get_names($context); + foreach ($roles as $key => $value) { + $roles[$key] = $value->localname; + } + } + + return $roles; + } + + /** + * Returns an array containing all the roles in a context wrapped for html select element. + * + * @param context|null $context $context + * @param bool $onlyviewableroles + * + * @return array $users + */ + protected static function get_roles_select(context $context = null, bool $onlyviewableroles = true) { + global $CFG; + + if ($onlyviewableroles == true && $CFG->branch >= 35) { + $roles = (array) get_viewable_roles($context); + foreach ($roles as $key => $value) { + $roles[$key] = ['id' => $key, 'name' => $value]; + } + } else { + $roles = (array) role_get_names($context); + foreach ($roles as $key => $value) { + $roles[$key] = ['id' => $value->id, 'name' => $value->localname]; + } + } + + return $roles; + } + + /** + * Returns role that corresponds to an id. + * + * @param string|integer $id + * + * @return stdClass $role + */ + protected static function get_role($id): stdClass { + $roles = (array) role_get_names(); + if (is_numeric($id) && isset($roles[$id])) { + return (object) $roles[$id]; + } + foreach ($roles as $role) { + if ($role->shortname == $id) { + return $role; + } + } + } + + /** + * Returns an array to populate a list of participants used in mod_form.js. + * + * @param context $context + * @param null|stdClass $bbactivity + * @return array $data + */ + public static function get_participant_data(context $context, ?stdClass $bbactivity = null) { + $data = [ + 'all' => [ + 'name' => get_string('mod_form_field_participant_list_type_all', 'bigbluebuttonbn'), + 'children' => [] + ], + ]; + $data['role'] = [ + 'name' => get_string('mod_form_field_participant_list_type_role', 'bigbluebuttonbn'), + 'children' => self::get_roles_select($context, true) + ]; + $data['user'] = [ + 'name' => get_string('mod_form_field_participant_list_type_user', 'bigbluebuttonbn'), + 'children' => self::get_users_array($context, $bbactivity), + ]; + return $data; + } + + /** + * Returns an array to populate a list of participants used in mod_form.php. + * + * @param stdClass|null $bigbluebuttonbn + * @param context $context + * + * @return array + */ + public static function get_participant_list(?stdClass $bigbluebuttonbn, context $context): array { + global $USER; + if ($bigbluebuttonbn == null) { + return self::get_participant_rules_encoded( + self::get_participant_list_default($context, $USER->id) + ); + } + if (empty($bigbluebuttonbn->participants)) { + $bigbluebuttonbn->participants = "[]"; + } + $rules = json_decode($bigbluebuttonbn->participants, true); + if (empty($rules)) { + $rules = self::get_participant_list_default($context, + bigbluebutton_proxy::get_instance_ownerid($bigbluebuttonbn)); + } + return self::get_participant_rules_encoded($rules); + } + + /** + * Returns an array to populate a list of participants used in mod_form.php with default values. + * + * @param context $context + * @param int $ownerid + * + * @return array + */ + protected static function get_participant_list_default(context $context, ?int $ownerid = null) { + $participantlist = []; + $participantlist[] = [ + 'selectiontype' => 'all', + 'selectionid' => 'all', + 'role' => self::ROLE_VIEWER, + ]; + $defaultrules = explode(',', \mod_bigbluebuttonbn\local\config::get('participant_moderator_default')); + foreach ($defaultrules as $defaultrule) { + if ($defaultrule == '0') { + if (!empty($ownerid) && is_enrolled($context, $ownerid)) { + $participantlist[] = [ + 'selectiontype' => 'user', + 'selectionid' => (string) $ownerid, + 'role' => self::ROLE_MODERATOR]; + } + continue; + } + $participantlist[] = [ + 'selectiontype' => 'role', + 'selectionid' => $defaultrule, + 'role' => self::ROLE_MODERATOR]; + } + return $participantlist; + } + + /** + * Returns an array to populate a list of participants used in mod_form.php with bigbluebuttonbn values. + * + * @param array $rules + * + * @return array + */ + protected static function get_participant_rules_encoded(array $rules): array { + foreach ($rules as $key => $rule) { + if ($rule['selectiontype'] !== 'role' || is_numeric($rule['selectionid'])) { + continue; + } + $role = self::get_role($rule['selectionid']); + if ($role == null) { + unset($rules[$key]); + continue; + } + $rule['selectionid'] = $role->id; + $rules[$key] = $rule; + } + return $rules; + } + + /** + * Returns an array to populate a list of participant_selection used in mod_form.php. + * + * @return array + */ + public static function get_participant_selection_data(): array { + return [ + 'type_options' => [ + 'all' => get_string('mod_form_field_participant_list_type_all', 'bigbluebuttonbn'), + 'role' => get_string('mod_form_field_participant_list_type_role', 'bigbluebuttonbn'), + 'user' => get_string('mod_form_field_participant_list_type_user', 'bigbluebuttonbn'), + ], + 'type_selected' => 'all', + 'options' => ['all' => '---------------'], + 'selected' => 'all', + ]; + } + + /** + * Evaluate if a user in a context is moderator based on roles and participation rules. + * + * @param context $context + * @param array $participantlist + * @param int $userid + * + * @return bool + */ + public static function is_moderator(context $context, array $participantlist, ?int $userid = null): bool { + global $USER; + // If an admin, then also a moderator. + if (has_capability('moodle/site:config', $context)) { + return true; + } + if (!is_array($participantlist)) { + return false; + } + if (empty($userid)) { + $userid = $USER->id; + } + $userroles = self::get_guest_role(); + if (!isguestuser()) { + $userroles = self::get_user_roles($context, $userid); + } + return self::is_moderator_validator($participantlist, $userid, $userroles); + } + + /** + * Iterates participant list rules to evaluate if a user is moderator. + * + * @param array $participantlist + * @param int $userid + * @param array $userroles + * + * @return bool + */ + protected static function is_moderator_validator(array $participantlist, int $userid, array $userroles): bool { + // Iterate participant rules. + foreach ($participantlist as $participant) { + if (self::is_moderator_validate_rule($participant, $userid, $userroles)) { + return true; + } + } + return false; + } + + /** + * Evaluate if a user is moderator based on roles and a particular participation rule. + * + * @param array $participant + * @param int $userid + * @param array $userroles + * + * @return bool + */ + protected static function is_moderator_validate_rule(array $participant, int $userid, array $userroles): bool { + if ($participant['role'] == self::ROLE_VIEWER) { + return false; + } + // Validation for the 'all' rule. + if ($participant['selectiontype'] == 'all') { + return true; + } + // Validation for a 'user' rule. + if ($participant['selectiontype'] == 'user') { + if ($participant['selectionid'] == $userid) { + return true; + } + return false; + } + // Validation for a 'role' rule. + $role = self::get_role($participant['selectionid']); + if ($role != null && array_key_exists($role->id, $userroles)) { + return true; + } + return false; + } + + /** + * Updates the meeting info cached object when a participant has joined. + * + * @param string $meetingid + * @param bool $ismoderator + * + * @return void + */ + public static function participant_joined(string $meetingid, bool $ismoderator): void { + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'meetings_cache'); + $result = $cache->get($meetingid); + $meetinginfo = json_decode($result['meeting_info']); + $meetinginfo->participantCount += 1; + if ($ismoderator) { + $meetinginfo->moderatorCount += 1; + } + $cache->set($meetingid, ['creation_time' => $result['creation_time'], + 'meeting_info' => json_encode($meetinginfo)]); + } + + /** + * Helper function returns a list of courses a user has access to, wrapped in an array that can be used + * by a html select. + * + * @param instance $instance + * @return array + */ + public static function import_get_courses_for_select(instance $instance): array { + if ($instance->is_admin()) { + $courses = get_courses('all', 'c.fullname ASC'); + // It includes the name of the site as a course (category 0), so remove the first one. + unset($courses['1']); + } else { + $courses = enrol_get_users_courses($instance->get_user_id(), false, 'id,shortname,fullname'); + } + $courses = array_filter($courses, function($course) { + $modules = get_fast_modinfo($course->id); + return !empty($modules->instances['bigbluebuttonbn']); + }); + $coursesforselect = []; + foreach ($courses as $course) { + $coursesforselect[$course->id] = $course->fullname . " (" . $course->shortname . ")"; + } + return $coursesforselect; + } +} diff --git a/mod/bigbluebuttonbn/classes/local/proxy/bigbluebutton_proxy.php b/mod/bigbluebuttonbn/classes/local/proxy/bigbluebutton_proxy.php new file mode 100644 index 00000000000..411b5d9d76a --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/proxy/bigbluebutton_proxy.php @@ -0,0 +1,483 @@ +. + +namespace mod_bigbluebuttonbn\local\proxy; + +use cache; +use completion_info; +use Exception; +use mod_bigbluebuttonbn\completion\custom_completion; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; +use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; +use moodle_url; +use stdClass; + +/** + * The bigbluebutton proxy class. + * + * This class acts as a proxy between Moodle and the BigBlueButton API server, + * and handles all requests relating to the server and meetings. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class bigbluebutton_proxy extends proxy_base { + + /** + * Builds and returns a url for joining a bigbluebutton meeting. + * + * @param string $meetingid + * @param string $username + * @param string $pw + * @param string $logouturl + * @param string|null $configtoken + * @param string|null $userid + * @param string|null $createtime + * + * @return string + */ + public static function get_join_url( + string $meetingid, + string $username, + string $pw, + string $logouturl, + string $configtoken = null, + string $userid = null, + string $createtime = null + ): ?string { + $data = [ + 'meetingID' => $meetingid, + 'fullName' => $username, + 'password' => $pw, + 'logoutURL' => $logouturl, + ]; + + if (!is_null($configtoken)) { + $data['configToken'] = $configtoken; + } + + if (!is_null($userid)) { + $data['userID'] = $userid; + } + + if (!is_null($createtime)) { + $data['createTime'] = $createtime; + } + + return self::action_url('join', $data); + } + + /** + * Perform api request on BBB. + * + * @return null|string + */ + public static function get_server_version(): ?string { + $cache = cache::make('mod_bigbluebuttonbn', 'serverinfo'); + $serverversion = $cache->get('serverversion'); + + if (!$serverversion) { + $xml = self::fetch_endpoint_xml(''); + if (!$xml || $xml->returncode != 'SUCCESS') { + return null; + } + + if (!isset($xml->version)) { + return null; + } + + $serverversion = (string) $xml->version; + $cache->set('serverversion', $serverversion); + } + + return (double) $serverversion; + } + + /** + * Helper for getting the owner userid of a bigbluebuttonbn instance. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN instance + * @return int ownerid (a valid user id or null if not registered/found) + */ + public static function get_instance_ownerid(stdClass $bigbluebuttonbn): int { + global $DB; + + $filters = [ + 'bigbluebuttonbnid' => $bigbluebuttonbn->id, + 'log' => 'Add', + ]; + + return (int) $DB->get_field('bigbluebuttonbn_logs', 'userid', $filters); + } + + /** + * Helper evaluates if a voicebridge number is unique. + * + * @param int $instance + * @param int $voicebridge + * @return bool + */ + public static function is_voicebridge_number_unique(int $instance, int $voicebridge): bool { + global $DB; + if ($voicebridge == 0) { + return true; + } + $select = 'voicebridge = ' . $voicebridge; + if ($instance != 0) { + $select .= ' AND id <>' . $instance; + } + if (!$DB->get_records_select('bigbluebuttonbn', $select)) { + return true; + } + return false; + } + + /** + * Helper function validates a remote resource. + * + * @param string $url + * @return bool + */ + public static function is_remote_resource_valid(string $url): bool { + $urlhost = parse_url($url, PHP_URL_HOST); + $serverurlhost = parse_url(\mod_bigbluebuttonbn\local\config::get('server_url'), PHP_URL_HOST); + + if ($urlhost == $serverurlhost) { + // Skip validation when the recording URL host is the same as the configured BBB server. + return true; + } + + $cache = cache::make('mod_bigbluebuttonbn', 'validatedurls'); + + if ($cachevalue = $cache->get($urlhost)) { + // Skip validation when the recording URL was already validated. + return $cachevalue == 1; + } + + $curl = new curl(); + $curl->head($url); + + $isvalid = false; + if ($info = $curl->get_info()) { + if ($info['http_code'] == 200) { + $isvalid = true; + } else { + debugging( + "Resources hosted by {$urlhost} are unreachable. Server responded with {$info['http_code']}", + DEBUG_DEVELOPER + ); + $isvalid = false; + } + + // Note: When a cache key is not found, it returns false. + // We need to distinguish between a result not found, and an invalid result. + $cache->set($urlhost, $isvalid ? 1 : 0); + } + + return $isvalid; + } + + /** + * Helper function enqueues one user for being validated as for completion. + * + * @param stdClass $bigbluebuttonbn + * @param int $userid + * @return void + */ + public static function enqueue_completion_event(stdClass $bigbluebuttonbn, int $userid): void { + try { + // Create the instance of completion_update_state task. + $task = new \mod_bigbluebuttonbn\task\completion_update_state(); + // Add custom data. + $data = [ + 'bigbluebuttonbn' => $bigbluebuttonbn, + 'userid' => $userid, + ]; + $task->set_custom_data($data); + // CONTRIB-7457: Task should be executed by a user, maybe Teacher as Student won't have rights for overriding. + // $ task -> set_userid ( $ user -> id );. + // Enqueue it. + \core\task\manager::queue_adhoc_task($task); + } catch (Exception $e) { + mtrace("Error while enqueuing completion_update_state task. " . (string) $e); + } + } + + /** + * Helper function enqueues completion trigger. + * + * @param stdClass $bigbluebuttonbn + * @param int $userid + * @return void + */ + public static function update_completion_state(stdClass $bigbluebuttonbn, int $userid) { + global $CFG; + require_once($CFG->libdir . '/completionlib.php'); + list($course, $cm) = get_course_and_cm_from_instance($bigbluebuttonbn, 'bigbluebuttonbn'); + $completion = new completion_info($course); + if (!$completion->is_enabled($cm)) { + mtrace("Completion not enabled"); + return; + } + + $bbbcompletion = new custom_completion($cm, $userid); + if ($bbbcompletion->get_overall_completion_state()) { + mtrace("Completion succeeded for user $userid"); + $completion->update_state($cm, COMPLETION_COMPLETE, $userid, true); + } else { + mtrace("Completion did not succeed for user $userid"); + } + } + + /** + * Helper function returns an array with the profiles (with features per profile) for the different types + * of bigbluebuttonbn instances. + * + * @return array + */ + public static function get_instance_type_profiles(): array { + $instanceprofiles = [ + instance::TYPE_ALL => [ + 'id' => instance::TYPE_ALL, + 'name' => get_string('instance_type_default', 'bigbluebuttonbn'), + 'features' => ['all'] + ], + instance::TYPE_ROOM_ONLY => [ + 'id' => instance::TYPE_ROOM_ONLY, + 'name' => get_string('instance_type_room_only', 'bigbluebuttonbn'), + 'features' => ['showroom', 'welcomemessage', 'voicebridge', 'waitformoderator', 'userlimit', + 'recording', 'sendnotifications', 'preuploadpresentation', 'permissions', 'schedule', 'groups', + 'modstandardelshdr', 'availabilityconditionsheader', 'tagshdr', 'competenciessection', + 'completionattendance', 'completionengagement', 'availabilityconditionsheader'] + ], + instance::TYPE_RECORDING_ONLY => [ + 'id' => instance::TYPE_RECORDING_ONLY, + 'name' => get_string('instance_type_recording_only', 'bigbluebuttonbn'), + 'features' => ['showrecordings', 'importrecordings', 'availabilityconditionsheader'] + ], + ]; + return $instanceprofiles; + } + + /** + * Helper function returns an array with the profiles (with features per profile) for the different types + * of bigbluebuttonbn instances that the user is allowed to create. + * + * @param bool $room + * @param bool $recording + * + * @return array + */ + public static function get_instance_type_profiles_create_allowed(bool $room, bool $recording): array { + $profiles = self::get_instance_type_profiles(); + if (!$room) { + unset($profiles[instance::TYPE_ROOM_ONLY]); + unset($profiles[instance::TYPE_ALL]); + } + if (!$recording) { + unset($profiles[instance::TYPE_RECORDING_ONLY]); + unset($profiles[instance::TYPE_ALL]); + } + return $profiles; + } + + /** + * Helper function returns an array with the profiles (with features per profile) for the different types + * of bigbluebuttonbn instances. + * + * @param array $profiles + * + * @return array + */ + public static function get_instance_profiles_array(array $profiles = []): array { + $profilesarray = []; + foreach ($profiles as $key => $profile) { + $profilesarray[$profile['id']] = $profile['name']; + } + return $profilesarray; + } + + /** + * Return the status of an activity [open|not_started|ended]. + * + * @param instance $instance + * @return string + */ + public static function view_get_activity_status(instance $instance): string { + $now = time(); + if (!empty($instance->get_instance_var('openingtime')) && $now < $instance->get_instance_var('openingtime')) { + // The activity has not been opened. + return 'not_started'; + } + if (!empty($instance->get_instance_var('closingtime')) && $now > $instance->get_instance_var('closingtime')) { + // The activity has been closed. + return 'ended'; + } + // The activity is open. + return 'open'; + } + + /** + * Ensure that the remote server was contactable. + * + * @param instance $instance + */ + public static function require_working_server(instance $instance): void { + try { + self::get_server_version(); + } catch (server_not_available_exception $e) { + self::handle_server_not_available($instance); + } + } + + /** + * Handle the server not being available. + * + * @param instance $instance + */ + public static function handle_server_not_available(instance $instance): void { + \core\notification::add( + self::get_server_not_available_message($instance), + \core\notification::ERROR + ); + redirect(self::get_server_not_available_url($instance)); + } + + /** + * Get message when server not available + * + * @param instance $instance + * @return string + */ + public static function get_server_not_available_message(instance $instance): string { + if ($instance->is_admin()) { + return get_string('view_error_unable_join', 'mod_bigbluebuttonbn'); + } else if ($instance->is_moderator()) { + return get_string('view_error_unable_join_teacher', 'mod_bigbluebuttonbn'); + } else { + return get_string('view_error_unable_join_student', 'mod_bigbluebuttonbn'); + } + } + + /** + * Get URL to the page displaying that the server is not available + * + * @param instance $instance + * @return string + */ + public static function get_server_not_available_url(instance $instance): string { + if ($instance->is_admin()) { + return new moodle_url('/admin/settings.php', ['section' => 'modsettingbigbluebuttonbn']); + } else if ($instance->is_moderator()) { + return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]); + } else { + return new moodle_url('/course/view.php', ['id' => $instance->get_course_id()]); + } + } + + /** + * Create a Meeting + * + * @param array $data + * @param array $metadata + * @param string|null $presentationname + * @param string|null $presentationurl + * @return array + * @throws bigbluebutton_exception + * @throws server_not_available_exception + */ + public static function create_meeting( + array $data, + array $metadata, + ?string $presentationname = null, + ?string $presentationurl = null + ): array { + $createmeetingurl = self::action_url('create', $data, $metadata); + + $curl = new curl(); + if (!is_null($presentationname) && !is_null($presentationurl)) { + $payload = ""; + + $xml = $curl->post($createmeetingurl, $payload); + } else { + $xml = $curl->get($createmeetingurl); + } + + self::assert_returned_xml($xml); + + if (empty($xml->meetingID)) { + throw new bigbluebutton_exception('general_error_cannot_create_meeting'); + } + + if ($xml->hasBeenForciblyEnded === 'true') { + throw new bigbluebutton_exception('index_error_forciblyended'); + } + + return [ + 'meetingID' => (string) $xml->meetingID, + 'internalMeetingID' => (string) $xml->internalMeetingID, + 'attendeePW' => (string) $xml->attendeePW, + 'moderatorPW' => (string) $xml->moderatorPW + ]; + } + + /** + * Get meeting info for a given meeting id + * + * @param string $meetingid + * @return array + */ + public static function get_meeting_info(string $meetingid): array { + $xmlinfo = self::fetch_endpoint_xml('getMeetingInfo', ['meetingID' => $meetingid]); + self::assert_returned_xml($xmlinfo, ['meetingid' => $meetingid]); + return (array) $xmlinfo; + } + + /** + * Perform end meeting on BBB. + * + * @param string $meetingid + * @param string $modpw + */ + public static function end_meeting(string $meetingid, string $modpw): void { + $xml = self::fetch_endpoint_xml('end', ['meetingID' => $meetingid, 'password' => $modpw]); + self::assert_returned_xml($xml, ['meetingid' => $meetingid]); + } + + /** + * Helper evaluates if the bigbluebutton server used belongs to blindsidenetworks domain. + * + * @return bool + */ + public static function is_bn_server() { + if (config::get('bn_server')) { + return true; + } + $parsedurl = parse_url(config::get('server_url')); + if (!isset($parsedurl['host'])) { + return false; + } + $h = $parsedurl['host']; + $hends = explode('.', $h); + $hendslength = count($hends); + return ($hends[$hendslength - 1] == 'com' && $hends[$hendslength - 2] == 'blindsidenetworks'); + } +} diff --git a/mod/bigbluebuttonbn/classes/local/proxy/curl.php b/mod/bigbluebuttonbn/classes/local/proxy/curl.php new file mode 100644 index 00000000000..701d7f42cdb --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/proxy/curl.php @@ -0,0 +1,156 @@ +. + +/** + * A curl wrapper for bbb. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_bigbluebuttonbn\local\proxy; + +use SimpleXMLElement; + +defined('MOODLE_INTERNAL') || die; +global $CFG; +require_once("{$CFG->libdir}/filelib.php"); + +/** + * A curl wrapper for bbb. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class curl extends \curl { + /** @var string */ + protected $contenttype; + + /** + * Constructor for the class. + */ + public function __construct() { + $settings = []; + if (debugging()) { + $settings = ['ignoresecurity' => true]; + } + parent::__construct($settings); + + $this->setopt(['SSL_VERIFYPEER' => true]); + $this->set_content_type('text/xml'); + } + + /** + * Fetch the content type. + */ + public function get_content_type(): string { + return $this->contenttype; + } + + /** + * Set the desired current content type. + * + * @param string $type + * @return self + */ + public function set_content_type(string $type): self { + $this->contenttype = $type; + + return $this; + } + + /** + * HTTP POST method + * + * @param string $url + * @param array|string $params + * @param array $options + * @return bool + */ + public function post($url, $params = '', $options = []) { + if (!is_string($params)) { + debugging('Only string paramaters are supported', DEBUG_DEVELOPER); + $params = ''; + } + + $options = [ + 'CURLOPT_HTTPHEADER' => [ + 'Content-Type: ' . $this->get_content_type(), + 'Content-Length: ' . strlen($params), + 'Content-Language: en-US', + ], + ]; + + return !empty($this->handle_response(parent::post($url, $params, $options))); + } + + /** + * Fetch the specified URL via a HEAD request. + * + * @param string $url + * @param array $options + */ + public function head($url, $options = []) { + $options['followlocation'] = true; + $options['timeout'] = 1; + + parent::head($url, $options); + + return $this->get_info(); + } + + /** + * Fetch the specified URL via a GET request. + * + * @param string $url + * @param string $params + * @param array $options + */ + public function get($url, $params = [], $options = []) { + return $this->handle_response(parent::get($url, $params, $options)); + } + + /** + * Handle the response. + * + * @param mixed $response + * @return null|SimpleXMLElement Null on error + */ + protected function handle_response($response): ?SimpleXMLElement { + if (!$response) { + debugging('No response returned for call', DEBUG_DEVELOPER); + return null; + } + + $previous = libxml_use_internal_errors(true); + try { + $xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOBLANKS); + } catch (Exception $e) { + libxml_use_internal_errors($previous); + debugging('Caught exception: ' . $e->getMessage(), DEBUG_DEVELOPER); + return null; + } + + if ($xml instanceof SimpleXMLElement) { + return $xml; + } + + debugging('Issue retrieving information from the server: ' . $response, DEBUG_DEVELOPER); + return null; + } +} diff --git a/mod/bigbluebuttonbn/classes/local/proxy/proxy_base.php b/mod/bigbluebuttonbn/classes/local/proxy/proxy_base.php new file mode 100644 index 00000000000..1c941018e84 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/proxy/proxy_base.php @@ -0,0 +1,152 @@ +. + +namespace mod_bigbluebuttonbn\local\proxy; + +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; +use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; +use mod_bigbluebuttonbn\plugin; +use moodle_url; +use SimpleXMLElement; + +/** + * The abstract proxy base class. + * + * This class provides common and shared functions used when interacting with + * the BigBlueButton API server. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +abstract class proxy_base { + + /** + * Sometimes the server sends back some error and errorKeys that + * can be converted to Moodle error messages + */ + const MEETING_ERROR = [ + 'checksumError' => 'index_error_checksum', + 'notFound' => 'general_error_not_found', + 'maxConcurrent' => 'view_error_max_concurrent', + ]; + + /** + * Returns the right URL for the action specified. + * + * @param string $action + * @param array $data + * @param array $metadata + * @return string + */ + protected static function action_url(string $action = '', array $data = [], array $metadata = []): string { + $baseurl = self::sanitized_url() . $action . '?'; + $metadata = array_combine(array_map(function($k) { + return 'meta_' . $k; + }, array_keys($metadata)), $metadata); + + $params = http_build_query($data + $metadata, '', '&'); + return $baseurl . $params . '&checksum=' . sha1($action . $params . self::sanitized_secret()); + } + + /** + * Makes sure the url used doesn't is in the format required. + * + * @return string + */ + protected static function sanitized_url() { + $serverurl = trim(config::get('server_url')); + if (PHPUNIT_TEST) { + $serverurl = (new moodle_url(TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER))->out(false); + } + if (substr($serverurl, -1) == '/') { + $serverurl = rtrim($serverurl, '/'); + } + if (substr($serverurl, -4) == '/api') { + $serverurl = rtrim($serverurl, '/api'); + } + return $serverurl . '/api/'; + } + + /** + * Makes sure the shared_secret used doesn't have trailing white characters. + * + * @return string + */ + protected static function sanitized_secret(): string { + return trim(config::get('shared_secret')); + } + + /** + * Throw an exception if there is a problem in the returned XML value + * + * @param SimpleXMLElement|bool $xml + * @param array|null $additionaldetails + * @throws bigbluebutton_exception + * @throws server_not_available_exception + */ + protected static function assert_returned_xml($xml, ?array $additionaldetails = null): void { + if (empty($xml)) { + throw new server_not_available_exception( + 'general_error_no_answer', + plugin::COMPONENT, + (new moodle_url('/admin/settings.php?section=modsettingbigbluebuttonbn'))->out() + ); + } + if (is_bool($xml) && $xml) { + // Nothing to do here, this might be a post returning that everything went well. + return; + } + if ((string) $xml->returncode == 'FAILED') { + $messagekey = (string) $xml->messageKey ?? ''; + if (!$additionaldetails) { + $additionaldetails = []; + } + $additionaldetails['xmlmessage'] = (string) $xml->message ?? ''; + if (empty($messagekey) || empty(self::MEETING_ERROR[$messagekey])) { + $messagekey = 'general_error_unable_connect'; + } + + throw new bigbluebutton_exception($messagekey, json_encode($additionaldetails)); + } + } + + /** + * Fetch the XML from an endpoint and test for success. + * + * If the result could not be loaded, or the returncode was not 'SUCCESS', a null value is returned. + * + * @param string $action + * @param array $data + * @param array $metadata + * @return null|bool|SimpleXMLElement + */ + protected static function fetch_endpoint_xml( + string $action, + array $data = [], + array $metadata = [] + ) { + if (PHPUNIT_TEST && !defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { + return true; // In case we still use fetch and mock server is not defined, this prevents + // an error. This can happen if a function from lib.php is called in test from other modules + // for example. + } + $curl = new curl(); + return $curl->get(self::action_url($action, $data, $metadata)); + } +} diff --git a/mod/bigbluebuttonbn/classes/local/proxy/recording_proxy.php b/mod/bigbluebuttonbn/classes/local/proxy/recording_proxy.php new file mode 100644 index 00000000000..507529eac48 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/local/proxy/recording_proxy.php @@ -0,0 +1,342 @@ +. + +namespace mod_bigbluebuttonbn\local\proxy; + +use cache; +use cache_helper; +use SimpleXMLElement; + +/** + * The recording proxy. + * + * This class acts as a proxy between Moodle and the BigBlueButton API server, + * and deals with all requests relating to recordings. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class recording_proxy extends proxy_base { + + /** + * Invalidate the MUC cache for the specified recording. + * + * @param string $recordid + */ + protected static function invalidate_cache_for_recording(string $recordid): void { + cache_helper::invalidate_by_event('mod_bigbluebuttonbn/recordingchanged', [$recordid]); + } + + /** + * Perform deleteRecordings on BBB. + * + * @param string $recordid a recording id + * @return bool + */ + public static function delete_recording(string $recordid): bool { + $result = self::fetch_endpoint_xml('deleteRecordings', ['recordID' => $recordid]); + if (!$result || $result->returncode != 'SUCCESS') { + return false; + } + return true; + } + + /** + * Perform publishRecordings on BBB. + * + * @param string $recordid + * @param string $publish + * @return bool + */ + public static function publish_recording(string $recordid, string $publish = 'true'): bool { + $result = self::fetch_endpoint_xml('publishRecordings', [ + 'recordID' => $recordid, + 'publish' => $publish, + ]); + + self::invalidate_cache_for_recording($recordid); + + if (!$result || $result->returncode != 'SUCCESS') { + return false; + } + + return true; + } + + /** + * Perform publishRecordings on BBB. + * + * @param string $recordid + * @param string $protected + * @return bool + */ + public static function protect_recording(string $recordid, string $protected = 'true'): bool { + $result = self::fetch_endpoint_xml('updateRecordings', [ + 'recordID' => $recordid, + 'protect' => $protected, + ]); + + self::invalidate_cache_for_recording($recordid); + + if (!$result || $result->returncode != 'SUCCESS') { + return false; + } + + return true; + } + + /** + * Perform updateRecordings on BBB. + * + * @param string $recordid a single record identifier + * @param array $params ['key'=>param_key, 'value'] + */ + public static function update_recording(string $recordid, array $params): bool { + $result = self::fetch_endpoint_xml('updateRecordings', array_merge([ + 'recordID' => $recordid + ], $params)); + + self::invalidate_cache_for_recording($recordid); + + return $result ? $result->returncode == 'SUCCESS' : false; + } + + /** + * Helper function to fetch a single recording from a BigBlueButton server. + * + * @param string $recordingid + * @return null|array + */ + public static function fetch_recording(string $recordingid): ?array { + $data = self::fetch_recordings([$recordingid]); + + if (array_key_exists($recordingid, $data)) { + return $data[$recordingid]; + } + + return null; + } + + /** + * Check whether the current recording is a protected recording and purge the cache if necessary. + * + * @param string $recordingid + */ + public static function purge_protected_recording(string $recordingid): void { + $cache = cache::make('mod_bigbluebuttonbn', 'recordings'); + + $recording = $cache->get($recordingid); + if (empty($recording)) { + // This value was not cached to begin with. + return; + } + + $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch'); + if ($currentfetchcache->has($recordingid)) { + // This item was fetched in the current request. + return; + } + + if (array_key_exists('protected', $recording) && $recording['protected'] === 'true') { + // This item is protected. Purge it from the cache. + $cache->delete($recordingid); + return; + } + } + + /** + * Helper function to fetch recordings from a BigBlueButton server. + * + * @param array $keyids list of meetingids or recordingids + * @param string $key the param name used for the BBB request (|meetingID) + * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array + */ + public static function fetch_recordings(array $keyids = [], string $key = 'recordID'): array { + $recordings = []; + + // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''. + if (empty($keyids)) { + return $recordings; + } + + $cache = cache::make('mod_bigbluebuttonbn', 'recordings'); + $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch'); + $recordings = array_filter($cache->get_many($keyids)); + $missingkeys = array_diff(array_values($keyids), array_keys($recordings)); + + $pagesize = 25; + while ($ids = array_splice($missingkeys, 0, $pagesize)) { + $fetchrecordings = self::fetch_recordings_page($ids, $key); + $cache->set_many($fetchrecordings); + $currentfetchcache->set_many(array_flip(array_keys($fetchrecordings))); + + $recordings += $fetchrecordings; + } + + // Sort recordings. + return self::sort_recordings($recordings); + } + + /** + * Helper function to fetch a page of recordings from the remote server. + * + * @param array $ids + * @param string $key + * @return array + */ + private static function fetch_recordings_page(array $ids, $key = 'recordID'): array { + // The getRecordings call is executed using a method GET (supported by all versions of BBB). + $xml = self::fetch_endpoint_xml('getRecordings', [$key => implode(',', $ids)]); + + if (!$xml) { + return []; + } + + if ($xml->returncode != 'SUCCESS') { + return []; + } + + if (!isset($xml->recordings)) { + return []; + } + + $recordings = []; + // If there were meetings already created. + foreach ($xml->recordings->recording as $recordingxml) { + $recording = self::parse_recording($recordingxml); + $recordings[$recording['recordID']] = $recording; + + // Check if there is childs. + if (isset($recordingxml->breakoutRooms->breakoutRoom)) { + foreach ($recordingxml->breakoutRooms->breakoutRoom as $breakoutroom) { + $xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', (array) $breakoutroom)]); + if (!$xml || $xml->returncode != 'SUCCESS' || empty($xml->recordings)) { + continue; + } + + // If there were meetings already created. + foreach ($xml->recordings->recording as $subrecordingxml) { + $recording = self::parse_recording($subrecordingxml); + $recordings[$recording['recordID']] = $recording; + } + } + } + } + + return $recordings; + } + + /** + * Helper function to sort an array of recordings. It compares the startTime in two recording objects. + * + * @param array $recordings + * @return array + */ + public static function sort_recordings(array $recordings): array { + global $CFG; + + $resultless = !empty($CFG->bigbluebuttonbn_recordings_sortorder) ? -1 : 1; + $resultmore = !empty($CFG->bigbluebuttonbn_recordings_sortorder) ? 1 : -1; + + uasort($recordings, function($a, $b) use ($resultless, $resultmore) { + if ($a['startTime'] < $b['startTime']) { + return $resultless; + } + if ($a['startTime'] == $b['startTime']) { + return 0; + } + return $resultmore; + }); + + return $recordings; + } + + /** + * Helper function to parse an xml recording object and produce an array in the format used by the plugin. + * + * @param SimpleXMLElement $recording + * + * @return array + */ + public static function parse_recording(SimpleXMLElement $recording): array { + // Add formats. + $playbackarray = []; + foreach ($recording->playback->format as $format) { + $playbackarray[(string) $format->type] = [ + 'type' => (string) $format->type, + 'url' => trim((string) $format->url), 'length' => (string) $format->length + ]; + // Add preview per format when existing. + if ($format->preview) { + $playbackarray[(string) $format->type]['preview'] = + self::parse_preview_images($format->preview); + } + } + // Add the metadata to the recordings array. + $metadataarray = + self::parse_recording_meta(get_object_vars($recording->metadata)); + $recordingarray = [ + 'recordID' => (string) $recording->recordID, + 'meetingID' => (string) $recording->meetingID, 'meetingName' => (string) $recording->name, + 'published' => (string) $recording->published, 'startTime' => (string) $recording->startTime, + 'endTime' => (string) $recording->endTime, 'playbacks' => $playbackarray + ]; + if (isset($recording->protected)) { + $recordingarray['protected'] = (string) $recording->protected; + } + return $recordingarray + $metadataarray; + } + + /** + * Helper function to convert an xml recording metadata object to an array in the format used by the plugin. + * + * @param array $metadata + * + * @return array + */ + public static function parse_recording_meta(array $metadata): array { + $metadataarray = []; + foreach ($metadata as $key => $value) { + if (is_object($value)) { + $value = ''; + } + $metadataarray['meta_' . $key] = $value; + } + return $metadataarray; + } + + /** + * Helper function to convert an xml recording preview images to an array in the format used by the plugin. + * + * @param SimpleXMLElement $preview + * + * @return array + */ + public static function parse_preview_images(SimpleXMLElement $preview): array { + $imagesarray = []; + foreach ($preview->images->image as $image) { + $imagearray = ['url' => trim((string) $image)]; + foreach ($image->attributes() as $attkey => $attvalue) { + $imagearray[$attkey] = (string) $attvalue; + } + array_push($imagesarray, $imagearray); + } + return $imagesarray; + } +} diff --git a/mod/bigbluebuttonbn/classes/logger.php b/mod/bigbluebuttonbn/classes/logger.php new file mode 100644 index 00000000000..068d17cd59f --- /dev/null +++ b/mod/bigbluebuttonbn/classes/logger.php @@ -0,0 +1,463 @@ +. + +namespace mod_bigbluebuttonbn; + +use mod_bigbluebuttonbn\event\events; +use stdClass; + +/** + * Utility class for all logs routines helper. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ +class logger { + + /** @var string The bigbluebuttonbn Add event */ + public const EVENT_ADD = 'Add'; + + /** @var string The bigbluebuttonbn Edit event */ + public const EVENT_EDIT = 'Edit'; + + /** @var string The bigbluebuttonbn Create event */ + public const EVENT_CREATE = 'Create'; + + /** @var string The bigbluebuttonbn Join event */ + public const EVENT_JOIN = 'Join'; + + /** @var string The bigbluebuttonbn Playback event */ + public const EVENT_PLAYED = 'Played'; + + /** @var string The bigbluebuttonbn Logout event */ + public const EVENT_LOGOUT = 'Logout'; + + /** @var string The bigbluebuttonbn Import event */ + public const EVENT_IMPORT = 'Import'; + + /** @var string The bigbluebuttonbn Delete event */ + public const EVENT_DELETE = 'Delete'; + + /** @var string The bigbluebuttonbn Callback event */ + public const EVENT_CALLBACK = 'Callback'; + + /** @var string The bigbluebuttonbn Summary event */ + public const EVENT_SUMMARY = 'Summary'; + + /** @var string The bigbluebuttonbn meeting_start event */ + public const EVENT_MEETING_START = 'meeting_start'; + + /** @var int The user accessed the session from activity page */ + public const ORIGIN_BASE = 0; + + /** @var int The user accessed the session from Timeline */ + public const ORIGIN_TIMELINE = 1; + + /** @var int The user accessed the session from Index */ + public const ORIGIN_INDEX = 2; + + /** + * Get the user event logs related to completion, for the specified user in the named instance. + * + * @param instance $instance + * @param int|null $userid + * @param array|null $filters + * @param int|null $timestart + * @return array + * @throws \coding_exception + * @throws \dml_exception + */ + public static function get_user_completion_logs( + instance $instance, + ?int $userid, + ?array $filters, + ?int $timestart = 0 + ): array { + global $DB; + $filters = $filters ?? [self::EVENT_JOIN, self::EVENT_PLAYED, self::EVENT_SUMMARY]; + [$wheresql, $params] = static::get_user_completion_sql_params($instance, $userid, $filters, $timestart); + return $DB->get_records_select('bigbluebuttonbn_logs', $wheresql, $params); + } + + /** + * Get the user event logs related to completion, for the specified user in the named instance. + * + * @param instance $instance + * @param int|null $userid + * @param array|null $filters + * @param int|null $timestart + * @return array + * @throws \coding_exception + * @throws \dml_exception + */ + public static function get_user_completion_logs_with_userfields( + instance $instance, + ?int $userid, + ?array $filters, + ?int $timestart = 0 + ): array { + global $DB; + $filters = $filters ?? [self::EVENT_JOIN, self::EVENT_PLAYED, self::EVENT_SUMMARY]; + [$wheresql, $params] = static::get_user_completion_sql_params($instance, $userid, $filters, $timestart, 'l'); + $userfieldsapi = \core_user\fields::for_userpic(); + $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; + $logtable = new \core\dml\table('bigbluebuttonbn_logs', 'l', ''); + $logtableselect = $logtable->get_field_select(); + $logtablefrom = $logtable->get_from_sql(); + $usertable = new \core\dml\table('user', 'u', ''); + $usertablefrom = $usertable->get_from_sql(); + $sql = <<get_records_sql($sql, $params); + } + + /** + * Get the latest timestamp for any event logs related to completion, for the specified user in the named instance. + * + * @param instance $instance + * @param int|null $userid + * @param array|null $filters + * @param int|null $timestart + * @return int + * @throws \coding_exception + * @throws \dml_exception + */ + public static function get_user_completion_logs_max_timestamp( + instance $instance, + ?int $userid, + ?array $filters, + ?int $timestart = 0 + ): int { + global $DB; + + [$wheresql, $params] = static::get_user_completion_sql_params($instance, $userid, $filters, $timestart); + $select = "SELECT MAX(timecreated) "; + $lastlogtime = $DB->get_field_sql($select . ' FROM {bigbluebuttonbn_logs} WHERE ' . $wheresql, $params); + return $lastlogtime; + } + + /** + * Helper method to get the right SQL query for completion + * + * @param instance $instance + * @param int|null $userid + * @param array|null $filters + * @param int|null $timestart + * @param string|null $logtablealias + * @return array + * @throws \coding_exception + * @throws \dml_exception + */ + protected static function get_user_completion_sql_params(instance $instance, ?int $userid, ?array $filters, ?int $timestart, + ?string $logtablealias = null) { + global $DB; + $filters = $filters ?? [self::EVENT_JOIN, self::EVENT_PLAYED, self::EVENT_SUMMARY]; + [$insql, $params] = $DB->get_in_or_equal($filters, SQL_PARAMS_NAMED); + $wheres = []; + $wheres['bigbluebuttonbnid'] = '= :instanceid'; + if ($timestart) { + $wheres['timecreated'] = ' > :timestart'; + $params['timestart'] = $timestart; + } + if ($userid) { + $wheres['userid'] = ' = :userid'; + $params['userid'] = $userid; + } + $params['instanceid'] = $instance->get_instance_id(); + $wheres['log'] = " $insql"; + $wheresqls = []; + foreach ($wheres as $key => $val) { + $prefix = !empty($logtablealias) ? "$logtablealias." : ""; + $wheresqls[] = "$prefix$key $val"; + } + return [join(' AND ', $wheresqls), $params]; + } + + /** + * Log that an instance was created. + * + * Note: This event cannot take the instance class as it is typically called before the cm has been configured. + * + * @param stdClass $instancedata + */ + public static function log_instance_created(stdClass $instancedata): void { + self::raw_log( + self::EVENT_ADD, + $instancedata->id, + $instancedata->course, + $instancedata->meetingid + ); + } + + /** + * Log that an instance was updated. + * + * @param instance $instance + */ + public static function log_instance_updated(instance $instance): void { + self::log($instance, self::EVENT_EDIT); + } + + /** + * Log an instance deleted event. + * + * @param instance $instance + */ + public static function log_instance_deleted(instance $instance): void { + global $DB; + + $wheresql = 'bigbluebuttonbnid = :instanceid AND log = :logtype AND ' . $DB->sql_compare_text('meta') . ' = :meta'; + $logs = $DB->get_records_select('bigbluebuttonbn_logs', $wheresql, [ + 'instanceid' => $instance->get_instance_id(), + 'logtype' => self::EVENT_CREATE, + 'meta' => "{\"record\":true}" + ]); + + $meta = "{\"has_recordings\":" . empty($logs) ? "true" : "false" . "}"; + self::log($instance, self::EVENT_DELETE, [], $meta); + } + + /** + * Log an event callback. + * + * @param instance $instance + * @param array $overrides + * @param array $meta + * @return int The new count of callback events + */ + public static function log_event_callback(instance $instance, array $overrides, array $meta): int { + self::log( + $instance, + self::EVENT_CALLBACK, + $overrides, + json_encode($meta) + ); + + return self::count_callback_events($meta['recordid'], 'meeting_events'); + } + + /** + * Log an event summary event. + * + * @param instance $instance + * @param array $overrides + * @param array $meta + */ + public static function log_event_summary(instance $instance, array $overrides = [], array $meta = []): void { + self::log( + $instance, + self::EVENT_SUMMARY, + $overrides, + json_encode($meta) + ); + } + + /** + * Log that an instance was viewed. + * + * @param instance $instance + */ + public static function log_instance_viewed(instance $instance): void { + self::log_moodle_event($instance, events::$events['view']); + } + + /** + * Log the events for when a meeting was ended. + * + * @param instance $instance + */ + public static function log_meeting_ended_event(instance $instance): void { + // Moodle event logger: Create an event for meeting ended. + self::log_moodle_event($instance, events::$events['meeting_end']); + + } + + /** + * Log the relevant events for when a meeting was joined. + * + * @param instance $instance + * @param int $origin + */ + public static function log_meeting_joined_event(instance $instance, int $origin): void { + // Moodle event logger: Create an event for meeting joined. + self::log_moodle_event($instance, events::$events['meeting_join']); + + // Internal logger: Instert a record with the meeting created. + self::log( + $instance, + self::EVENT_JOIN, + ['meetingid' => $instance->get_meeting_id()], + json_encode((object) ['origin' => $origin]) + ); + } + + /** + * Log the relevant events for when a user left a meeting. + * + * @param instance $instance + */ + public static function log_meeting_left_event(instance $instance): void { + // Moodle event logger: Create an event for meeting left. + self::log_moodle_event($instance, events::$events['meeting_left']); + } + + /** + * Log the relevant events for when a recording has been played. + * + * @param instance $instance + * @param int $rid RecordID + */ + public static function log_recording_played_event(instance $instance, int $rid): void { + // Moodle event logger: Create an event for recording played. + self::log_moodle_event($instance, events::$events['recording_play'], ['other' => $rid]); + + // Internal logger: Insert a record with the playback played. + self::log( + $instance, + self::EVENT_PLAYED, + [ + 'meetingid' => $instance->get_meeting_id(), + ], + json_encode(['recordingid' => $rid]) + ); + } + + /** + * Register a bigbluebuttonbn event from an instance. + * + * @param instance $instance + * @param string $event + * @param array $overrides + * @param string|null $meta + * @return bool + */ + protected static function log(instance $instance, string $event, array $overrides = [], ?string $meta = null): bool { + return self::raw_log( + $event, + $instance->get_instance_id(), + $instance->get_course_id(), + $instance->get_meeting_id(), + $overrides, + $meta + ); + } + + /** + * Register a bigbluebuttonbn event from raw data. + * + * @param string $event + * @param int $instanceid + * @param int $courseid + * @param string $meetingid + * @param array $overrides + * @param string|null $meta + * @return bool + * @throws \dml_exception + */ + protected static function raw_log( + string $event, + int $instanceid, + int $courseid, + string $meetingid, + array $overrides = [], + ?string $meta = null + ): bool { + global $DB, $USER; + + $log = (object) array_merge([ + // Default values. + 'courseid' => $courseid, + 'bigbluebuttonbnid' => $instanceid, + 'userid' => $USER->id, + 'meetingid' => $meetingid, + 'timecreated' => time(), + 'log' => $event, + 'meta' => $meta, + ], $overrides); + + return !!$DB->insert_record('bigbluebuttonbn_logs', $log); + } + + /** + * Helper register a bigbluebuttonbn event. + * + * @param instance $instance + * @param string $type + * @param array $options [timecreated, userid, other] + */ + protected static function log_moodle_event(instance $instance, string $type, array $options = []): void { + if (!in_array($type, \mod_bigbluebuttonbn\event\events::$events)) { + // No log will be created. + return; + } + + $params = [ + 'context' => $instance->get_context(), + 'objectid' => $instance->get_instance_id(), + ]; + + if (array_key_exists('timecreated', $options)) { + $params['timecreated'] = $options['timecreated']; + } + + if (array_key_exists('userid', $options)) { + $params['userid'] = $options['userid']; + } + + if (array_key_exists('other', $options)) { + $params['other'] = $options['other']; + } + + $event = call_user_func_array("\\mod_bigbluebuttonbn\\event\\{$type}::create", [$params]); + $event->add_record_snapshot('course_modules', $instance->get_cm()); + $event->add_record_snapshot('course', $instance->get_course()); + $event->add_record_snapshot('bigbluebuttonbn', $instance->get_instance_data()); + $event->trigger(); + } + + /** + * Helper function to count the number of callback logs matching the supplied specifications. + * + * @param string $recordid + * @param string $callbacktype + * @return int + */ + protected static function count_callback_events(string $recordid, string $callbacktype = 'recording_ready'): int { + global $DB; + $sql = 'SELECT count(DISTINCT id) FROM {bigbluebuttonbn_logs} WHERE log = ? AND meta LIKE ? AND meta LIKE ?'; + // Callback type added on version 2.4, validate recording_ready first or assume it on records with no callback. + if ($callbacktype == 'recording_ready') { + $sql .= ' AND (meta LIKE ? OR meta NOT LIKE ? )'; + $count = + $DB->count_records_sql($sql, [ + self::EVENT_CALLBACK, '%recordid%', + "%$recordid%", + $callbacktype, 'callback' + ]); + return $count; + } + $sql .= ' AND meta LIKE ?;'; + $count = $DB->count_records_sql($sql, + [self::EVENT_CALLBACK, '%recordid%', "%$recordid%", "%$callbacktype%"]); + return $count; + } +} diff --git a/mod/bigbluebuttonbn/classes/meeting.php b/mod/bigbluebuttonbn/classes/meeting.php new file mode 100644 index 00000000000..4b81b8c7061 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/meeting.php @@ -0,0 +1,526 @@ +. + +namespace mod_bigbluebuttonbn; + +use cache; +use cache_store; +use context_course; +use core_tag_tag; +use Exception; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; +use mod_bigbluebuttonbn\local\exceptions\meeting_join_exception; +use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; +use mod_bigbluebuttonbn\local\helpers\roles; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use stdClass; + +/** + * Class to describe a BBB Meeting. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting { + + /** @var instance The bbb instance */ + protected $instance; + + /** @var stdClass Info about the meeting */ + protected $meetinginfo = null; + + /** + * Constructor for the meeting object. + * + * @param instance $instance + */ + public function __construct(instance $instance) { + $this->instance = $instance; + } + + /** + * Helper to join a meeting. + * + * + * It will create the meeting if not already created. + * + * @param instance $instance + * @param int $origin + * @return string + * @throws meeting_join_exception this is sent if we cannot join (meeting full, user needs to wait...) + * @throws server_not_available_exception + */ + public static function join_meeting(instance $instance, $origin = logger::ORIGIN_BASE): string { + // See if the session is in progress. + $meeting = new meeting($instance); + // As the meeting doesn't exist, try to create it. + if (empty($meeting->get_meeting_info(true)->createtime)) { + $meeting->create_meeting(); + } + return $meeting->join($origin); + } + + /** + * Get currently stored meeting info + * + * @return mixed|stdClass + * @throws \coding_exception + */ + public function get_meeting_info() { + if (!$this->meetinginfo) { + $this->meetinginfo = $this->do_get_meeting_info(); + } + return $this->meetinginfo; + } + + /** + * Return meeting information for the specified instance. + * + * @param instance $instance + * @param bool $updatecache Whether to update the cache when fetching the information + * @return stdClass + */ + public static function get_meeting_info_for_instance(instance $instance, bool $updatecache = false): stdClass { + $meeting = new self($instance); + return $meeting->do_get_meeting_info($updatecache); + } + + /** + * Helper function returns a sha1 encoded string that is unique and will be used as a seed for meetingid. + * + * @return string + */ + public static function get_unique_meetingid_seed() { + global $DB; + do { + $encodedseed = sha1(plugin::random_password(12)); + $meetingid = (string) $DB->get_field('bigbluebuttonbn', 'meetingid', ['meetingid' => $encodedseed]); + } while ($meetingid == $encodedseed); + return $encodedseed; + } + + /** + * Is meeting running ? + * + * @return bool + */ + public function is_running() { + return $this->get_meeting_info()->statusrunning ?? false; + } + + /** + * Force update the meeting in cache. + */ + public function update_cache() { + $this->meetinginfo = $this->do_get_meeting_info(true); + } + + /** + * Get meeting attendees + * + * @return mixed + */ + public function get_attendees() { + return $this->get_meeting_info()->attendees ?? []; + } + + /** + * Can the meeting be joined ? + * + * @return bool + */ + public function can_join() { + return $this->get_meeting_info()->canjoin; + } + + /** + * Number of participants + * + * @return int + */ + public function get_participant_count() { + return $this->get_meeting_info()->participantcount; + } + + /** + * Creates a bigbluebutton meeting, send the message to BBB and returns the response in an array. + * + * @return array + * @throws bigbluebutton_exception + * @throws server_not_available_exception + */ + public function create_meeting() { + $data = $this->create_meeting_data(); + $metadata = $this->create_meeting_metadata(); + $presentation = $this->instance->get_presentation_for_bigbluebutton_upload(); // The URL must contain nonce. + $presentationname = $presentation['name'] ?? null; + $presentationurl = $presentation['url'] ?? null; + $response = bigbluebutton_proxy::create_meeting($data, $metadata, $presentationname, $presentationurl); + // New recording management: Insert a recordingID that corresponds to the meeting created. + if ($this->instance->is_recorded()) { + $recording = new recording(0, (object) [ + 'courseid' => $this->instance->get_course_id(), + 'bigbluebuttonbnid' => $this->instance->get_instance_id(), + 'recordingid' => $response['internalMeetingID'], + 'groupid' => $this->instance->get_group_id()] + ); + $recording->create(); + } + return $response; + } + + /** + * Send an end meeting message to BBB server + */ + public function end_meeting() { + bigbluebutton_proxy::end_meeting($this->instance->get_meeting_id(), $this->instance->get_moderator_password()); + } + + /** + * Get meeting join URL + * + * @return string + * @throws \coding_exception + */ + public function get_join_url() { + return bigbluebutton_proxy::get_join_url( + $this->instance->get_meeting_id(), + $this->instance->get_user_fullname(), + $this->instance->get_current_user_password(), + $this->instance->get_logout_url()->out(false), + null, + $this->instance->get_user_id(), + $this->get_meeting_info()->createtime + ); + } + + /** + * Return meeting information for this meeting. + * + * @param bool $updatecache Whether to update the cache when fetching the information + * @return stdClass + */ + protected function do_get_meeting_info(bool $updatecache = false): stdClass { + $instance = $this->instance; + $meetinginfo = (object) [ + 'instanceid' => $instance->get_instance_id(), + 'bigbluebuttonbnid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id(), + 'meetingid' => $instance->get_meeting_id(), + 'cmid' => $instance->get_cm_id(), + 'ismoderator' => $instance->is_moderator(), + 'joinurl' => $instance->get_join_url()->out(), + 'userlimit' => $instance->get_user_limit(), + 'presentations' => [], + ]; + if ($instance->get_instance_var('openingtime')) { + $meetinginfo->openingtime = intval($instance->get_instance_var('openingtime')); + } + if ($instance->get_instance_var('closingtime')) { + $meetinginfo->closingtime = intval($instance->get_instance_var('closingtime')); + } + $activitystatus = bigbluebutton_proxy::view_get_activity_status($instance); + // This might raise an exception if info cannot be retrieved. + // But this might be totally fine as the meeting is maybe not yet created on BBB side. + $participantcount = 0; + try { + $info = self::retrieve_cached_meeting_info($this->instance->get_meeting_id(), $updatecache); + $meetinginfo->statusrunning = $info['running'] === 'true'; + $meetinginfo->createtime = $info['createTime'] ?? null; + $participantcount = isset($info['participantCount']) ? $info['participantCount'] : 0; + } catch (bigbluebutton_exception $e) { + // The meeting is not created on BBB side, so we have to setup a couple of values here. + $meetinginfo->statusrunning = false; + $meetinginfo->createtime = null; + } + $meetinginfo->statusclosed = $activitystatus === 'ended'; + $meetinginfo->statusopen = !$meetinginfo->statusrunning && $activitystatus === 'open'; + $meetinginfo->participantcount = $participantcount; + + $canjoin = !$instance->user_must_wait_to_join() || $meetinginfo->statusrunning; + // Limit has not been reached or user does not count toward limit. + $canjoin = $canjoin && ( + !$instance->has_user_limit_been_reached($participantcount) + || !$instance->does_current_user_count_towards_user_limit() + ); + $canjoin = $canjoin && ($instance->is_currently_open() || $instance->user_can_force_join()); + // Double check that the user has the capabilities to join. + $canjoin = $canjoin && $instance->can_join(); + $meetinginfo->canjoin = $canjoin; + + // If user is administrator, moderator or if is viewer and no waiting is required, join allowed. + if ($meetinginfo->statusrunning) { + $meetinginfo->statusmessage = get_string('view_message_conference_in_progress', 'bigbluebuttonbn'); + $meetinginfo->startedat = floor(intval($info['startTime']) / 1000); // Milliseconds. + $meetinginfo->moderatorcount = $info['moderatorCount']; + $meetinginfo->moderatorplural = $info['moderatorCount'] > 1; + $meetinginfo->participantcount = $participantcount - $meetinginfo->moderatorcount; + $meetinginfo->participantplural = $meetinginfo->participantcount > 1; + } else { + if ($instance->user_must_wait_to_join() && !$instance->user_can_force_join()) { + $meetinginfo->statusmessage = get_string('view_message_conference_wait_for_moderator', 'bigbluebuttonbn'); + } else { + $meetinginfo->statusmessage = get_string('view_message_conference_room_ready', 'bigbluebuttonbn'); + } + } + + $presentation = $instance->get_presentation(); // This is for internal use. + if (!empty($presentation)) { + $meetinginfo->presentations[] = $presentation; + } + $meetinginfo->attendees = []; + if (!empty($info['attendees'])) { + // Make sure attendees is an array of object, not a simpleXML object. + foreach ($info['attendees'] as $attendee) { + $meetinginfo->attendees[] = (array) $attendee; + } + } + return $meetinginfo; + } + + /** + * Gets a meeting info object cached or fetched from the live session. + * + * @param string $meetingid + * @param bool $updatecache + * + * @return array + * @throws \coding_exception + * @throws bigbluebutton_exception + */ + protected static function retrieve_cached_meeting_info($meetingid, $updatecache = false) { + $cachettl = (int) config::get('waitformoderator_cache_ttl'); + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'meetings_cache'); + $result = $cache->get($meetingid); + $now = time(); + if (!$updatecache && !empty($result) && $now < ($result['creation_time'] + $cachettl)) { + // Use the value in the cache. + return (array) json_decode($result['meeting_info']); + } + $cache->delete($meetingid); // Make sure we purges the cache before checking info. + // Pings again and refreshes the cache. + $meetinginfo = bigbluebutton_proxy::get_meeting_info($meetingid); + + $cache->set($meetingid, ['creation_time' => time(), 'meeting_info' => json_encode($meetinginfo)]); + return $meetinginfo; + } + + /** + * Helper to prepare data used for create meeting. + * + * @return array + */ + protected function create_meeting_data() { + $data = ['meetingID' => $this->instance->get_meeting_id(), + 'name' => \mod_bigbluebuttonbn\plugin::html2text($this->instance->get_meeting_name(), 64), + 'attendeePW' => $this->instance->get_viewer_password(), + 'moderatorPW' => $this->instance->get_moderator_password(), + 'logoutURL' => $this->instance->get_logout_url()->out(false), + ]; + $data['record'] = $this->instance->should_record() ? 'true' : 'false'; + // Check if auto_start_record is enable. + if ($data['record'] == 'true' && $this->instance->should_record_from_start()) { + $data['autoStartRecording'] = 'true'; + // Check if hide_record_button is enable. + if (!$this->instance->should_show_recording_button()) { + $data['allowStartStopRecording'] = 'false'; + } + } + $data['welcome'] = trim($this->instance->get_welcome_message()); + $voicebridge = intval($this->instance->get_voice_bridge()); + if ($voicebridge > 0 && $voicebridge < 79999) { + $data['voiceBridge'] = $voicebridge; + } + $maxparticipants = intval($this->instance->get_user_limit()); + if ($maxparticipants > 0) { + $data['maxParticipants'] = $maxparticipants; + } + if ($this->instance->get_mute_on_start()) { + $data['muteOnStart'] = 'true'; + } + return $data; + } + + /** + * Helper for preparing metadata used while creating the meeting. + * + * @return array + */ + protected function create_meeting_metadata() { + global $USER; + // Create standard metadata. + $origindata = $this->instance->get_origin_data(); + $metadata = [ + 'bbb-origin' => $origindata->origin, + 'bbb-origin-version' => $origindata->originVersion, + 'bbb-origin-server-name' => $origindata->originServerName, + 'bbb-origin-server-common-name' => $origindata->originServerCommonName, + 'bbb-origin-tag' => $origindata->originTag, + 'bbb-context' => $this->instance->get_course()->fullname, + 'bbb-context-id' => $this->instance->get_course_id(), + 'bbb-context-name' => trim(html_to_text($this->instance->get_course()->fullname, 0)), + 'bbb-context-label' => trim(html_to_text($this->instance->get_course()->shortname, 0)), + 'bbb-recording-name' => plugin::html2text($this->instance->get_meeting_name(), 64), + 'bbb-recording-description' => plugin::html2text($this->instance->get_meeting_description(), + 64), + 'bbb-recording-tags' => + implode(',', core_tag_tag::get_item_tags_array('core', + 'course_modules', $this->instance->get_cm_id())), // Same as $id. + 'bbb-meeting-size-hint' => + count_enrolled_users(context_course::instance($this->instance->get_course_id()), + '', $this->instance->get_group_id()), + ]; + // Special metadata for recording processing. + if ((boolean) config::get('recordingstatus_enabled')) { + $metadata["bn-recording-status"] = json_encode( + [ + 'email' => ['"' . fullname($USER) . '" <' . $USER->email . '>'], + 'context' => $this->instance->get_view_url(), + ] + ); + } + if ((boolean) config::get('recordingready_enabled')) { + $metadata['bn-recording-ready-url'] = $this->instance->get_record_ready_url()->out(false); + } + if ((boolean) config::get('meetingevents_enabled')) { + $metadata['analytics-callback-url'] = $this->instance->get_meeting_event_notification_url()->out(false); + } + return $metadata; + } + + /** + * Helper for responding when storing live meeting events is requested. + * + * The callback with a POST request includes: + * - Authentication: Bearer } encoded with HS512> + * - Content Type: application/json + * - Body: + * + * @param instance $instance + * @return void + */ + public static function meeting_events(instance $instance) { + $bigbluebuttonbn = $instance->get_instance_data(); + // Decodes the received JWT string. + try { + // Get the HTTP headers (getallheaders is a PHP function that may only work with Apache). + $headers = getallheaders(); + + // Pull the Bearer from the headers. + if (!array_key_exists('Authorization', $headers)) { + $msg = 'Authorization failed'; + header('HTTP/1.0 400 Bad Request. ' . $msg); + return; + } + $authorization = explode(" ", $headers['Authorization']); + + // Verify the authenticity of the request. + $token = \Firebase\JWT\JWT::decode( + $authorization[1], + config::get('shared_secret'), + ['HS512'] + ); + + // Get JSON string from the body. + $jsonstr = file_get_contents('php://input'); + + // Convert JSON string to a JSON object. + $jsonobj = json_decode($jsonstr); + } catch (Exception $e) { + $msg = 'Caught exception: ' . $e->getMessage(); + header('HTTP/1.0 400 Bad Request. ' . $msg); + return; + } + + // Validate that the bigbluebuttonbn activity corresponds to the meeting_id received. + $meetingidelements = explode('[', $jsonobj->{'meeting_id'}); + $meetingidelements = explode('-', $meetingidelements[0]); + if (!isset($bigbluebuttonbn) || $bigbluebuttonbn->meetingid != $meetingidelements[0]) { + $msg = 'The activity may have been deleted'; + header('HTTP/1.0 410 Gone. ' . $msg); + return; + } + + // We make sure events are processed only once. + $overrides = ['meetingid' => $jsonobj->{'meeting_id'}]; + $meta['recordid'] = $jsonobj->{'internal_meeting_id'}; + $meta['callback'] = 'meeting_events'; + + $eventcount = logger::log_event_callback($instance, $overrides, $meta); + if ($eventcount === 1) { + // Process the events. + self::process_meeting_events($instance, $jsonobj); + header('HTTP/1.0 200 Accepted. Enqueued.'); + } else { + header('HTTP/1.0 202 Accepted. Already processed.'); + } + } + + /** + * Helper function enqueues list of meeting events to be stored and processed as for completion. + * + * @param instance $instance + * @param stdClass $jsonobj + */ + protected static function process_meeting_events(instance $instance, stdClass $jsonobj) { + $meetingid = $jsonobj->{'meeting_id'}; + $recordid = $jsonobj->{'internal_meeting_id'}; + $attendees = $jsonobj->{'data'}->{'attendees'}; + foreach ($attendees as $attendee) { + $userid = $attendee->{'ext_user_id'}; + $overrides['meetingid'] = $meetingid; + $overrides['userid'] = $userid; + $meta['recordid'] = $recordid; + $meta['data'] = $attendee; + + // Stores the log. + logger::log_event_summary($instance, $overrides, $meta); + + // Enqueue a task for processing the completion. + bigbluebutton_proxy::enqueue_completion_event($instance->get_instance_data(), $userid); + } + } + + /** + * Join a meeting. + * + * @param int $origin The spec + * @return string The URL to redirect to + * @throws meeting_join_exception + */ + public function join(int $origin): string { + $this->do_get_meeting_info(true); + if ($this->is_running()) { + if ($this->instance->has_user_limit_been_reached($this->get_participant_count()) + && $this->instance->does_current_user_count_towards_user_limit()) { + throw new meeting_join_exception('userlimitreached'); + } + } + // If user is not administrator nor moderator (user is student) and waiting is required. + if ($this->instance->user_must_wait_to_join()) { + throw new meeting_join_exception('waitformoderator'); + } + + // Moodle event logger: Create an event for meeting joined. + logger::log_meeting_joined_event($this->instance, $origin); + + // Before executing the redirect, increment the number of participants. + roles::participant_joined($this->instance->get_meeting_id(), $this->instance->is_moderator()); + return $this->get_join_url(); + } +} diff --git a/mod/bigbluebuttonbn/classes/output/import_view.php b/mod/bigbluebuttonbn/classes/output/import_view.php new file mode 100644 index 00000000000..a3f26885540 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/import_view.php @@ -0,0 +1,149 @@ +. + +namespace mod_bigbluebuttonbn\output; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\local\helpers\roles; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Renderable for the import page. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ +class import_view implements renderable, templatable { + /** + * @var instance $destinationinstance + */ + protected $destinationinstance; + + /** + * @var int|null $sourceinstanceid the source instance id or null if it is not yet set. + */ + protected $sourceinstanceid; + + /** + * @var int|null $sourcecourseid the source instance id or null if it is not yet set. + */ + protected $sourcecourseid; + + /** + * import_view constructor. + * + * @param instance $destinationinstance + * @param int $sourcecourseid + * @param int $sourceinstanceid + */ + public function __construct(instance $destinationinstance, int $sourcecourseid, int $sourceinstanceid) { + $this->destinationinstance = $destinationinstance; + $this->sourcecourseid = $sourcecourseid >= 0 ? $sourcecourseid : null; + $this->sourceinstanceid = $sourceinstanceid >= 0 ? $sourceinstanceid : null; + } + + /** + * Defer to template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output): stdClass { + $courses = roles::import_get_courses_for_select($this->destinationinstance); + if (config::get('importrecordings_from_deleted_enabled')) { + $courses[0] = get_string('recordings_from_deleted_activities', 'mod_bigbluebuttonbn'); + ksort($courses); + } + $context = (object) [ + 'bbbid' => $this->destinationinstance->get_instance_id(), + 'has_recordings' => true, + 'bbbsourceid' => 0 + ]; + + if (!empty($this->sourceinstanceid)) { + $context->sourceid = $this->sourceinstanceid; + $context->search = [ + 'value' => '' + ]; + $sourceinstance = instance::get_from_instanceid($this->sourceinstanceid); + if ($sourceinstance->is_type_room_only()) { + $context->has_recordings = false; + } + $context->bbbsourceid = $sourceinstance->get_instance_id(); + } + + // Now the selects. + if (!empty($this->sourcecourseid)) { + $selectrecords = []; + + $cms = get_fast_modinfo($this->sourcecourseid)->instances['bigbluebuttonbn']; + foreach ($cms as $cm) { + if ($cm->id == $this->destinationinstance->get_cm_id()) { + // Skip the target instance. + continue; + } + + if ($cm->deletioninprogress) { + // Check if the BBB is not currently scheduled for deletion. + continue; + } + + $selectrecords[$cm->instance] = $cm->name; + } + if (config::get('importrecordings_from_deleted_enabled')) { + $selectrecords[0] = + get_string('recordings_from_deleted_activities', 'mod_bigbluebuttonbn'); + } + $actionurl = $this->destinationinstance->get_import_url(); + $actionurl->param('sourcecourseid', $this->sourcecourseid); + + $select = new \single_select( + $actionurl, + 'sourcebn', + $selectrecords, + $this->sourceinstanceid ?? "" + ); + $context->bbb_select = $select->export_for_template($output); + } + $context->sourcecourseid = $this->sourcecourseid ?? 0; + + // Course selector. + $context->course_select = (new \single_select( + $this->destinationinstance->get_import_url(), + 'sourcecourseid', + $courses, + $this->sourcecourseid ?? "" + ))->export_for_template($output); + + if (!is_null($this->sourcecourseid)) { + $context->has_selected_course = true; + } + + // Back button. + $context->back_button = (new \single_button( + $this->destinationinstance->get_view_url(), + get_string('view_recording_button_return', 'mod_bigbluebuttonbn') + ))->export_for_template($output); + + return $context; + } +} diff --git a/mod/bigbluebuttonbn/classes/output/index.php b/mod/bigbluebuttonbn/classes/output/index.php new file mode 100644 index 00000000000..2192a4afcc9 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/index.php @@ -0,0 +1,208 @@ +. +namespace mod_bigbluebuttonbn\output; + +use html_table; +use html_writer; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\meeting; +use mod_bigbluebuttonbn\plugin; +use renderable; +use renderer_base; +use stdClass; + +/** + * Renderer for the Index page. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class index implements renderable { + + /** @var stdClass */ + protected $course; + + /** @var stdClass[] */ + protected $instances; + + /** + * Constructor for the index renderable. + * + * @param stdClass $course + * @param instance[] List of bbbbn instances + */ + public function __construct(stdClass $course, array $instances) { + $this->course = $course; + $this->instances = $instances; + } + + /** + * Get the table for the index page. + * + * @param renderer_base $output + * @return html_table + */ + public function get_table(renderer_base $output): html_table { + // Print the list of instances. + $table = new html_table(); + $table->head = [ + get_string('week'), + get_string('index_heading_name', plugin::COMPONENT), + get_string('index_heading_group', plugin::COMPONENT), + get_string('index_heading_users', plugin::COMPONENT), + get_string('index_heading_viewer', plugin::COMPONENT), + get_string('index_heading_moderator', plugin::COMPONENT), + get_string('index_heading_recording', plugin::COMPONENT), + get_string('index_heading_actions', plugin::COMPONENT), + ]; + $table->align = ['center', 'left', 'center', 'center', 'center', 'center', 'center']; + + foreach ($this->instances as $instance) { + $this->add_instance_to_table($output, $table, $instance); + } + + return $table; + } + + /** + * Add details of the bigbluebuttonbn instance to the table. + * + * @param renderer_base $output + * @param html_table $table + * @param instance $instance + */ + protected function add_instance_to_table(renderer_base $output, html_table $table, instance $instance): void { + $cm = $instance->get_cm(); + if (!$cm->uservisible) { + return; + } + if (groups_get_activity_groupmode($cm) == 0) { + $table->data[] = $this->add_room_row_to_table($output, $instance); + } else { + // Add 'All participants' room information. + $table->data[] = $this->add_room_row_to_table($output, $instance, 0); + + // Add data for the groups belonging to the bbb instance, if any. + $groups = groups_get_activity_allowed_groups($cm); + foreach ($groups as $group) { + $table->data[] = $this->add_room_row_to_table($output, $instance, $group->id); + } + } + } + + /** + * Displays the general view. + * + * @param renderer_base $output + * @param instance $instance + * @param int|null $group + * @return array + */ + protected function add_room_row_to_table(renderer_base $output, instance $instance, ?int $group = null): array { + if ($group) { + $instance = instance::get_group_instance_from_instance($instance, $group); + } + $meeting = new meeting($instance); + + $viewurl = $instance->get_view_url(); + if ($groupid = $instance->get_group_id()) { + $viewurl->param('group', $groupid); + } + + $joinurl = html_writer::link($viewurl, format_string($instance->get_meeting_name())); + + // The meeting info was returned. + if ($meeting->is_running()) { + return [ + $instance->get_cm()->sectionnum, + $joinurl, + $instance->get_group_name(), + $this->get_room_usercount($meeting), + $this->get_room_attendee_list($meeting, 'VIEWER'), + $this->get_room_attendee_list($meeting, 'MODERATOR'), + $this->get_room_record_info($output, $instance), + $this->get_room_actions($output, $instance, $meeting), + ]; + } + + return [$instance->get_cm()->sectionnum, $joinurl, $instance->get_group_name(), '', '', '', '', '']; + } + + /** + * Count the number of users in the meeting. + * + * @param meeting $meeting + * @return int + */ + protected function get_room_usercount(meeting $meeting): int { + return count($meeting->get_attendees()); + } + + /** + * Returns attendee list. + * + * @param meeting $meeting + * @param string $role + * @return string + */ + protected function get_room_attendee_list(meeting $meeting, string $role): string { + $attendees = []; + + foreach ($meeting->get_attendees() as $attendee) { + if ((string) $attendee->role == $role) { + $attendees[] = $attendee->fullName; + } + } + + return implode(', ', $attendees); + } + + /** + * Returns indication of recording enabled. + * + * @param renderer_base $output + * @param instance $instance + * @return string + */ + protected function get_room_record_info(renderer_base $output, instance $instance): string { + if ($instance->is_recorded()) { + // If it has been set when meeting created, set the variable on/off. + return get_string('index_enabled', 'bigbluebuttonbn'); + } + return ''; + } + + /** + * Returns room actions. + * + * @param renderer_base $output + * @param instance $instance + * @param meeting $meeting + * @return string + */ + protected function get_room_actions(renderer_base $output, instance $instance, meeting $meeting): string { + if ($instance->is_moderator()) { + return $output->render_from_template('mod_bigbluebuttonbn/end_session_button', (object) [ + 'bigbluebuttonbnid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id(), + 'statusrunning' => $meeting->is_running(), + ]); + } + + return ''; + } +} diff --git a/mod/bigbluebuttonbn/classes/output/instance_updated_message.php b/mod/bigbluebuttonbn/classes/output/instance_updated_message.php new file mode 100644 index 00000000000..b58ccea28cc --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/instance_updated_message.php @@ -0,0 +1,76 @@ +. + +namespace mod_bigbluebuttonbn\output; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\helpers\roles; +use moodle_url; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Renderable for the instance notification updated message + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ +class instance_updated_message implements renderable, templatable { + + /** @var int The activity was created */ + const TYPE_CREATED = 0; + + /** @var int The activity was updated */ + const TYPE_UPDATED = 1; + + /** + * @var instance $instance + */ + protected $instance; + + /** + * Instance updated constructor + * + * @param instance $instance + * @param int $type + */ + public function __construct(instance $instance, int $type) { + $this->instance = $instance; + $this->type = $type; + } + + /** + * Defer to template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output): stdClass { + return (object) [ + 'is_update' => $this->type === self::TYPE_UPDATED, + 'is_create' => $this->type === self::TYPE_CREATED, + 'name' => $this->instance->get_meeting_name(), + 'link' => $this->instance->get_view_url()->out(), + 'description' => $this->instance->get_meeting_description(), + 'openingtime' => $this->instance->get_instance_var('openingtime'), + 'closingtime' => $this->instance->get_instance_var('closingtime'), + ]; + } +} diff --git a/mod/bigbluebuttonbn/classes/output/mobile.php b/mod/bigbluebuttonbn/classes/output/mobile.php new file mode 100644 index 00000000000..ce059a54cd0 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/mobile.php @@ -0,0 +1,227 @@ +. + +/** + * Mobile output class for bigbluebuttonbn + * + * @package mod_bigbluebuttonbn + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +namespace mod_bigbluebuttonbn\output; + +defined('MOODLE_INTERNAL') || die(); + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception; +use mod_bigbluebuttonbn\local\exceptions\meeting_join_exception; +use mod_bigbluebuttonbn\local\exceptions\server_not_available_exception; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use mod_bigbluebuttonbn\logger; +use mod_bigbluebuttonbn\meeting; + +global $CFG; +require_once($CFG->dirroot . '/lib/grouplib.php'); + +/** + * Mobile output class for bigbluebuttonbn + * + * @package mod_bigbluebuttonbn + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class mobile { + + /** + * Returns the bigbluebuttonbn course view for the mobile app. + * + * @param mixed $args + * @return array HTML, javascript and other data. + */ + public static function mobile_course_view($args): array { + global $OUTPUT; + + $args = (object) $args; + $versionname = $args->appversioncode >= 3950 ? 'latest' : 'ionic3'; + + $instance = instance::get_from_cmid($args->cmid); + if (!$instance) { + return self::mobile_print_error(get_string('view_error_url_missing_parameters', 'bigbluebuttonbn')); + } + + $cm = $instance->get_cm(); + $course = $instance->get_course(); + + // Check activity status. + if ($instance->before_start_time()) { + $message = get_string('view_message_conference_not_started', 'bigbluebuttonbn'); + + $notstarted = [ + 'starts_at' => '', + 'ends_at' => '', + ]; + if (!empty($instance->get_instance_var('openingtime'))) { + $notstarted['starts_at'] = sprintf( + '%s: %s', + get_string('mod_form_field_openingtime', 'bigbluebuttonbn'), + userdate($instance->get_instance_var('openingtime')) + ); + } + + if (!empty($instance->get_instance_var('closingtime'))) { + $notstarted['ends_at'] = sprintf( + '%s: %s', + get_string('mod_form_field_closingtime', 'bigbluebuttonbn'), + userdate($instance->get_instance_var('closingtime')) + ); + } + + return self::mobile_print_notification($instance, $message, $notstarted); + } + + if ($instance->has_ended()) { + $message = get_string('view_message_conference_has_ended', 'bigbluebuttonbn'); + return self::mobile_print_notification($instance, $message); + } + + // Check if the BBB server is working. + $serverversion = bigbluebutton_proxy::get_server_version(); + if ($serverversion === null) { + return self::mobile_print_error(bigbluebutton_proxy::get_server_not_available_message($instance)); + } + + // Mark viewed by user (if required). + $completion = new \completion_info($course); + $completion->set_module_viewed($cm); + + // Validate if the user is in a role allowed to join. + if (!$instance->can_join()) { + return self::mobile_print_error(get_string('view_nojoin', 'bigbluebuttonbn')); + } + + // Note: This logic should match bbb_view.php. + + // Logic of bbb_view for join to session. + if ($instance->user_must_wait_to_join()) { + // If user is not administrator nor moderator (user is student) and waiting is required. + return self::mobile_print_notification( + $instance, + get_string('view_message_conference_wait_for_moderator', 'bigbluebuttonbn') + ); + } + + // See if the BBB session is already in progress. + $urltojoin = ''; + try { + $urltojoin = meeting::join_meeting($instance); + } catch (meeting_join_exception $e) { + return self::mobile_print_notification($instance, $e->getMessage()); + } catch (server_not_available_exception $e) { + return self::mobile_print_error(bigbluebutton_proxy::get_server_not_available_message($instance)); + } + + // Check groups access and show message. + $msjgroup = []; + $groupmode = groups_get_activity_groupmode($instance->get_cm()); + if ($groupmode != NOGROUPS) { + $msjgroup['message'] = get_string('view_mobile_message_groups_not_supported', 'bigbluebuttonbn'); + } + + $data = [ + 'bigbluebuttonbn' => $instance->get_instance_data(), + 'msjgroup' => $msjgroup, + 'urltojoin' => $urltojoin, + 'cmid' => $cm->id, + 'courseid' => $course->id, + ]; + + // We want to show a notification when user excedded 45 seconds without click button. + $jstimecreatedmeeting = 'setTimeout(function(){ + document.getElementById("bigbluebuttonbn-mobile-notifications").style.display = "block"; + document.getElementById("bigbluebuttonbn-mobile-join").disabled = true; + document.getElementById("bigbluebuttonbn-mobile-meetingready").style.display = "none"; + }, 45000);'; + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template("mod_bigbluebuttonbn/mobile_view_page_$versionname", $data), + ], + ], + 'javascript' => $jstimecreatedmeeting, + 'otherdata' => '', + 'files' => '', + ]; + } + + /** + * Returns the view for errors. + * + * @param string $error Error to display. + * @return array HTML, javascript and otherdata + */ + protected static function mobile_print_error($error): array { + global $OUTPUT; + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_bigbluebuttonbn/mobile_view_error', [ + 'error' => $error, + ]), + ], + ], + 'javascript' => '', + 'otherdata' => '', + 'files' => '', + ]; + } + + /** + * Returns the view for messages. + * + * @param instance $instance + * @param string $message Message to display. + * @param array $notstarted Extra messages for not started session. + * @return array HTML, javascript and otherdata + */ + protected static function mobile_print_notification(instance $instance, $message, $notstarted = []): array { + global $OUTPUT, $CFG; + + $data = [ + 'bigbluebuttonbn' => $instance->get_instance_data(), + 'cmid' => $instance->get_cm_id(), + 'message' => $message, + 'not_started' => $notstarted, + ]; + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_bigbluebuttonbn/mobile_view_notification', $data), + ], + ], + 'javascript' => file_get_contents($CFG->dirroot . '/mod/bigbluebuttonbn/mobileapp/mobile.notification.js'), + 'otherdata' => '', + 'files' => '' + ]; + } +} diff --git a/mod/bigbluebuttonbn/classes/output/recording_description_editable.php b/mod/bigbluebuttonbn/classes/output/recording_description_editable.php new file mode 100644 index 00000000000..2f654004cdd --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/recording_description_editable.php @@ -0,0 +1,60 @@ +. +namespace mod_bigbluebuttonbn\output; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\recording; + +/** + * Renderer for recording name in place editable. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent.david [at] call-learning [dt] fr) + */ +class recording_description_editable extends recording_editable { + + /** + * Specific constructor with the right label/hint for this editable + * + * @param recording $rec + * @param instance $instance + */ + public function __construct(recording $rec, instance $instance) { + parent::__construct($rec, $instance, + get_string('view_recording_description_editlabel', 'mod_bigbluebuttonbn'), + get_string('view_recording_description_edithint', 'mod_bigbluebuttonbn')); + } + + /** + * Get the value to display + * + * @param recording $recording a recording + * @return string + */ + public function get_recording_value(recording $recording): string { + $metadescription = $recording->get('description'); + return \html_writer::span($metadescription); + } + + /** + * Get the type of editable + */ + protected static function get_type() { + return 'description'; + } +} diff --git a/mod/bigbluebuttonbn/classes/output/recording_editable.php b/mod/bigbluebuttonbn/classes/output/recording_editable.php new file mode 100644 index 00000000000..11b1ebd9e2f --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/recording_editable.php @@ -0,0 +1,137 @@ +. +namespace mod_bigbluebuttonbn\output; + +use lang_string; +use mod_bigbluebuttonbn\local\bigbluebutton; +use moodle_exception; +use core\output\inplace_editable; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use mod_bigbluebuttonbn\local\proxy\recording_proxy; +use mod_bigbluebuttonbn\recording; +use stdClass; + +/** + * Renderer for recording in place editable. + * + * Generic class + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent.david [at] call-learning [dt] fr) + */ +abstract class recording_editable extends \core\output\inplace_editable { + + /** @var instance The bbb instance */ + protected $instance; + + /** + * Constructor. + * + * @param recording $rec + * @param instance $instance + * @param string $edithint + * @param string $editlabel + * @throws moodle_exception + */ + public function __construct(recording $rec, instance $instance, string $edithint, string $editlabel) { + $this->instance = $instance; + + $editable = $this->check_capability(); + $displayvalue = format_string( + $this->get_recording_value($rec), + true, + [ + 'context' => $instance->get_context(), + ] + ); + + // Hack here: the ID is the recordID and the meeting ID. + parent::__construct( + 'mod_bigbluebuttonbn', + static::get_type(), + $rec->get('id'), + $editable, + $displayvalue, + $displayvalue, + $edithint, + $editlabel + ); + } + + /** + * Check user can access and or modify this item. + * + * @return bool + * @throws \moodle_exception + */ + protected function check_capability() { + global $USER; + + if (!can_access_course($this->instance->get_course(), $USER)) { + throw new moodle_exception('noaccess', 'mod_bigbluebuttonbn'); + } + + return $this->instance->can_manage_recordings(); + } + + /** + * Get the type of editable + */ + protected static function get_type() { + return ''; + } + + /** + * Get the real recording value + * + * @param recording $rec + * @return mixed + */ + abstract public function get_recording_value(recording $rec): string; + + /** + * Update the recording with the new value + * + * @param int $itemid + * @param mixed $value + * @return recording_editable + */ + public static function update($itemid, $value) { + $recording = recording::get_record(['id' => $itemid]); + $instance = instance::get_from_instanceid($recording->get('bigbluebuttonbnid')); + + require_login($instance->get_course()); + + $recording->set(static::get_type(), $value); + $recording->update(); + + return new static($recording, $instance); + } + + /** + * Helper function evaluates if a row for the data used by the recording table is editable. + * + * @return bool + */ + protected function row_editable() { + // Since the request to BBB are cached, it is safe to use the wrapper to check the server version. + return $this->instance->can_manage_recordings() + && (bigbluebutton_proxy::get_server_version() >= 1.0 || $this->instance->is_blindside_network_server()); + } +} diff --git a/mod/bigbluebuttonbn/classes/output/recording_name_editable.php b/mod/bigbluebuttonbn/classes/output/recording_name_editable.php new file mode 100644 index 00000000000..438b9de2673 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/recording_name_editable.php @@ -0,0 +1,61 @@ +. + +namespace mod_bigbluebuttonbn\output; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\recording; + +/** + * Renderer for recording name in place editable. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent.david [at] call-learning [dt] fr) + */ +class recording_name_editable extends recording_editable { + /** + * Specific constructor with the right label/hint for this editable + * + * @param recording $rec + * @param instance $instance + * @throws \coding_exception + */ + public function __construct(recording $rec, instance $instance) { + parent::__construct($rec, $instance, + get_string('view_recording_name_editlabel', 'mod_bigbluebuttonbn'), + get_string('view_recording_name_edithint', 'mod_bigbluebuttonbn')); + } + + /** + * Get the value to display + * + * @param recording $recording + * @return string + */ + public function get_recording_value(recording $recording): string { + $metaname = $recording->get('name'); + return \html_writer::span($metaname); + } + + /** + * Get the type of editable + */ + protected static function get_type() { + return 'name'; + } +} diff --git a/mod/bigbluebuttonbn/classes/output/recording_row_actionbar.php b/mod/bigbluebuttonbn/classes/output/recording_row_actionbar.php new file mode 100644 index 00000000000..d60eb1cde84 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/recording_row_actionbar.php @@ -0,0 +1,193 @@ +. + +namespace mod_bigbluebuttonbn\output; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\recording; +use pix_icon; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Renderer for recording row actionbar column + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent.david [at] call-learning [dt] fr) + */ +class recording_row_actionbar implements renderable, templatable { + /** + * @var $recording + */ + protected $recording; + + /** + * @var $tools + */ + protected $tools; + /** + * @var array TOOLS_DEFINITION a list of definition for the the specific tools + */ + const TOOL_ACTION_DEFINITIONS = [ + 'protect' => [ + 'action' => 'unprotect', + 'icon' => 'lock', + 'hidewhen' => '!protected', + 'requireconfirmation' => true, + 'disablewhen' => 'imported' + ], + 'unprotect' => [ + 'action' => 'protect', + 'icon' => 'unlock', + 'hidewhen' => 'protected', + 'requireconfirmation' => true, + 'disablewhen' => 'imported' + ], + 'publish' => [ + 'action' => 'publish', + 'icon' => 'show', + 'hidewhen' => 'published', + 'requireconfirmation' => true, + 'disablewhen' => 'imported' + ], + 'unpublish' => [ + 'action' => 'unpublish', + 'icon' => 'hide', + 'hidewhen' => '!published', + 'requireconfirmation' => true, + 'disablewhen' => 'imported' + ], + 'delete' => [ + 'action' => 'delete', + 'icon' => 'trash', + 'requireconfirmation' => true + ], + 'import' => [ + 'action' => 'import', + 'icon' => 'import', + ] + ]; + + /** + * recording_row_actionbar constructor. + * + * @param recording $recording + * @param array $tools + */ + public function __construct(recording $recording, array $tools) { + $this->recording = $recording; + $this->tools = $tools; + } + + /** + * Export for template + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output): stdClass { + $context = new stdClass(); + $context->id = 'recording-actionbar-' . $this->recording->get('id'); + $context->recordingid = $this->recording->get('id'); + $context->tools = []; + foreach ($this->tools as $tool) { + if (!empty(self::TOOL_ACTION_DEFINITIONS[$tool])) { + $buttonpayload = self::TOOL_ACTION_DEFINITIONS[$tool]; + $conditionalhiding = $buttonpayload['hidewhen'] ?? null; + $disabledwhen = $buttonpayload['disablewhen'] ?? null; + $this->actionbar_update_diplay($buttonpayload, $disabledwhen, $this->recording, 'disabled'); + $this->actionbar_update_diplay($buttonpayload, $conditionalhiding, $this->recording); + + if (!empty($buttonpayload)) { + // With text for $manageaction. + $iconortext = ''; + $linkattributes = [ + 'class' => 'btn btn-xs btn-danger', + ]; + + $target = $buttonpayload['action']; + if (isset($buttonpayload['target'])) { + $target .= '-' . $buttonpayload['target']; + } + $id = 'recording-' . $target . '-' . $this->recording->get('recordingid'); + if ((boolean) config::get('recording_icons_enabled')) { + // With icon for $manageaction. + $iconattributes = [ + 'id' => $id, + 'class' => 'iconsmall', + ]; + $linkattributes = [ + 'id' => $id, + 'data-action' => $buttonpayload['action'], + 'data-require-confirmation' => !empty($buttonpayload['requireconfirmation']), + 'class' => 'action-icon' + ]; + if ($this->recording->get('imported')) { + $linkattributes['data-links'] = recording::count_records( + [ + 'recordingid' => $this->recording->get('recordingid'), + 'imported' => true, + ] + ); + } + if (isset($buttonpayload['disabled'])) { + $iconattributes['class'] .= ' fa-' . $buttonpayload['disabled']; + $linkattributes['class'] .= ' disabled'; + } + $icon = new pix_icon( + 'i/' . $buttonpayload['icon'], + get_string('view_recording_list_actionbar_' . $buttonpayload['action'], 'bigbluebuttonbn'), + 'moodle', + $iconattributes + ); + $iconortext = $output->render($icon); + } else { + $iconortext = get_string($buttonpayload['action']); + $linkattributes['title'] = get_string($buttonpayload['action']); + } + $actionlink = new \action_link(new \moodle_url('#'), $iconortext, null, $linkattributes); + $context->tools [] = $actionlink->export_for_template($output); + } + + } + } + return $context; + } + + /** + * Read the settings for this action and disable or hide the tool from the toolbar + * + * @param array $buttonpayload + * @param string $condition + * @param recording $rec + * @param string $value + */ + private function actionbar_update_diplay(&$buttonpayload, $condition, $rec, $value = 'invisible') { + if ($condition) { + $negates = $condition[0] === '!'; + $conditionalvariable = ltrim($condition, '!'); + if ($rec->get($conditionalvariable) xor $negates) { + $buttonpayload['disabled'] = $value; + } + } + } +} diff --git a/mod/bigbluebuttonbn/classes/output/recording_row_playback.php b/mod/bigbluebuttonbn/classes/output/recording_row_playback.php new file mode 100644 index 00000000000..f33fdd86021 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/recording_row_playback.php @@ -0,0 +1,130 @@ +. + +namespace mod_bigbluebuttonbn\output; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data; +use mod_bigbluebuttonbn\local\helpers\roles; +use mod_bigbluebuttonbn\recording; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Renderer for recording row playback column + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent.david [at] call-learning [dt] fr) + */ +class recording_row_playback implements renderable, templatable { + + /** + * @var $instance + */ + protected $instance; + + /** + * @var $recording + */ + protected $recording; + + /** + * recording_row_playback constructor. + * + * @param recording $rec + * @param instance $instance + */ + public function __construct(recording $rec, ?instance $instance) { + $this->instance = $instance ?? null; + $this->recording = $rec; + } + + /** + * Export for template + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output): stdClass { + $ispublished = $this->recording->get('published'); + $recordingid = $this->recording->get('id'); + $context = (object) [ + 'dataimported' => $this->recording->get('imported'), + 'id' => 'playbacks-' . $this->recording->get('id'), + 'recordingid' => $recordingid, + 'additionaloptions' => '', + 'playbacks' => [], + ]; + + $playbacks = $this->recording->get('playbacks'); + if ($ispublished && $playbacks) { + foreach ($playbacks as $playback) { + if ($this->should_be_included($playback)) { + $linkattributes = [ + 'id' => "recording-play-{$playback['type']}-{$recordingid}", + 'class' => 'btn btn-sm btn-default', + 'data-action' => 'play', + 'data-target' => $playback['type'], + ]; + $actionlink = new \action_link( + $playback['url'], + recording_data::type_text($playback['type']), + null, + $linkattributes + ); + $context->playbacks[] = $actionlink->export_for_template($output); + } + } + } + return $context; + } + /** + * Helper function renders the link used for recording type in row for the data used by the recording table. + * + * @param array $playback + * @return bool + */ + protected function should_be_included(array $playback): bool { + // All types that are not restricted are included. + if (array_key_exists('restricted', $playback) && strtolower($playback['restricted']) == 'false') { + return true; + } + // All types that are not statistics are included. + if ($playback['type'] != 'statistics') { + return true; + } + + // Exclude imported recordings. + if ($this->recording->get('imported')) { + return false; + } + + // Exclude non moderators. + if ($this->instance) { + if (!$this->instance->is_admin() && !$this->instance->is_moderator()) { + return false; + } + } else { + return roles::has_capability_in_course($this->recording->get('courseid'), 'mod/bigbluebuttonbn:managerecordings'); + } + return true; + + } +} diff --git a/mod/bigbluebuttonbn/classes/output/recording_row_preview.php b/mod/bigbluebuttonbn/classes/output/recording_row_preview.php new file mode 100644 index 00000000000..a74586fa6f9 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/recording_row_preview.php @@ -0,0 +1,92 @@ +. + +namespace mod_bigbluebuttonbn\output; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use mod_bigbluebuttonbn\recording; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Renderer for recording_row_preview column + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent.david [at] call-learning [dt] fr) + */ +class recording_row_preview implements renderable, templatable { + /** + * @var $recording + */ + protected $recording; + + /** + * recording_row_playback constructor. + * + * @param recording $rec + */ + public function __construct(recording $rec) { + $this->recording = $rec; + } + + /** + * Export for template + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output): stdClass { + global $CFG; + + $context = (object) [ + 'id' => 'preview-' . $this->recording->get('id'), + 'hidden' => !$this->recording->get('published'), + 'recordingpreviews' => [], + ]; + + $playbacks = $this->recording->get('playbacks'); + foreach ($playbacks as $playback) { + $thumbnails = []; + + if (isset($playback['preview'])) { + foreach ($playback['preview'] as $image) { + $validated = true; + $url = trim($image['url']); + if ($CFG->bigbluebuttonbn_recordings_validate_url) { + $validated = bigbluebutton_proxy::is_remote_resource_valid($url); + } + if ($validated) { + $thumbnails[] = $url . '?' . time(); + } + } + + if (!empty($thumbnails)) { + $context->recordingpreviews[] = (object) [ + 'thumbnails' => $thumbnails, + ]; + } + } + } + + return $context; + } +} diff --git a/mod/bigbluebuttonbn/classes/output/recordings_session.php b/mod/bigbluebuttonbn/classes/output/recordings_session.php new file mode 100644 index 00000000000..5af3196c89c --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/recordings_session.php @@ -0,0 +1,77 @@ +. + +namespace mod_bigbluebuttonbn\output; + +use mod_bigbluebuttonbn\instance; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * Renderer for recording section. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent.david [at] call-learning [dt] fr) + */ +class recordings_session implements renderable, templatable { + + /** + * @var instance + */ + protected $instance; + + /** + * recording_section constructor. + * + * @param instance $instance + */ + public function __construct(instance $instance) { + $this->instance = $instance; + } + + /** + * Export for template + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output): stdClass { + $isrecordedtype = $this->instance->is_type_room_and_recordings() || $this->instance->is_type_recordings_only(); + + $context = (object) [ + 'bbbid' => $this->instance->get_instance_id(), + 'groupid' => $this->instance->get_group_id(), + 'has_recordings' => $this->instance->is_recorded() && $isrecordedtype, + 'searchbutton' => [ + 'value' => '', + ], + ]; + + if ($this->instance->can_import_recordings()) { + $button = new \single_button( + $this->instance->get_import_url(), + get_string('view_recording_button_import', 'mod_bigbluebuttonbn') + ); + $context->import_button = $button->export_for_template($output); + } + + return $context; + } +} diff --git a/mod/bigbluebuttonbn/classes/output/renderer.php b/mod/bigbluebuttonbn/classes/output/renderer.php new file mode 100644 index 00000000000..58b33d9a47c --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/renderer.php @@ -0,0 +1,110 @@ +. + +namespace mod_bigbluebuttonbn\output; + +use core\notification; +use core\output\inplace_editable; +use html_table; +use html_writer; +use mod_bigbluebuttonbn\instance; +use plugin_renderer_base; + +/** + * Renderer for the mod_bigbluebuttonbn plugin. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ +class renderer extends plugin_renderer_base { + + /** + * Render the index table. + * + * @param index $index + * @return string + */ + protected function render_index(index $index): string { + $this->page->requires->js_call_amd('mod_bigbluebuttonbn/index', 'init'); + + return html_writer::table($index->get_table($this)); + } + + /** + * Render the groups selector. + * + * @param instance $instance + * @return string + */ + public function render_groups_selector(instance $instance): string { + $groupmode = groups_get_activity_groupmode($instance->get_cm()); + if ($groupmode === NOGROUPS) { + return ''; + } + + // Separate or visible group mode. + $groups = groups_get_activity_allowed_groups($instance->get_cm()); + if (empty($groups)) { + // No groups in this course. + notification::add(get_string('view_groups_nogroups_warning', 'bigbluebuttonbn'), notification::INFO); + return ''; + } + + // Assign group default values. + if (count($groups) == 0) { + // Only the All participants group exists. + notification::add(get_string('view_groups_notenrolled_warning', 'bigbluebuttonbn'), notification::INFO); + return ''; + } + + if (count($groups) > 1) { + notification::add(get_string('view_groups_selection_warning', 'bigbluebuttonbn'), notification::INFO); + } + + $groupsmenu = groups_print_activity_menu( + $instance->get_cm(), + $instance->get_view_url(), + true + ); + + return $groupsmenu . '

'; + } + + /** + * Render the view page. + * + * @param view_page $page + * @return string + */ + public function render_view_page(view_page $page): string { + return $this->render_from_template( + 'mod_bigbluebuttonbn/view_page', + $page->export_for_template($this) + ); + } + + /** + * Render inplace editable + * + * @param inplace_editable $e + * @return bool|string + */ + public function render_inplace_editable(inplace_editable $e) { + return $this->render_from_template('core/inplace_editable', $e->export_for_template($this)); + } +} diff --git a/mod/bigbluebuttonbn/classes/output/view_page.php b/mod/bigbluebuttonbn/classes/output/view_page.php new file mode 100644 index 00000000000..8449e7065a8 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/view_page.php @@ -0,0 +1,156 @@ +. + +namespace mod_bigbluebuttonbn\output; + +use core\output\notification; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\meeting; +use renderable; +use renderer_base; +use stdClass; +use templatable; + +/** + * View Page template renderable. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class view_page implements renderable, templatable { + + /** @var instance The instance being rendered */ + protected $instance; + + /** + * Constructor for the View Page. + * + * @param instance $instance + */ + public function __construct(instance $instance) { + $this->instance = $instance; + } + + /** + * Export the content required to render the template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output): stdClass { + $templatedata = (object) [ + 'instanceid' => $this->instance->get_instance_id(), + 'groupselector' => $output->render_groups_selector($this->instance), + 'meetingname' => $this->instance->get_meeting_name(), + 'description' => $this->instance->get_meeting_description(true), + 'joinurl' => $this->instance->get_join_url(), + ]; + + if ($this->show_default_server_warning()) { + $templatedata->serverwarning = (new notification( + get_string('view_warning_default_server', 'mod_bigbluebuttonbn'), + notification::NOTIFY_WARNING, + false + ))->export_for_template($output); + } + + $viewwarningmessage = config::get('general_warning_message'); + if ($this->show_view_warning() && !empty($viewwarningmessage)) { + $templatedata->sitenotification = (object) [ + 'message' => $viewwarningmessage, + 'type' => config::get('general_warning_box_type'), + 'icon' => [ + 'pix' => 'i/bullhorn', + 'component' => 'core', + ], + ]; + + if ($url = config::get('general_warning_button_href')) { + $templatedata->sitenotification->actions = [[ + 'url' => $url, + 'title' => config::get('general_warning_button_text'), + ]]; + } + } + + if ($this->instance->is_feature_enabled('showroom')) { + $roomdata = meeting::get_meeting_info_for_instance($this->instance); + $roomdata->haspresentations = false; + if (!empty($roomdata->presentations)) { + $roomdata->haspresentations = true; + } + $templatedata->room = $roomdata; + } + + if ($this->instance->is_feature_enabled('showrecordings') && $this->instance->is_recorded()) { + $recordings = new recordings_session($this->instance); + $templatedata->recordings = $recordings->export_for_template($output); + } else if ($this->instance->is_type_recordings_only()) { + $templatedata->recordingwarning = (new notification( + get_string('view_message_recordings_disabled', 'mod_bigbluebuttonbn'), + notification::NOTIFY_WARNING, + false + ))->export_for_template($output); + } + + return $templatedata; + } + + /** + * Whether to show the default server warning. + * + * @return bool + */ + protected function show_default_server_warning(): bool { + if (!$this->instance->is_admin()) { + return false; + } + + if (config::DEFAULT_SERVER_URL != config::get('server_url')) { + return false; + } + + return true; + } + + /** + * Whether to show the view warning. + * + * @return bool + */ + protected function show_view_warning(): bool { + if ($this->instance->is_admin()) { + return true; + } + + $generalwarningroles = explode(',', config::get('general_warning_roles')); + $userroles = \mod_bigbluebuttonbn\local\helpers\roles::get_user_roles( + $this->instance->get_context(), + $this->instance->get_user_id() + ); + + foreach ($userroles as $userrole) { + if (in_array($userrole->shortname, $generalwarningroles)) { + return true; + } + } + + return false; + } + +} diff --git a/mod/bigbluebuttonbn/classes/plugin.php b/mod/bigbluebuttonbn/classes/plugin.php new file mode 100644 index 00000000000..cf106e3a589 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/plugin.php @@ -0,0 +1,68 @@ +. + +namespace mod_bigbluebuttonbn; + +/** + * Class plugin. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ +abstract class plugin { + + /** + * Component name. + */ + const COMPONENT = 'mod_bigbluebuttonbn'; + + /** + * Helper function to convert an html string to plain text. + * + * @param string $html + * @param int $len + * + * @return string + */ + public static function html2text($html, $len = 0) { + $text = strip_tags($html); + $text = str_replace(' ', ' ', $text); + $textlen = strlen($text); + $text = mb_substr($text, 0, $len); + if ($textlen > $len) { + $text .= '...'; + } + return $text; + } + + /** + * Helper generates a random password. + * + * @param int $length + * @param string $unique + * + * @return string + */ + public static function random_password($length = 8, $unique = "") { + $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + do { + $password = substr(str_shuffle($chars), 0, $length); + } while ($unique == $password); + return $password; + } +} diff --git a/mod/bigbluebuttonbn/classes/privacy/provider.php b/mod/bigbluebuttonbn/classes/privacy/provider.php new file mode 100644 index 00000000000..e919840ca45 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/privacy/provider.php @@ -0,0 +1,316 @@ +. + +namespace mod_bigbluebuttonbn\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\helper; +use core_privacy\local\request\transform; +use core_privacy\local\request\userlist; +use core_privacy\local\request\writer; + +/** + * Privacy class for requesting user data. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class provider implements + // This plugin has data. + \core_privacy\local\metadata\provider, + + // This plugin currently implements the original plugin\provider interface. + \core_privacy\local\request\plugin\provider, + + // This plugin is capable of determining which users have data within it. + \core_privacy\local\request\core_userlist_provider { + + /** + * Returns metadata. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection): collection { + + // The table bigbluebuttonbn stores only the room properties. + // However, there is a chance that some personal information is stored as metadata. + // This would be done in the column 'participants' where rules can be set to define BBB roles. + // It is fair to say that only the userid is stored, which is useless if user is removed. + // But if this is a concern a refactoring on the way the rules are stored will be required. + $collection->add_database_table('bigbluebuttonbn', [ + 'participants' => 'privacy:metadata:bigbluebuttonbn:participants', + ], 'privacy:metadata:bigbluebuttonbn'); + + // The table bigbluebuttonbn_logs stores events triggered by users when using the plugin. + // Some personal information along with the resource accessed is stored. + $collection->add_database_table('bigbluebuttonbn_logs', [ + 'userid' => 'privacy:metadata:bigbluebuttonbn_logs:userid', + 'timecreated' => 'privacy:metadata:bigbluebuttonbn_logs:timecreated', + 'meetingid' => 'privacy:metadata:bigbluebuttonbn_logs:meetingid', + 'log' => 'privacy:metadata:bigbluebuttonbn_logs:log', + 'meta' => 'privacy:metadata:bigbluebuttonbn_logs:meta', + ], 'privacy:metadata:bigbluebuttonbn_logs'); + + $collection->add_database_table('bigbluebuttonbn_recordings', [ + 'userid' => 'privacy:metadata:bigbluebuttonbn_logs:userid', + ], 'privacy:metadata:bigbluebuttonbn_recordings'); + + // Personal information has to be passed to BigBlueButton. + // This includes the user ID and fullname. + $collection->add_external_location_link('bigbluebutton', [ + 'userid' => 'privacy:metadata:bigbluebutton:userid', + 'fullname' => 'privacy:metadata:bigbluebutton:fullname', + ], 'privacy:metadata:bigbluebutton'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid): contextlist { + // If user was already deleted, do nothing. + if (!\core_user::get_user($userid)) { + return new contextlist(); + } + // Fetch all bigbluebuttonbn logs. + $sql = "SELECT c.id + FROM {context} c + INNER JOIN {course_modules} cm + ON cm.id = c.instanceid + AND c.contextlevel = :contextlevel + INNER JOIN {modules} m + ON m.id = cm.module + AND m.name = :modname + INNER JOIN {bigbluebuttonbn} bigbluebuttonbn + ON bigbluebuttonbn.id = cm.instance + INNER JOIN {bigbluebuttonbn_logs} bigbluebuttonbnlogs + ON bigbluebuttonbnlogs.bigbluebuttonbnid = bigbluebuttonbn.id + WHERE bigbluebuttonbnlogs.userid = :userid"; + + $params = [ + 'modname' => 'bigbluebuttonbn', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid, + ]; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + return $contextlist; + } + + /** + * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + // Filter out any contexts that are not related to modules. + $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_MODULE) { + $carry[] = $context->instanceid; + } + return $carry; + }, []); + + if (empty($cmids)) { + return; + } + + $user = $contextlist->get_user(); + + // Get all the bigbluebuttonbn activities associated with the above course modules. + $instanceidstocmids = self::get_instance_ids_to_cmids_from_cmids($cmids); + $instanceids = array_keys($instanceidstocmids); + + list($insql, $inparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED); + $params = array_merge($inparams, ['userid' => $user->id]); + $recordset = $DB->get_recordset_select( + 'bigbluebuttonbn_logs', + "bigbluebuttonbnid $insql AND userid = :userid", + $params, + 'timecreated, id' + ); + self::recordset_loop_and_export($recordset, 'bigbluebuttonbnid', [], + function($carry, $record) use ($user, $instanceidstocmids) { + $carry[] = [ + 'timecreated' => transform::datetime($record->timecreated), + 'meetingid' => $record->meetingid, + 'log' => $record->log, + 'meta' => $record->meta, + ]; + return $carry; + }, + function($instanceid, $data) use ($user, $instanceidstocmids) { + $context = \context_module::instance($instanceidstocmids[$instanceid]); + $contextdata = helper::get_context_data($context, $user); + $finaldata = (object) array_merge((array) $contextdata, ['logs' => $data]); + helper::export_context_files($context, $user); + writer::with_context($context)->export_data([], $finaldata); + } + ); + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context the context to delete in. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if (!$context instanceof \context_module) { + return; + } + + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->delete_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instanceid]); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist a list of contexts approved for deletion. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + $count = $contextlist->count(); + if (empty($count)) { + return; + } + $userid = $contextlist->get_user()->id; + foreach ($contextlist->get_contexts() as $context) { + if (!$context instanceof \context_module) { + return; + } + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->delete_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instanceid, 'userid' => $userid]); + } + } + + /** + * Return a dict of bigbluebuttonbn IDs mapped to their course module ID. + * + * @param array $cmids The course module IDs. + * @return array In the form of [$bigbluebuttonbnid => $cmid]. + */ + protected static function get_instance_ids_to_cmids_from_cmids(array $cmids) { + global $DB; + + list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); + $sql = "SELECT bigbluebuttonbn.id, cm.id AS cmid + FROM {bigbluebuttonbn} bigbluebuttonbn + JOIN {modules} m + ON m.name = :bigbluebuttonbn + JOIN {course_modules} cm + ON cm.instance = bigbluebuttonbn.id + AND cm.module = m.id + WHERE cm.id $insql"; + $params = array_merge($inparams, ['bigbluebuttonbn' => 'bigbluebuttonbn']); + + return $DB->get_records_sql_menu($sql, $params); + } + + /** + * Loop and export from a recordset. + * + * @param \moodle_recordset $recordset The recordset. + * @param string $splitkey The record key to determine when to export. + * @param mixed $initial The initial data to reduce from. + * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. + * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. + * @return void + */ + protected static function recordset_loop_and_export( + \moodle_recordset $recordset, + $splitkey, + $initial, + callable $reducer, + callable $export + ) { + $data = $initial; + $lastid = null; + + foreach ($recordset as $record) { + if ($lastid && $record->{$splitkey} != $lastid) { + $export($lastid, $data); + $data = $initial; + } + $data = $reducer($data, $record); + $lastid = $record->{$splitkey}; + } + $recordset->close(); + + if (!empty($lastid)) { + $export($lastid, $data); + } + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) { + $context = $userlist->get_context(); + + if (!$context instanceof \context_module) { + return; + } + + $params = [ + 'instanceid' => $context->instanceid, + 'modulename' => 'bigbluebuttonbn', + ]; + + $sql = "SELECT bnl.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {bigbluebuttonbn} bn ON bn.id = cm.instance + JOIN {bigbluebuttonbn_logs} bnl ON bnl.bigbluebuttonbnid = bn.id + WHERE cm.id = :instanceid"; + + $userlist->add_from_sql('userid', $sql, $params); + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); + + list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); + $params = array_merge(['bigbluebuttonbnid' => $cm->instance], $userinparams); + $sql = "bigbluebuttonbnid = :bigbluebuttonbnid AND userid {$userinsql}"; + + $DB->delete_records_select('bigbluebuttonbn_logs', $sql, $params); + } +} diff --git a/mod/bigbluebuttonbn/classes/recording.php b/mod/bigbluebuttonbn/classes/recording.php new file mode 100644 index 00000000000..a10a836d752 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/recording.php @@ -0,0 +1,788 @@ +. + +namespace mod_bigbluebuttonbn; + +use cache; +use context; +use context_course; +use context_module; +use core\persistent; +use mod_bigbluebuttonbn\local\proxy\recording_proxy; +use moodle_url; +use stdClass; + +/** + * The recording entity. + * + * This is utility class that defines a single recording, and provides methods for their local handling locally, and + * communication with the bigbluebutton server. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording extends persistent { + /** The table name. */ + const TABLE = 'bigbluebuttonbn_recordings'; + + /** @var int Defines that the activity used to create the recording no longer exists */ + public const RECORDING_HEADLESS = 1; + + /** @var int Defines that the recording is not the original but an imported one */ + public const RECORDING_IMPORTED = 1; + + /** @var int Defines that the list should include imported recordings */ + public const INCLUDE_IMPORTED_RECORDINGS = true; + + /** @var int A meeting set to be recorded still awaits for a recording update */ + public const RECORDING_STATUS_AWAITING = 0; + + /** @var int A meeting set to be recorded was not recorded and dismissed by BBB */ + public const RECORDING_STATUS_DISMISSED = 1; + + /** @var int A meeting set to be recorded has a recording processed */ + public const RECORDING_STATUS_PROCESSED = 2; + + /** @var int A meeting set to be recorded received notification callback from BBB */ + public const RECORDING_STATUS_NOTIFIED = 3; + + /** @var int A meeting set to be recorded was processed and set back to an awaiting state */ + public const RECORDING_STATUS_RESET = 4; + + /** @var bool Whether metadata been changed so the remote information needs to be updated ? */ + protected $metadatachanged = false; + + /** @var int A refresh period for recordings, defaults to 300s (5mins) */ + public const RECORDING_REFRESH_DEFAULT_PERIOD = 300; + + /** @var array A cached copy of the metadata */ + protected $metadata = null; + + /** @var instance A cached copy of the instance */ + protected $instance; + + /** + * Create an instance of this class. + * + * @param int $id If set, this is the id of an existing record, used to load the data. + * @param stdClass|null $record If set will be passed to from_record + * @param null|array $metadata + */ + public function __construct($id = 0, stdClass $record = null, ?array $metadata = null) { + if ($record) { + $record->headless = $record->headless ?? false; + $record->imported = $record->imported ?? false; + $record->groupid = $record->groupid ?? 0; + $record->status = $record->status ?? self::RECORDING_STATUS_AWAITING; + } + parent::__construct($id, $record); + + if ($metadata) { + $this->metadata = $metadata; + } + } + + /** + * Helper function to retrieve recordings from the BigBlueButton. + * + * @param instance $instance + * @param bool $includeimported + * @param bool $onlyimported + * + * @return recording[] containing the recordings indexed by recordID, each recording is also a + * non sequential associative array itself that corresponds to the actual recording in BBB + */ + public static function get_recordings_for_instance( + instance $instance, + bool $includeimported = false, + bool $onlyimported = false + ): array { + global $DB; + + [$selects, $params] = self::get_basic_select_from_parameters(false, $includeimported, $onlyimported); + $selects[] = "bigbluebuttonbnid = :bbbid"; + $params['bbbid'] = $instance->get_instance_id(); + $groupmode = groups_get_activity_groupmode($instance->get_cm()); + $context = $instance->get_context(); + if ($groupmode) { + [$groupselects, $groupparams] = self::get_select_for_group( + $groupmode, + $context, + $instance->get_course_id(), + $instance->get_group_id(), + $instance->get_cm()->groupingid + ); + if ($groupselects) { + $selects[] = $groupselects; + $params = array_merge_recursive($params, $groupparams); + } + } + + $recordings = self::fetch_records($selects, $params); + foreach ($recordings as $recording) { + $recording->instance = $instance; + } + + return $recordings; + } + + /** + * Helper function to retrieve recordings from a given course. + * + * @param int $courseid id for a course record or null + * @param array $excludedinstanceid exclude recordings from instance ids + * @param bool $includeimported + * @param bool $onlyimported + * @param bool $includedeleted + * @param bool $onlydeleted + * + * @return recording[] containing the recordings indexed by recordID, each recording is also a + * non sequential associative array itself that corresponds to the actual recording in BBB + */ + public static function get_recordings_for_course( + int $courseid, + array $excludedinstanceid = [], + bool $includeimported = false, + bool $onlyimported = false, + bool $includedeleted = false, + bool $onlydeleted = false + ): array { + global $DB; + + [$selects, $params] = self::get_basic_select_from_parameters( + $includedeleted, $includeimported, $onlyimported, $onlydeleted + ); + if ($courseid) { + $selects[] = "courseid = :courseid"; + $params['courseid'] = $courseid; + $course = $DB->get_record('course', ['id' => $courseid]); + $groupmode = groups_get_course_groupmode($course); + $context = context_course::instance($courseid); + } else { + $context = \context_system::instance(); + $groupmode = NOGROUPS; + } + + if ($groupmode) { + [$groupselects, $groupparams] = self::get_select_for_group($groupmode, $context, $course->id); + if ($groupselects) { + $selects[] = $groupselects; + $params = array_merge($params, $groupparams); + } + } + + if ($excludedinstanceid) { + [$sqlexcluded, $paramexcluded] = $DB->get_in_or_equal($excludedinstanceid, SQL_PARAMS_NAMED, 'param', false); + $selects[] = "bigbluebuttonbnid {$sqlexcluded}"; + $params = array_merge($params, $paramexcluded); + } + + return self::fetch_records($selects, $params); + } + + /** + * Get select for given group mode and context + * + * @param int $groupmode + * @param context $context + * @param int $courseid + * @param int $groupid + * @param int $groupingid + * @return array + */ + protected static function get_select_for_group($groupmode, $context, $courseid, $groupid = 0, $groupingid = 0): array { + global $DB, $USER; + + $selects = []; + $params = []; + if ($groupmode) { + $accessallgroups = has_capability('moodle/site:accessallgroups', $context) || $groupmode == VISIBLEGROUPS; + if ($accessallgroups) { + if ($context instanceof context_module) { + $allowedgroups = groups_get_all_groups($courseid, 0, $groupingid); + } else { + $allowedgroups = groups_get_all_groups($courseid); + } + } else { + if ($context instanceof context_module) { + $allowedgroups = groups_get_all_groups($courseid, $USER->id, $groupingid); + } else { + $allowedgroups = groups_get_all_groups($courseid, $USER->id); + } + } + $allowedgroupsid = array_map(function($g) { + return $g->id; + }, $allowedgroups); + if ($groupid || empty($allowedgroups)) { + $selects[] = "groupid = :groupid"; + $params['groupid'] = ($groupid && in_array($groupid, $allowedgroupsid)) ? + $groupid : 0; + } else { + if ($accessallgroups) { + $allowedgroupsid[] = 0; + } + list($groupselects, $groupparams) = $DB->get_in_or_equal($allowedgroupsid, SQL_PARAMS_NAMED); + $selects[] = 'groupid ' . $groupselects; + $params = array_merge_recursive($params, $groupparams); + } + } + return [ + implode(" AND ", $selects), + $params, + ]; + } + + /** + * Get basic sql select from given parameters + * + * @param bool $includedeleted + * @param bool $includeimported + * @param bool $onlyimported + * @param bool $onlydeleted + * @return array + */ + protected static function get_basic_select_from_parameters( + bool $includedeleted = false, + bool $includeimported = false, + bool $onlyimported = false, + bool $onlydeleted = false + ): array { + $selects = []; + $params = []; + + // Start with the filters. + if ($onlydeleted) { + // Only headless recordings when only deleted is set. + $selects[] = "headless = :headless"; + $params['headless'] = self::RECORDING_HEADLESS; + } else if (!$includedeleted) { + // Exclude headless recordings unless includedeleted. + $selects[] = "headless != :headless"; + $params['headless'] = self::RECORDING_HEADLESS; + } + + if (!$includeimported) { + // Exclude imported recordings unless includedeleted. + $selects[] = "imported != :imported"; + $params['imported'] = self::RECORDING_IMPORTED; + } else if ($onlyimported) { + // Exclude non-imported recordings. + $selects[] = "imported = :imported"; + $params['imported'] = self::RECORDING_IMPORTED; + } + + // Now get only recordings that have been validated by recording ready callback. + $selects[] = "status IN (:status_processed, :status_notified)"; + $params['status_processed'] = self::RECORDING_STATUS_PROCESSED; + $params['status_notified'] = self::RECORDING_STATUS_NOTIFIED; + return [$selects, $params]; + } + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return [ + 'courseid' => [ + 'type' => PARAM_INT, + ], + 'bigbluebuttonbnid' => [ + 'type' => PARAM_INT, + ], + 'groupid' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + ], + 'recordingid' => [ + 'type' => PARAM_RAW, + ], + 'headless' => [ + 'type' => PARAM_BOOL, + ], + 'imported' => [ + 'type' => PARAM_BOOL, + ], + 'status' => [ + 'type' => PARAM_INT, + ], + 'importeddata' => [ + 'type' => PARAM_RAW, + 'null' => NULL_ALLOWED, + 'default' => '' + ], + 'name' => [ + 'type' => PARAM_TEXT, + 'null' => NULL_ALLOWED, + 'default' => null + ], + 'description' => [ + 'type' => PARAM_TEXT, + 'null' => NULL_ALLOWED, + 'default' => 0 + ], + 'protected' => [ + 'type' => PARAM_BOOL, + 'null' => NULL_ALLOWED, + 'default' => null + ], + 'starttime' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + 'default' => null + ], + 'endtime' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + 'default' => null + ], + 'published' => [ + 'type' => PARAM_BOOL, + 'null' => NULL_ALLOWED, + 'default' => null + ], + 'playbacks' => [ + 'type' => PARAM_RAW, + 'null' => NULL_ALLOWED, + 'default' => null + ], + ]; + } + + /** + * Get the instance that this recording relates to. + * + * @return instance + */ + public function get_instance(): instance { + if ($this->instance === null) { + $this->instance = instance::get_from_instanceid($this->get('bigbluebuttonbnid')); + } + + return $this->instance; + } + + /** + * Before doing the database update, let's check if we need to update metadata + * + * @return void + */ + protected function before_update() { + // We update if the remote metadata has been changed locally. + if ($this->metadatachanged && !$this->get('imported')) { + $metadata = $this->fetch_metadata(); + if ($metadata) { + recording_proxy::update_recording( + $this->get('recordingid'), + $metadata + ); + } + $this->metadatachanged = false; + } + } + + /** + * Create a new imported recording from current recording + * + * @param instance $targetinstance + * @return recording + * @throws \coding_exception + * @throws \core\invalid_persistent_exception + */ + public function create_imported_recording(instance $targetinstance) { + $recordingrec = $this->to_record(); + $remotedata = $this->fetch_metadata(); + unset($recordingrec->id); + $recordingrec->bigbluebuttonbnid = $targetinstance->get_instance_id(); + $recordingrec->courseid = $targetinstance->get_course_id(); + $recordingrec->groupid = 0; // The recording is available to everyone. + $recordingrec->importeddata = json_encode($remotedata); + $recordingrec->imported = true; + $recordingrec->headless = false; + $importedrecording = new self(0, $recordingrec); + $importedrecording->create(); + return $importedrecording; + } + + /** + * Delete the recording in the BBB button + * + * @return void + */ + protected function before_delete() { + $recordid = $this->get('recordingid'); + if ($recordid && !$this->get('imported')) { + recording_proxy::delete_recording($recordid); + // Delete in cache if needed. + $cachedrecordings = cache::make('mod_bigbluebuttonbn', 'recordings'); + $cachedrecordings->delete($recordid); + } + } + + /** + * Set name + * + * @param string $value + */ + protected function set_name($value) { + $this->metadata_set('name', trim($value)); + } + + /** + * Set Description + * + * @param string $value + */ + protected function set_description($value) { + $this->metadata_set('description', trim($value)); + } + + /** + * Recording is protected + * + * @param bool $value + */ + protected function set_protected($value) { + $realvalue = $value ? "true" : "false"; + $this->metadata_set('protected', $realvalue); + recording_proxy::protect_recording($this->get('recordingid'), $realvalue); + } + + /** + * Recording starttime + * + * @param int $value + */ + protected function set_starttime($value) { + $this->metadata_set('starttime', $value); + } + + /** + * Recording endtime + * + * @param int $value + */ + protected function set_endtime($value) { + $this->metadata_set('endtime', $value); + } + + /** + * Recording is published + * + * @param bool $value + */ + protected function set_published($value) { + $realvalue = $value ? "true" : "false"; + $this->metadata_set('published', $realvalue); + // Now set this flag onto the remote bbb server. + recording_proxy::publish_recording($this->get('recordingid'), $realvalue); + } + + /** + * POSSIBLE_REMOTE_META_SOURCE match a field type and its metadataname (historical and current). + */ + const POSSIBLE_REMOTE_META_SOURCE = [ + 'description' => ['meta_bbb-recording-description', 'meta_contextactivitydescription'], + 'name' => ['meta_bbb-recording-name', 'meta_contextactivity', 'meetingName'], + 'playbacks' => ['playbacks'], + 'starttime' => ['startTime'], + 'endtime' => ['endTime'], + 'published' => ['published'], + 'protected' => ['protected'], + 'tags' => ['meta_bbb-recording-tags'] + ]; + + /** + * Get the real metadata name for the possible source. + * + * @param string $sourcetype the name of the source we look for (name, description...) + * @param array $metadata current metadata + */ + protected function get_possible_meta_name_for_source($sourcetype, $metadata): string { + $possiblesource = self::POSSIBLE_REMOTE_META_SOURCE[$sourcetype]; + $possiblesourcename = $possiblesource[0]; + foreach ($possiblesource as $possiblesname) { + if (isset($meta[$possiblesname])) { + $possiblesourcename = $possiblesname; + } + } + return $possiblesourcename; + } + + /** + * Convert string (metadata) to json object + * + * @return mixed|null + */ + protected function remote_meta_convert() { + $remotemeta = $this->raw_get('importeddata'); + return json_decode($remotemeta, true); + } + + /** + * Description is stored in the metadata, so we sometimes needs to do some conversion. + */ + protected function get_description() { + return trim($this->metadata_get('description')); + + } + + /** + * Name is stored in the metadata + */ + protected function get_name() { + return trim($this->metadata_get('name')); + } + + /** + * List of playbacks for this recording. + * + * @return array[] + */ + protected function get_playbacks() { + if ($playbacks = $this->metadata_get('playbacks')) { + return array_map(function(array $playback): array { + $clone = array_merge([], $playback); + $clone['url'] = new moodle_url('/mod/bigbluebuttonbn/bbb_view.php', [ + 'action' => 'play', + 'bn' => $this->raw_get('bigbluebuttonbnid'), + 'rid' => $this->get('id'), + 'rtype' => $clone['type'], + ]); + + return $clone; + }, $playbacks); + } + + return []; + } + + /** + * Get the playback URL for the specified type. + * + * @param string $type + * @return null|string + */ + public function get_remote_playback_url(string $type): ?string { + $this->refresh_metadata_if_required(); + + $playbacks = $this->metadata_get('playbacks'); + foreach ($playbacks as $playback) { + if ($playback['type'] == $type) { + return $playback['url']; + } + } + + return null; + } + + /** + * Is protected. Return null if protected is not implemented. + * + * @return bool|null + * @throws \coding_exception + */ + protected function get_protected() { + $protectedtext = $this->metadata_get('protected'); + return is_null($protectedtext) ? null : $protectedtext === "true"; + } + + /** + * Start time + * + * @return mixed|null + * @throws \coding_exception + */ + protected function get_starttime() { + return $this->metadata_get('starttime'); + } + + /** + * Start time + * + * @return mixed|null + * @throws \coding_exception + */ + protected function get_endtime() { + return $this->metadata_get('endtime'); + } + + /** + * Is published + * + * @return bool + * @throws \coding_exception + */ + protected function get_published() { + $publishedtext = $this->metadata_get('published'); + return $publishedtext === "true"; + } + + /** + * Set locally stored metadata from this instance + * + * @param string $fieldname + * @param mixed $value + * @throws \coding_exception + */ + protected function metadata_set($fieldname, $value) { + // Can we can change the metadata on the imported record ? + if ($this->get('imported')) { + return; + } + + $this->metadatachanged = true; + + $metadata = $this->fetch_metadata(); + $possiblesourcename = $this->get_possible_meta_name_for_source($fieldname, $metadata); + $metadata[$possiblesourcename] = $value; + + $this->metadata = $metadata; + } + + /** + * Get information stored in the recording metadata such as description, name and other info + * + * @param string $fieldname + * @return mixed|null + */ + protected function metadata_get($fieldname) { + $metadata = $this->fetch_metadata(); + + $possiblesourcename = $this->get_possible_meta_name_for_source($fieldname, $metadata); + return $metadata[$possiblesourcename] ?? null; + } + + /** + * @var string Default sort for recordings when fetching from the database. + */ + const DEFAULT_RECORDING_SORT = 'timecreated ASC'; + + /** + * Fetch all records which match the specified parameters, including all metadata that relates to them. + * + * @param array $selects + * @param array $params + * @return recording[] + */ + protected static function fetch_records(array $selects, array $params): array { + global $DB; + + // Fetch the local data. Arbitrary sort by id, so we get the same result on different db engines. + $recordings = $DB->get_records_select(static::TABLE, implode(" AND ", $selects), $params, + self::DEFAULT_RECORDING_SORT); + + // Grab the recording IDs. + $recordingids = array_filter(array_map(function($recording) { + return $recording->recordingid; + }, $recordings)); + + // Fetch all metadata for these recordings. + $metadatas = recording_proxy::fetch_recordings($recordingids); + + // Return the instances. + return array_map(function($recording) use ($metadatas) { + $metadata = null; + if (array_key_exists($recording->recordingid, $metadatas)) { + $metadata = $metadatas[$recording->recordingid]; + } + + return new self(0, $recording, $metadata); + }, $recordings); + } + + /** + * Fetch metadata + * + * If metadata has changed locally or if it an imported recording, nothing will be done. + * + * @param bool $force + * @return array + */ + protected function fetch_metadata(bool $force = false): ?array { + if ($this->metadata !== null && !$force) { + // Metadata is already up-to-date. + return $this->metadata; + } + + if ($this->get('imported')) { + $this->metadata = json_decode($this->get('importeddata'), true); + } else { + $this->metadata = recording_proxy::fetch_recording($this->get('recordingid')); + } + + return $this->metadata; + } + + /** + * Refresh metadata if required. + * + * If this is a protected recording which whose data was not fetched in the current request, then the metadata will + * be purged and refetched. This ensures that the url is safe for use with a protected recording. + */ + protected function refresh_metadata_if_required() { + recording_proxy::purge_protected_recording($this->get('recordingid')); + $this->fetch_metadata(true); + } + + /** + * Synchronise pending recordings from the server. + * + * This function should be called by the check_pending_recordings scheduled task. + */ + public static function sync_pending_recordings_from_server(): void { + global $DB; + + $timelimitdays = 30; + + // Fetch the local data. + mtrace("=> Looking for any recording awaiting processing from the past {$timelimitdays} days."); + $select = 'status = :status_awaiting AND timecreated > :withindays OR status = :status_reset'; + $recordings = $DB->get_records_select(static::TABLE, $select, [ + 'status_awaiting' => self::RECORDING_STATUS_AWAITING, + 'withindays' => time() - ($timelimitdays * DAYSECS), + 'status_reset' => self::RECORDING_STATUS_RESET, + ], self::DEFAULT_RECORDING_SORT); + // Sort by DEFAULT_RECORDING_SORT we get the same result on different db engines. + + $recordingcount = count($recordings); + mtrace("=> Found {$recordingcount} recordings to query"); + + // Grab the recording IDs. + $recordingids = array_map(function($recording) { + return $recording->recordingid; + }, $recordings); + + // Fetch all metadata for these recordings. + mtrace("=> Fetching recording metadata from server"); + $metadatas = recording_proxy::fetch_recordings($recordingids); + + $foundcount = 0; + foreach ($metadatas as $recordingid => $metadata) { + mtrace("==> Found updated metadata for {$recordingid}. Updating local cache."); + $id = array_search($recordingid, $recordingids); + + $recording = new self(0, $recordings[$id], $metadata); + $recording->raw_set('status', self::RECORDING_STATUS_PROCESSED); + $recording->update(); + $foundcount++; + } + + mtrace("=> Finished processing recordings. Updated status for {$foundcount} / {$recordingcount} recordings."); + } +} diff --git a/mod/bigbluebuttonbn/classes/search/activity.php b/mod/bigbluebuttonbn/classes/search/activity.php new file mode 100644 index 00000000000..2609e51fc9f --- /dev/null +++ b/mod/bigbluebuttonbn/classes/search/activity.php @@ -0,0 +1,36 @@ +. + +namespace mod_bigbluebuttonbn\search; + +/** + * Search area for mod_bigbluebuttonbn activities. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity extends \core_search\base_activity { + + /** + * Returns true if this area uses file indexing. + * + * @return bool + */ + public function uses_file_indexing() { + return true; + } +} diff --git a/mod/bigbluebuttonbn/classes/search/tags.php b/mod/bigbluebuttonbn/classes/search/tags.php new file mode 100644 index 00000000000..79d749d9b65 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/search/tags.php @@ -0,0 +1,117 @@ +. + +namespace mod_bigbluebuttonbn\search; + +use stdClass; + +/** + * Search area for mod_bigbluebuttonbn tags. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tags extends \core_search\base_activity { + + /** + * Returns true if this area uses file indexing. + * + * @return bool + */ + public function uses_file_indexing() { + return false; + } + + /** + * Overwritting get_document_recordset() + * In this search implementation, we need to re-index all instances (and not only the last modified) because we + * are working with core tags and these can be removed from "manage tags" without change the timemodified in + * BBB instances. + * @param int $modifiedfrom + * @param \context|null $context + * @return \moodle_recordset|null + */ + public function get_document_recordset($modifiedfrom = 0, \context $context = null) { + global $DB; + [$contextjoin, $contextparams] = $this->get_context_restriction_sql($context, $this->get_module_name(), 'modtable'); + if ($contextjoin === null) { + return null; + } + + $result = $DB->get_recordset_sql( + 'SELECT modtable.* FROM {' . $this->get_module_name() . '} modtable ' . $contextjoin, + array_merge($contextparams) + ); + + return($result); + } + + /** + * Overriding method to index tags of module as string separated by comma. + * + * @param stdClass $record + * @param array $options + * @return \core_search\document|bool + */ + public function get_document($record, $options = []) { + + try { + $cm = $this->get_cm($this->get_module_name(), $record->id, $record->course); + $context = \context_module::instance($cm->id); + + $tags = \core_tag_tag::get_tags_by_area_in_contexts("core", "course_modules", [$context]); + $tagsstring = ""; + if (!empty($tags)) { + $res = []; + foreach ($tags as $t) { + $res[] = $t->name; + } + $tagsstring = implode(", ", $res); + } + + } catch (\dml_missing_record_exception $ex) { + // Notify it as we run here as admin, we should see everything. + debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' . + $ex->getMessage(), DEBUG_DEVELOPER); + return false; + } catch (\dml_exception $ex) { + // Notify it as we run here as admin, we should see everything. + debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER); + return false; + } + + // Prepare array with data from DB. + $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname); + $doc->set('title', content_to_text($record->name, false)); + $doc->set('content', $tagsstring); + $doc->set('contextid', $context->id); + $doc->set('courseid', $record->course); + $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); + $doc->set('modified', $record->{static::MODIFIED_FIELD_NAME}); + + // Check if this document should be considered new. + if (isset($options['lastindexedtime'])) { + $createdfield = static::CREATED_FIELD_NAME; + if (!empty($createdfield) && ($options['lastindexedtime'] < $record->{$createdfield})) { + // If the document was created after the last index time, it must be new. + $doc->set_is_new(true); + } + } + + return $doc; + } +} diff --git a/mod/bigbluebuttonbn/classes/setting_validator.php b/mod/bigbluebuttonbn/classes/setting_validator.php new file mode 100644 index 00000000000..a38e58123ea --- /dev/null +++ b/mod/bigbluebuttonbn/classes/setting_validator.php @@ -0,0 +1,273 @@ +. + +namespace mod_bigbluebuttonbn; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->libdir.'/adminlib.php'); + +/** + * Helper class for validating settings used HTML for settings.php. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class setting_validator { + + /** + * Validate if general section will be shown. + * + * @return bool + */ + public static function section_general_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['server_url']) || + !isset($CFG->bigbluebuttonbn['shared_secret'])); + } + + /** + * Validate if record meeting section will be shown. + * + * @return bool + */ + public static function section_record_meeting_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['recording_default']) || + !isset($CFG->bigbluebuttonbn['recording_editable']) || + !isset($CFG->bigbluebuttonbn['recording_icons_enabled']) || + !isset($CFG->bigbluebuttonbn['recording_all_from_start_default']) || + !isset($CFG->bigbluebuttonbn['recording_all_from_start_editable']) || + !isset($CFG->bigbluebuttonbn['recording_hide_button_default']) || + !isset($CFG->bigbluebuttonbn['recording_hide_button_editable']) ); + } + + /** + * Validate if import recording section will be shown. + * + * @return bool + */ + public static function section_import_recordings_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['importrecordings_enabled']) || + !isset($CFG->bigbluebuttonbn['importrecordings_from_deleted_enabled'])); + } + + /** + * Validate if show recording section will be shown. + * + * @return bool + */ + public static function section_show_recordings_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['recordings_html_default']) || + !isset($CFG->bigbluebuttonbn['recordings_html_editable']) || + !isset($CFG->bigbluebuttonbn['recordings_deleted_default']) || + !isset($CFG->bigbluebuttonbn['recordings_deleted_editable']) || + !isset($CFG->bigbluebuttonbn['recordings_imported_default']) || + !isset($CFG->bigbluebuttonbn['recordings_imported_editable']) || + !isset($CFG->bigbluebuttonbn['recordings_preview_default']) || + !isset($CFG->bigbluebuttonbn['recordings_preview_editable']) || + !isset($CFG->bigbluebuttonbn['recordings_validate_url']) + ); + } + + /** + * Validate if wait moderator section will be shown. + * + * @return bool + */ + public static function section_wait_moderator_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['waitformoderator_default']) || + !isset($CFG->bigbluebuttonbn['waitformoderator_editable']) || + !isset($CFG->bigbluebuttonbn['waitformoderator_ping_interval']) || + !isset($CFG->bigbluebuttonbn['waitformoderator_cache_ttl'])); + } + + /** + * Validate if static voice bridge section will be shown. + * + * @return bool + */ + public static function section_static_voice_bridge_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['voicebridge_editable'])); + } + + /** + * Validate if preupload presentation section will be shown. + * + * @return bool + */ + public static function section_preupload_presentation_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['preuploadpresentation_enabled'])); + } + + /** + * Validate if user limit section will be shown. + * + * @return bool + */ + public static function section_user_limit_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['userlimit_default']) || + !isset($CFG->bigbluebuttonbn['userlimit_editable'])); + } + + /** + * Validate if moderator default section will be shown. + * + * @return bool + */ + public static function section_moderator_default_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['participant_moderator_default'])); + } + + /** + * Validate if send notification section will be shown. + * + * @return bool + */ + public static function section_send_notifications_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['sendnotifications_enabled'])); + } + + /** + * Validate if settings extended section will be shown. + * + * @return bool + */ + public static function section_settings_extended_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['recordingready_enabled']) || + !isset($CFG->bigbluebuttonbn['meetingevents_enabled'])); + } + + /** + * Validate if muteonstart section will be shown. + * + * @return bool + */ + public static function section_muteonstart_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['muteonstart_default']) || + !isset($CFG->bigbluebuttonbn['muteonstart_editable'])); + } + + /** + * Validate if disablecam section will be shown. + * + * @return bool + */ + public static function section_disablecam_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disablecam_default']) || + !isset($CFG->bigbluebuttonbn['disablecam_editable'])); + } + + /** + * Validate if disablemic section will be shown. + * + * @return bool + */ + public static function section_disablemic_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disablemic_default']) || + !isset($CFG->bigbluebuttonbn['disablemic_editable'])); + } + + /** + * Validate if disableprivatechat section will be shown. + * + * @return bool + */ + public static function section_disableprivatechat_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disableprivatechat_default']) || + !isset($CFG->bigbluebuttonbn['disableprivatechat_editable'])); + } + + /** + * Validate if disablepublicchat section will be shown. + * + * @return bool + */ + public static function section_disablepublicchat_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disablepublicchat_default']) || + !isset($CFG->bigbluebuttonbn['disablepublicchat_editable'])); + } + + /** + * Validate if disablenote section will be shown. + * + * @return bool + */ + public static function section_disablenote_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disablenote_default']) || + !isset($CFG->bigbluebuttonbn['disablenote_editable'])); + } + + /** + * Validate if hideuserlist section will be shown. + * + * @return bool + */ + public static function section_hideuserlist_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['hideuserlist_default']) || + !isset($CFG->bigbluebuttonbn['hideuserlist_editable'])); + } + + /** + * Validate if lockedlayout section will be shown. + * + * @return bool + */ + public static function section_lockedlayout_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['lockedlayout_default']) || + !isset($CFG->bigbluebuttonbn['lockedlayout_editable'])); + } + + /** + * Validate if lockonjoin section will be shown. + * + * @return bool + */ + public static function section_lockonjoin_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['lockonjoin_default']) || + !isset($CFG->bigbluebuttonbn['lockonjoin_editable'])); + } + + /** + * Validate if lockonjoinconfigurable section will be shown. + * + * @return bool + */ + public static function section_lockonjoinconfigurable_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['lockonjoinconfigurable_default']) || + !isset($CFG->bigbluebuttonbn['lockonjoinconfigurable_editable'])); + } +} diff --git a/mod/bigbluebuttonbn/classes/settings.php b/mod/bigbluebuttonbn/classes/settings.php new file mode 100644 index 00000000000..f572e5ca597 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/settings.php @@ -0,0 +1,1284 @@ +. + +namespace mod_bigbluebuttonbn; + +use admin_category; +use admin_setting; +use admin_setting_configcheckbox; +use admin_setting_configmultiselect; +use admin_setting_configpasswordunmask; +use admin_setting_configstoredfile; +use admin_setting_configtext; +use admin_setting_configtextarea; +use admin_setting_heading; +use admin_settingpage; +use cache_helper; +use lang_string; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\local\helpers\roles; + +/** + * The mod_bigbluebuttonbn settings helper + * + * @package mod_bigbluebuttonbn + * @copyright 2021 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ +class settings { + + /** @var admin_setting shared value */ + private $admin; + + /** @var bool Module is enabled */ + private $moduleenabled; + + /** @var string The name of the section */ + private $section; + + /** @var string The section name prefix */ + private $sectionnameprefix = "mod_bigbluebuttonbn"; + + /** + * Constructor for the bigbluebuttonbn settings. + * + * @param admin_category $admin + * @param \core\plugininfo\mod $module + * @param string $categoryname for the plugin setting (main setting page) + */ + public function __construct(admin_category $admin, \core\plugininfo\mod $module, string $categoryname) { + $this->moduleenabled = $module->is_enabled() === true; + $this->admin = $admin; + $this->section = $categoryname; + + $modbigbluebuttobnfolder = new admin_category( + $categoryname, + new lang_string('pluginname', 'mod_bigbluebuttonbn'), + $module->is_enabled() === false + ); + + $admin->add('modsettings', $modbigbluebuttobnfolder); + + $mainsettings = $this->add_general_settings(); + $admin->add($categoryname, $mainsettings); + } + + /** + * Add all settings. + */ + public function add_all_settings(): void { + // Evaluates if recordings are enabled for the Moodle site. + + // Renders settings for record feature. + $this->add_record_settings(); + // Renders settings for import recordings. + $this->add_importrecordings_settings(); + // Renders settings for showing recordings. + $this->add_showrecordings_settings(); + + // Renders settings for meetings. + $this->add_waitmoderator_settings(); + $this->add_voicebridge_settings(); + $this->add_preupload_settings(); + $this->add_userlimit_settings(); + $this->add_participants_settings(); + $this->add_notifications_settings(); + $this->add_muteonstart_settings(); + $this->add_locksettings_settings(); + // Renders settings for extended capabilities. + $this->add_extended_settings(); + // Renders settings for experimental features. + $this->add_experimental_settings(); + } + + /** + * Add the setting and lock it conditionally. + * + * @param string $name + * @param admin_setting $item + * @param admin_settingpage $settings + */ + protected function add_conditional_element(string $name, admin_setting $item, admin_settingpage $settings): void { + global $CFG; + if (isset($CFG->bigbluebuttonbn) && isset($CFG->bigbluebuttonbn[$name])) { + if ($item->config_read($item->name)) { + // A value has been set, we can safely omit the setting and it won't interfere with installation + // process. + // The idea behind it is to hide values from end-users in case we use multitenancy for example. + return; + } + } + $settings->add($item); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @return admin_settingpage + * @throws \coding_exception + */ + protected function add_general_settings(): admin_settingpage { + $settingsgeneral = new admin_settingpage( + "{$this->sectionnameprefix}_general", + get_string('config_general', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_general_shown()) && ($this->moduleenabled) + ); + if ($this->admin->fulltree) { + // Configuration for BigBlueButton. + $item = new admin_setting_heading('bigbluebuttonbn_config_general', + '', + get_string('config_general_description', 'bigbluebuttonbn')); + + $settingsgeneral->add($item); + $item = new admin_setting_configtext( + 'bigbluebuttonbn_server_url', + get_string('config_server_url', 'bigbluebuttonbn'), + get_string('config_server_url_description', 'bigbluebuttonbn'), + config::DEFAULT_SERVER_URL, + PARAM_RAW + ); + $item->set_updatedcallback( + function() { + $this->reset_cache(); + $task = new \mod_bigbluebuttonbn\task\reset_recordings(); + \core\task\manager::queue_adhoc_task($task); + } + ); + $this->add_conditional_element( + 'server_url', + $item, + $settingsgeneral + ); + $item = new admin_setting_configpasswordunmask( + 'bigbluebuttonbn_shared_secret', + get_string('config_shared_secret', 'bigbluebuttonbn'), + get_string('config_shared_secret_description', 'bigbluebuttonbn'), + config::DEFAULT_SHARED_SECRET + ); + $this->add_conditional_element( + 'shared_secret', + $item, + $settingsgeneral + ); + $settingsgeneral->add($item); + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_default_messages', + get_string('config_default_messages', 'bigbluebuttonbn'), + get_string('config_default_messages_description', 'bigbluebuttonbn') + ); + $settingsgeneral->add($item); + $item = new admin_setting_configtextarea( + 'bigbluebuttonbn_welcome_default', + get_string('config_welcome_default', 'bigbluebuttonbn'), + get_string('config_welcome_default_description', 'bigbluebuttonbn'), + '', + PARAM_TEXT + ); + $this->add_conditional_element( + 'welcome_default', + $item, + $settingsgeneral + ); + } + return $settingsgeneral; + } + + /** + * Helper function renders record settings if the feature is enabled. + */ + protected function add_record_settings(): void { + // Configuration for 'recording' feature. + $recordingsetting = new admin_settingpage( + "{$this->sectionnameprefix}_recording", + get_string('config_recording', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_record_meeting_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_recording', + '', + get_string('config_recording_description', 'bigbluebuttonbn') + ); + $recordingsetting->add($item); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recording_default', + get_string('config_recording_default', 'bigbluebuttonbn'), + get_string('config_recording_default_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'recording_default', + $item, + $recordingsetting + ); + $item = new admin_setting_configtext( + 'bigbluebuttonbn_recording_refresh_period', + get_string('config_recording_refresh_period', 'bigbluebuttonbn'), + get_string('config_recording_refresh_period_description', 'bigbluebuttonbn'), + recording::RECORDING_REFRESH_DEFAULT_PERIOD, + PARAM_INT + ); + $this->add_conditional_element( + 'recording_refresh_period', + $item, + $recordingsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recording_editable', + get_string('config_recording_editable', 'bigbluebuttonbn'), + get_string('config_recording_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'recording_editable', + $item, + $recordingsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recording_icons_enabled', + get_string('config_recording_icons_enabled', 'bigbluebuttonbn'), + get_string('config_recording_icons_enabled_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'recording_icons_enabled', + $item, + $recordingsetting + ); + + // Add recording start to load and allow/hide stop/pause. + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recording_all_from_start_default', + get_string('config_recording_all_from_start_default', 'bigbluebuttonbn'), + get_string('config_recording_all_from_start_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recording_all_from_start_default', + $item, + $recordingsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recording_all_from_start_editable', + get_string('config_recording_all_from_start_editable', 'bigbluebuttonbn'), + get_string('config_recording_all_from_start_editable_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recording_all_from_start_editable', + $item, + $recordingsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recording_hide_button_default', + get_string('config_recording_hide_button_default', 'bigbluebuttonbn'), + get_string('config_recording_hide_button_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recording_hide_button_default', + $item, + $recordingsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recording_hide_button_editable', + get_string('config_recording_hide_button_editable', 'bigbluebuttonbn'), + get_string('config_recording_hide_button_editable_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recording_hide_button_editable', + $item, + $recordingsetting + ); + } + $this->admin->add($this->section, $recordingsetting); + } + + /** + * Helper function renders import recording settings if the feature is enabled. + */ + protected function add_importrecordings_settings(): void { + // Configuration for 'import recordings' feature. + $importrecordingsettings = new admin_settingpage( + "{$this->sectionnameprefix}_importrecording", + get_string('config_importrecordings', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_import_recordings_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_importrecordings', + '', + get_string('config_importrecordings_description', 'bigbluebuttonbn') + ); + $importrecordingsettings->add($item); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_importrecordings_enabled', + get_string('config_importrecordings_enabled', 'bigbluebuttonbn'), + get_string('config_importrecordings_enabled_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'importrecordings_enabled', + $item, + $importrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_importrecordings_from_deleted_enabled', + get_string('config_importrecordings_from_deleted_enabled', 'bigbluebuttonbn'), + get_string('config_importrecordings_from_deleted_enabled_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'importrecordings_from_deleted_enabled', + $item, + $importrecordingsettings + ); + } + $this->admin->add($this->section, $importrecordingsettings); + } + + /** + * Helper function renders show recording settings if the feature is enabled. + */ + protected function add_showrecordings_settings(): void { + // Configuration for 'show recordings' feature. + $showrecordingsettings = new admin_settingpage( + "{$this->sectionnameprefix}_showrecordings", + get_string('config_recordings', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_show_recordings_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_recordings', + '', + get_string('config_recordings_description', 'bigbluebuttonbn') + ); + $showrecordingsettings->add($item); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_html_default', + get_string('config_recordings_html_default', 'bigbluebuttonbn'), + get_string('config_recordings_html_default_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'recordings_html_default', + $item, + $showrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_html_editable', + get_string('config_recordings_html_editable', 'bigbluebuttonbn'), + get_string('config_recordings_html_editable_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recordings_html_editable', + $item, + $showrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_deleted_default', + get_string('config_recordings_deleted_default', 'bigbluebuttonbn'), + get_string('config_recordings_deleted_default_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'recordings_deleted_default', + $item, + $showrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_deleted_editable', + get_string('config_recordings_deleted_editable', 'bigbluebuttonbn'), + get_string('config_recordings_deleted_editable_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recordings_deleted_editable', + $item, + $showrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_imported_default', + get_string('config_recordings_imported_default', 'bigbluebuttonbn'), + get_string('config_recordings_imported_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recordings_imported_default', + $item, + $showrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_imported_editable', + get_string('config_recordings_imported_editable', 'bigbluebuttonbn'), + get_string('config_recordings_imported_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'recordings_imported_editable', + $item, + $showrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_preview_default', + get_string('config_recordings_preview_default', 'bigbluebuttonbn'), + get_string('config_recordings_preview_default_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'recordings_preview_default', + $item, + $showrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_preview_editable', + get_string('config_recordings_preview_editable', 'bigbluebuttonbn'), + get_string('config_recordings_preview_editable_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recordings_preview_editable', + $item, + $showrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_sortorder', + get_string('config_recordings_sortorder', 'bigbluebuttonbn'), + get_string('config_recordings_sortorder_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recordings_sortorder', + $item, + $showrecordingsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordings_validate_url', + get_string('config_recordings_validate_url', 'bigbluebuttonbn'), + get_string('config_recordings_validate_url_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'recordings_validate_url', + $item, + $showrecordingsettings + ); + } + $this->admin->add($this->section, $showrecordingsettings); + } + + /** + * Helper function renders wait for moderator settings if the feature is enabled. + */ + protected function add_waitmoderator_settings(): void { + // Configuration for wait for moderator feature. + $waitmoderatorsettings = new admin_settingpage( + "{$this->sectionnameprefix}_waitformoderator", + get_string('config_waitformoderator', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_wait_moderator_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_waitformoderator', + '', + get_string('config_waitformoderator_description', 'bigbluebuttonbn') + ); + $waitmoderatorsettings->add($item); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_waitformoderator_default', + get_string('config_waitformoderator_default', 'bigbluebuttonbn'), + get_string('config_waitformoderator_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'waitformoderator_default', + $item, + $waitmoderatorsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_waitformoderator_editable', + get_string('config_waitformoderator_editable', 'bigbluebuttonbn'), + get_string('config_waitformoderator_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'waitformoderator_editable', + $item, + $waitmoderatorsettings + ); + $item = new admin_setting_configtext( + 'bigbluebuttonbn_waitformoderator_ping_interval', + get_string('config_waitformoderator_ping_interval', 'bigbluebuttonbn'), + get_string('config_waitformoderator_ping_interval_description', 'bigbluebuttonbn'), + 10, + PARAM_INT + ); + $this->add_conditional_element( + 'waitformoderator_ping_interval', + $item, + $waitmoderatorsettings + ); + $item = new admin_setting_configtext( + 'bigbluebuttonbn_waitformoderator_cache_ttl', + get_string('config_waitformoderator_cache_ttl', 'bigbluebuttonbn'), + get_string('config_waitformoderator_cache_ttl_description', 'bigbluebuttonbn'), + 60, + PARAM_INT + ); + $this->add_conditional_element( + 'waitformoderator_cache_ttl', + $item, + $waitmoderatorsettings + ); + } + $this->admin->add($this->section, $waitmoderatorsettings); + } + + /** + * Helper function renders static voice bridge settings if the feature is enabled. + */ + protected function add_voicebridge_settings(): void { + // Configuration for "static voice bridge" feature. + $voicebridgesettings = new admin_settingpage( + "{$this->sectionnameprefix}_voicebridge", + get_string('config_voicebridge', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_static_voice_bridge_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_voicebridge', + '', + get_string('config_voicebridge_description', 'bigbluebuttonbn') + ); + $voicebridgesettings->add($item); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_voicebridge_editable', + get_string('config_voicebridge_editable', 'bigbluebuttonbn'), + get_string('config_voicebridge_editable_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'voicebridge_editable', + $item, + $voicebridgesettings + ); + } + $this->admin->add($this->section, $voicebridgesettings); + } + + /** + * Helper function renders preuploaded presentation settings if the feature is enabled. + */ + protected function add_preupload_settings(): void { + // Configuration for "preupload presentation" feature. + $preuploadsettings = new admin_settingpage( + "{$this->sectionnameprefix}_preupload", + get_string('config_preuploadpresentation', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_preupload_presentation_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + // This feature only works if curl is installed (but it is as now required by Moodle). The checks have been removed. + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_preuploadpresentation', + '', + get_string('config_preuploadpresentation_description', 'bigbluebuttonbn') + ); + $preuploadsettings->add($item); + + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_preuploadpresentation_enabled', + get_string('config_preuploadpresentation_enabled', 'bigbluebuttonbn'), + get_string('config_preuploadpresentation_enabled_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'preuploadpresentation_enabled', + $item, + $preuploadsettings + ); + // Note: checks on curl library have been removed as it is a requirement from Moodle. + $filemanageroptions = []; + $filemanageroptions['accepted_types'] = '*'; + $filemanageroptions['maxbytes'] = 0; + $filemanageroptions['subdirs'] = 0; + $filemanageroptions['maxfiles'] = 1; + $filemanageroptions['mainfile'] = true; + + $filemanager = new admin_setting_configstoredfile( + 'mod_bigbluebuttonbn/presentationdefault', + get_string('config_presentation_default', 'bigbluebuttonbn'), + get_string('config_presentation_default_description', 'bigbluebuttonbn'), + 'presentationdefault', + 0, + $filemanageroptions + ); + + $preuploadsettings->add($filemanager); + } + $this->admin->add($this->section, $preuploadsettings); + } + + /** + * Helper function renders userlimit settings if the feature is enabled. + */ + protected function add_userlimit_settings(): void { + $userlimitsettings = new admin_settingpage( + "{$this->sectionnameprefix}_userlimit", + get_string('config_userlimit', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_user_limit_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + // Configuration for "user limit" feature. + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_userlimit', + '', + get_string('config_userlimit_description', 'bigbluebuttonbn') + ); + $userlimitsettings->add($item); + $item = new admin_setting_configtext( + 'bigbluebuttonbn_userlimit_default', + get_string('config_userlimit_default', 'bigbluebuttonbn'), + get_string('config_userlimit_default_description', 'bigbluebuttonbn'), + 0, + PARAM_INT + ); + $this->add_conditional_element( + 'userlimit_default', + $item, + $userlimitsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_userlimit_editable', + get_string('config_userlimit_editable', 'bigbluebuttonbn'), + get_string('config_userlimit_editable_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'userlimit_editable', + $item, + $userlimitsettings + ); + } + $this->admin->add($this->section, $userlimitsettings); + } + + /** + * Helper function renders participant settings if the feature is enabled. + */ + protected function add_participants_settings(): void { + // Configuration for defining the default role/user that will be moderator on new activities. + $participantsettings = new admin_settingpage( + "{$this->sectionnameprefix}_participant", + get_string('config_participant', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_moderator_default_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_participant', + '', + get_string('config_participant_description', 'bigbluebuttonbn') + ); + $participantsettings->add($item); + + // UI for 'participants' feature. + $roles = roles::get_roles(null, false); + $owner = [ + '0' => get_string('mod_form_field_participant_list_type_owner', 'bigbluebuttonbn') + ]; + $item = new admin_setting_configmultiselect( + 'bigbluebuttonbn_participant_moderator_default', + get_string('config_participant_moderator_default', 'bigbluebuttonbn'), + get_string('config_participant_moderator_default_description', 'bigbluebuttonbn'), + array_keys($owner), + $owner + $roles + ); + $this->add_conditional_element( + 'participant_moderator_default', + $item, + $participantsettings + ); + } + $this->admin->add($this->section, $participantsettings); + } + + /** + * Helper function renders notification settings if the feature is enabled. + */ + protected function add_notifications_settings(): void { + // Configuration for "send notifications" feature. + $notificationssettings = new admin_settingpage( + "{$this->sectionnameprefix}_notifications", + get_string('config_sendnotifications', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_send_notifications_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_sendnotifications', + '', + get_string('config_sendnotifications_description', 'bigbluebuttonbn') + ); + $notificationssettings->add($item); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_sendnotifications_enabled', + get_string('config_sendnotifications_enabled', 'bigbluebuttonbn'), + get_string('config_sendnotifications_enabled_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'sendnotifications_enabled', + $item, + $notificationssettings + ); + } + $this->admin->add($this->section, $notificationssettings); + } + + /** + * Helper function renders general settings if the feature is enabled. + */ + protected function add_muteonstart_settings(): void { + // Configuration for BigBlueButton. + $muteonstartsetting = new admin_settingpage( + "{$this->sectionnameprefix}_muteonstart", + get_string('config_muteonstart', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_muteonstart_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_muteonstart', + '', + get_string('config_muteonstart_description', 'bigbluebuttonbn') + ); + $muteonstartsetting->add($item); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_muteonstart_default', + get_string('config_muteonstart_default', 'bigbluebuttonbn'), + get_string('config_muteonstart_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'muteonstart_default', + $item, + $muteonstartsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_muteonstart_editable', + get_string('config_muteonstart_editable', 'bigbluebuttonbn'), + get_string('config_muteonstart_editable_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'muteonstart_editable', + $item, + $muteonstartsetting + ); + } + $this->admin->add($this->section, $muteonstartsetting); + } + + /** + * Helper function renders general settings if the feature is enabled. + */ + protected function add_locksettings_settings(): void { + $category = new admin_category( + "{$this->sectionnameprefix}_locksettings", + get_string('config_locksettings', 'bigbluebuttonbn'), + get_string('config_locksettings_description', 'bigbluebuttonbn') + ); + + $this->admin->add($this->section, $category); + + // Configuration for various lock settings for meetings. + $this->add_disablecam_settings($category); + $this->add_disablemic_settings($category); + $this->add_disablepublicchat_settings($category); + $this->add_disablenote_settings($category); + $this->add_hideuserlist_settings($category); + $this->add_lockedlayout_settings($category); + $this->add_lockonjoin_settings($category); + $this->add_lockonjoinconfigurable_settings($category); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @param admin_category $category The parent category to add to + */ + protected function add_disablecam_settings(admin_category $category): void { + // Configuration for BigBlueButton. + $disablecamsettings = new admin_settingpage( + "{$this->sectionnameprefix}_disablecam", + get_string('config_disablecam_default', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_disablecam_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_disablecam_default', + get_string('config_disablecam_default', 'bigbluebuttonbn'), + get_string('config_disablecam_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'disablecam_default', + $item, + $disablecamsettings + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_disablecam_editable', + get_string('config_disablecam_editable', 'bigbluebuttonbn'), + get_string('config_disablecam_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'disablecam_editable', + $item, + $disablecamsettings + ); + } + $this->admin->add($category->name, $disablecamsettings); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @param admin_category $category The parent category to add to + */ + protected function add_disablemic_settings(admin_category $category): void { + // Configuration for BigBlueButton. + $disablemicsetting = new admin_settingpage( + "{$this->sectionnameprefix}_disablemic", + get_string('config_disablemic_default', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_disablemic_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_disablemic_default', + get_string('config_disablemic_default', 'bigbluebuttonbn'), + get_string('config_disablemic_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'disablemic_default', + $item, + $disablemicsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_disablemic_editable', + get_string('config_disablemic_editable', 'bigbluebuttonbn'), + get_string('config_disablemic_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'disablecam_editable', + $item, + $disablemicsetting + ); + } + $this->admin->add($category->name, $disablemicsetting); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @param admin_category $category The parent category to add to + */ + protected function add_disableprivatechat_settings(admin_category $category): void { + // Configuration for BigBlueButton. + $disableprivatechatsetting = new admin_settingpage( + "{$this->sectionnameprefix}_disableprivatechat", + get_string('config_disableprivatechat_default', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_disableprivatechat_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_disableprivatechat_default', + get_string('config_disableprivatechat_default', 'bigbluebuttonbn'), + get_string('config_disableprivatechat_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'disableprivatechat_default', + $item, + $disableprivatechatsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_disableprivatechat_editable', + get_string('config_disableprivatechat_editable', 'bigbluebuttonbn'), + get_string('config_disableprivatechat_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'disableprivatechat_editable', + $item, + $disableprivatechatsetting + ); + } + $this->admin->add($category->name, $disableprivatechatsetting); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @param admin_category $category The parent category to add to + */ + protected function add_disablepublicchat_settings(admin_category $category): void { + // Configuration for BigBlueButton. + $disablepublicchatsetting = new admin_settingpage( + "{$this->sectionnameprefix}_disablepublicchat", + get_string('config_disableprivatechat_default', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_disablepublicchat_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_configcheckbox('bigbluebuttonbn_disablepublicchat_default', + get_string('config_disablepublicchat_default', 'bigbluebuttonbn'), + get_string('config_disablepublicchat_default_description', 'bigbluebuttonbn'), + 0); + $this->add_conditional_element( + 'disablepublicchat_default', + $item, + $disablepublicchatsetting + ); + $item = new admin_setting_configcheckbox('bigbluebuttonbn_disablepublicchat_editable', + get_string('config_disablepublicchat_editable', 'bigbluebuttonbn'), + get_string('config_disablepublicchat_editable_description', 'bigbluebuttonbn'), + 1); + $this->add_conditional_element( + 'disablepublicchat_editable', + $item, + $disablepublicchatsetting + ); + } + $this->admin->add($category->name, $disablepublicchatsetting); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @param admin_category $category The parent category to add to + */ + protected function add_disablenote_settings(admin_category $category): void { + // Configuration for BigBlueButton. + $disablenotesetting = new admin_settingpage( + "{$this->sectionnameprefix}_disablenote", + get_string('config_disablenote_default', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_disablenote_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_disablenote_default', + get_string('config_disablenote_default', 'bigbluebuttonbn'), + get_string('config_disablenote_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'disablenote_default', + $item, + $disablenotesetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_disablenote_editable', + get_string('config_disablenote_editable', 'bigbluebuttonbn'), + get_string('config_disablenote_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'disablenote_editable', + $item, + $disablenotesetting + ); + } + $this->admin->add($category->name, $disablenotesetting); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @param admin_category $category The parent category to add to + */ + protected function add_hideuserlist_settings(admin_category $category): void { + // Configuration for BigBlueButton. + $hideuserlistsetting = new admin_settingpage( + "{$this->sectionnameprefix}_hideuserlist", + get_string('config_hideuserlist_default', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_hideuserlist_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_hideuserlist_default', + get_string('config_hideuserlist_default', 'bigbluebuttonbn'), + get_string('config_hideuserlist_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'hideuserlist_default', + $item, + $hideuserlistsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_hideuserlist_editable', + get_string('config_hideuserlist_editable', 'bigbluebuttonbn'), + get_string('config_hideuserlist_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'hideuserlist_editable', + $item, + $hideuserlistsetting + ); + } + $this->admin->add($category->name, $hideuserlistsetting); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @param admin_category $category The parent category to add to + */ + protected function add_lockedlayout_settings(admin_category $category): void { + // Configuration for BigBlueButton. + $lockedlayoutsetting = new admin_settingpage( + "{$this->sectionnameprefix}_lockedlayout", + get_string('config_lockedlayout_default', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_lockedlayout_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_lockedlayout_default', + get_string('config_lockedlayout_default', 'bigbluebuttonbn'), + get_string('config_lockedlayout_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'lockedlayout_default', + $item, + $lockedlayoutsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_lockedlayout_editable', + get_string('config_lockedlayout_editable', 'bigbluebuttonbn'), + get_string('config_lockedlayout_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'lockedlayout_editable', + $item, + $lockedlayoutsetting + ); + } + $this->admin->add($category->name, $lockedlayoutsetting); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @param admin_category $category The parent category to add to + */ + protected function add_lockonjoin_settings(admin_category $category): void { + // Configuration for BigBlueButton. + $lockonjoinsetting = new admin_settingpage( + "{$this->sectionnameprefix}_lockonjoin", + get_string('config_lockonjoin_default', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_lockonjoin_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + if ((boolean) setting_validator::section_lockonjoin_shown()) { + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_lockonjoin_default', + get_string('config_lockonjoin_default', 'bigbluebuttonbn'), + get_string('config_lockonjoin_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'lockonjoin_default', + $item, + $lockonjoinsetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_lockonjoin_editable', + get_string('config_lockonjoin_editable', 'bigbluebuttonbn'), + get_string('config_lockonjoin_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'lockonjoin_editable', + $item, + $lockonjoinsetting + ); + } + } + $this->admin->add($category->name, $lockonjoinsetting); + } + + /** + * Helper function renders general settings if the feature is enabled. + * + * @param admin_category $category The parent category to add to + */ + protected function add_lockonjoinconfigurable_settings(admin_category $category): void { + // Configuration for BigBlueButton. + $lockonjoinconfigurablesetting = new admin_settingpage( + "{$this->sectionnameprefix}_lockonjoinconfigurable", + get_string('config_lockonjoinconfigurable_default', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_lockonjoinconfigurable_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_lockonjoinconfigurable_default', + get_string('config_lockonjoinconfigurable_default', 'bigbluebuttonbn'), + get_string('config_lockonjoinconfigurable_default_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'lockonjoinconfigurable_default', + $item, + $lockonjoinconfigurablesetting + ); + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_lockonjoinconfigurable_editable', + get_string('config_lockonjoinconfigurable_editable', 'bigbluebuttonbn'), + get_string('config_lockonjoinconfigurable_editable_description', 'bigbluebuttonbn'), + 1 + ); + $this->add_conditional_element( + 'lockonjoinconfigurable_editable', + $item, + $lockonjoinconfigurablesetting + ); + } + $this->admin->add($category->name, $lockonjoinconfigurablesetting); + } + + /** + * Helper function renders extended settings if any of the features there is enabled. + */ + protected function add_extended_settings(): void { + // Configuration for extended capabilities. + $extendedcapabilitiessetting = new admin_settingpage( + "{$this->sectionnameprefix}_extendedcapabilities", + get_string('config_extended_capabilities', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_settings_extended_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_extended_capabilities', + '', + get_string('config_extended_capabilities_description', 'bigbluebuttonbn') + ); + $extendedcapabilitiessetting->add($item); + // UI for 'notify users when recording ready' feature. + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_recordingready_enabled', + get_string('config_recordingready_enabled', 'bigbluebuttonbn'), + get_string('config_recordingready_enabled_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'recordingready_enabled', + $item, + $extendedcapabilitiessetting + ); + } + $this->admin->add($this->section, $extendedcapabilitiessetting); + // Configuration for extended BN capabilities should go here. + } + + /** + * Helper function renders experimental settings if any of the features there is enabled. + */ + protected function add_experimental_settings(): void { + // Configuration for experimental features should go here. + $experimentalfeaturessetting = new admin_settingpage( + "{$this->sectionnameprefix}_experimentalfeatures", + get_string('config_experimental_features', 'bigbluebuttonbn'), + 'moodle/site:config', + !((boolean) setting_validator::section_settings_extended_shown()) && ($this->moduleenabled) + ); + + if ($this->admin->fulltree) { + $item = new admin_setting_heading( + 'bigbluebuttonbn_config_experimental_features', + '', + get_string('config_experimental_features_description', 'bigbluebuttonbn') + ); + $experimentalfeaturessetting->add($item); + // UI for 'register meeting events' feature. + $item = new admin_setting_configcheckbox( + 'bigbluebuttonbn_meetingevents_enabled', + get_string('config_meetingevents_enabled', 'bigbluebuttonbn'), + get_string('config_meetingevents_enabled_description', 'bigbluebuttonbn'), + 0 + ); + $this->add_conditional_element( + 'meetingevents_enabled', + $item, + $experimentalfeaturessetting + ); + } + $this->admin->add($this->section, $experimentalfeaturessetting); + } + + /** + * Process reset cache. + */ + protected function reset_cache() { + // Reset serverinfo cache. + cache_helper::purge_by_event('mod_bigbluebuttonbn/serversettingschanged'); + } +} diff --git a/mod/bigbluebuttonbn/classes/task/check_pending_recordings.php b/mod/bigbluebuttonbn/classes/task/check_pending_recordings.php new file mode 100644 index 00000000000..570149fe266 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/check_pending_recordings.php @@ -0,0 +1,45 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use core\task\scheduled_task; +use mod_bigbluebuttonbn\recording; + +/** + * Synchronise pending recordings from the server. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class check_pending_recordings extends scheduled_task { + /** + * Run the migration task. + */ + public function execute() { + recording::sync_pending_recordings_from_server(); + } + + /** + * Get the name of the task for use in the interface. + * + * @return string + */ + public function get_name(): string { + return get_string('taskname:check_pending_recordings', 'mod_bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/task/completion_update_state.php b/mod/bigbluebuttonbn/classes/task/completion_update_state.php new file mode 100644 index 00000000000..acef35cf633 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/completion_update_state.php @@ -0,0 +1,41 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use core\task\adhoc_task; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; + +/** + * Class containing the scheduled task for updating the completion state. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class completion_update_state extends adhoc_task { + /** + * Run bigbluebuttonbn cron. + */ + public function execute() { + // Get the custom data. + $data = $this->get_custom_data(); + mtrace("Task completion_update_state running for user {$data->userid}"); + + // Process the completion. + bigbluebutton_proxy::update_completion_state($data->bigbluebuttonbn, $data->userid); + } +} diff --git a/mod/bigbluebuttonbn/classes/task/reset_recordings.php b/mod/bigbluebuttonbn/classes/task/reset_recordings.php new file mode 100644 index 00000000000..9b9637a2558 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/reset_recordings.php @@ -0,0 +1,96 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use core\task\adhoc_task; +use mod_bigbluebuttonbn\recording; + +/** + * Class containing the scheduled task for converting recordings for the BigBlueButton version 2.5 in Moodle 4.0. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Jesus Federico, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reset_recordings extends adhoc_task { + + /** @var int Chunk size to use when resetting recordings */ + protected static $chunksize = 100; + + /** + * Run the migration task. + */ + public function execute() { + if ($this->process_reset_recordings()) { + \core\task\manager::queue_adhoc_task(new static()); + } + } + + /** + * Process all bigbluebuttonbn_recordings looking for entries which should be reset to be fetched again. + * + * @return bool Whether any more recodgins are waiting to be processed + */ + protected function process_reset_recordings(): bool { + global $DB; + + $classname = static::class; + + mtrace("Executing {$classname}..."); + + // Read a block of recordings to be updated. + $recs = $this->get_recordngs_to_reset(); + + if (empty($recs)) { + mtrace("No recordings were found for reset..."); + // No more logs. Stop queueing. + return false; + } + + // Reset status of {chunksize} recordings. + mtrace("Reset status of " . self::$chunksize . " recordings..."); + $sql = "UPDATE {bigbluebuttonbn_recordings} + SET status = :status_reset + WHERE id = " . implode(' OR id = ', array_keys($recs)); + $DB->execute($sql, + ['status_reset' => recording::RECORDING_STATUS_RESET] + ); + + return true; + } + + /** + * Get the list of recordings to be reset. + * + * @return array + */ + protected function get_recordngs_to_reset(): array { + global $DB; + + return $DB->get_records_sql( + 'SELECT * FROM {bigbluebuttonbn_recordings} + WHERE status = :status_processed OR status = :status_notified + ORDER BY timecreated DESC', [ + 'status_processed' => recording::RECORDING_STATUS_PROCESSED, + 'status_notified' => recording::RECORDING_STATUS_NOTIFIED + ], + 0, + self::$chunksize + ); + } + +} diff --git a/mod/bigbluebuttonbn/classes/task/send_instance_update_notification.php b/mod/bigbluebuttonbn/classes/task/send_instance_update_notification.php new file mode 100644 index 00000000000..155f9940377 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/send_instance_update_notification.php @@ -0,0 +1,124 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use core\task\adhoc_task; +use mod_bigbluebuttonbn\output\instance_updated_message; + +/** + * Class containing the adhoc task to send the notification that an update was updated. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class send_instance_update_notification extends send_notification { + + /** @var int The activity was created */ + const TYPE_CREATED = 0; + + /** @var int The activity was updated */ + const TYPE_UPDATED = 1; + + /** + * Set the update type. + * + * @param int $type + */ + public function set_update_type(int $type): void { + $this->append_custom_data(['type' => $type]); + } + + /** + * Get the type of update. + * + * @return null|int + */ + protected function get_update_type(): ?int { + $data = (object) $this->get_custom_data(); + if (property_exists($data, 'type')) { + return $data->type; + } + + return self::TYPE_UPDATED; + } + + /** + * Get the notification type. + * + * @return string + */ + protected function get_notification_type(): string { + return 'instance_updated'; + } + + /** + * Get the subject of the notification. + * + * @return string + */ + protected function get_subject(): string { + $instance = $this->get_instance(); + + if ($this->get_update_type() == self::TYPE_CREATED) { + $key = 'notification_instance_created_subject'; + } else { + $key = 'notification_instance_updated_subject'; + } + return get_string($key, 'mod_bigbluebuttonbn', $this->get_string_vars()); + } + + /** + * Get the HTML message content. + * + * @return string + */ + protected function get_html_message(): string { + global $PAGE; + + $renderer = $PAGE->get_renderer('mod_bigbluebuttonbn', '', RENDERER_TARGET_HTMLEMAIL); + return $renderer->render(new instance_updated_message($this->instance, $this->get_update_type())); + } + + /** + * Get the short summary message. + * + * @return string + */ + protected function get_small_message(): string { + if ($this->get_update_type() == self::TYPE_CREATED) { + $key = 'notification_instance_created_small'; + } else { + $key = 'notification_instance_updated_small'; + } + return get_string($key, 'mod_bigbluebuttonbn', $this->get_string_vars()); + } + + /** + * Get variables to make available to strings. + * + * @return array + */ + protected function get_string_vars(): array { + return [ + 'course_fullname' => $this->instance->get_course()->fullname, + 'course_shortname' => $this->instance->get_course()->shortname, + 'name' => $this->instance->get_cm()->name, + 'link' => $this->instance->get_view_url()->out(), + ]; + } +} diff --git a/mod/bigbluebuttonbn/classes/task/send_notification.php b/mod/bigbluebuttonbn/classes/task/send_notification.php new file mode 100644 index 00000000000..c2110b1b90c --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/send_notification.php @@ -0,0 +1,224 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use core\message\message; +use core\task\adhoc_task; +use mod_bigbluebuttonbn\instance; +use moodle_exception; +use stdClass; + +/** + * Class containing the abstract class for notification processes in BBB. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class send_notification extends adhoc_task { + + /** @var instance */ + protected $instance = null; + + /** @var object */ + protected $coursecontact = null; + + /** + * Execute the task. + */ + public function execute() { + $this->send_all_notifications(); + } + + /** + * Append additional elements of custom data + * + * @param array $newdata + */ + protected function append_custom_data(array $newdata): void { + if ($currentdata = (array) $this->get_custom_data()) { + $newdata = array_merge($currentdata, $newdata); + } + + $this->set_custom_data($newdata); + } + + /** + * Set the instanceid in the custom data. + * + * @param int $instanceid + */ + public function set_instance_id(int $instanceid): void { + $this->append_custom_data(['instanceid' => $instanceid]); + } + + /** + * Get the bigbluebutton instance that this notification is for. + * + * @return instance + */ + protected function get_instance(): instance { + if ($this->instance === null) { + $this->instance = instance::get_from_instanceid($this->get_custom_data()->instanceid); + } + + return $this->instance; + } + + /** + * Get the preferred course contact for this notification. + * + * @return stdClass + */ + protected function get_course_contact(): stdClass { + global $DB; + + if ($this->coursecontact === null) { + // Get course managers so they can be highlighted in the list. + $coursecontext = $this->get_instance()->get_course_context(); + + if ($managerroles = get_config('', 'coursecontact')) { + $coursecontactroles = explode(',', $managerroles); + foreach ($coursecontactroles as $roleid) { + $contacts = get_role_users($roleid, $coursecontext, true, 'u.id', 'u.id ASC'); + foreach ($contacts as $contact) { + $this->coursecontact = $contact; + break; + } + } + } + + if ($this->coursecontact === null) { + $this->coursecontact = \core_user::get_noreply_user(); + } + } + + return $this->coursecontact; + } + + /** + * Get the list of recipients for the notification. + * + * @return stdClass[] + */ + protected function get_recipients(): array { + // Potential users should be active users only. + return get_enrolled_users( + $this->get_instance()->get_course_context(), + 'mod/bigbluebuttonbn:view', + 0, + 'u.*', + null, + 0, + 0, + true + ); + } + + /** + * Get the HTML message content. + * + * @return string + */ + abstract protected function get_html_message(): string; + + /** + * Get the plain text message content. + * + * @return string + */ + protected function get_message(): string { + return html_to_text($this->get_html_message()); + } + + /** + * Get the short summary message. + * + * @return string + */ + abstract protected function get_small_message(): string; + + /** + * Get the preferred message format + * + * @return string + */ + protected function get_message_format(): string { + return FORMAT_HTML; + } + + /** + * Get the notification type. + * + * @return string + */ + abstract protected function get_notification_type(): string; + + /** + * Get the subject of the notification. + * + * @return string + */ + abstract protected function get_subject(): string; + + /** + * Send all of the notifications + */ + protected function send_all_notifications(): void { + $instance = $this->get_instance(); + foreach ($this->get_recipients() as $recipient) { + try { + \core_user::require_active_user($recipient, true, true); + cron_setup_user($recipient); + } catch (moodle_exception $e) { + // Skip sending. + continue; + } + + $this->send_notification_to_current_user(); + } + + cron_setup_user(); + } + + /** + * Send the notificiation to the current user. + */ + protected function send_notification_to_current_user(): void { + global $USER; + + $instance = $this->get_instance(); + + $eventdata = new message(); + $eventdata->courseid = $instance->get_course_id(); + $eventdata->component = 'mod_bigbluebuttonbn'; + $eventdata->name = $this->get_notification_type(); + $eventdata->userfrom = $this->get_course_contact(); + $eventdata->userto = $USER; + + $eventdata->subject = $this->get_subject(); + $eventdata->smallmessage = $this->get_small_message(); + $eventdata->fullmessage = $this->get_message(); + $eventdata->fullmessageformat = $this->get_message_format(); + $eventdata->fullmessagehtml = $this->get_html_message(); + $eventdata->notification = 1; + $eventdata->contexturl = $this->get_instance()->get_view_url(); + $eventdata->contexturlname = $this->get_instance()->get_meeting_name(); + + message_send($eventdata); + } +} diff --git a/mod/bigbluebuttonbn/classes/task/send_recording_ready_notification.php b/mod/bigbluebuttonbn/classes/task/send_recording_ready_notification.php new file mode 100644 index 00000000000..adbda589bab --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/send_recording_ready_notification.php @@ -0,0 +1,83 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use html_writer; + +/** + * Class containing the adhoc task to send a recording ready notification. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class send_recording_ready_notification extends send_notification { + /** + * Get the notification type. + * + * @return string + */ + protected function get_notification_type(): string { + return 'recording_ready'; + } + + /** + * Get the subject of the notification. + * + * @return string + */ + protected function get_subject(): string { + $instance = $this->get_instance(); + + return get_string('notification_recording_ready_subject', 'mod_bigbluebuttonbn', $this->get_string_vars()); + } + + /** + * Get the short summary message. + * + * @return string + */ + protected function get_small_message(): string { + return get_string('notification_recording_ready_small', 'mod_bigbluebuttonbn', $this->get_string_vars()); + } + + /** + * Get the HTML message content. + * + * @return string + */ + protected function get_html_message(): string { + return html_writer::tag( + 'p', + get_string('notification_recording_ready_html', 'mod_bigbluebuttonbn', $this->get_string_vars()) + ); + } + + /** + * Get variables to make available to strings. + * + * @return array + */ + protected function get_string_vars(): array { + return [ + 'course_fullname' => $this->instance->get_course()->fullname, + 'course_shortname' => $this->instance->get_course()->shortname, + 'name' => $this->instance->get_cm()->name, + 'link' => $this->instance->get_view_url()->out(), + ]; + } +} diff --git a/mod/bigbluebuttonbn/classes/task/upgrade_imported_recordings.php b/mod/bigbluebuttonbn/classes/task/upgrade_imported_recordings.php new file mode 100644 index 00000000000..3dea4a9af46 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/upgrade_imported_recordings.php @@ -0,0 +1,55 @@ +. + +namespace mod_bigbluebuttonbn\task; + +/** + * Class containing the scheduled task for converting legacy recordings to 2.5. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Jesus Federico, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class upgrade_imported_recordings extends upgrade_recording_base_task { + + /** + * Get the list of logs to convert. + * + * @return array + */ + protected function get_logs_to_convert(): array { + global $DB; + + return $DB->get_records( + 'bigbluebuttonbn_logs', + ['log' => 'Import'], + 'timecreated DESC', + 'id, meetingid, timecreated', + 0, + self::$chunksize + ); + } + + /** + * Fetch the imported data for a recording. + * + * @param array $recording + * @return string + */ + protected function get_imported_data(array $recording): string { + return json_encode($recording); + } +} diff --git a/mod/bigbluebuttonbn/classes/task/upgrade_recording_base_task.php b/mod/bigbluebuttonbn/classes/task/upgrade_recording_base_task.php new file mode 100644 index 00000000000..86be62937dd --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/upgrade_recording_base_task.php @@ -0,0 +1,160 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use core\task\adhoc_task; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\recording; +use mod_bigbluebuttonbn\local\proxy\recording_proxy; +use moodle_exception; + +/** + * Class containing the scheduled task for converting recordings for the BigBlueButton version 2.5 in Moodle 4.0. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Jesus Federico, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class upgrade_recording_base_task extends adhoc_task { + + /** @var int Chunk size to use when fetching recording */ + protected static $chunksize = 50; + + /** + * Run the migration task. + */ + public function execute() { + if ($this->process_bigbluebuttonbn_logs()) { + \core\task\manager::queue_adhoc_task(new static()); + } + } + + /** + * Process all bigbluebuttonbn logs looking for entries which should be converted to meetings. + * + * @return bool Whether any more logs are waiting to be processed + */ + protected function process_bigbluebuttonbn_logs(): bool { + global $DB; + + $classname = static::class; + + mtrace("Executing {$classname}..."); + + // Fetch the logs queued for upgrade. + mtrace("Fetching logs for conversion"); + $logs = $this->get_logs_to_convert(); + + if (empty($logs)) { + mtrace("No logs were found for conversion."); + // No more logs. Stop queueing. + return false; + } + + $meetingidmap = []; + foreach ($logs as $log) { + $meetingidmap[$log->meetingid] = $log->id; + } + + // Retrieve recordings from the meetingids with paginated requests. + $recordings = recording_proxy::fetch_recordings(array_keys($meetingidmap), 'meetingID'); + + // Create an instance of bigbluebuttonbn_recording per valid recording. + mtrace("Creating new recording records..."); + $recordingcount = 0; + foreach ($recordings as $recordingid => $recording) { + $meetingid = $recording['meetingID']; + $logid = $meetingidmap[$meetingid]; + $log = $logs[$logid]; + + $importeddata = $this->get_imported_data($recording); + try { + $instance = instance::get_from_meetingid($recording['meetingID']); + } catch (moodle_exception $e) { + mtrace("Unable to parse meetingID " . $e->getMessage()); + continue; + } + + if ($instance) { + $newrecording = [ + 'courseid' => $instance->get_course_id(), + 'bigbluebuttonbnid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id(), // The groupid should be taken from the meetingID. + 'recordingid' => $recordingid, + 'status' => recording::RECORDING_STATUS_PROCESSED, + 'imported' => empty($importeddata) ? 0 : 1, + ]; + } else { + mtrace("Unable to find an activity for {$recording['meetingID']}. This recording is headless."); + // This instance does not exist any more. + // Use the data in the log instead of the instance. + $meetingdata = instance::parse_meetingid($recording['meetingID']); + $newrecording = [ + 'courseid' => $meetingdata['courseid'], + 'bigbluebuttonbnid' => $meetingdata['instanceid'], + 'groupid' => 0, + 'recordingid' => $recordingid, + 'status' => recording::RECORDING_STATUS_PROCESSED, + 'imported' => empty($importeddata) ? 0 : 1, + ]; + + if (array_key_exists('groupid', $meetingdata)) { + $newrecording['groupid'] = $meetingdata['groupid']; + } + } + + if ($DB->record_exists('bigbluebuttonbn_recordings', $newrecording)) { + mtrace("A recording already exists for {$recording['recordingID']}. Skipping."); + // A recording matching these characteristics alreay exists. + continue; + } + + $newrecording['headless'] = 0; + $newrecording['importeddata'] = $importeddata; + $newrecording['timecreated'] = $newrecording['timemodified'] = $log->timecreated; + + $newrecording = $DB->insert_record('bigbluebuttonbn_recordings', $newrecording); + $recordingcount++; + } + mtrace("Migrated {$recordingcount} recordings."); + + // Delete processed logs. + mtrace("Deleting migrated log records..."); + [$inidsql, $params] = $DB->get_in_or_equal(array_keys($meetingidmap)); + + $DB->delete_records_select('bigbluebuttonbn_logs', "meetingid {$inidsql}", $params); + + return true; + } + + /** + * Get the list of logs to convert. + * + * @return array + */ + abstract protected function get_logs_to_convert(): array; + + /** + * Fetch the imported data for a recording. + * + * @param array $recording + * @return string + */ + protected function get_imported_data(array $recording): string { + return ''; + } +} diff --git a/mod/bigbluebuttonbn/classes/task/upgrade_recordings.php b/mod/bigbluebuttonbn/classes/task/upgrade_recordings.php new file mode 100644 index 00000000000..4717a17dbb2 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/upgrade_recordings.php @@ -0,0 +1,50 @@ +. + +namespace mod_bigbluebuttonbn\task; + +/** + * Class containing the scheduled task for converting legacy recordings to 2.5. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Jesus Federico, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class upgrade_recordings extends upgrade_recording_base_task { + + /** + * Get the list of logs to convert. + * + * @return array + */ + protected function get_logs_to_convert(): array { + global $DB; + + // Fetch 'Create' logs that correspond to recorded meetings. + return $DB->get_records_select( + 'bigbluebuttonbn_logs', + 'log = :logmatch AND ' . $DB->sql_like('meta', ':match'), + [ + 'logmatch' => 'Create', + 'match' => '%true%', + ], + 'timecreated DESC', + 'id, meetingid, timecreated', + 0, + self::$chunksize + ); + } +} diff --git a/mod/bigbluebuttonbn/classes/test/testcase_helper_trait.php b/mod/bigbluebuttonbn/classes/test/testcase_helper_trait.php new file mode 100644 index 00000000000..516421ac529 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/test/testcase_helper_trait.php @@ -0,0 +1,323 @@ +. + +/** + * BBB Library tests class trait. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +namespace mod_bigbluebuttonbn\test; + +use context_module; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\proxy\recording_proxy; +use mod_bigbluebuttonbn\meeting; +use stdClass; +use testing_data_generator; + +trait testcase_helper_trait { + /** @var testing_data_generator|null */ + protected $generator = null; + + /** @var object|null */ + protected $course = null; + + /** + * Convenience function to create an instance of a bigbluebuttonactivty. + * + * @param stdClass|null $course course to add the module to + * @param array $params Array of parameters to pass to the generator + * @param array $options Array of options to pass to the generator + * @return array($context, $cm, $instance) Testable wrapper around the assign class. + */ + protected function create_instance(?stdClass $course = null, array $params = [], array $options = []): array { + if (!$course) { + $course = $this->get_course(); + } + $params['course'] = $course->id; + $options['visible'] = 1; + $instance = $this->getDataGenerator()->create_module('bigbluebuttonbn', $params, $options); + list($course, $cm) = get_course_and_cm_from_instance($instance, 'bigbluebuttonbn'); + $context = context_module::instance($cm->id); + + return [$context, $cm, $instance]; + } + + /** + * Get the matching form data + * + * @param stdClass $bbactivity the current bigbluebutton activity + * @param stdClass|null $course the course or null (taken from $this->get_course() if null) + * @return mixed + */ + protected function get_form_data_from_instance(stdClass $bbactivity, ?stdClass $course = null): object { + global $USER; + + if (!$course) { + $course = $this->get_course(); + } + $this->setAdminUser(); + $bbactivitycm = get_coursemodule_from_instance('bigbluebuttonbn', $bbactivity->id); + list($cm, $context, $module, $data, $cw) = get_moduleinfo_data($bbactivitycm, $course); + $this->setUser($USER); + return $data; + } + + /** + * Get or create course if it does not exist + * + * @return stdClass|null + */ + protected function get_course(): stdClass { + if (!$this->course) { + $this->course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + } + return $this->course; + } + + /** + * Generate a course, several students and several groups + * + * @param stdClass $courserecord + * @param int $numstudents + * @param int $numteachers + * @param int $groupsnum + * @return array + */ + protected function setup_course_students_teachers(stdClass $courserecord, int $numstudents, int $numteachers, + int $groupsnum): array { + global $DB; + $generator = $this->getDataGenerator(); + $course = $generator->create_course($courserecord); + $groups = []; + for ($i = 0; $i < $groupsnum; $i++) { + $groups[] = $generator->create_group(['courseid' => $course->id]); + } + $generator->create_group(['courseid' => $course->id]); + $generator->create_group(['courseid' => $course->id]); + + $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); + + $students = []; + for ($i = 0; $i < $numstudents; $i++) { + $student = $generator->create_user(); + $generator->enrol_user($student->id, $course->id, $roleids['student']); + $groupid = $groups[$i % $groupsnum]->id; + groups_add_member($groupid, $student->id); + $students[] = $student; + } + + $teachers = []; + for ($i = 0; $i < $numteachers; $i++) { + $teacher = $generator->create_user(); + $generator->enrol_user($teacher->id, $course->id, $roleids['teacher']); + $groupid = $groups[$i % $groupsnum]->id; + groups_add_member($groupid, $teacher->id); + $teachers[] = $teacher; + } + $bbactivity = $generator->create_module( + 'bigbluebuttonbn', + ['course' => $course->id], + ['visible' => true]); + + get_fast_modinfo(0, 0, true); + return [$course, $groups, $students, $teachers, $bbactivity, $roleids]; + } + + /** + * This test requires mock server to be present. + */ + protected function initialise_mock_server(): void { + if (!defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { + $this->markTestSkipped( + 'The TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER constant must be defined to run mod_bigbluebuttonbn tests' + ); + } + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn')->reset_mock(); + } + + /** + * Create an return an array of recordings + * + * @param instance $instance + * @param array $recordingdata array of recording information + * @return array + * @throws \coding_exception + */ + protected function create_recordings_for_instance(instance $instance, array $recordingdata = []): array { + $recordings = []; + $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + // Create the meetings on the mock server, so like this we can find the recordings. + $meeting = new meeting($instance); + if (!$meeting->is_running()) { + $bbbgenerator->create_meeting([ + 'instanceid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id() + + ]); + } + foreach ($recordingdata as $rindex => $data) { + $recordings[] = $bbbgenerator->create_recording( + array_merge([ + 'bigbluebuttonbnid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id() + ], $data) + ); + } + return $recordings; + } + + /** + * Create an activity which includes a set of recordings. + * + * @param stdClass $course + * @param int $type + * @param array $recordingdata array of recording information + * @param int $groupid + * @return array + */ + protected function create_activity_with_recordings(stdClass $course, int $type, array $recordingdata, int $groupid = 0): array { + $generator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + + $activity = $generator->create_instance([ + 'course' => $course->id, + 'type' => $type + ]); + + $instance = instance::get_from_instanceid($activity->id); + $instance->set_group_id($groupid); + $recordings = $this->create_recordings_for_instance($instance, $recordingdata); + return [ + 'course' => $course, + 'activity' => $activity, + 'recordings' => $recordings, + ]; + } + + /** + * Create a course, users and recording from dataset given in an array form + * + * @param array $dataset + * @return mixed + * @throws \coding_exception + * @throws \dml_exception + */ + protected function create_from_dataset(array $dataset) { + list('type' => $type, 'recordingsdata' => $recordingsdata, 'groups' => $groups, + 'users' => $users) = $dataset; + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + + $coursedata = empty($groups) ? [] : ['groupmodeforce' => true, 'groupmode' => VISIBLEGROUPS]; + $this->course = $this->getDataGenerator()->create_course($coursedata); + + foreach ($users as $userdata) { + $this->getDataGenerator()->create_and_enrol($this->course, $userdata['role'], ['username' => $userdata['username']]); + } + + if ($groups) { + foreach ($groups as $groupname => $students) { + $group = $this->getDataGenerator()->create_group(['name' => $groupname, 'courseid' => $this->course->id]); + foreach ($students as $username) { + $user = \core_user::get_user_by_username($username); + $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); + } + } + } + $instancesettings = [ + 'course' => $this->course->id, + 'type' => $type, + 'name' => 'Example', + ]; + if (!empty($dataset['additionalsettings'])) { + $instancesettings = array_merge($instancesettings, $dataset['additionalsettings']); + } + $activity = $plugingenerator->create_instance($instancesettings); + $instance = instance::get_from_instanceid($activity->id); + foreach ($recordingsdata as $groupname => $recordings) { + if ($groups) { + $groupid = groups_get_group_by_name($this->course->id, $groupname); + $instance->set_group_id($groupid); + } + $this->create_recordings_for_instance($instance, $recordings); + } + return $activity->id; + } + + /** + * Create the legacy log entries for this task. + * + * @param instance $instance + * @param int $userid + * @param int $count + * @return array + */ + protected function create_legacy_log_entries(instance $instance, int $userid, int $count = 30, + $importrecordings = false): array { + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + $plugingenerator->create_meeting([ + 'instanceid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id(), + ]); + + // Create log entries for each (30 for the ungrouped, 30 for the grouped). + $baselogdata = [ + 'courseid' => $instance->get_course_id(), + 'userid' => $userid, + 'log' => $importrecordings ? 'Import' : 'Create', + 'meta' => json_encode(['record' => true]), + 'imported' => $importrecordings, + ]; + + for ($i = 0; $i < $count; $i++) { + // Create a recording. + $recording = $plugingenerator->create_recording([ + 'bigbluebuttonbnid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id() + ]); + + $baselogdata['meetingid'] = $instance->get_meeting_id(); + if ($importrecordings) { + // Fetch the data. + $data = recording_proxy::fetch_recordings([$recording->recordingid]); + $data = end($data); + if ($data) { + $metaonly = array_filter($data, function($key) { + return strstr($key, 'meta_'); + }, ARRAY_FILTER_USE_KEY); + } else { + $data = []; + } + $baselogdata['meta'] = json_encode(array_merge([ + 'recording' => array_diff_key($data, $metaonly), + ], $metaonly)); + + } else { + $baselogdata['meta'] = json_encode((object) ['record' => true]); + } + + // Insert the legacy log entry. + $logs[] = $plugingenerator->create_log(array_merge($baselogdata, [ + 'bigbluebuttonbnid' => $instance->get_instance_id(), + 'timecreated' => time() - WEEKSECS + (HOURSECS * $i), + ])); + } + + return $logs; + } +} diff --git a/mod/bigbluebuttonbn/db/access.php b/mod/bigbluebuttonbn/db/access.php new file mode 100644 index 00000000000..214532fc283 --- /dev/null +++ b/mod/bigbluebuttonbn/db/access.php @@ -0,0 +1,160 @@ +. + +/** + * Capabilities for BigBlueButton. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Darko Miletic (darko.miletic@gmail.com) + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + + // Ability to add a new bigbluebuttonbn instance. + 'mod/bigbluebuttonbn:addinstance' => [ + 'riskbitmask' => RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + 'clonepermissionsfrom' => 'moodle/course:manageactivities', + ], + + // Ability to create instances with live meeting capabilities. + 'mod/bigbluebuttonbn:addinstancewithmeeting' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + 'clonepermissionsfrom' => 'moodle/course:manageactivities', + ], + + // Ability to create instances with recording capabilities. + 'mod/bigbluebuttonbn:addinstancewithrecording' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + 'clonepermissionsfrom' => 'moodle/course:manageactivities', + ], + + // Ability to join a meeting. + 'mod/bigbluebuttonbn:join' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'student' => CAP_ALLOW, + 'guest' => CAP_ALLOW, + ], + ], + + // Ability to access instances, regardless of the type. + 'mod/bigbluebuttonbn:view' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ] + ], + + // Ability to manage recordings. + 'mod/bigbluebuttonbn:managerecordings' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], + + // Ability to publish recordings. + 'mod/bigbluebuttonbn:publishrecordings' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], + + // Ability to unpublish recordings. + 'mod/bigbluebuttonbn:unpublishrecordings' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], + + // Ability to protect recordings. + 'mod/bigbluebuttonbn:protectrecordings' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], + + // Ability to unprotect recordings. + 'mod/bigbluebuttonbn:unprotectrecordings' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], + + // Ability to delete recordings. + 'mod/bigbluebuttonbn:deleterecordings' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], + + // Ability to import recordings. + 'mod/bigbluebuttonbn:importrecordings' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ], + ], +]; diff --git a/mod/bigbluebuttonbn/db/caches.php b/mod/bigbluebuttonbn/db/caches.php new file mode 100644 index 00000000000..ee37cc4ce3a --- /dev/null +++ b/mod/bigbluebuttonbn/db/caches.php @@ -0,0 +1,59 @@ +. + +/** + * Caches for Bigbluebuttonbn + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ + +defined('MOODLE_INTERNAL') || die(); + +$definitions = [ + // Server information + // version (double) => server version. + 'serverinfo' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'invalidationevents' => [ + 'mod_bigbluebuttonbn/serversettingschanged', + ], + ], + + // The validatedurls cache stores a list of URLs which are either valid, or invalid. + // Keys are a URL + // Values are an integer. + 'validatedurls' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'simpledata' => true, + ], + + // The 'recordings' cache stores a cache of recording data. + 'recordings' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'invalidationevents' => [ + 'mod_bigbluebuttonbn/recordingchanged', + 'mod_bigbluebuttonbn/serversettingschanged', + ], + 'ttl' => HOURSECS, + ], + + 'currentfetch' => [ + 'mode' => cache_store::MODE_REQUEST, + ], +]; diff --git a/mod/bigbluebuttonbn/db/install.xml b/mod/bigbluebuttonbn/db/install.xml new file mode 100644 index 00000000000..27b5d74a110 --- /dev/null +++ b/mod/bigbluebuttonbn/db/install.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/mod/bigbluebuttonbn/db/log.php b/mod/bigbluebuttonbn/db/log.php new file mode 100644 index 00000000000..10f76ad928c --- /dev/null +++ b/mod/bigbluebuttonbn/db/log.php @@ -0,0 +1,43 @@ +. + +/** + * Definition of log events. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +global $DB; + +$logs = [ + ['module' => 'bigbluebuttonbn', 'action' => 'add', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'update', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'view', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'view all', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'create', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'end', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'join', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'left', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'publish', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'unpublish', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], + ['module' => 'bigbluebuttonbn', 'action' => 'delete', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'], +]; diff --git a/mod/bigbluebuttonbn/db/messages.php b/mod/bigbluebuttonbn/db/messages.php new file mode 100644 index 00000000000..f19c13df81c --- /dev/null +++ b/mod/bigbluebuttonbn/db/messages.php @@ -0,0 +1,39 @@ +. + +/** + * Defines message providers (types of messages being sent) + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$messageproviders = [ + // Recording ready notification posts. + 'recording_ready' => [ + 'defaults' => [ + ], + ], + + // The instance was created or updated. + 'instance_updated' => [ + 'defaults' => [ + ], + ], +]; diff --git a/mod/bigbluebuttonbn/db/mobile.php b/mod/bigbluebuttonbn/db/mobile.php new file mode 100644 index 00000000000..1ca2dbae612 --- /dev/null +++ b/mod/bigbluebuttonbn/db/mobile.php @@ -0,0 +1,48 @@ +. + +/** + * Mobile app definition for BigBlueButton. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; +global $CFG; +$addons = [ + "mod_bigbluebuttonbn" => [ + "handlers" => [ // Different places where the add-on will display content. + 'coursebigbluebuttonbn' => [ // Handler unique name (can be anything). + 'displaydata' => [ + 'title' => 'pluginname', + 'icon' => $CFG->wwwroot . '/mod/bigbluebuttonbn/pix/icon.gif', + 'class' => '', + ], + 'delegate' => 'CoreCourseModuleDelegate', // Delegate (where to display the link to the add-on). + 'method' => 'mobile_course_view' // Main function in \mod_bigbluebuttonbn\output\mobile. + ] + ], + 'lang' => [ + ['pluginname', 'bigbluebuttonbn'], + ['view_conference_action_join', 'bigbluebuttonbn'], + ['view_message_conference_room_ready', 'bigbluebuttonbn'], + ['view_mobile_message_reload_page_creation_time_meeting', 'bigbluebuttonbn'] + ] + ] +]; diff --git a/mod/bigbluebuttonbn/db/services.php b/mod/bigbluebuttonbn/db/services.php new file mode 100644 index 00000000000..c96e06bc4b9 --- /dev/null +++ b/mod/bigbluebuttonbn/db/services.php @@ -0,0 +1,117 @@ +. + +/** + * URL external functions and service definitions. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; + +$functions = [ + 'mod_bigbluebuttonbn_can_join' => [ + 'classname' => 'mod_bigbluebuttonbn\external\can_join', + 'methodname' => 'execute', + 'description' => 'Returns information if the current user can join or not.', + 'type' => 'read', + 'capabilities' => 'mod/bigbluebuttonbn:view', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'mod_bigbluebuttonbn_get_recordings' => [ + 'classname' => 'mod_bigbluebuttonbn\external\get_recordings', + 'methodname' => 'execute', + 'description' => 'Returns a list of recordings ready to be processed by a datatable.', + 'type' => 'read', + 'ajax' => true, + 'capabilities' => 'mod/bigbluebuttonbn:view', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'mod_bigbluebuttonbn_get_recordings_to_import' => [ + 'classname' => 'mod_bigbluebuttonbn\external\get_recordings_to_import', + 'methodname' => 'execute', + 'description' => 'Returns a list of recordings ready to import to be processed by a datatable.', + 'type' => 'read', + 'ajax' => true, + 'capabilities' => 'mod/bigbluebuttonbn:importrecordings', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'mod_bigbluebuttonbn_update_recording' => [ + 'classname' => 'mod_bigbluebuttonbn\external\update_recording', + 'methodname' => 'execute', + 'description' => 'Update a single recording', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'mod/bigbluebuttonbn:managerecordings', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'mod_bigbluebuttonbn_end_meeting' => [ + 'classname' => 'mod_bigbluebuttonbn\external\end_meeting', + 'methodname' => 'execute', + 'description' => 'End a meeting', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'mod/bigbluebuttonbn:join', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'mod_bigbluebuttonbn_completion_validate' => [ + 'classname' => 'mod_bigbluebuttonbn\external\completion_validate', + 'methodname' => 'execute', + 'description' => 'Validate completion', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'mod/bigbluebuttonbn:view', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'mod_bigbluebuttonbn_meeting_info' => [ + 'classname' => 'mod_bigbluebuttonbn\external\meeting_info', + 'methodname' => 'execute', + 'description' => 'Get displayable information on the meeting', + 'type' => 'read', + 'ajax' => true, + 'capabilities' => 'mod/bigbluebuttonbn:view', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'mod_bigbluebuttonbn_get_bigbluebuttonbns_by_courses' => [ + 'classname' => 'mod_bigbluebuttonbn\external\get_bigbluebuttonbns_by_courses', + 'methodname' => 'execute', + 'description' => 'Returns a list of bigbluebuttonbns in a provided list of courses, if no list is provided + all bigbluebuttonbns that the user can view will be returned.', + 'type' => 'read', + 'capabilities' => 'mod/bigbluebuttonbn:view', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'mod_bigbluebuttonbn_view_bigbluebuttonbn' => [ + 'classname' => 'mod_bigbluebuttonbn\external\view_bigbluebuttonbn', + 'methodname' => 'execute', + 'description' => 'Trigger the course module viewed event and update the module completion status.', + 'type' => 'write', + 'capabilities' => 'mod/bigbluebuttonbn:view', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] + ], + 'mod_bigbluebuttonbn_get_join_url' => [ + 'classname' => 'mod_bigbluebuttonbn\external\get_join_url', + 'methodname' => 'execute', + 'description' => 'Get the join URL for the meeting and create if it does not exist.', + 'type' => 'write', + 'capabilities' => 'mod/bigbluebuttonbn:join', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], +]; diff --git a/mod/bigbluebuttonbn/db/tasks.php b/mod/bigbluebuttonbn/db/tasks.php new file mode 100644 index 00000000000..6faed032c85 --- /dev/null +++ b/mod/bigbluebuttonbn/db/tasks.php @@ -0,0 +1,37 @@ +. + +/** + * Definition of scheduled tasks for mod_bigbluebuttonbn. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = [ + [ + 'classname' => 'mod_bigbluebuttonbn\task\check_pending_recordings', + 'blocking' => 0, + 'minute' => '*/5', + 'hour' => '*', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*' + ], +]; diff --git a/mod/bigbluebuttonbn/db/upgrade.php b/mod/bigbluebuttonbn/db/upgrade.php new file mode 100644 index 00000000000..1481aa2287a --- /dev/null +++ b/mod/bigbluebuttonbn/db/upgrade.php @@ -0,0 +1,478 @@ +. + +/** + * Upgrade logic. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +use mod_bigbluebuttonbn\plugin; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Performs data migrations and updates on upgrade. + * + * @param int $oldversion + * @return bool + */ +function xmldb_bigbluebuttonbn_upgrade($oldversion = 0) { + global $DB; + $dbman = $DB->get_manager(); + if ($oldversion < 2015080605) { + // Drop field description. + $table5 = new xmldb_table('bigbluebuttonbn'); + $field4 = new xmldb_field('description'); + if ($dbman->field_exists($table5, $field4)) { + $dbman->drop_field($table5, $field4, true, true); + } + // Change welcome, allow null. + $fielddefinition = ['type' => XMLDB_TYPE_TEXT, 'precision' => null, 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => null, 'previous' => 'type']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'welcome', + $fielddefinition); + // Change userid definition in bigbluebuttonbn_log. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '10', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => null, + 'previous' => 'bigbluebuttonbnid']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn_log', 'userid', + $fielddefinition); + // No settings to migrate. + // Update db version tag. + upgrade_mod_savepoint(true, 2015080605, 'bigbluebuttonbn'); + } + if ($oldversion < 2016011305) { + // Define field type to be droped from bigbluebuttonbn. + $table4 = new xmldb_table('bigbluebuttonbn'); + $field3 = new xmldb_field('type'); + if ($dbman->field_exists($table4, $field3)) { + $dbman->drop_field($table4, $field3, true, true); + } + // Rename table bigbluebuttonbn_log to bigbluebuttonbn_logs. + $table = new xmldb_table('bigbluebuttonbn_log'); + if ($dbman->table_exists($table)) { + $dbman->rename_table($table, 'bigbluebuttonbn_logs', true, true); + } + // Rename field event to log in table bigbluebuttonbn_logs. + $table1 = new xmldb_table('bigbluebuttonbn_logs'); + $field = new xmldb_field('event'); + if ($dbman->field_exists($table1, $field)) { + $dbman->rename_field($table1, $field, 'log', true, true); + } + // No settings to migrate. + // Update db version tag. + upgrade_mod_savepoint(true, 2016011305, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101000) { + // Drop field newwindow. + $table3 = new xmldb_table('bigbluebuttonbn'); + $field2 = new xmldb_field('newwindow'); + if ($dbman->field_exists($table3, $field2)) { + $dbman->drop_field($table3, $field2, true, true); + } + // Add field type. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '2', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'id']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'type', + $fielddefinition); + // Add field recordings_html. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_html', + $fielddefinition); + // Add field recordings_deleted. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 1, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_deleted', + $fielddefinition); + // Add field recordings_imported. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_imported', + $fielddefinition); + // Drop field newwindow. + $table2 = new xmldb_table('bigbluebuttonbn'); + $field1 = new xmldb_field('tagging'); + if ($dbman->field_exists($table2, $field1)) { + $dbman->drop_field($table2, $field1, true, true); + } + // Migrate settings. + unset_config('bigbluebuttonbn_recordingtagging_default', ''); + unset_config('bigbluebuttonbn_recordingtagging_editable', ''); + $cfgvalue = get_config('', 'bigbluebuttonbn_importrecordings_from_deleted_activities_enabled'); + set_config('bigbluebuttonbn_importrecordings_from_deleted_enabled', $cfgvalue, ''); + unset_config('bigbluebuttonbn_importrecordings_from_deleted_activities_enabled', ''); + $cfgvalue = get_config('', 'bigbluebuttonbn_moderator_default'); + set_config('bigbluebuttonbn_participant_moderator_default', $cfgvalue, ''); + unset_config('bigbluebuttonbn_moderator_default', ''); + // Update db version tag. + upgrade_mod_savepoint(true, 2017101000, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101009) { + // Add field recordings_preview. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_preview', + $fielddefinition); + // Update db version tag. + upgrade_mod_savepoint(true, 2017101009, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101010) { + // Fix for CONTRIB-7221. + if ($oldversion == 2017101003) { + // A bug intorduced in 2017101003 causes new instances to be created without BBB passwords. + // A workaround was put in place in version 2017101004 that was relabeled to 2017101005. + // However, as the code was relocated to upgrade.php in version 2017101010, a new issue came up. + // There is now a timeout error when the plugin is upgraded in large Moodle sites. + // The script should only be considered when migrating from this version. + $sql = "SELECT * FROM {bigbluebuttonbn} "; + $sql .= "WHERE moderatorpass = ? OR viewerpass = ?"; + $instances = $DB->get_records_sql($sql, ['', '']); + foreach ($instances as $instance) { + $instance->moderatorpass = plugin::random_password(12); + $instance->viewerpass = plugin::random_password(12, $instance->moderatorpass); + // Store passwords in the database. + $DB->update_record('bigbluebuttonbn', $instance); + } + } + // Update db version tag. + upgrade_mod_savepoint(true, 2017101010, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101012) { + // Update field type (Fix for CONTRIB-7302). + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '2', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'id']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'type', + $fielddefinition); + // Update field meetingid (Fix for CONTRIB-7302). + $fielddefinition = ['type' => XMLDB_TYPE_CHAR, 'precision' => '255', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => null, 'previous' => 'introformat']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'meetingid', + $fielddefinition); + // Update field recordings_imported (Fix for CONTRIB-7302). + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_imported', + $fielddefinition); + // Add field recordings_preview.(Fix for CONTRIB-7302). + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_preview', + $fielddefinition); + // Update db version tag. + upgrade_mod_savepoint(true, 2017101012, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101015) { + // Add field for client technology choice. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'clienttype', + $fielddefinition); + // Update db version tag. + upgrade_mod_savepoint(true, 2017101015, 'bigbluebuttonbn'); + } + if ($oldversion < 2019042000) { + // Add field for Mute on start feature. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'muteonstart', + $fielddefinition); + // Add field for record all from start. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordallfromstart', + $fielddefinition); + // Add field for record hide button. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordhidebutton', + $fielddefinition); + // Update db version tag. + upgrade_mod_savepoint(true, 2019042000, 'bigbluebuttonbn'); + } + if ($oldversion < 2019101001) { + // Add field for Completion with attendance. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionattendance', + $fielddefinition); + // Add field for Completion with engagement through chats. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementchats', + $fielddefinition); + // Add field for Completion with engagement through talks. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementtalks', + $fielddefinition); + // Add field for Completion with engagement through raisehand. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementraisehand', + $fielddefinition); + // Add field for Completion with engagement through pollvotes. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementpollvotes', + $fielddefinition); + // Add field for Completion with engagement through emojis. + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null]; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementemojis', + $fielddefinition); + // Add index to bigbluebuttonbn_logs (Fix for CONTRIB-8157). + xmldb_bigbluebuttonbn_index_table($dbman, 'bigbluebuttonbn_logs', 'courseid', + ['courseid']); + xmldb_bigbluebuttonbn_index_table($dbman, 'bigbluebuttonbn_logs', 'log', + ['log']); + xmldb_bigbluebuttonbn_index_table($dbman, 'bigbluebuttonbn_logs', 'logrow', + ['courseid', 'bigbluebuttonbnid', 'userid', 'log']); + // Update db version tag. + upgrade_mod_savepoint(true, 2019101001, 'bigbluebuttonbn'); + } + + if ($oldversion < 2019101002) { + + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'muteonstart']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disablecam', + $fielddefinition); + + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disablecam']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disablemic', + $fielddefinition); + + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disablemic']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disableprivatechat', + $fielddefinition); + + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disableprivatechat']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disablepublicchat', + $fielddefinition); + + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disablepublicchat']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disablenote', + $fielddefinition); + + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disablenote']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'hideuserlist', + $fielddefinition); + + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'hideuserlist']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'lockedlayout', + $fielddefinition); + + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'lockedlayout']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'lockonjoin', + $fielddefinition); + + $fielddefinition = ['type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'lockonjoin']; + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'lockonjoinconfigurable', + $fielddefinition); + + // Bigbluebuttonbn savepoint reached. + upgrade_mod_savepoint(true, 2019101002, 'bigbluebuttonbn'); + } + + if ($oldversion < 2019101004) { + // Add index to bigbluebuttonbn_logs (Leftover for CONTRIB-8157). + xmldb_bigbluebuttonbn_index_table($dbman, 'bigbluebuttonbn_logs', 'userlog', + ['userid', 'log']); + // Bigbluebuttonbn savepoint reached. + upgrade_mod_savepoint(true, 2019101004, 'bigbluebuttonbn'); + } + + if ($oldversion < 2021072905) { + // Add table bigbluebuttonbn_recordings (CONTRIB-7994). + // Define table bigbluebuttonbn_recordings to be created. + $table = new xmldb_table('bigbluebuttonbn_recordings'); + + // Adding fields to table bigbluebuttonbn_recordings. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('bigbluebuttonbnid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('groupid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('recordingid', XMLDB_TYPE_CHAR, '64', null, XMLDB_NOTNULL, null, null); + $table->add_field('headless', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('imported', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('state', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('recording', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table bigbluebuttonbn_recordings. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('fk_bigbluebuttonbnid', XMLDB_KEY_FOREIGN, ['bigbluebuttonbnid'], 'bigbluebuttonbn', ['id']); + $table->add_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']); + + // Adding indexes to table bigbluebuttonbn_recordings. + $table->add_index('courseid', XMLDB_INDEX_NOTUNIQUE, ['courseid']); + $table->add_index('recordingid', XMLDB_INDEX_NOTUNIQUE, ['recordingid']); + + // Conditionally launch create table for bigbluebuttonbn_recordings. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + // Bigbluebuttonbn savepoint reached. + upgrade_mod_savepoint(true, 2021072905, 'bigbluebuttonbn'); + } + if ($oldversion < 2021072906) { + + // Rename field recording on table bigbluebuttonbn_recordings to remotedata, add new remotedatatstamp and status. + $table = new xmldb_table('bigbluebuttonbn_recordings'); + + $field = new xmldb_field('recording', XMLDB_TYPE_TEXT, null, null, null, null, null, 'state'); + // Launch rename field recording to remotedata. + $dbman->rename_field($table, $field, 'remotedata'); + + $field = new xmldb_field('remotedatatstamp', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'remotedata'); + // Conditionally launch add field remotedatatstamp. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // State is already used on remote bigbluebutton entity and has not the same semantic. + $field = new xmldb_field('state', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'imported'); + // Launch rename field state to status. + $dbman->rename_field($table, $field, 'status'); + + // Bigbluebuttonbn savepoint reached. + upgrade_mod_savepoint(true, 2021072906, 'bigbluebuttonbn'); + } + + if ($oldversion < 2021072907) { + // Define field id to be dropped from bigbluebuttonbn_recordings. + $table = new xmldb_table('bigbluebuttonbn_recordings'); + $remotedatatstamp = new xmldb_field('remotedatatstamp'); + $remotedata = new xmldb_field('remotedata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'status'); + // Conditionally launch drop field remotedatatstamp. + if ($dbman->field_exists($table, $remotedatatstamp)) { + $dbman->drop_field($table, $remotedatatstamp); + } + // Launch rename field importeddata. + $dbman->rename_field($table, $remotedata, 'importeddata'); + + // Bigbluebuttonbn savepoint reached. + upgrade_mod_savepoint(true, 2021072907, 'bigbluebuttonbn'); + } + + if ($oldversion < 2021083100) { + // Update the legacy notifications to use the legacy class which will be removed as per the deprecation policy. + $DB->set_field('task_adhoc', 'classname', '\mod_bigbluebuttonbn\task\send_legacy_notification', [ + 'component' => 'mod_bigbluebuttonbn', + 'classname' => '\mod_bigbluebuttonbn\task\send_notification', + ]); + + // Bigbluebuttonbn savepoint reached. + upgrade_mod_savepoint(true, 2021083100, 'bigbluebuttonbn'); + } + + if ($oldversion < 2021083101) { + // Create adhoc task for upgrading of existing bigbluebuttonbn_logs related to recordings. + $task = new \stdClass(); + $task->classname = '\mod_bigbluebuttonbn\task\upgrade_recordings'; + $task->component = 'mod_bigbluebuttonbn'; + + // Next run time based from nextruntime computation in \core\task\manager::queue_adhoc_task(). + $nextruntime = time() - 1; + $task->nextruntime = $nextruntime; + $DB->insert_record('task_adhoc', $task); + + // Create adhoc task for upgrading of existing bigbluebuttonbn_logs related to imported recordings. + $task = new \stdClass(); + $task->classname = '\mod_bigbluebuttonbn\task\upgrade_imported_recordings'; + $task->component = 'mod_bigbluebuttonbn'; + + // Next run time based from nextruntime computation in \core\task\manager::queue_adhoc_task(). + $nextruntime = time() - 1; + $task->nextruntime = $nextruntime; + $DB->insert_record('task_adhoc', $task); + + // Bigbluebuttonbn savepoint reached. + upgrade_mod_savepoint(true, 2021083101, 'bigbluebuttonbn'); + } + + return true; +} + +/** + * Generic helper function for adding or changing a field in a table. + * + * @param database_manager $dbman + * @param string $tablename + * @param string $fieldname + * @param array $fielddefinition + * @deprecated please do not use this anymore (historical migrations) + */ +function xmldb_bigbluebuttonbn_add_change_field(database_manager $dbman, string $tablename, string $fieldname, + array $fielddefinition) { + $table = new xmldb_table($tablename); + $field = new xmldb_field($fieldname); + $field->set_attributes($fielddefinition['type'], $fielddefinition['precision'], $fielddefinition['unsigned'], + $fielddefinition['notnull'], $fielddefinition['sequence'], $fielddefinition['default'], + $fielddefinition['previous']); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field, true, true); + return; + } + // Drop key before if needed. + $fieldkey = new xmldb_key($fieldname, XMLDB_KEY_FOREIGN, [$fieldname], 'user', ['id']); + if ($dbman->find_key_name($table, $fieldkey)) { + $dbman->drop_key($table, $fieldkey); + } + $dbman->change_field_type($table, $field, true, true); + $dbman->change_field_precision($table, $field, true, true); + $dbman->change_field_notnull($table, $field, true, true); + $dbman->change_field_default($table, $field, true, true); +} + +/** + * Generic helper function for adding index to a table. + * + * @param database_manager $dbman + * @param string $tablename + * @param string $indexname + * @param array $indexfields + * @param string|false|null $indextype + * @deprecated please do not use this anymore (historical migrations) + */ +function xmldb_bigbluebuttonbn_index_table(database_manager $dbman, string $tablename, string $indexname, array $indexfields, + $indextype = XMLDB_INDEX_NOTUNIQUE) { + $table = new xmldb_table($tablename); + if (!$dbman->table_exists($table)) { + return; + } + $index = new xmldb_index($indexname, $indextype, $indexfields); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + $dbman->add_index($table, $index, true, true); +} diff --git a/mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg b/mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg new file mode 100644 index 00000000000..846eedb42a1 --- /dev/null +++ b/mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg @@ -0,0 +1,11 @@ + + + +Generated by Fontastic.me + + + + + + + diff --git a/mod/bigbluebuttonbn/fonts/bigbluebutton-font.ttf b/mod/bigbluebuttonbn/fonts/bigbluebutton-font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c291be5bac36707ddeb5e20a39f979c0eb176ebb GIT binary patch literal 1544 zcmds1&r4KM6#nj;8PgndX3`lYe>}{_LT8+LshMdg>KI}qB$7nMjJ_F-`Gt;P(V|6* z7J;q=m$hoyqD9M$79m0q1TIA852zr}t%aa3u+-5`&zs>n=gY4@iFYYI^CYYu&Bn1cv|HyBnH46p(|$wS z5l_vA(MtpiIh;;}SJJqFcJ@nFPlglbyK_V9?0*hu)^utqGc`W+mib?>zZY_Dlu!NN z_a0RDAD~hr0qhKIJuYcG>|$T+(=JD$J3l7R_zMKFjC8zN^NbZ2)fnk0^4y?u6B@)C zY$z8KB1&#d=>yc`8LNe@LU~d-jvywZqjMO{Ze%w^HkXB8?B`^4s)AyPQd8u*aJ}== zpnOJXN$&>8`oR5@)zUe;!QEji=;nvTm<-NkFHVt#=R-@+QY&_ri2oYM)1`533n$|g z+5lIL+vw@?ww$dgYZoov0bzLDjiTn7Fa|_dyYRMoyDyWfwXbfiu5RsiG|e=3)H|Hc z8HdBw;`W3aJ#JyjuGPa2O&uLgdmfLz=5&rYHAi(_ZCxWX+Oe)SLBIvC!WwsPSYZn~ z@mgUkeGV12p;BB@xC}0FSDpZC1({aHEnto3`>Mhg+~++i@K*ZlC~SjUI2A5KtLR1q z3yAR_$1+UnWn}oJkc1D>yv_iTg;*rMY(|zdnN-pjO(n^dItyk<`AWUWh$eIx@FPIo zM>$EJf~UbTKE6r@{}Kn~3lU<@P--z|>V`j{_vwFDMA8WOf`-os1UcX&`TjbuX_mf- zB`%9x1fA*SBrt6*E-j>zdeBefpM%SrchtlYRLDxrn=G`d-zF$o-fTWNL}w^<6nTOL X_maEJ6I8s>`Iy@SuRcqMn(@Ob5u924V#UeFhf)V0|N?mn3J56 zkeHZ|k|N@F%E$kdrmwG-uI9<}mwiv4*9HnFC8QMmQ9S&`dD_o*7H@w3WOM#vbN+4_ z@#d5b7yiG?vuJLw{q;p&*4`>2RYL5;X#)d}69zhBQxYFcNHh^nN??;be0;%ygq9@s zWvvl&4jedejdY5+|EL zLQ+DDlbAt2BLhn)g9%tJEhz!$vr`8SzBoMN*ntBN_?GTB@-W(BU|?Xlp~&b!BSX_k zfeFuM9eVH~;aT&{9{zv!2^W(8eEQEFFhg*i10#ndqyG=Sbr5?J64DwHB-z~91PT~G zNgS*ZW8+mg%eF*Pa)yM2+?hZB<(DuxolbYS%FL)N&D><{#K<7NfWsc{Mutao+BfDu z`kA}WsKD5;;Q;UP;Hw3*+8f(Haz7SVnBL5&!^_|-&dm$+)1N~J4lFo+gn`MyOqp3L zfr-J?g-MLm_ebW;5*t~(_PlVrR{v|x9LFM=sViph zc;tD?dvflp*%{6+-yZu{JCUWiJhb3o_@shw`^%EvTY6QQMP|;oKdATZxLN5ot-n8y Y)-!knL?wgbI?aKthbIFV2z?B!06%n%9smFU literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/import_view.php b/mod/bigbluebuttonbn/import_view.php new file mode 100644 index 00000000000..cde4a0393fd --- /dev/null +++ b/mod/bigbluebuttonbn/import_view.php @@ -0,0 +1,66 @@ +. + +/** + * View for importing BigBlueButtonBN recordings. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +use core\notification; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\output\import_view; +use mod_bigbluebuttonbn\plugin; + +require(__DIR__ . '/../../config.php'); + +$destbn = required_param('destbn', PARAM_INT); +$sourcebn = optional_param('sourcebn', -1, PARAM_INT); +$sourcecourseid = optional_param('sourcecourseid', -1, PARAM_INT); + +$destinationinstance = instance::get_from_instanceid($destbn); +if (!$destinationinstance) { + throw new moodle_exception('view_error_url_missing_parameters', plugin::COMPONENT); +} + +$cm = $destinationinstance->get_cm(); +$course = $destinationinstance->get_course(); + +require_login($course, true, $cm); + +if (!(boolean) \mod_bigbluebuttonbn\local\config::importrecordings_enabled()) { + notification::add( + get_string('view_message_importrecordings_disabled', plugin::COMPONENT), + notification::ERROR + ); + redirect($destinationinstance->get_view_url()); +} + +// Print the page header. +$PAGE->set_url($destinationinstance->get_import_url()); +$PAGE->set_title($destinationinstance->get_meeting_name()); +$PAGE->set_cacheable(false); +$PAGE->set_heading($course->fullname); + +/** @var \mod_bigbluebuttonbn\renderer $renderer */ +$renderer = $PAGE->get_renderer(plugin::COMPONENT); + +echo $OUTPUT->header(); +echo $renderer->render(new import_view($destinationinstance, $sourcecourseid, $sourcebn)); +echo $OUTPUT->footer(); diff --git a/mod/bigbluebuttonbn/index.php b/mod/bigbluebuttonbn/index.php new file mode 100644 index 00000000000..3055bc30daf --- /dev/null +++ b/mod/bigbluebuttonbn/index.php @@ -0,0 +1,59 @@ +. + +/** + * View all BigBlueButton instances in this course. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +use core\notification; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\output\index; +use mod_bigbluebuttonbn\plugin; + +require(__DIR__.'/../../config.php'); +global $PAGE, $OUTPUT; +$id = required_param('id', PARAM_INT); +$course = get_course($id); +require_login($course, true); + +$PAGE->set_url('/mod/bigbluebuttonbn/index.php', ['id' => $id]); +$PAGE->set_title(get_string('modulename', plugin::COMPONENT)); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(false); +$PAGE->set_pagelayout('incourse'); + +$PAGE->navbar->add($PAGE->title, $PAGE->url); + +$instances = instance::get_all_instances_in_course($course->id); +if (empty($instances)) { + notification::add( + get_string('index_error_noinstances', plugin::COMPONENT), + notification::ERROR + ); + redirect(new moodle_url('/course/view.php', ['id' => $course->id])); +} + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('index_heading', plugin::COMPONENT)); +$renderer = $PAGE->get_renderer(plugin::COMPONENT); +echo $renderer->render(new index($course, $instances)); +echo $OUTPUT->footer(); diff --git a/mod/bigbluebuttonbn/lang/en/bigbluebuttonbn.php b/mod/bigbluebuttonbn/lang/en/bigbluebuttonbn.php new file mode 100644 index 00000000000..7289e77607e --- /dev/null +++ b/mod/bigbluebuttonbn/lang/en/bigbluebuttonbn.php @@ -0,0 +1,616 @@ +. + +/** + * Language File. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ +defined('MOODLE_INTERNAL') || die(); + +$string['activityoverview'] = 'You have upcoming bigbluebuttonbn sessions'; +$string['bbbduetimeoverstartingtime'] = 'The due time for this activity must be greater than the starting time'; +$string['bbbdurationwarning'] = 'The maximum duration for this session is %duration% minutes.'; +$string['bbbrecordwarning'] = 'This session may be recorded.'; +$string['bbbrecordallfromstartwarning'] = 'This session is being recorded from start.'; +$string['bigbluebuttonbn:addinstance'] = 'Add a new bigbluebuttonbn room/activity'; +$string['bigbluebuttonbn:join'] = 'Join a bigbluebuttonbn meeting'; +$string['bigbluebuttonbn:view'] = 'View a room/activity'; +$string['bigbluebuttonbn:addinstancewithmeeting'] = 'Create instances with live meeting capabilities'; +$string['bigbluebuttonbn:addinstancewithrecording'] = 'Create instances with recording capabilities'; +$string['bigbluebuttonbn:managerecordings'] = 'Manage recordings'; +$string['bigbluebuttonbn:publishrecordings'] = 'Publish recordings'; +$string['bigbluebuttonbn:unpublishrecordings'] = 'Unpublish recordings'; +$string['bigbluebuttonbn:protectrecordings'] = 'Protect recordings'; +$string['bigbluebuttonbn:unprotectrecordings'] = 'Unprotect recordings'; +$string['bigbluebuttonbn:deleterecordings'] = 'Delete recordings'; +$string['bigbluebuttonbn:importrecordings'] = 'Import recordings'; +$string['bigbluebuttonbn'] = 'BigBlueButton'; +$string['cannotperformaction'] = 'Cannot perform action {$a} on this recording'; +$string['indicator:cognitivedepth'] = 'BigBlueButton cognitive'; +$string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in a BigBlueButton activity.'; +$string['indicator:socialbreadth'] = 'BigBlueButton social'; +$string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in a BigBlueButton activity.'; +$string['modulename'] = 'BigBlueButton'; +$string['modulenameplural'] = 'BigBlueButton'; +$string['modulename_help'] = 'BigBlueButton lets you create from within Moodle links to real-time on-line classrooms using BigBlueButton, an open source web conferencing system for distance education. + +Using BigBlueButton you can specify for the title, description, calendar entry (which gives a date range for joining the session), groups, and details about the recording of the on-line session.'; +$string['modulename_link'] = 'BigBlueButton/view'; +$string['nosuchinstance'] = 'No such an instance {$a->entity} with id: {$a->id} '; +$string['pluginadministration'] = 'BigBlueButton administration'; +$string['pluginname'] = 'BigBlueButton'; + +$string['removedevents'] = 'Deleted events'; +$string['removedtags'] = 'Deleted tags'; +$string['removedlogs'] = 'Deleted custom logs'; +$string['removedrecordings'] = 'Deleted recordings'; +$string['resetevents'] = 'Delete events'; +$string['resettags'] = 'Delete tags'; +$string['resetlogs'] = 'Delete custom logs'; +$string['resetrecordings'] = 'Delete recordings'; +$string['resetlogs_help'] = 'Deleting the logs will cause the lost of references to recordings'; +$string['resetrecordings_help'] = 'Deleting the recordings will make them inaccessible from anywhere and it can not be undone'; + +$string['search:activity'] = 'BigBlueButton - activity information'; +$string['search:tags'] = 'BigBlueButton - tags information'; +$string['settings'] = 'BigBlueButton settings'; +$string['privacy:metadata:bigbluebuttonbn'] = 'Stores the configuration for the room or activity that defines the features and general behaviour of the BigBlueButton session.'; +$string['privacy:metadata:bigbluebuttonbn:participants'] = 'A list of rules that define the role users will in the live meeting. A user ID may be stored as permissions can be granted per role or user.'; +$string['privacy:metadata:bigbluebuttonbn_logs'] = 'Stores events triggered when using the plugin.'; +$string['privacy:metadata:bigbluebuttonbn_logs:userid'] = 'The user ID of the user who triggered the event.'; +$string['privacy:metadata:bigbluebuttonbn_logs:timecreated'] = 'The time at which the log was created.'; +$string['privacy:metadata:bigbluebuttonbn_logs:meetingid'] = 'The meeting ID the user had access to.'; +$string['privacy:metadata:bigbluebuttonbn_logs:log'] = 'The type of event triggered by the user.'; +$string['privacy:metadata:bigbluebuttonbn_logs:meta'] = 'May include extra information related to the meeting or the recording afected by the event.'; +$string['privacy:metadata:bigbluebutton'] = 'In order to create and join BigBlueButton sessions, user data needs to be exchanged with the server.'; +$string['privacy:metadata:bigbluebutton:userid'] = 'The userid of the user accessing the BigBlueButton server.'; +$string['privacy:metadata:bigbluebutton:fullname'] = 'The fullname of the user accessing the BigBlueButton server.'; +$string['privacy:metadata:bigbluebuttonbn_recordings'] = 'Stores metadata about recordings.'; +$string['privacy:metadata:bigbluebuttonbn_recordings:userid'] = 'The user ID of the user who last changed a recording.'; + +$string['completionattendance'] = 'Student must attend the meeting for:'; +$string['completionattendance_desc'] = 'Student must attend the meeting and remain in the session for at least {$a} minute(s)'; +$string['completionattendance_event_desc'] = 'Student has attended the meeting or remained in the session for at least {$a} minute(s)'; +$string['completionattendancegroup'] = 'Require attendance'; +$string['completionattendancegroup_help'] = 'Attending the meeting for (n) minutes is required for completion'; + +$string['completionengagementchats'] = 'Require chats events'; +$string['completionengagementchats_desc'] = 'Student must take participate into {$a} chat(s) events to complete it'; +$string['completionengagementchats_event_desc'] = 'Has raised {$a} Chat(s) event(s)'; +$string['completionengagementtalks'] = 'Talk events'; +$string['completionengagementtalks_desc'] = 'Student must talk {$a} time(s) to complete it'; +$string['completionengagementtalks_event_desc'] = 'Has raised {$a} Talk(s) event(s)'; +$string['completionengagementraisehand'] = 'Require raise hand events'; +$string['completionengagementraisehand_desc'] = 'Student must raise hand {$a} time(s) to complete it'; +$string['completionengagementraisehand_event_desc'] = 'Has raised {$a} Raise Hand event(s)'; +$string['completionengagementpollvotes'] = 'Poll votes'; +$string['completionengagementpollvotes_desc'] = 'Student must vote into polls {$a} time(s) to complete it'; +$string['completionengagementpollvotes_event_desc'] = 'Has raised {$a} Poll vote(s) events'; +$string['completionengagementemojis'] = 'Emojis'; +$string['completionengagementemojis_desc'] = 'Student must send {$a} emoji(s) into polls to complete it'; +$string['completionengagementemojis_event_desc'] = 'Has raised {$a} Emoji(s) event(s)'; + +$string['completionengagement_desc'] = 'Student must engage in activities during the meeting'; +$string['completionengagementgroup'] = 'Require engagement'; +$string['completionengagementgroup_help'] = 'Active participation during the session is required for completion'; + +$string['completionupdatestate'] = 'Completion update state'; +$string['completionvalidatestate'] = 'Validate completion'; +$string['completionvalidatestatetriggered'] = 'Validate completion has been triggered.'; + +$string['completionview'] = 'Require view'; +$string['completionview_desc'] = 'Student must join a meeting or play a recording to complete it.'; +$string['completionview_event_desc'] = 'Has joined the meeting or played a recording {$a} time(s).'; +$string['sendnotification'] = 'Send notification'; + +$string['minute'] = 'minute'; +$string['minutes'] = 'minutes'; + +$string['config_general'] = 'General configuration'; +$string['config_general_description'] = 'These settings are always used'; +$string['config_server_url'] = 'BigBlueButton Server URL'; +$string['config_server_url_description'] = 'The URL of your BigBlueButton server must end with /bigbluebutton/. (This default URL is for a BigBlueButton server provided by Blindside Networks that you can use for testing.)'; +$string['config_shared_secret'] = 'BigBlueButton Shared Secret'; +$string['config_shared_secret_description'] = 'The security salt of your BigBlueButton server. (This default salt is for a BigBlueButton server provided by Blindside Networks that you can use for testing.)'; + +$string['config_recording'] = 'Configuration for "Record meeting" feature'; +$string['config_recording_description'] = 'These settings are feature specific'; +$string['config_recording_default'] = 'Recording feature enabled by default'; +$string['config_recording_default_description'] = 'If enabled the sessions created in BigBlueButton will have recording capabilities.'; +$string['config_recording_editable'] = 'Recording feature can be edited'; +$string['config_recording_editable_description'] = 'If checked the interface includes an option for enable and disable the recording feature.'; +$string['config_recording_icons_enabled'] = 'Icons for recording management'; +$string['config_recording_icons_enabled_description'] = 'When enabled, the recording management panel shows icons for the publish/unpublish and delete actions.'; +$string['config_recording_all_from_start_default'] = 'Record all from start'; +$string['config_recording_all_from_start_default_description'] = 'If checked the meeting will record to start'; +$string['config_recording_all_from_start_editable'] = 'Record all from start can be edited'; +$string['config_recording_all_from_start_editable_description'] = 'If checked the interface includes an option for enable and disable the record all from start feature.'; +$string['config_recording_hide_button_default'] = 'Hide recording button'; +$string['config_recording_hide_button_default_description'] = 'If checked the button for record will be hide'; +$string['config_recording_hide_button_editable'] = 'Hide recording button can be edited'; +$string['config_recording_hide_button_editable_description'] = 'If checked the interface includes an option for enable and disable the hide recording button feature.'; +$string['config_recording_refresh_period'] = 'Recording refresh period (in seconds)'; +$string['config_recording_refresh_period_description'] = 'To avoid querying the Bigbluebutton server too often we cache information + for recording. This is the refresh period in seconds which will decide how often we can/would refresh remote information for a given recording. Defaults to 300s (5mins).'; +$string['config_recordings'] = 'Configuration for "Show recordings" feature'; +$string['config_recordings_description'] = 'These settings are feature specific'; +$string['config_recordings_general'] = 'Show recording settings'; +$string['config_recordings_general_description'] = 'These settings are used only when showing recordings'; +$string['config_recordings_html_default'] = 'UI as html is enabled by default'; +$string['config_recordings_html_default_description'] = 'If enabled the recording table is shown in plain HTML by default.'; +$string['config_recordings_html_editable'] = 'UI as html feature can be edited'; +$string['config_recordings_html_editable_description'] = 'UI as html value by default can be edited when the instance is added or updated.'; +$string['config_recordings_deleted_default'] = 'Include recordings from deleted activities enabled by default'; +$string['config_recordings_deleted_default_description'] = 'If enabled the recording table will include the recordings belonging to deleted activities if there is any.'; +$string['config_recordings_deleted_editable'] = 'Include recordings from deleted activities feature can be edited'; +$string['config_recordings_deleted_editable_description'] = 'Include recordings from deleted activities by default can be edited when the instance is added or updated.'; +$string['config_recordings_imported_default'] = 'Show only imported links enabled by default'; +$string['config_recordings_imported_default_description'] = 'If enabled the recording table will include only the imported links to recordings.'; +$string['config_recordings_imported_editable'] = 'Show only imported links feature can be edited'; +$string['config_recordings_imported_editable_description'] = 'Show only imported links by default can be edited when the instance is added or updated.'; +$string['config_recordings_preview_default'] = 'Preview is enabled by default'; +$string['config_recordings_preview_default_description'] = 'If enabled the table includes a preview of the presentation.'; +$string['config_recordings_preview_editable'] = 'Preview feature can be edited'; +$string['config_recordings_preview_editable_description'] = 'Preview feature can be edited when the instance is added or updated.'; +$string['config_recordings_sortorder'] = 'Order the recordings in ascending order.'; +$string['config_recordings_sortorder_description'] = 'By default recordings are displayed in descending order. When checked they will be sorted in ascending order.'; +$string['config_recordings_validate_url'] = 'Validate URL'; +$string['config_recordings_validate_url_description'] = 'If checked the playback URL will be validated before the user access it.'; + +$string['config_importrecordings'] = 'Configuration for "Import recordings" feature'; +$string['config_importrecordings_description'] = 'These settings are feature specific'; +$string['config_importrecordings_enabled'] = 'Import recordings enabled'; +$string['config_importrecordings_enabled_description'] = 'When this and the recording feature are enabled, it is possible to import recordings from different courses into an activity.'; +$string['config_importrecordings_from_deleted_enabled'] = 'Import recordings from deleted activities enabled'; +$string['config_importrecordings_from_deleted_enabled_description'] = 'When this and the import recording feature are enabled, it is possible to import recordings from activities that are no longer in the course.'; + +$string['config_waitformoderator'] = 'Configuration for "Wait for moderator" feature'; +$string['config_waitformoderator_description'] = 'These settings are feature specific'; +$string['config_waitformoderator_default'] = 'Wait for moderator enabled by default'; +$string['config_waitformoderator_default_description'] = 'Wait for moderator feature is enabled by default when a new room or conference is added.'; +$string['config_waitformoderator_editable'] = 'Wait for moderator feature can be edited'; +$string['config_waitformoderator_editable_description'] = 'Wait for moderator value by default can be edited when the room or conference is added or updated.'; +$string['config_waitformoderator_ping_interval'] = 'Wait for moderator ping (seconds)'; +$string['config_waitformoderator_ping_interval_description'] = 'When the wait for moderator feature is enabled, the client pings for the status of the session each [number] seconds. This parameter defines the interval for requests made to the Moodle server'; +$string['config_waitformoderator_cache_ttl'] = 'Wait for moderator cache TTL (seconds)'; +$string['config_waitformoderator_cache_ttl_description'] = 'To support a heavy load of clients this plugin makes use of a cache. This parameter defines the time the cache will be kept before the next request is sent to the BigBlueButton server.'; + +$string['config_voicebridge'] = 'Configuration for "Voice bridge" feature'; +$string['config_voicebridge_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; +$string['config_voicebridge_editable'] = 'Conference voice bridge can be edited'; +$string['config_voicebridge_editable_description'] = 'Conference voice bridge number can be permanently assigned to a room conference. When assigned, the number can not be used by any other room or conference'; + +$string['config_preuploadpresentation'] = 'Configuration for "Pre-upload presentation" feature'; +$string['config_preuploadpresentation_description'] = 'These settings enable or disable options in the UI and also define default values for these options. The feature works only if the Moodle server is accessible to BigBlueButton..'; +$string['config_preuploadpresentation_enabled'] = 'Pre-uploading presentation enabled'; +$string['config_preuploadpresentation_enabled_description'] = 'Preupload presentation feature is enabled in the UI when the room or conference is added or updated.'; + +$string['config_presentation_default'] = 'Default file for "Pre-upload presentation" feature'; +$string['config_presentation_default_description'] = 'This setting allow to select a file to use as default in all BBB instances if "Pre-upload presentation" is enabled.'; + +$string['config_participant'] = 'Participant configuration'; +$string['config_participant_description'] = 'These settings define the role by default for participants in a conference.'; +$string['config_participant_moderator_default'] = 'Moderator by default'; +$string['config_participant_moderator_default_description'] = 'This rule is used by default when a new room is added.'; + +$string['config_userlimit'] = 'Configuration for "User limit" feature'; +$string['config_userlimit_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; +$string['config_userlimit_default'] = 'User limit enabled by default'; +$string['config_userlimit_default_description'] = 'The number of users allowed in a session by default when a new room or conference is added. If the number is set to 0, no limit is established'; +$string['config_userlimit_editable'] = 'User limit feature can be edited'; +$string['config_userlimit_editable_description'] = 'User limit value by default can be edited when the room or conference is added or updated.'; + +$string['config_scheduled'] = 'Configuration for "Scheduled sessions"'; +$string['config_scheduled_description'] = 'These settings define some of the behaviour by default for scheduled sessions.'; +$string['config_scheduled_pre_opening'] = 'Accessible before opening time (minutes)'; +$string['config_scheduled_pre_opening_description'] = 'The time in minutes for the session to be acceessible before the schedules opening time is due.'; + +$string['config_sendnotifications'] = 'Configuration for "Send notifications" feature'; +$string['config_sendnotifications_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; +$string['config_sendnotifications_enabled'] = 'Send notifications enabled'; +$string['config_sendnotifications_enabled_description'] = 'If enabled the UI for editing the activity includes an option for sending a notification to enrolled user when the activity is added or updated.'; + +$string['config_extended_capabilities'] = 'Configuration for extended capabilities'; +$string['config_extended_capabilities_description'] = 'Configuration for extended capabilities when the BigBlueButton server offers them.'; +$string['config_uidelegation_enabled'] = 'UI delegation is enabled'; +$string['config_uidelegation_enabled_description'] = 'These settings enable or disable the UI delegation to the BigBlueButton server.'; +$string['config_recordingready_enabled'] = 'Send notifications when a recording is ready'; +$string['config_recordingready_enabled_description'] = 'Enable the plugin for sending notifications when the recording is ready. (It will only work if the script post_publish_recording_ready_callback is enabled in the BigBlueButton server)'; +$string['config_meetingevents_enabled'] = 'Register live events'; +$string['config_meetingevents_enabled_description'] = 'Enable the plugin for accepting and processing live events after the session ends. (It must be enabled for "Activity completion" and will only work if the BigBlueButton server is capable of processing post_events scripts)'; + +$string['config_warning_curl_not_installed'] = 'This feature requires the CURL extension for php installed and enabled. The settings will be accessible only if this condition is fulfilled.'; +$string['config_warning_bigbluebuttonbn_cfg_deprecated'] = 'BigBlueButton is making use of config.php with a global variable that has been deprecated. Please convert the file as it will not be supported in future versions'; + +$string['config_muteonstart'] = 'Configuration for "Mute on Start" feature'; +$string['config_muteonstart_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; +$string['config_muteonstart_default'] = 'Mute on start enabled by default'; +$string['config_muteonstart_default_description'] = 'If enabled the session will be muted on start.'; +$string['config_muteonstart_editable'] = 'Mute on start can be edited'; +$string['config_muteonstart_editable_description'] = 'Mute on start by default can be edited when the instance is added or updated.'; +$string['config_welcome_default'] = 'Default welcome message'; +$string['config_welcome_default_description'] = 'Replaces the default message setted up for the BigBlueButton server. The message can includes keywords (%%CONFNAME%%, %%DIALNUM%%, %%CONFNUM%%) which will be substituted automatically, and also html tags like ... or '; +$string['config_default_messages'] = 'Default messages'; +$string['config_default_messages_description'] = 'Set message defaults for activities'; + +$string['config_locksettings'] = 'Configuration for locking settings'; +$string['config_locksettings_description'] = 'These setttings enable or disable options in the UI for locking settings, and also define default values for these options.'; + +$string['config_disablecam_default'] = 'Disable cam enabled by default'; +$string['config_disablecam_default_description'] = 'If enabled the webcams will be disabled.'; +$string['config_disablecam_editable'] = 'Disable cam can be edited'; +$string['config_disablecam_editable_description'] = 'Disable cam by default can be edited when the instance is added or updated.'; + +$string['config_disablemic_default'] = 'Disable mic enabled by default'; +$string['config_disablemic_default_description'] = 'If enabled the microphones will be disabled.'; +$string['config_disablemic_editable'] = 'Disable mic can be edited'; +$string['config_disablemic_editable_description'] = 'Disable mic by default can be edited when the instance is added or updated.'; + +$string['config_disableprivatechat_default'] = 'Disable private chat enabled by default'; +$string['config_disableprivatechat_default_description'] = 'If enabled the private chat will be disabled.'; +$string['config_disableprivatechat_editable'] = 'Disable private chat can be edited'; +$string['config_disableprivatechat_editable_description'] = 'Disable private chat by default can be edited when the instance is added or updated.'; + +$string['config_disablepublicchat_default'] = 'Disable public chat enabled by default'; +$string['config_disablepublicchat_default_description'] = 'If enabled the public chat will be disabled.'; +$string['config_disablepublicchat_editable'] = 'Disable public chat can be edited'; +$string['config_disablepublicchat_editable_description'] = 'Disable public chat by default can be edited when the instance is added or updated.'; + +$string['config_disablenote_default'] = 'Disable shared notes enabled by default'; +$string['config_disablenote_default_description'] = 'If enabled the shared notes will be disabled.'; +$string['config_disablenote_editable'] = 'Disable shared notes can be edited'; +$string['config_disablenote_editable_description'] = 'Disable shared notes by default can be edited when the instance is added or updated.'; + +$string['config_hideuserlist_default'] = 'Hide user list enabled by default'; +$string['config_hideuserlist_default_description'] = 'If enabled the session user list will be hidden.'; +$string['config_hideuserlist_editable'] = 'Hide user list can be edited'; +$string['config_hideuserlist_editable_description'] = 'Hide user list by default can be edited when the instance is added or updated.'; + +$string['config_lockedlayout_default'] = 'Locked layout enabled by default'; +$string['config_lockedlayout_default_description'] = 'If enabled the session layout will be locked.'; +$string['config_lockedlayout_editable'] = 'Locked layout can be edited'; +$string['config_lockedlayout_editable_description'] = 'Locked layout by default can be edited when the instance is added or updated.'; + +$string['config_lockonjoin_default'] = 'Ignore lock on join enabled by default'; +$string['config_lockonjoin_default_description'] = 'If enabled the lock settings will be ignored. Lock configuration must be enabled for this to apply.'; +$string['config_lockonjoin_editable'] = 'Ignore lock on join can be edited'; +$string['config_lockonjoin_editable_description'] = 'Ignore lock on join by default can be edited when the instance is added or updated.'; + +$string['config_lockonjoinconfigurable_default'] = 'Lock configuration enabled by default'; +$string['config_lockonjoinconfigurable_default_description'] = 'If enabled the session lock settings can be enabled or disabled from the above control.'; +$string['config_lockonjoinconfigurable_editable'] = 'Lock configuration can be edited'; +$string['config_lockonjoinconfigurable_editable_description'] = 'Lock configuration by default can be edited when the instance is added or updated.'; + +$string['config_experimental_features'] = 'Configuration for experimental features'; +$string['config_experimental_features_description'] = 'Configuration for experimental features.'; + +$string['general_error_unable_connect'] = 'Unable to connect. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running. +Details : {$a}'; +$string['general_error_no_answer'] = 'Empty response. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running.'; +$string['general_error_not_allowed_to_create_instances'] = 'User is not allowed to create any type of instances.'; +$string['general_error_not_found'] = 'Entity not found : {$a}.'; +$string['general_error_cannot_create_meeting'] = 'Cannot create meeting.'; +$string['general_error_cannot_get_recordings'] = 'Cannot get recordings.'; +$string['index_confirm_end'] = 'Do you wish to end the virtual class?'; +$string['index_disabled'] = 'disabled'; +$string['index_enabled'] = 'enabled'; +$string['index_ending'] = 'Ending the virtual classroom ... please wait'; +$string['index_error_checksum'] = 'A checksum error occurred. Make sure you entered the correct salt.'; +$string['index_error_forciblyended'] = 'Unable to join this meeting because it has been manually ended.'; +$string['index_error_unable_display'] = 'Unable to display the meetings. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running.'; +$string['index_heading_actions'] = 'Actions'; +$string['index_heading_group'] = 'Group'; +$string['index_heading_moderator'] = 'Moderators'; +$string['index_heading_name'] = 'Room'; +$string['index_heading_recording'] = 'Recording'; +$string['index_heading_users'] = 'Users'; +$string['index_heading_viewer'] = 'Viewers'; +$string['index_heading'] = 'BigBlueButton Rooms'; +$string['instanceprofilewithoutrecordings'] = 'This instance profile cannot display recordings'; +$string['mod_form_block_general'] = 'General settings'; +$string['mod_form_block_room'] = 'Activity/Room settings'; +$string['mod_form_block_recordings'] = 'View for recording'; +$string['mod_form_block_presentation'] = 'Presentation content'; +$string['mod_form_block_presentation_default'] = 'Presentation default content'; +$string['mod_form_block_participants'] = 'Role assigned during live session'; +$string['mod_form_block_schedule'] = 'Schedule for session'; +$string['mod_form_block_record'] = 'Record settings'; +$string['mod_form_field_openingtime'] = 'Join open'; +$string['mod_form_field_closingtime'] = 'Join closed'; +$string['mod_form_field_intro'] = 'Description'; +$string['mod_form_field_intro_help'] = 'A short description for the room or conference.'; +$string['mod_form_field_duration_help'] = 'Setting the duration for a meeting will establish the maximum time for a meeting to keep alive before the recording finish'; +$string['mod_form_field_duration'] = 'Duration'; +$string['mod_form_field_userlimit'] = 'User limit'; +$string['mod_form_field_userlimit_help'] = 'Maximum limit of users allowed in a meeting. If the limit is set to 0 the number of users will be unlimited.'; +$string['mod_form_field_name'] = 'Virtual classroom name'; +$string['mod_form_field_room_name'] = 'Room name'; +$string['mod_form_field_conference_name'] = 'Conference name'; +$string['mod_form_field_record'] = 'Session can be recorded'; +$string['mod_form_field_voicebridge'] = 'Voice bridge [####]'; +$string['mod_form_field_voicebridge_help'] = 'Voice conference number that participants enter to join the voice conference when using dial-in. A number between 1 and 9999 must be typed. If the value is 0 the static voicebridge number will be ignored and a random number will be generated by BigBlueButton. A number 7 will preced to the four digits typed'; +$string['mod_form_field_voicebridge_format_error'] = 'Format error. You should input a number between 1 and 9999.'; +$string['mod_form_field_voicebridge_notunique_error'] = 'Not a unique value. This number is being used by another room or conference.'; +$string['mod_form_field_wait'] = 'Wait for moderator'; +$string['mod_form_field_wait_help'] = 'Viewers must wait until a moderator enters the session before they can do so'; +$string['mod_form_field_welcome'] = 'Welcome message'; +$string['mod_form_field_welcome_help'] = 'Replaces the default message setted up for the BigBlueButton server. The message can includes keywords (%%CONFNAME%%, %%DIALNUM%%, %%CONFNUM%%) which will be substituted automatically, and also html tags like <b>...</b>, <br />, <u></u> or <i></i> '; +$string['mod_form_field_welcome_default'] = '
Welcome to %%CONFNAME%%!

For help on using BigBlueButton see these (short)
tutorial videos.

To join the audio bridge click the phone icon (top center). Please use a headset to avoid causing background noise for others.'; +$string['mod_form_field_participant_add'] = 'Add assignation'; +$string['mod_form_field_participant_list'] = 'Assignation list'; +$string['mod_form_field_participant_list_type_all'] = 'All users enrolled'; +$string['mod_form_field_participant_list_type_role'] = 'Role'; +$string['mod_form_field_participant_list_type_user'] = 'User'; +$string['mod_form_field_participant_list_type_owner'] = 'Owner'; +$string['mod_form_field_participant_list_text_as'] = 'joins session as'; +$string['mod_form_field_participant_list_action_add'] = 'Add'; +$string['mod_form_field_participant_list_action_remove'] = 'Remove'; +$string['mod_form_field_participant_bbb_role_moderator'] = 'Moderator'; +$string['mod_form_field_participant_bbb_role_viewer'] = 'Viewer'; +$string['mod_form_field_instanceprofiles'] = 'Instance type'; +$string['mod_form_field_instanceprofiles_help'] = 'Select the type for this BigBlueButton instance.'; +$string['mod_form_field_muteonstart'] = 'Mute on start'; +$string['mod_form_field_notification'] = 'Notify this change to users enrolled'; +$string['mod_form_field_notification_help'] = 'Send a notification to all users enrolled to let them know that this activity has been added or updated'; +$string['mod_form_field_notification_created_help'] = 'Send a notification to all users enrolled to let them know that this activity has been created'; +$string['mod_form_field_notification_modified_help'] = 'Send a notification to all users enrolled to let them know that this activity has been updated'; +$string['mod_form_field_notification_msg_at'] = 'at'; +$string['mod_form_field_recordings_html'] = 'Show the table in plain html'; +$string['mod_form_field_recordings_deleted'] = 'Include recordings from deleted activities'; +$string['mod_form_field_recordings_imported'] = 'Show only imported links'; +$string['mod_form_field_recordings_preview'] = 'Show recording preview'; +$string['mod_form_field_recordallfromstart'] = 'Record all from start'; +$string['mod_form_field_recordhidebutton'] = 'Hide recording button'; +$string['mod_form_field_nosettings'] = 'No settings can be edited'; +$string['mod_form_field_disablecam'] = 'Disable webcams'; +$string['mod_form_field_disablemic'] = 'Disable microphones'; +$string['mod_form_field_disableprivatechat'] = 'Disable private chat'; +$string['mod_form_field_disablepublicchat'] = 'Disable public chat'; +$string['mod_form_field_disablenote'] = 'Disable shared notes'; +$string['mod_form_field_hideuserlist'] = 'Hide user list'; +$string['mod_form_field_lockedlayout'] = 'Lock room layout'; +$string['mod_form_field_lockonjoin'] = 'Ignore lock settings'; +$string['mod_form_field_lockonjoinconfigurable'] = 'Allow ignore locking settings'; +$string['mod_form_locksettings'] = 'Lock settings'; + + +$string['starts_at'] = 'Starts'; +$string['started_at'] = 'Started'; +$string['ends_at'] = 'Ends'; +$string['calendarstarts'] = '{$a} is scheduled for'; +$string['recordings_from_deleted_activities'] = 'Recordings from deleted activities'; +$string['view_error_no_group_student'] = 'You have not been enrolled in a group. Please contact your Teacher or the Administrator.'; +$string['view_error_no_group_teacher'] = 'There are no groups configured yet. Please set up groups or contact the Administrator.'; +$string['view_error_no_group'] = 'There are no groups configured yet. Please set up groups before trying to join the meeting.'; +$string['view_error_unable_join_student'] = 'Unable to connect to the BigBlueButton server. Please contact your Teacher or the Administrator.'; +$string['view_error_unable_join_teacher'] = 'Unable to connect to the BigBlueButton server. Please contact the Administrator.'; +$string['view_error_unable_join'] = 'Unable to join the meeting. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running.'; +$string['view_error_bigbluebutton'] = 'BigBlueButton responded with errors. {$a}'; +$string['view_error_create'] = 'The BigBlueButton server responded with an error message, the meeting could not be created.'; +$string['view_error_max_concurrent'] = 'Number of concurrent meetings allowed has been reached.'; +$string['view_error_userlimit_reached'] = 'The number of users allowed in a meeting has been reached.'; +$string['view_error_url_missing_parameters'] = 'There are parameters missing in this URL'; +$string['view_error_import_no_courses'] = 'No courses to look up for recordings'; +$string['view_error_import_no_recordings'] = 'No recordings in this course for importing'; +$string['view_error_invalid_session'] = 'The session is expired. Go back to the activity main page.'; +$string['view_groups_selection_join'] = 'Join'; +$string['view_groups_selection'] = 'Select the group you want to join and confirm the action'; +$string['view_login_moderator'] = 'Logging in as moderator ...'; +$string['view_login_viewer'] = 'Logging in as viewer ...'; +$string['view_noguests'] = 'The BigBlueButton is not open to guests'; +$string['view_nojoin'] = 'You are not in a role allowed to join this session.'; +$string['view_recording_list_actionbar_edit'] = 'Edit'; +$string['view_recording_list_actionbar_delete'] = 'Delete'; +$string['view_recording_list_actionbar_import'] = 'Import'; +$string['view_recording_list_actionbar_hide'] = 'Hide'; +$string['view_recording_list_actionbar_show'] = 'Show'; +$string['view_recording_list_actionbar_publish'] = 'Publish'; +$string['view_recording_list_actionbar_unpublish'] = 'Unpublish'; +$string['view_recording_list_actionbar_protect'] = 'Make it private'; +$string['view_recording_list_actionbar_unprotect'] = 'Make it public'; +$string['view_recording_list_action_publish'] = 'Publishing'; +$string['view_recording_list_action_unpublish'] = 'Unpublishing'; +$string['view_recording_list_action_process'] = 'Processing'; +$string['view_recording_list_action_delete'] = 'Deleting'; +$string['view_recording_list_action_protect'] = 'Protecting'; +$string['view_recording_list_action_unprotect'] = 'Unprotecting'; +$string['view_recording_list_action_update'] = 'Updating'; +$string['view_recording_list_action_edit'] = 'Updating'; +$string['view_recording_list_action_play'] = 'Play'; +$string['view_recording_list_actionbar'] = 'Toolbar'; +$string['view_recording_list_activity'] = 'Activity'; +$string['view_recording_list_course'] = 'Course'; +$string['view_recording_list_date'] = 'Date'; +$string['view_recording_list_description'] = 'Description'; +$string['view_recording_list_duration'] = 'Duration'; +$string['view_recording_list_recording'] = 'Recording'; +$string['view_recording_button_import'] = 'Import recording links'; +$string['view_recording_button_return'] = 'Go back'; +$string['view_recording_format_notes'] = 'Notes'; +$string['view_recording_format_podcast'] = 'Podcast'; +$string['view_recording_format_presentation'] = 'Presentation'; +$string['view_recording_format_screenshare'] = 'Screenshare'; +$string['view_recording_format_statistics'] = 'Statistics'; +$string['view_recording_format_video'] = 'Video'; +$string['view_recording_format_errror_unreachable'] = 'The URL for this recording format is unreachable.'; +$string['view_section_title_presentation'] = 'Presentation file'; +$string['view_section_title_recordings'] = 'Recordings'; +$string['view_message_norecordings'] = 'There are no recording to show.'; +$string['view_message_finished'] = 'This activity is over.'; +$string['view_message_notavailableyet'] = 'This session is not yet available.'; +$string['view_recording_select_course'] = 'Select a course first in the drop down menu'; + + +$string['view_message_session_started_at'] = 'This session started at'; +$string['view_message_session_running_for'] = 'This session has been running for'; +$string['view_message_hour'] = 'hour'; +$string['view_message_hours'] = 'hours'; +$string['view_message_minute'] = 'minute'; +$string['view_message_minutes'] = 'minutes'; +$string['view_message_moderator'] = 'moderator'; +$string['view_message_moderators'] = 'moderators'; +$string['view_message_viewer'] = 'viewer'; +$string['view_message_viewers'] = 'viewers'; +$string['view_message_user'] = 'user'; +$string['view_message_users'] = 'users'; +$string['view_message_has_joined'] = 'has joined'; +$string['view_message_have_joined'] = 'have joined'; +$string['view_message_session_no_users'] = 'There are no users in this session'; +$string['view_message_session_has_user'] = 'There is'; +$string['view_message_session_has_users'] = 'There are'; +$string['view_message_session_for'] = 'the session for'; +$string['view_message_times'] = 'times'; +$string['view_message_and'] = 'and'; + +$string['view_message_room_closed'] = 'This room is closed.'; +$string['view_message_room_ready'] = 'This room is ready.'; +$string['view_message_room_open'] = 'This room is open.'; +$string['view_message_conference_room_ready'] = 'This conference room is ready. You can join the session now.'; +$string['view_message_conference_not_started'] = 'This conference has not started yet.'; +$string['view_message_conference_wait_for_moderator'] = 'Waiting for a moderator to join.'; +$string['view_message_conference_in_progress'] = 'This conference is in progress.'; +$string['view_message_conference_has_ended'] = 'This conference has ended.'; +$string['view_message_tab_close'] = 'This tab/window must be closed manually'; +$string['view_message_recordings_disabled'] = 'Recordings were disabled on this server. BigBlueButton instances for recordings only can not be used.'; +$string['view_message_importrecordings_disabled'] = 'Feature for import recording links is disabled on this server.'; + +$string['view_groups_selection_warning'] = 'There is a conference room for each group and you have access to more than one. Be sure to select the correct one.'; +$string['view_groups_nogroups_warning'] = 'The room was configured for using groups but the course does not have groups defined.'; +$string['view_groups_notenrolled_warning'] = 'The room was configured for using groups but you are not enrolled in any of them.'; +$string['view_conference_action_join'] = 'Join session'; +$string['view_conference_action_end'] = 'End session'; + +$string['view_recording'] = 'recording'; +$string['view_recording_link'] = 'imported link'; +$string['view_recording_link_warning'] = 'This is a link pointing to a recording that was created in a different course or activity'; +$string['view_recording_delete_confirmation'] = 'Are you sure to delete this {$a}?'; +$string['view_recording_delete_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported in a different course or activity. If the recording is deleted that link will also be removed'; +$string['view_recording_delete_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported in different courses or activities. If the recording is deleted those links will also be removed'; +$string['view_recording_publish_confirmation'] = 'Are you sure to publish this {$a}?'; +$string['view_recording_publish_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported in a different course or activity. If the recording is published that link will also be published'; +$string['view_recording_publish_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported in different courses or activities. If the recording is published those links will also be published'; +$string['view_recording_publish_link_deleted'] = 'This link can not be re-published because the actual recording does not exist in the current BigBlueButton server. The link should be removed.'; +$string['view_recording_publish_link_not_published'] = 'This link can not be re-published because the actual recording is unpublished'; +$string['view_recording_unpublish_confirmation'] = 'Are you sure to unpublish this {$a}?'; +$string['view_recording_unpublish_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported in a different course or activity. If the recording is unpublished that link will also be unpublished'; +$string['view_recording_unpublish_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported in different courses or activities. If the recording is unpublished those links will also be unpublished'; +$string['view_recording_protect_confirmation'] = 'Are you sure to protect this {$a}?'; +$string['view_recording_protect_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported in a different course or activity. If the recording is protected it will also affect the imported links'; +$string['view_recording_protect_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported in different courses or activities. If the recording is protected it will also affect the imported links'; +$string['view_recording_unprotect_confirmation'] = 'Are you sure to unprotect this {$a}?'; +$string['view_recording_unprotect_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported in a different course or activity. If the recording is unprotected it will also affect the imported links'; +$string['view_recording_unprotect_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported in different courses or activities. If the recording is unprotected it will also affect the imported links'; +$string['view_recording_import_confirmation'] = 'Are you sure to import this recording?'; +$string['view_recording_unprotect_link_deleted'] = 'This link can not be un-protected because the actual recording does not exist in the current BigBlueButton server. The link should be removed.'; +$string['view_recording_unprotect_link_not_unprotected'] = 'This link can not be un-protected because the actual recording is protected'; +$string['view_recording_actionbar'] = 'Toolbar'; +$string['view_recording_activity'] = 'Activity'; +$string['view_recording_course'] = 'Course'; +$string['view_recording_date'] = 'Date'; +$string['view_recording_description'] = 'Description'; +$string['view_recording_description_editlabel'] = 'Edit Description'; +$string['view_recording_description_edithint'] = 'Edit the description. It will help to find the recording later'; +$string['view_recording_length'] = 'Length'; +$string['view_recording_meeting'] = 'Meeting'; +$string['view_recording_duration'] = 'Duration'; +$string['view_recording_recording'] = 'Recording'; +$string['view_recording_duration_min'] = 'min'; +$string['view_recording_name'] = 'Name'; +$string['view_recording_name_editlabel'] = 'Edit Name'; +$string['view_recording_name_edithint'] = 'Edit the name. It will help to find the recording later'; +$string['view_recording_tags'] = 'Tags'; +$string['view_recording_playback'] = 'Playback'; +$string['view_recording_preview'] = 'Preview'; +$string['view_recording_preview_help'] = 'Hover over an image to view it in full size'; +$string['view_recording_modal_button'] = 'Apply'; +$string['view_recording_modal_title'] = 'Set values for recording'; +$string['view_recording_yui_first'] = 'First'; +$string['view_recording_yui_prev'] = 'Previous'; +$string['view_recording_yui_next'] = 'Next'; +$string['view_recording_yui_last'] = 'Last'; +$string['view_recording_yui_page'] = 'Page'; +$string['view_recording_yui_go'] = 'Go'; +$string['view_recording_yui_rows'] = 'Rows'; +$string['view_recording_yui_show_all'] = 'Show all'; + +$string['event_activity_created'] = 'Activity created'; +$string['event_activity_viewed'] = 'Activity viewed'; +$string['event_activity_deleted'] = 'Activity deleted'; +$string['event_activity_updated'] = 'Activity updated'; +$string['event_meeting_created'] = 'Meeting created'; +$string['event_meeting_ended'] = 'Meeting forcibly ended'; +$string['event_meeting_joined'] = 'Meeting joined'; +$string['event_meeting_left'] = 'Meeting left'; +$string['event_recording_viewed'] = 'Recording viewed'; +$string['event_recording_edited'] = 'Recording edited'; +$string['event_recording_deleted'] = 'Recording deleted'; +$string['event_recording_imported'] = 'Recording imported'; +$string['event_recording_published'] = 'Recording published'; +$string['event_recording_unpublished'] = 'Recording unpublished'; +$string['event_recording_protected'] = 'Recording protected'; +$string['event_recording_unprotected'] = 'Recording unprotected'; +$string['event_live_session'] = 'Live session event'; + +$string['instance_type_default'] = 'Room/Activity with recordings'; +$string['instance_type_room_only'] = 'Room/Activity only'; +$string['instance_type_recording_only'] = 'Recordings only'; + +$string['messageprovider:instance_updated'] = 'BigBlueButton meeting updated'; +$string['messageprovider:recording_ready'] = 'BigBlueButton recording ready to view'; +$string['notification_instance_created_intro'] = 'The {$a->name} BigBlueButton activity has been created.'; +$string['notification_instance_created_small'] = 'A new BigBlueButton meeting named {$a->name} was created'; +$string['notification_instance_created_subject'] = 'A new BigBlueButton meeting activity has been created'; +$string['notification_instance_description'] = 'Description'; +$string['notification_instance_end_date'] = 'End date'; +$string['notification_instance_name'] = 'Title'; +$string['notification_instance_start_date'] = 'Start date'; +$string['notification_instance_updated_intro'] = 'The {$a->name} BigBlueButton activity has been updated.'; +$string['notification_instance_updated_small'] = 'The {$a->name} BigBlueButton meeting was updated'; +$string['notification_instance_updated_subject'] = 'Your BigBlueButton meeting activity has been updated'; +$string['notification_recording_ready_small'] = 'A new recording is available for the {$a->name} BigBlueButton meeting'; +$string['notification_recording_ready_html'] = 'A recording is now available for a recent meeting in {$a->name}.'; +$string['notification_recording_ready_plain'] = 'A recording is now available for a recent meeting in {$a->name}. See {$a->link} to view the meeting.'; +$string['notification_recording_ready_subject'] = 'Recording ready'; + +$string['view_error_meeting_not_running'] = 'Something went wrong, the meeting is not running.'; +$string['view_error_current_state_not_found'] = 'Current state was not found. The recording may have been deleted or the BigBlueButton server is not compatible with the action performed.'; +$string['view_error_action_not_completed'] = 'Action could not be completed'; +$string['view_warning_default_server'] = 'This Moodle server is making use of the BigBlueButton testing server that comes pre-configured by default. It should be replaced for production.'; + +$string['view_room'] = 'View room'; +$string['index_error_noinstances'] = 'There are no instances of bigbluebuttonbn'; +$string['index_error_bbtn'] = 'BigBlueButton ID {$a} is incorrect'; + +$string['view_mobile_message_reload_page_creation_time_meeting'] = 'You exceeded the 45 seconds in this page, please reload the page to join correctly to the meeting.'; +$string['view_mobile_message_groups_not_supported'] = 'This instance is enable to work with groups but the mobile app has not support for this. Please open in desktop if you want to use the group support.'; + +$string['end_session_confirm_title'] = 'Really end session?'; +$string['end_session_confirm'] = 'Are you sure you want to end the virtual classroom session?'; +$string['end_session_notification'] = 'The session has now been closed.'; +$string['cachedef_currentfetch'] = 'Data to list any recording fetched recently.'; +$string['cachedef_serverinfo'] = 'Remote server information'; +$string['cachedef_recordings'] = 'Recording metadata'; +$string['cachedef_validatedurls'] = 'Cache of validated URL checks'; +$string['taskname:check_pending_recordings'] = 'Fetch pending recordings'; +$string['userlimitreached'] = 'The number of users allowed in a meeting has been reached.'; +$string['waitformoderator'] = 'Waiting for a moderator to join.'; + +$string['recordingurlnotfound'] = 'The recording URL is invalid.'; diff --git a/mod/bigbluebuttonbn/lib.php b/mod/bigbluebuttonbn/lib.php new file mode 100644 index 00000000000..726622118ab --- /dev/null +++ b/mod/bigbluebuttonbn/lib.php @@ -0,0 +1,656 @@ +. + +/** + * Library calls for Moodle and BigBlueButton. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ +defined('MOODLE_INTERNAL') || die; + +use core_calendar\action_factory; +use core_calendar\local\event\entities\action_interface; +use mod_bigbluebuttonbn\completion\custom_completion; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\bigbluebutton; +use mod_bigbluebuttonbn\local\helpers\files; +use mod_bigbluebuttonbn\local\helpers\mod_helper; +use mod_bigbluebuttonbn\local\helpers\reset; +use mod_bigbluebuttonbn\logger; +use mod_bigbluebuttonbn\meeting; +use mod_bigbluebuttonbn\recording; + +global $CFG; + +/** + * Indicates API features that the bigbluebuttonbn supports. + * + * @param string $feature + * @return mixed True if yes (some features may use other values) + * @uses FEATURE_IDNUMBER + * @uses FEATURE_GROUPS + * @uses FEATURE_GROUPINGS + * @uses FEATURE_GROUPMEMBERSONLY + * @uses FEATURE_MOD_INTRO + * @uses FEATURE_BACKUP_MOODLE2 + * @uses FEATURE_COMPLETION_TRACKS_VIEWS + * @uses FEATURE_COMPLETION_HAS_RULES + * @uses FEATURE_GRADE_HAS_GRADE + * @uses FEATURE_GRADE_OUTCOMES + * @uses FEATURE_SHOW_DESCRIPTION + */ +function bigbluebuttonbn_supports($feature) { + if (!$feature) { + return null; + } + $features = [ + FEATURE_IDNUMBER => true, + FEATURE_GROUPS => true, + FEATURE_GROUPINGS => true, + FEATURE_MOD_INTRO => true, + FEATURE_BACKUP_MOODLE2 => true, + FEATURE_COMPLETION_TRACKS_VIEWS => true, + FEATURE_COMPLETION_HAS_RULES => true, + FEATURE_GRADE_HAS_GRADE => false, + FEATURE_GRADE_OUTCOMES => false, + FEATURE_SHOW_DESCRIPTION => true, + ]; + if (isset($features[(string) $feature])) { + return $features[$feature]; + } + return null; +} + +/** + * Given an object containing all the necessary data, + * (defined by the form in mod_form.php) this function + * will create a new instance and return the id number + * of the new instance. + * + * @param stdClass $bigbluebuttonbn An object from the form in mod_form.php + * @return int The id of the newly inserted bigbluebuttonbn record + */ +function bigbluebuttonbn_add_instance($bigbluebuttonbn) { + global $DB; + // Excecute preprocess. + mod_helper::process_pre_save($bigbluebuttonbn); + // Pre-set initial values. + $bigbluebuttonbn->presentation = files::save_media_file($bigbluebuttonbn); + // Encode meetingid. + $bigbluebuttonbn->meetingid = meeting::get_unique_meetingid_seed(); + // Insert a record. + $bigbluebuttonbn->id = $DB->insert_record('bigbluebuttonbn', $bigbluebuttonbn); + // Log insert action. + logger::log_instance_created($bigbluebuttonbn); + // Complete the process. + mod_helper::process_post_save($bigbluebuttonbn); + return $bigbluebuttonbn->id; +} + +/** + * Given an object containing all the necessary data, + * (defined by the form in mod_form.php) this function + * will update an existing instance with new data. + * + * @param stdClass $bigbluebuttonbn An object from the form in mod_form.php + * @return bool Success/Fail + */ +function bigbluebuttonbn_update_instance($bigbluebuttonbn) { + global $DB; + // Excecute preprocess. + mod_helper::process_pre_save($bigbluebuttonbn); + + // Pre-set initial values. + $bigbluebuttonbn->id = $bigbluebuttonbn->instance; + $bigbluebuttonbn->presentation = files::save_media_file($bigbluebuttonbn); + + // Update a record. + $DB->update_record('bigbluebuttonbn', $bigbluebuttonbn); + + // Get the meetingid column in the bigbluebuttonbn table. + $bigbluebuttonbn->meetingid = (string) $DB->get_field('bigbluebuttonbn', 'meetingid', ['id' => $bigbluebuttonbn->id]); + + // Log update action. + logger::log_instance_updated(instance::get_from_instanceid($bigbluebuttonbn->id)); + + // Complete the process. + mod_helper::process_post_save($bigbluebuttonbn); + return true; +} + +/** + * Given an ID of an instance of this module, + * this function will permanently delete the instance + * and any data that depends on it. + * + * @param int $id Id of the module instance + * + * @return bool Success/Failure + */ +function bigbluebuttonbn_delete_instance($id) { + global $DB; + + $instance = instance::get_from_instanceid($id); + if (empty($instance)) { + return false; + } + // End all meeting if any still running. + try { + $meeting = new meeting($instance); + $meeting->end_meeting(); + $groups = groups_get_course_group($instance->get_course()); + if ($groups) { + foreach ($groups as $group) { + $instance->set_group_id($group->id); + $meeting = new meeting($instance); + $meeting->end_meeting(); + } + } + } catch (moodle_exception $e) { + // Do not log any issue when testing. + if (!(defined('PHPUNIT_TEST') && PHPUNIT_TEST) && !defined('BEHAT_SITE_RUNNING')) { + debugging($e->getMessage(), DEBUG_NORMAL, $e->getTrace()); + } + } + + $result = true; + + // Delete the instance. + if (!$DB->delete_records('bigbluebuttonbn', ['id' => $id])) { + $result = false; + } + + // Delete dependant events. + if (!$DB->delete_records('event', ['modulename' => 'bigbluebuttonbn', 'instance' => $id])) { + $result = false; + } + + // Log action performed. + logger::log_instance_deleted($instance); + + // Mark dependent recordings as headless. + foreach (recording::get_records(['bigbluebuttonbnid' => $id]) as $recording) { + $recording->set('headless', recording::RECORDING_HEADLESS); + $recording->update(); + } + + return $result; +} + +/** + * Return a small object with summary information about what a + * user has done with a given particular instance of this module + * Used for user activity reports. + * + * @param stdClass $course + * @param stdClass $user + * @param cm_info $mod + * @param stdClass $bigbluebuttonbn + * + * @return stdClass with info and time (timestamp of the last log) + */ +function bigbluebuttonbn_user_outline(stdClass $course, stdClass $user, cm_info $mod, stdClass $bigbluebuttonbn): stdClass { + $customcompletion = new custom_completion($mod, $user->id); + $completed = $customcompletion->get_overall_completion_state(); + $result = new stdClass(); + if ($completed) { + $results = []; + $lastlog = 0; + foreach ($customcompletion->get_available_custom_rules() as $rule) { + $results[] = $customcompletion->get_printable_state($rule); + $lastlogrule = $customcompletion->get_last_log_timestamp($rule); + if ($lastlogrule > $lastlog) { + $lastlog = $lastlogrule; + } + } + $result = new stdClass(); + $result->info = join(', ', $results); + $result->time = $lastlog; + } + return $result; +} + +/** + * Print a detailed representation of what a user has done with + * a given particular instance of this module, for user activity reports. + * + * @param stdClass $course + * @param stdClass $user + * @param cm_info $mod + * @param stdClass $bigbluebuttonbn + * + */ +function bigbluebuttonbn_user_complete(stdClass $course, stdClass $user, cm_info $mod, stdClass $bigbluebuttonbn) { + $customcompletion = new custom_completion($mod, $user->id); + $result = []; + foreach ($customcompletion->get_available_custom_rules() as $rule) { + $result[] = $customcompletion->get_printable_state($rule); + } + echo join(', ', $result); +} + +/** + * This flags this module with the capability to override the completion status with the custom completion rules. + * + * + * @return int + */ +function bigbluebuttonbn_get_completion_aggregation_state() { + return COMPLETION_CUSTOM_MODULE_FLOW; +} + +/** + * Returns all other caps used in module. + * + * @return string[] + */ +function bigbluebuttonbn_get_extra_capabilities() { + return ['moodle/site:accessallgroups']; +} + +/** + * Called by course/reset.php + * + * @param object $mform + */ +function bigbluebuttonbn_reset_course_form_definition(object &$mform) { + $items = reset::reset_course_items(); + $mform->addElement('header', 'bigbluebuttonbnheader', get_string('modulenameplural', 'bigbluebuttonbn')); + foreach ($items as $item => $default) { + $mform->addElement( + 'advcheckbox', + "reset_bigbluebuttonbn_{$item}", + get_string("reset{$item}", 'bigbluebuttonbn') + ); + if ($item == 'logs' || $item == 'recordings') { + $mform->addHelpButton("reset_bigbluebuttonbn_{$item}", "reset{$item}", 'bigbluebuttonbn'); + } + } +} + +/** + * Course reset form defaults. + * + * @param stdClass $course + * @return array + */ +function bigbluebuttonbn_reset_course_form_defaults(stdClass $course) { + $formdefaults = []; + $items = reset::reset_course_items(); + // All unchecked by default. + foreach ($items as $item => $default) { + $formdefaults["reset_bigbluebuttonbn_{$item}"] = $default; + } + return $formdefaults; +} + +/** + * This function is used by the reset_course_userdata function in moodlelib. + * + * @param stdClass $data the data submitted from the reset course. + * @return array status array + */ +function bigbluebuttonbn_reset_userdata(stdClass $data) { + $items = reset::reset_course_items(); + $status = []; + + // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. + // See MDL-9367. + if (array_key_exists('recordings', $items) && !empty($data->reset_bigbluebuttonbn_recordings)) { + // Remove all the recordings from a BBB server that are linked to the room/activities in this course. + reset::reset_recordings($data->courseid); + unset($items['recordings']); + $status[] = reset::reset_getstatus('recordings'); + } + + if (!empty($data->reset_bigbluebuttonbn_tags)) { + // Remove all the tags linked to the room/activities in this course. + reset::reset_tags($data->courseid); + unset($items['tags']); + $status[] = reset::reset_getstatus('tags'); + } + return $status; +} + +/** + * Given a course_module object, this function returns any + * "extra" information that may be needed when printing + * this activity in a course listing. + * See get_array_of_activities() in course/lib.php. + * + * @param stdClass $coursemodule + * + * @return null|cached_cm_info + */ +function bigbluebuttonbn_get_coursemodule_info($coursemodule) { + global $DB; + + $dbparams = ['id' => $coursemodule->instance]; + $fields = 'id, name, intro, introformat, completionattendance'; + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', $dbparams, $fields); + if (!$bigbluebuttonbn) { + return null; + } + $info = new cached_cm_info(); + $info->name = $bigbluebuttonbn->name; + if ($coursemodule->showdescription) { + // Convert intro to html. Do not filter cached version, filters run at display time. + $info->content = format_module_intro('bigbluebuttonbn', $bigbluebuttonbn, $coursemodule->id, false); + } + // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. + if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { + $info->customdata['customcompletionrules']['completionattendance'] = $bigbluebuttonbn->completionattendance; + } + + return $info; +} + +/** + * Serves the bigbluebuttonbn attachments. Implements needed access control ;-). + * + * @param stdClass $course course object + * @param cm_info $cm course module object + * @param context $context context object + * @param string $filearea file area + * @param array $args extra arguments + * @param bool $forcedownload whether or not force download + * @param array $options additional options affecting the file serving + * + * @return false|null false if file not found, does not return if found - justsend the file + * @category files + * + */ +function bigbluebuttonbn_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []) { + if (!files::pluginfile_valid($context, $filearea)) { + return false; + } + $file = files::pluginfile_file($course, $cm, $context, $filearea, $args); + if (empty($file)) { + return false; + } + // Finally send the file. + return send_stored_file($file, 0, 0, $forcedownload, $options); // Download MUST be forced - security! +} + +/** + * Mark the activity completed (if required) and trigger the course_module_viewed event. + * + * @param stdClass $bigbluebuttonbn bigbluebuttonbn object + * @param stdClass $course course object + * @param cm_info $cm course module object + * @param context $context context object + * @since Moodle 3.0 + */ +function bigbluebuttonbn_view($bigbluebuttonbn, $course, $cm, $context) { + + // Trigger course_module_viewed event. + $params = [ + 'context' => $context, + 'objectid' => $bigbluebuttonbn->id + ]; + + $event = \mod_bigbluebuttonbn\event\activity_viewed::create($params); // Fix event name. + $event->add_record_snapshot('course_modules', $cm->get_course_module_record()); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('bigbluebuttonbn', $bigbluebuttonbn); + $event->trigger(); + + // Completion. + $completion = new completion_info($course); + $completion->set_module_viewed($cm); +} + +/** + * Check if the module has any update that affects the current user since a given time. + * + * @param cm_info $cm course module data + * @param int $from the time to check updates from + * @param array $filter if we need to check only specific updates + * @return stdClass an object with the different type of areas indicating if they were updated or not + * @since Moodle 3.2 + */ +function bigbluebuttonbn_check_updates_since(cm_info $cm, $from, $filter = []) { + $updates = course_check_module_updates_since($cm, $from, ['content'], $filter); + return $updates; +} + +/** + * Get icon mapping for font-awesome. + */ +function mod_bigbluebuttonbn_get_fontawesome_icon_map() { + return [ + 'mod_bigbluebuttonbn:icon' => 'icon-bigbluebutton', + ]; +} + +/** + * This function receives a calendar event and returns the action associated with it, or null if there is none. + * + * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event + * is not displayed on the block. + * + * @param calendar_event $event + * @param action_factory $factory + * @return action_interface|null + */ +function mod_bigbluebuttonbn_core_calendar_provide_event_action( + calendar_event $event, + action_factory $factory +) { + global $DB; + + $time = time(); + + // Get mod info. + $cm = get_fast_modinfo($event->courseid)->instances['bigbluebuttonbn'][$event->instance]; + + // Get bigbluebuttonbn activity. + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', ['id' => $event->instance], '*', MUST_EXIST); + + // Set flag haspassed if closingtime has already passed only if it is defined. + $haspassed = ($bigbluebuttonbn->closingtime) && $bigbluebuttonbn->closingtime < $time; + + // Set flag hasstarted if startingtime has already passed or not defined. + $hasstarted = $bigbluebuttonbn->openingtime < $time; + + // Return null if it has passed or not started. + if ($haspassed || !$hasstarted) { + return null; + } + + // Get if the user has joined in live session or viewed the recorded. + $customcompletion = new custom_completion($cm, $event->userid); + $usercomplete = $customcompletion->get_overall_completion_state(); + $instance = instance::get_from_instanceid($bigbluebuttonbn->id); + // Get if the room is available. + $roomavailable = $instance->is_currently_open(); + // Get if the user can join. + $meetinginfo = meeting::get_meeting_info_for_instance($instance); + $usercanjoin = $meetinginfo->canjoin; + + // Check if the room is closed and the user has already joined this session or played the record. + if (!$roomavailable && $usercomplete) { + return null; + } + + // Check if the user can join this session. + $actionable = ($roomavailable && $usercanjoin); + + // Action data. + $string = get_string('view_room', 'bigbluebuttonbn'); + $url = new moodle_url('/mod/bigbluebuttonbn/view.php', ['id' => $cm->id]); + if (groups_get_activity_groupmode($cm) == NOGROUPS) { + // No groups mode. + $string = get_string('view_conference_action_join', 'bigbluebuttonbn'); + $url = new moodle_url('/mod/bigbluebuttonbn/bbb_view.php', [ + 'action' => 'join', + 'id' => $cm->id, + 'bn' => $bigbluebuttonbn->id, + 'timeline' => 1] + ); + } + + return $factory->create_instance($string, $url, 1, $actionable); +} + +/** + * Is the event visible? + * + * @param calendar_event $event + * @return bool Returns true if the event is visible to the current user, false otherwise. + */ +function mod_bigbluebuttonbn_core_calendar_is_event_visible(calendar_event $event) { + $instance = instance::get_from_instanceid($event->instance); + if (!$instance) { + return false; + } + $activitystatus = mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy::view_get_activity_status($instance); + return $activitystatus != 'ended'; +} + +/** + * Adds module specific settings to the settings block + * + * @param settings_navigation $settingsnav The settings navigation object + * @param navigation_node $nodenav The node to add module settings to + */ +function bigbluebuttonbn_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $nodenav) { + global $PAGE, $USER; + // Don't add validate completion if the callback for meetingevents is NOT enabled. + if (!(boolean) \mod_bigbluebuttonbn\local\config::get('meetingevents_enabled')) { + return; + } + // Don't add validate completion if user is not allowed to edit the activity. + $context = context_module::instance($PAGE->cm->id); + if (!has_capability('moodle/course:manageactivities', $context, $USER->id)) { + return; + } + $completionvalidate = '#action=completion_validate&bigbluebuttonbn=' . $PAGE->cm->instance; + $nodenav->add(get_string('completionvalidatestate', 'bigbluebuttonbn'), + $completionvalidate, navigation_node::TYPE_CONTAINER); +} + +/** + * In place editable for the recording table + * + * @param string $itemtype + * @param string $itemid + * @param mixed $newvalue + * @return mixed|null + */ +function bigbluebuttonbn_inplace_editable($itemtype, $itemid, $newvalue) { + $editableclass = "\\mod_bigbluebuttonbn\\output\\recording_{$itemtype}_editable"; + if (class_exists($editableclass)) { + return call_user_func([$editableclass, 'update'], $itemid, $newvalue); + } + return null; // Will raise an exception in core update_inplace_editable method. +} + +/** + * Returns all events since a given time in specified bigbluebutton activity. + * We focus here on the two events: play and join. + * + * @param array $activities + * @param int $index + * @param int $timestart + * @param int $courseid + * @param int $cmid + * @param int $userid + * @param int $groupid + * @return array + */ +function bigbluebuttonbn_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid = 0, + $groupid = 0): array { + $instance = instance::get_from_cmid($cmid); + $instance->set_group_id($groupid); + $cm = $instance->get_cm(); + $logs = + + logger::get_user_completion_logs_with_userfields($instance, + $userid ?? null, + [logger::EVENT_JOIN, logger::EVENT_PLAYED], + $timestart); + foreach ($logs as $log) { + $activity = new stdClass(); + + $activity->type = 'bigbluebuttonbn'; + $activity->cmid = $cm->id; + $activity->name = format_string($instance->get_meeting_name(), true); + $activity->sectionnum = $cm->sectionnum; + $activity->timestamp = $log->timecreated; + $activity->user = new stdClass(); + $userfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); + foreach ($userfields as $userfield) { + if ($userfield == 'id') { + // Aliased in SQL above. + $activity->user->{$userfield} = $log->userid; + } else { + $activity->user->{$userfield} = $log->{$userfield}; + } + } + $activity->user->fullname = fullname($log); + $activity->content = ''; + switch ($log->log) { + case logger::EVENT_JOIN: + $activity->eventname = get_string('event_meeting_joined', 'mod_bigbluebuttonbn'); + break; + case logger::EVENT_PLAYED: + $activity->eventname = get_string('event_recording_viewed', 'mod_bigbluebuttonbn'); + if (!empty($log->meta)) { + $meta = json_decode($log->meta); + if (!empty($meta->recordingid)) { + $recording = recording::get_record(['id' => $meta->recordingid]); + if ($recording) { + $activity->content = $recording->get('name'); + } + } + } + break; + } + $activities[$index++] = $activity; + } + return $activities; +} + +/** + * Outputs the bigbluebutton logs indicated by $activity. + * + * @param stdClass $activity the activity object the bigbluebuttonbn resides in + * @param int $courseid the id of the course the bigbluebuttonbn resides in + * @param bool $detail not used, but required for compatibilty with other modules + * @param array $modnames not used, but required for compatibilty with other modules + * @param bool $viewfullnames not used, but required for compatibilty with other modules + */ +function bigbluebuttonbn_print_recent_mod_activity(stdClass $activity, int $courseid, bool $detail, array $modnames, + bool $viewfullnames) { + global $OUTPUT; + $modinfo = []; + $userpicture = $OUTPUT->user_picture($activity->user); + + $template = ['userpicture' => $userpicture, + 'submissiontimestamp' => $activity->timestamp, + 'modinfo' => $modinfo, + 'userurl' => new moodle_url('/user/view.php', array('id' => $activity->user->id, 'course' => $courseid)), + 'fullname' => $activity->user->fullname]; + if (isset($activity->eventname)) { + $template['eventname'] = $activity->eventname; + } + echo $OUTPUT->render_from_template('mod_bigbluebuttonbn/recentactivity', $template); +} diff --git a/mod/bigbluebuttonbn/mobileapp/mobile.notification.js b/mod/bigbluebuttonbn/mobileapp/mobile.notification.js new file mode 100644 index 00000000000..4ca9173d4a4 --- /dev/null +++ b/mod/bigbluebuttonbn/mobileapp/mobile.notification.js @@ -0,0 +1,39 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * This file is part of the Moodle apps support for the choicegroup plugin. + * Defines the function to be used from the mobile course view template. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ + +// That will be the global this (ionic convention). +var that = this; + +var TIMEOUTCHECK = 20000; + +that.onCanJoinReturns = function (data) { + if (data && data.can_join) { + that.openContent('', {'cmid': data.cmid}, 'mod_bigbluebuttonbn', 'mobile_course_view'); + } else { + setTimeout(function () { + that.refreshContent(true); + }, TIMEOUTCHECK); + } +}; \ No newline at end of file diff --git a/mod/bigbluebuttonbn/mod_form.php b/mod/bigbluebuttonbn/mod_form.php new file mode 100644 index 00000000000..a3210b7ae77 --- /dev/null +++ b/mod/bigbluebuttonbn/mod_form.php @@ -0,0 +1,718 @@ +. + +/** + * Config all BigBlueButtonBN instances in this course. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +use mod_bigbluebuttonbn\local\helpers\roles; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/course/moodleform_mod.php'); + +/** + * Moodle class for mod_form. + * + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_bigbluebuttonbn_mod_form extends moodleform_mod { + + /** + * Define (add) particular settings this activity can have. + * + * @return void + * @throws moodle_exception + */ + public function definition(): void { + global $CFG, $DB, $PAGE; + $mform = &$this->_form; + + // Validates if the BigBlueButton server is running. + $serverversion = bigbluebutton_proxy::get_server_version(); + if (is_null($serverversion)) { + throw new moodle_exception('general_error_unable_connect', + 'bigbluebuttonbn', + $CFG->wwwroot . '/admin/settings.php?section=modsettingbigbluebuttonbn', + \mod_bigbluebuttonbn\local\config::get('server_url') + ); + } + $bigbluebuttonbn = null; + if ($this->current->id) { + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', ['id' => $this->current->id], '*', MUST_EXIST); + } + // UI configuration options. + $cfg = \mod_bigbluebuttonbn\local\config::get_options(); + + // Get only those that are allowed. + $course = $this->_course; + $context = context_course::instance($course->id); + + $instancetyperofiles = $this->get_instance_type_profiles(); + $this->bigbluebuttonbn_mform_add_block_profiles($mform, $instancetyperofiles); + // Data for participant selection. + $participantlist = roles::get_participant_list($bigbluebuttonbn, $context); + // Add block 'General'. + $this->bigbluebuttonbn_mform_add_block_general($mform, $cfg); + // Add block 'Room'. + $this->bigbluebuttonbn_mform_add_block_room($mform, $cfg); + // Add block 'Lock'. + $this->bigbluebuttonbn_mform_add_block_locksettings($mform, $cfg); + // Add block 'Preuploads'. + $this->bigbluebuttonbn_mform_add_block_preuploads($mform, $cfg); + // Add block 'Participant List'. + $this->bigbluebuttonbn_mform_add_block_user_role_mapping($mform, $participantlist); + // Add block 'Schedule'. + $this->bigbluebuttonbn_mform_add_block_schedule($mform, $this->current); + // Add standard elements, common to all modules. + $this->standard_coursemodule_elements(); + // Add standard buttons, common to all modules. + $this->add_action_buttons(); + + $jsvars = [ + 'instanceTypeDefault' => array_keys($instancetyperofiles)[0], + ]; + + // Now add the instance type profiles to the form as a html hidden field. + $mform->addElement('html', html_writer::div('', 'd-none', [ + 'data-profile-types' => json_encode($instancetyperofiles), + 'data-participant-data' => json_encode(roles::get_participant_data($context, $bigbluebuttonbn)), + ])); + + $PAGE->requires->js_call_amd('mod_bigbluebuttonbn/modform', 'init', [$jsvars]); + } + + /** + * Get instance type profile. + * + * @return array|array[] + * @throws moodle_exception + */ + protected function get_instance_type_profiles() { + // Add profile data here instead of passing it by parameters. + $context = context_course::instance($this->_course->id); + $instancetyperofiles = bigbluebutton_proxy::get_instance_type_profiles_create_allowed( + has_capability('mod/bigbluebuttonbn:addinstancewithmeeting', $context), + has_capability('mod/bigbluebuttonbn:addinstancewithrecording', $context) + ); + // If none is allowed, fail and return. + if (empty($instancetyperofiles)) { + global $CFG; + // Also check module context for those that are allowed. + $contextm = context_module::instance($this->_cm->id); + $instancetyperofiles = bigbluebutton_proxy::get_instance_type_profiles_create_allowed( + has_capability('mod/bigbluebuttonbn:addinstancewithmeeting', $contextm), + has_capability('mod/bigbluebuttonbn:addinstancewithrecording', $contextm) + ); + // If still none is allowed, fail and return. + if (empty($instancetyperofiles)) { + throw new moodle_exception('general_error_not_allowed_to_create_instances', 'bigbluebuttonbn', + $CFG->wwwroot . '/admin/settings.php?section=modsettingbigbluebuttonbn'); + } + } + return $instancetyperofiles; + } + + /** + * Prepare the attachment for being stored. + * + * @param array|null $defaultvalues + * @return void + */ + public function data_preprocessing(&$defaultvalues) { + parent::data_preprocessing($defaultvalues); + + // Completion: tick by default if completion attendance settings is set to 1 or more. + $defaultvalues['completionattendanceenabled'] = 0; + if (!empty($defaultvalues['completionattendance'])) { + $defaultvalues['completionattendanceenabled'] = 1; + } + // Check if we are Editing an existing instance. + if ($this->current->instance) { + // Pre-uploaded presentation: copy existing files into draft area. + try { + $draftitemid = file_get_submitted_draft_itemid('presentation'); + file_prepare_draft_area($draftitemid, $this->context->id, 'mod_bigbluebuttonbn', 'presentation', 0, + ['subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1, 'mainfile' => true] + ); + $defaultvalues['presentation'] = $draftitemid; + } catch (Exception $e) { + debugging('Presentation could not be loaded: ' . $e->getMessage(), DEBUG_DEVELOPER); + return; + } + // Completion: tick if completion attendance settings is set to 1 or more. + $defaultvalues['completionattendanceenabled'] = 0; + if (!empty($this->current->completionattendance)) { + $defaultvalues['completionattendanceenabled'] = 1; + } + } + } + + /** + * Validates the data processed by the form. + * + * @param mixed $data + * @param array $files + * @return array + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + if (isset($data['openingtime']) && isset($data['closingtime'])) { + if ($data['openingtime'] != 0 && $data['closingtime'] != 0 && + $data['closingtime'] < $data['openingtime']) { + $errors['closingtime'] = get_string('bbbduetimeoverstartingtime', 'bigbluebuttonbn'); + } + } + if (isset($data['voicebridge'])) { + if (!bigbluebutton_proxy::is_voicebridge_number_unique($data['instance'], $data['voicebridge'])) { + $errors['voicebridge'] = get_string('mod_form_field_voicebridge_notunique_error', 'bigbluebuttonbn'); + } + } + return $errors; + } + + /** + * Add elements for setting the custom completion rules. + * + * @return array List of added element names, or names of wrapping group elements. + * @category completion + */ + public function add_completion_rules(): array { + $mform = $this->_form; + if (!(boolean) \mod_bigbluebuttonbn\local\config::get('meetingevents_enabled')) { + return []; + } + + // Elements for completion by Attendance. + $attendance['grouplabel'] = get_string('completionattendancegroup', 'bigbluebuttonbn'); + $attendance['rulelabel'] = get_string('completionattendance', 'bigbluebuttonbn'); + $attendance['group'] = [ + $mform->createElement('advcheckbox', 'completionattendanceenabled', '', $attendance['rulelabel'] . ' '), + $mform->createElement('text', 'completionattendance', '', ['size' => 3]), + $mform->createElement('static', 'completionattendanceunit', ' ', get_string('minutes', 'bigbluebuttonbn')) + ]; + $mform->setType('completionattendance', PARAM_INT); + $mform->addGroup($attendance['group'], 'completionattendancegroup', $attendance['grouplabel'], [' '], false); + $mform->addHelpButton('completionattendancegroup', 'completionattendancegroup', 'bigbluebuttonbn'); + $mform->disabledIf('completionattendancegroup', 'completionview', 'notchecked'); + $mform->disabledIf('completionattendance', 'completionattendanceenabled', 'notchecked'); + + // Elements for completion by Engagement. + $engagement['grouplabel'] = get_string('completionengagementgroup', 'bigbluebuttonbn'); + $engagement['chatlabel'] = get_string('completionengagementchats', 'bigbluebuttonbn'); + $engagement['talklabel'] = get_string('completionengagementtalks', 'bigbluebuttonbn'); + $engagement['raisehand'] = get_string('completionengagementraisehand', 'bigbluebuttonbn'); + $engagement['pollvotes'] = get_string('completionengagementpollvotes', 'bigbluebuttonbn'); + $engagement['emojis'] = get_string('completionengagementemojis', 'bigbluebuttonbn'); + $engagement['group'] = [ + $mform->createElement('advcheckbox', 'completionengagementchats', '', $engagement['chatlabel'] . '  '), + $mform->createElement('advcheckbox', 'completionengagementtalks', '', $engagement['talklabel'] . '  '), + $mform->createElement('advcheckbox', 'completionengagementraisehand', '', $engagement['raisehand'] . '  '), + $mform->createElement('advcheckbox', 'completionengagementpollvotes', '', $engagement['pollvotes'] . '  '), + $mform->createElement('advcheckbox', 'completionengagementemojis', '', $engagement['emojis'] . '  '), + ]; + $mform->addGroup($engagement['group'], 'completionengagementgroup', $engagement['grouplabel'], [' '], false); + $mform->addGroupRule('completionattendancegroup', [ + 'completionattendance' => [ + [null, 'numeric', null, 'client'] + ] + ]); + $mform->addHelpButton('completionengagementgroup', 'completionengagementgroup', 'bigbluebuttonbn'); + $mform->disabledIf('completionengagementgroup', 'completionview', 'notchecked'); + + return ['completionattendancegroup', 'completionengagementgroup']; + } + + /** + * Called during validation to see whether some module-specific completion rules are selected. + * + * @param array $data Input data not yet validated. + * @return bool True if one or more rules is enabled, false if none are. + */ + public function completion_rule_enabled($data) { + return (!empty($data['completionattendanceenabled']) && $data['completionattendance'] != 0) + || !empty($data['completionengagementchats']) + || !empty($data['completionengagementtalks']) + || !empty($data['completionengagementraisehand']) + || !empty($data['completionengagementpollvotes']) + || !empty($data['completionengagementemojis']); + } + + /** + * Allows module to modify the data returned by form get_data(). + * This method is also called in the bulk activity completion form. + * + * Only available on moodleform_mod. + * + * @param stdClass $data the form data to be modified. + */ + public function data_postprocessing($data) { + parent::data_postprocessing($data); + // Turn off completion settings if the checkboxes aren't ticked. + if (!empty($data->completionunlocked)) { + $autocompletion = !empty($data->completion) && $data->completion == COMPLETION_TRACKING_AUTOMATIC; + if (empty($data->completionattendanceenabled) || !$autocompletion) { + $data->completionattendance = 0; + } + } + } + + /** + * Function for showing the block for selecting profiles. + * + * @param MoodleQuickForm $mform + * @param array $profiles + * @return void + */ + private function bigbluebuttonbn_mform_add_block_profiles(MoodleQuickForm &$mform, array $profiles): void { + if ((boolean) \mod_bigbluebuttonbn\local\config::recordings_enabled()) { + $mform->addElement('select', 'type', get_string('mod_form_field_instanceprofiles', 'bigbluebuttonbn'), + bigbluebutton_proxy::get_instance_profiles_array($profiles)); + $mform->addHelpButton('type', 'mod_form_field_instanceprofiles', 'bigbluebuttonbn'); + } + } + + /** + * Function for showing the block for general settings. + * + * @param MoodleQuickForm $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_general(MoodleQuickForm &$mform, array $cfg): void { + global $CFG; + $mform->addElement('header', 'general', get_string('mod_form_block_general', 'bigbluebuttonbn')); + $mform->addElement('text', 'name', get_string('mod_form_field_name', 'bigbluebuttonbn'), + 'maxlength="64" size="32"'); + $mform->setType('name', empty($CFG->formatstringstriptags) ? PARAM_CLEANHTML : PARAM_TEXT); + $mform->addRule('name', null, 'required', null, 'client'); + $this->standard_intro_elements(get_string('mod_form_field_intro', 'bigbluebuttonbn')); + $mform->setAdvanced('introeditor'); + $mform->setAdvanced('showdescription'); + if ($cfg['sendnotifications_enabled']) { + $field = ['type' => 'checkbox', 'name' => 'notification', 'data_type' => PARAM_INT, + 'description_key' => 'mod_form_field_notification']; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], 0); + } + } + + /** + * Function for showing details of the room settings for the room. + * + * @param MoodleQuickForm $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_room_room(MoodleQuickForm &$mform, array $cfg): void { + $field = ['type' => 'textarea', 'name' => 'welcome', 'data_type' => PARAM_CLEANHTML, + 'description_key' => 'mod_form_field_welcome']; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['welcome_default'], ['wrap' => 'virtual', 'rows' => 5, 'cols' => '60']); + $field = ['type' => 'hidden', 'name' => 'voicebridge', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['voicebridge_editable']) { + $field['type'] = 'text'; + $field['data_type'] = PARAM_TEXT; + $field['description_key'] = 'mod_form_field_voicebridge'; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], 0, ['maxlength' => 4, 'size' => 6], + ['message' => get_string('mod_form_field_voicebridge_format_error', 'bigbluebuttonbn'), + 'type' => 'numeric', 'rule' => '####', 'validator' => 'server'] + ); + } else { + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], 0, ['maxlength' => 4, 'size' => 6]); + } + $field = ['type' => 'hidden', 'name' => 'wait', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['waitformoderator_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_wait'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['waitformoderator_default']); + $field = ['type' => 'hidden', 'name' => 'userlimit', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['userlimit_editable']) { + $field['type'] = 'text'; + $field['data_type'] = PARAM_TEXT; + $field['description_key'] = 'mod_form_field_userlimit'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['userlimit_default']); + $field = ['type' => 'hidden', 'name' => 'record', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['recording_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_record'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recording_default']); + + // Record all from start and hide button. + $field = ['type' => 'hidden', 'name' => 'recordallfromstart', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['recording_all_from_start_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordallfromstart'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recording_all_from_start_default']); + + $field = ['type' => 'hidden', 'name' => 'recordhidebutton', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['recording_hide_button_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordhidebutton'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recording_hide_button_default']); + + $mform->disabledIf('recordallfromstart', 'record', $condition = 'notchecked', $value = '0'); + $mform->disabledIf('recordhidebutton', 'record', $condition = 'notchecked', $value = '0'); + $mform->disabledIf('recordhidebutton', 'recordallfromstart', $condition = 'notchecked', $value = '0'); + // End Record all from start and hide button. + + $field = ['type' => 'hidden', 'name' => 'muteonstart', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['muteonstart_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_muteonstart'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['muteonstart_default']); + + } + + /** + * Function for showing details of the lock settings for the room. + * + * @param MoodleQuickForm $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_locksettings(MoodleQuickForm &$mform, array $cfg): void { + $mform->addElement('header', 'lock', get_string('mod_form_locksettings', 'bigbluebuttonbn')); + + $locksettings = false; + + $field = ['type' => 'hidden', 'name' => 'disablecam', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disablecam_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disablecam'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disablecam_default']); + + $field = ['type' => 'hidden', 'name' => 'disablemic', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disablemic_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disablemic'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disablemic_default']); + + $field = ['type' => 'hidden', 'name' => 'disableprivatechat', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disableprivatechat_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disableprivatechat'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disableprivatechat_default']); + + $field = ['type' => 'hidden', 'name' => 'disablepublicchat', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disablepublicchat_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disablepublicchat'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disablepublicchat_default']); + + $field = ['type' => 'hidden', 'name' => 'disablenote', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disablenote_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disablenote'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disablenote_default']); + + $field = ['type' => 'hidden', 'name' => 'hideuserlist', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['hideuserlist_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_hideuserlist'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['hideuserlist_default']); + + $field = ['type' => 'hidden', 'name' => 'lockedlayout', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['lockedlayout_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_lockedlayout'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['lockedlayout_default']); + + $field = ['type' => 'hidden', 'name' => 'lockonjoin', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['lockonjoin_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_lockonjoin'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['lockonjoin_default']); + + $field = ['type' => 'hidden', 'name' => 'lockonjoinconfigurable', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['lockonjoinconfigurable_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_lockonjoinconfigurable'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['lockonjoinconfigurable_default']); + + // Output message if no settings. + if (!$locksettings) { + $field = ['type' => 'static', 'name' => 'no_locksettings', + 'defaultvalue' => get_string('mod_form_field_nosettings', 'bigbluebuttonbn')]; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], null, null, + $field['defaultvalue']); + } + } + + /** + * Function for showing details of the recording settings for the room. + * + * @param MoodleQuickForm $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_room_recordings(MoodleQuickForm &$mform, array $cfg): void { + $recordingsettings = false; + $field = ['type' => 'hidden', 'name' => 'recordings_html', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['recordings_html_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordings_html'; + $recordingsettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recordings_html_default']); + $field = ['type' => 'hidden', 'name' => 'recordings_deleted', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['recordings_deleted_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordings_deleted'; + $recordingsettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recordings_deleted_default']); + $field = ['type' => 'hidden', 'name' => 'recordings_imported', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['importrecordings_enabled'] && $cfg['recordings_imported_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordings_imported'; + $recordingsettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recordings_imported_default']); + $field = ['type' => 'hidden', 'name' => 'recordings_preview', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['recordings_preview_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordings_preview'; + $recordingsettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recordings_preview_default']); + + if (!$recordingsettings) { + $field = ['type' => 'static', 'name' => 'no_recordings', + 'defaultvalue' => get_string('mod_form_field_nosettings', 'bigbluebuttonbn')]; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], null, null, + $field['defaultvalue']); + } + } + + /** + * Function for showing the block for room settings. + * + * @param MoodleQuickForm $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_room(MoodleQuickForm &$mform, array $cfg) { + if ($cfg['voicebridge_editable'] || $cfg['waitformoderator_editable'] || + $cfg['userlimit_editable'] || $cfg['recording_editable']) { + $mform->addElement('header', 'room', get_string('mod_form_block_room', 'bigbluebuttonbn')); + $this->bigbluebuttonbn_mform_add_block_room_room($mform, $cfg); + } + if ($cfg['recordings_html_editable'] || $cfg['recordings_deleted_editable'] || + $cfg['recordings_imported_editable'] || $cfg['recordings_preview_editable']) { + $mform->addElement('header', 'recordings', get_string('mod_form_block_recordings', 'bigbluebuttonbn')); + $this->bigbluebuttonbn_mform_add_block_room_recordings($mform, $cfg); + } + } + + /** + * Function for showing the block for preuploaded presentation. + * + * @param MoodleQuickForm $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_preuploads(MoodleQuickForm &$mform, array $cfg): void { + if ($cfg['preuploadpresentation_enabled']) { + $mform->addElement('header', 'preuploadpresentation', + get_string('mod_form_block_presentation', 'bigbluebuttonbn')); + $mform->setExpanded('preuploadpresentation'); + $filemanageroptions = []; + $filemanageroptions['accepted_types'] = '*'; + $filemanageroptions['maxbytes'] = 0; + $filemanageroptions['subdirs'] = 0; + $filemanageroptions['maxfiles'] = 1; + $filemanageroptions['mainfile'] = true; + $mform->addElement('filemanager', 'presentation', get_string('selectfiles'), + null, $filemanageroptions); + } + } + + /** + * Function for showing the block for setting participant roles. + * + * @param MoodleQuickForm $mform + * @param array $participantlist + * @return void + */ + private function bigbluebuttonbn_mform_add_block_user_role_mapping(MoodleQuickForm &$mform, array $participantlist): void { + global $OUTPUT; + $participantselection = roles::get_participant_selection_data(); + $mform->addElement('header', 'permissions', get_string('mod_form_block_participants', 'bigbluebuttonbn')); + $mform->setExpanded('permissions'); + $mform->addElement('hidden', 'participants', json_encode($participantlist)); + $mform->setType('participants', PARAM_TEXT); + $selectiontype = new single_select(new moodle_url(qualified_me()), + 'bigbluebuttonbn_participant_selection_type', + $participantselection['type_options'], + $participantselection['type_selected']); + $selectionparticipants = new single_select(new moodle_url(qualified_me()), + 'bigbluebuttonbn_participant_selection', + $participantselection['options'], + $participantselection['selected']); + $action = new single_button(new moodle_url(qualified_me()), + get_string('mod_form_field_participant_list_action_add', 'bigbluebuttonbn'), + 'post', + false, + ['name' => 'bigbluebuttonbn_participant_selection_add'] + ); + $pformcontext = [ + 'selectionType' => $selectiontype->export_for_template($OUTPUT), + 'selectionParticipant' => $selectionparticipants->export_for_template($OUTPUT), + 'action' => $action->export_for_template($OUTPUT), + ]; + $html = $OUTPUT->render_from_template('mod_bigbluebuttonbn/participant_form', $pformcontext); + $mform->addElement('static', 'static_participant_list', + get_string('mod_form_field_participant_list', 'bigbluebuttonbn'), $html); + } + + /** + * Function for showing the block for integration with the calendar. + * + * @param MoodleQuickForm $mform + * @param stdClass $activity + * @return void + */ + private function bigbluebuttonbn_mform_add_block_schedule(MoodleQuickForm &$mform, stdClass &$activity) { + $mform->addElement('header', 'schedule', get_string('mod_form_block_schedule', 'bigbluebuttonbn')); + if (!empty($activity->openingtime) || !empty($activity->closingtime)) { + $mform->setExpanded('schedule'); + } + $mform->addElement('date_time_selector', 'openingtime', + get_string('mod_form_field_openingtime', 'bigbluebuttonbn'), ['optional' => true]); + $mform->setDefault('openingtime', 0); + $mform->addElement('date_time_selector', 'closingtime', + get_string('mod_form_field_closingtime', 'bigbluebuttonbn'), ['optional' => true]); + $mform->setDefault('closingtime', 0); + } + + /** + * Function for showing an element. + * + * @param MoodleQuickForm $mform + * @param string $type + * @param string $name + * @param string|null $datatype + * @param string|null $descriptionkey + * @param mixed|null $defaultvalue + * @param array|null $options + * @param string|null $rule + * @return void + * @throws coding_exception + */ + private function bigbluebuttonbn_mform_add_element(MoodleQuickForm &$mform, string $type, string $name, ?string $datatype, + ?string $descriptionkey = "", $defaultvalue = null, ?array $options = null, ?string $rule = null): void { + $datatype = $datatype ?? 'hidden'; + if ($type === 'hidden' || $type === 'static') { + $mform->addElement($type, $name, $defaultvalue); + $mform->setType($name, $datatype); + return; + } + if ($descriptionkey) { + $mform->addElement($type, $name, get_string($descriptionkey, 'bigbluebuttonbn'), $options); + if (get_string_manager()->string_exists($descriptionkey . '_help', 'bigbluebuttonbn')) { + $mform->addHelpButton($name, $descriptionkey, 'bigbluebuttonbn'); + } + } + if (!empty($rule)) { + $mform->addRule($name, $rule['message'], $rule['type'], $rule['rule'], $rule['validator']); + } + $mform->setDefault($name, $defaultvalue); + $mform->setType($name, $datatype); + } + + /** + * Definition after data + * + * Here just to tweak form group in completion that should not be frozen. This avoid + * unwanted warnings. + */ + public function definition_after_data() { + global $COURSE; + parent::definition_after_data(); + // Completion: If necessary, un-freeze group fields. + $completion = new completion_info($COURSE); + if ($completion->is_enabled()) { + $mform = $this->_form; + foreach (['completionattendancegroup', 'completionengagementgroup'] as $groupname) { + if ($mform->elementExists($groupname)) { + $element = $mform->getElement($groupname); + if ($element->isFrozen()) { + $element->unfreeze(); + } + } + } + } + } +} diff --git a/mod/bigbluebuttonbn/pix/flex_icons.php b/mod/bigbluebuttonbn/pix/flex_icons.php new file mode 100644 index 00000000000..7c30ad06dec --- /dev/null +++ b/mod/bigbluebuttonbn/pix/flex_icons.php @@ -0,0 +1,34 @@ +. + +/** + * Pix icon handler for Totara + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +$icons = [ + 'mod_bigbluebuttonbn|icon' => [ + 'data' => [ + 'classes' => 'icon-bigbluebutton', + ], + ], +]; diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-128.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-128.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d32bb37cf79e3c541425dd15911e420c6bba35 GIT binary patch literal 7752 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 literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-24.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-24.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed24ea8d98aa0326a7d2561650696062b8e117a GIT binary patch literal 1386 zcmV-w1(o`VP)&#v~>fqo8yE8R$>Dy4vol?sxAjs=91LrlhBSxBBVcbI&{X zlmY1V2ClXJGa`(7p3CCm?WEako(H7g?`yx`H-$pcnVz2h<;aoO>H{l)f+!eJG!Iq9 z7~_ON;NJ|2Me6~m+G%U`2Vi%-UZ>S+6Gah8lF;o&jE=s-sZ%G}yLT^1G62$~IxL`? zF3E}^k>i+Cz;aeV3&xm16m__F?=G(E;QKz-TK4T5V{YzUia~%_n zv%0#<)2B}f!;tm$b=KC_c>MS=BFg(8e2C|HsDg-K3@EbI!RBT%c>C@5Qng6>N4wo7 z2#U1ZZIUEmYHErw4D+?Iv4ORgyLW%4R(rNXMQIn?Iyiaq3RkaQWo2cBOP4OOy1EL$!(Sid$wdTIP}$aj9Zu-BAsP%UtyYspqd~XZ z#TY}qUZ+~Ek|YUpb8}d0aa}j3V@Q!GxpY19iQK94Lx(20e}9=Mim=ua1Ob&wg*c9> zR4O?Ip64+&bu?FzY@6)7z>e|H4jnpl7*)k}T^>Gs$i<5nnV+9WRVfq-T)%#u$;n9y zg#rh&vyxF{jA3{aX$Qhf-cqR?;MlP@xpQZcYPHJz{5;2xAIBI&7=|n^F0yCO9*!QJ zX8-;%WV?!Fs|#;vy)Sx;>$$vl{%dC^|1; zU=UH_c(d`R$%!{Am3<8_myf(`xvGlqdrV9mWPJQpW@b*Oj1Dgpu>AqWqDN(HoXyQ1 z&1O4{x}8rem1B*ZB2j(Ga#jCdtoene9mZJO$BLoRXx4xF`Iqyb{o~`GcBlwVKY#jy zkw&8#-dkSTkvyMdi~(az{uwe`2eQ{_wwJ8{BL-uj`umgc z=8ymRt<807*qoM6N<$g14}r8UO$Q literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-256.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-256.png new file mode 100644 index 0000000000000000000000000000000000000000..a4f79dfbecc3a490ea4995daaf8ef553c1de9e11 GIT binary patch literal 18500 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 literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-72.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-72.png new file mode 100644 index 0000000000000000000000000000000000000000..4f15cadc6ff5c20e989840de725c1d12091e07eb GIT binary patch literal 5183 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 V=s%Zj1pc~$^mXoO*J(ON{|_P*l1Tsn literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-96.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-96.png new file mode 100644 index 0000000000000000000000000000000000000000..df2f650e2b4d090e7b694be308861e6e730f876a GIT binary patch literal 6229 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 j&0WV^L8zgW3z~;-S_XgWN{;|P06= + + + + + +{ + "fontFamily": "bigbluebutton", + "majorVersion": 1, + "minorVersion": 0, + "version": "Version 1.0", + "fontId": "bigbluebutton", + "psName": "bigbluebutton", + "subFamily": "Regular", + "fullName": "bigbluebutton", + "description": "Font generated by IcoMoon." +} + + + + + + + + + + + \ No newline at end of file diff --git a/mod/bigbluebuttonbn/pix/icon.gif b/mod/bigbluebuttonbn/pix/icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..8a29f62e7be0ce4e15843f0db3950f14058ab90b GIT binary patch literal 1246 zcmZ?wbhEHblwgox_$jilwp@Jp_}!TckM7>RTiv#G!pwuqSMJiVYPfdmMPcoF z4a<5BvnpM)if`Y(X;{>0ST|~z*TkpJ@Q;|ZZU6mQi;h(^ZpO zyU{*jk5}lV+P1A>@zcYT=Y_^i^9t@Ww5sl&azMkPR^6uA#iwoB{NtL&tcdgiTEjD98L+AD>bB|>e zE-0v3?;APQCua7htw&CuyV)~&i(5c%eEKXc$M%xiwX3(@sH$JEWbLJC^G~eVdVTV| z6V5@CoC7ClIkbBO_HIA$z%z12Quaa%$HvuLuIE;+jY^(nU|H4Fv#YsxcXQ9qg{v=i zPB~auwPxP(vt|yB2A0)fNpo~e%QH(?2gJ^D3+j(bUl^J&YuWnC8#Wzw3YpqAX@7pj zs;5t%?m2w#Q94~L)V&h!4dDnFftHvi!-)-D=$Hui{=b?vY_H}0V^)|l!TlU`FedO`{ z73bnI=V(~gc}7f|z2vl+WAlWWhXdm0oV)PA!m+{GuUEsmsdd8MJ9i$ud-u*WYNnB0 zm4;QlhIwsv$#QkO*4>96)Hg2(h@BP?GsD8Me#QFp(-)jb$Xb|EuzcmFt8M+eHH}LD zg8{>!0g69a7#SEYGU$L@1j-W(9RC>>bIM5Y9$=6?CuDWmkb&V~J2NB8kBckLvsp`C zd$BWEqrtg@iM1o^ao3^A%IgCrd|2o>LrkB^fzP8!EmYeqE3Nj?1IA`<8}AbWk{<#N z@N*S)bZRa-!zs@#UewZ2tm)63X2`B}Vo?gSN=E;&1}o;4V=c0)Ic%%1IUhX0Q0M2m z(TPiHi$ULu8iB&(maeVY{j35WD`zS>#7w&RG_*5VAcmKLneT~N<8q#9S(eMJ)?OB8HlT@~UpS9SF- zo8(;XD9-;=Y;UXXOnG)b=t#q(HHS9st<8zF z3;U)D4hfZ)?>7rCkX3lV*xm5W+hC)@2PYp^4y8!-^{yVSoZJovycn5y84htZ%r$Ur zDRZ5~$+Rm&i8YYP@-kzlkVk^Er1OEKGfYdbC>)kKC&1#ssO`0EZY{q8LnG(mA3+D) znK=Uvh|6p%kmC_iN?0tJATr^qGlN8fhu&lc&NCAV3_dk6dNfUGFt(65)FM6GB$1ns UO@%?ogN-#Ioq_kPqyU380Gwp~B>(^b literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/icon.png b/mod/bigbluebuttonbn/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e9ec8689cc0fa0ac6bf563e749fec37c2bf39627 GIT binary patch literal 1393 zcmV-%1&;cOP)yX-kZ4s>Nt)8xaMov;`v?QYg{`OYtue zkAIdzM2h~SP!SQ)Hqhgb>K}y$gchnj#J14ZQZ=DR?!(RQ%+9eh^XB=-ZYG;HsrA9* z&Ad17^Siz?ZwMaJy1I_DP|SXgyUte5(FUNtup-77Bi8`>V}3et{=|tl_+kvQ(Bz&y zU;8wvY&XDrW7QS@XjwomFAq`52u#CRDUikTUuII2je3)harVcRx_?~R897yX-4 zXC%*n!NPlDHR1Z@$vE-6(o6urrsp?+I77%}avro4t`(n~ZBrA|%rMMYDC=WsafDo`92Ll5``0mIB zj8DxMqn6Xb9qtR(I31GfI@`j*u&gNhJo!-~ybVGC(`gegbau=Gbv*MlUft4(ox2a> z@7wd=we<8%FxF7LjdI~WK^dX5MVu>SuEc=Aflp7P_ux?s4&H@jS>9SS8pVh2ZGRw5 zx^7A?7p)QjX>e=@*{nWKemacAaKA#V(SFOPIEjM7>b4rnAKOSF}003mOS>#A?9=mB`z+6DswGa~J0?UdN`5?TE)4(0gG2qS-n3>s1I+^qx|L zBOyqVWF=z*7un65C&ZIay{?p3Mm9}Prv0TGFI>8fj`dBbsg5oRzx3NxeDKN7a9jpZ z20l zPK@m0!tLb__odTH&@gQ&?{EMV{(yv_?01$_Moft@i-LYMTvz|?BOUtp|F#5coDN6u zt>9=AM8pAr2r>eOYd%JPTv|PFA(cwGi!uHM6mF@nBF%RF00000NkvXXu0mjfAf}cm literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/icon.svg b/mod/bigbluebuttonbn/pix/icon.svg new file mode 100644 index 00000000000..7b92ab21b12 --- /dev/null +++ b/mod/bigbluebuttonbn/pix/icon.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mod/bigbluebuttonbn/settings.php b/mod/bigbluebuttonbn/settings.php new file mode 100644 index 00000000000..889361b1ea9 --- /dev/null +++ b/mod/bigbluebuttonbn/settings.php @@ -0,0 +1,32 @@ +. + +/** + * Settings for BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; + +$bbbsettings = new mod_bigbluebuttonbn\settings($ADMIN, $module, $section); +$bbbsettings->add_all_settings(); + +$settings = null; diff --git a/mod/bigbluebuttonbn/styles.css b/mod/bigbluebuttonbn/styles.css new file mode 100644 index 00000000000..817357a82b4 --- /dev/null +++ b/mod/bigbluebuttonbn/styles.css @@ -0,0 +1,64 @@ +@charset "UTF-8"; + +/* bigbluebutton-font */ +@font-face { + font-family: "bigbluebutton-font"; + src: + url("../../../../mod/bigbluebuttonbn/fonts/bigbluebutton-font.woff") format("woff"), + url("../../../../mod/bigbluebuttonbn/fonts/bigbluebutton-font.ttf") format("truetype"), + url("../../../../mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg#bigbluebutton-font") format("svg"); + font-weight: normal; + font-style: normal; +} + +[class^="icon-bigbluebutton"]:before, +[class*=" icon-bigbluebutton"]:before { + font-family: "bigbluebutton-font"; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + speak: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-bigbluebutton:before { + content: "\e099"; +} + +/* bigbluebuttonbn resources */ +.recording-thumbnail { + border: 1px solid #ddd; + border-radius: 4px; + padding: 0 0 0 0; + transition: transform .2s; /* Animation */ + width: 113px; + height: 64px; +} + +.recording-thumbnail:hover { + box-shadow: 0 0 2px 1px rgba(0, 140, 186, 0.5); + transform: scale(2.0); /* (200% zoom - Note: if the zoom is too large, it will go outside of the viewport) */ + -moz-transform: scale(2.0); /* Firefox */ + -webkit-transform: scale(2.0); /* Safari and Chrome */ + -o-transform: scale(2.0); /* Opera */ + position: relative; + display: block; + z-index: 999; +} + +.fa-disabled { + cursor: not-allowed; + opacity: 0.2; +} + +.fa-invisible { + cursor: not-allowed; + visibility: hidden; +} + +.bbb_index_form { + display: inline-block; +} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/templates/end_session_button.mustache b/mod/bigbluebuttonbn/templates/end_session_button.mustache new file mode 100644 index 00000000000..9e9ae452d42 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/end_session_button.mustache @@ -0,0 +1,41 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/end_session_button + + This template renders the End session button. + + Example context (json): + { + "statusrunning": true, + "bigbluebuttonbnid": 12345, + "groupid": 98765 + } +}} + +{{#statusrunning}} + + {{#str}}view_conference_action_end, mod_bigbluebuttonbn{{/str}} + +{{/statusrunning}} diff --git a/mod/bigbluebuttonbn/templates/import_view.mustache b/mod/bigbluebuttonbn/templates/import_view.mustache new file mode 100644 index 00000000000..8fca33bef39 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/import_view.mustache @@ -0,0 +1,77 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/import_view + + This template renders the import recordings page. + + Example context (json): + { + "bbbid": 3, + "has_selected_course": 0, + "back_button": { + "method" : "get", + "url" : "#", + "primary" : 1, + "tooltip" : "This is a tooltip", + "label" : "This is a the button text", + "id": "thisismybutton", + "attributes": [{ + "name": "data-attribute", + "value": "yeah" + }] + } + } +}} + +
+
+
+ {{#str}}view_recording_list_course, mod_bigbluebuttonbn{{/str}} + {{#course_select}} + {{>core/single_select}} + {{/course_select}} +
+
+ {{#bbb_select}} + {{#str}}view_recording_activity, mod_bigbluebuttonbn{{/str}} + {{>core/single_select}} + {{/bbb_select}} +
+
+ {{#has_selected_course}} + {{< mod_bigbluebuttonbn/recordings_table}} + {{$title}}{{#str}} view_recording_button_import, mod_bigbluebuttonbn {{/str}}{{/title}} + {{$tools}}import{{/tools}} + {{$removeimportedid}}{{bbboriginid}}{{/removeimportedid}} + {{$otherattributes}} {{! + }} id="bigbluebuttonbn-recording-table-{{uniqid}}" {{! + }} data-import-mode="1" {{! + }} data-bbb-source-course-id="{{sourcecourseid}}" {{! + }} data-bbb-source-instance-id="{{bbbsourceid}}" {{! + }} {{/otherattributes}} + {{/mod_bigbluebuttonbn/recordings_table}} + {{/has_selected_course}} + {{^has_selected_course}} + {{#str}} view_recording_select_course, mod_bigbluebuttonbn {{/str}} + {{/has_selected_course}} +
+ {{#back_button}} + {{>core/single_button}} + {{/back_button}} +
+
diff --git a/mod/bigbluebuttonbn/templates/instance_updated_message.mustache b/mod/bigbluebuttonbn/templates/instance_updated_message.mustache new file mode 100644 index 00000000000..07f18fd46d7 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/instance_updated_message.mustache @@ -0,0 +1,70 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/instance_updated_message.mustache + + This template renders a message used to inform users that an activity has been created or updated. + + Example context (json): + { + "link": "https://example.com/mod/bigbluebuttonbn/view.php?id=12345", + "is_create": 1, + "is_update": 0, + "name": "Daily standup", + "description": "A place to discuss daily progress, blockers, and the weather.", + "openingtime": 1234556623, + "closingtime": 1234559623 + } +}} + +{{#is_create}} +

{{#str}} notification_instance_created_intro, mod_bigbluebuttonbn, { + "name": {{#quote}}{{name}}{{/quote}}, + "link": {{#quote}}{{{link}}}{{/quote}} +} {{/str}} +{{/is_create}} + +{{#is_update}} +

{{#str}} notification_instance_updated_intro, mod_bigbluebuttonbn, { + "name": {{#quote}}{{name}}{{/quote}}, + "link": {{#quote}}{{link}}{{/quote}} +} {{/str}} +{{/is_update}} + + + + + + + + + + +{{#openingtime}} + + + + +{{/openingtime}} +{{#closingtime}} + + + + +{{/closingtime}} + +
{{#str}}notification_instance_name, mod_bigbluebuttonbn{{/str}}name
{{#str}}notification_instance_description, mod_bigbluebuttonbn{{/str}}{{{description}}}
{{#str}}notification_instance_start_date, mod_bigbluebuttonbn{{/str}}{{#userdate}} {{openingtime}} , {{#str}}strftimedatetime, langconfig{{/str}} {{/userdate}}
{{#str}}notification_instance_end_date, mod_bigbluebuttonbn{{/str}}{{#userdate}} {{closingtime}} , {{#str}}strftimedatetime, langconfig{{/str}} {{/userdate}}
diff --git a/mod/bigbluebuttonbn/templates/mobile_view_error.mustache b/mod/bigbluebuttonbn/templates/mobile_view_error.mustache new file mode 100644 index 00000000000..a218f217137 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/mobile_view_error.mustache @@ -0,0 +1,34 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/mobile_view_error + + This template renders the error for mobile page. + + Example context (json): + { + "error": "This is wrong!" + } +}} +{{=<% %>=}} +

+ + + <% error %> + + +
diff --git a/mod/bigbluebuttonbn/templates/mobile_view_notification.mustache b/mod/bigbluebuttonbn/templates/mobile_view_notification.mustache new file mode 100644 index 00000000000..4a4310a89b5 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/mobile_view_notification.mustache @@ -0,0 +1,51 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/mobile_view_notification + + This template renders the notifications for mobile page. + + Example context (json): + { + "bigbluebuttonbn.intro": "foo", + "cmid": 3, + "message": "A message", + "not_started": [ + {"not_started.starts_at": "aaa", "not_started.starts_at": "bbb"}, + {"not_started.starts_at": "aaa", "not_started.starts_at": "bbb"} + ] + } +}} +{{=<% %>=}} +
+ + + + <% message %> + + <%#not_started%> + + +

<% not_started.starts_at %>

+

<% not_started.ends_at %>

+
+
+ <%/not_started%> + +
+
diff --git a/mod/bigbluebuttonbn/templates/mobile_view_page_ionic3.mustache b/mod/bigbluebuttonbn/templates/mobile_view_page_ionic3.mustache new file mode 100644 index 00000000000..578c9ba4f30 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/mobile_view_page_ionic3.mustache @@ -0,0 +1,62 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/mobile_view_page + + This template renders the mobile page. + + Example context (json): + { + "bigbluebuttonbn.intro": "foo", + "cmid": 3, + "message": "A message", + "errors": [ + {"error": "aaa"}, + {"error": "bbb"} + ], + "urltojoin": "http://foo" + } +}} +{{=<% %>=}} +
+ <%#msjgroup%> +
+ <% message %> +
+ <%/msjgroup%> + + + + <%#errors%> + + <% error %> + + <%/errors%> + <%^errors%> + + {{ 'plugin.mod_bigbluebuttonbn.view_message_conference_room_ready' | translate }} + + + + + <%/errors%> + +
diff --git a/mod/bigbluebuttonbn/templates/mobile_view_page_latest.mustache b/mod/bigbluebuttonbn/templates/mobile_view_page_latest.mustache new file mode 100644 index 00000000000..adffe0d859d --- /dev/null +++ b/mod/bigbluebuttonbn/templates/mobile_view_page_latest.mustache @@ -0,0 +1,64 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/mobile_view_page + + This template renders the mobile page. + + Example context (json): + { + "bigbluebuttonbn.intro": "foo", + "cmid": 3, + "message": "A message", + "errors": [ + {"error": "aaa"}, + {"error": "bbb"} + ], + "urltojoin": "http://foo" + } +}} +{{=<% %>=}} +
+ <%#msjgroup%> +
+ <% message %> +
+ <%/msjgroup%> + + + + <%#errors%> + + + <% error %> + + + <%/errors%> + <%^errors%> + + + {{ 'plugin.mod_bigbluebuttonbn.view_message_conference_room_ready' | translate }} + + + + {{ 'plugin.mod_bigbluebuttonbn.view_conference_action_join' | translate }} + + <%/errors%> + +
diff --git a/mod/bigbluebuttonbn/templates/participant_form.mustache b/mod/bigbluebuttonbn/templates/participant_form.mustache new file mode 100644 index 00000000000..f376cca1afb --- /dev/null +++ b/mod/bigbluebuttonbn/templates/participant_form.mustache @@ -0,0 +1,49 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/participant_form + + This template renders a row in the participant add form + + Example context (json): + { + "instanceTypeProfiles": [{}], + "select": {}, + "participanttype": "student", + "participantvalue": "", + "roles": [], + "canRemove": true + } +}} +
+ {{#selectionType}} + {{> mod_bigbluebuttonbn/participant_single_select}} + {{/selectionType}} + {{#selectionParticipant}} + {{> mod_bigbluebuttonbn/participant_single_select}} + {{/selectionParticipant}} + {{#action}} + + {{/action}} + + +
+
diff --git a/mod/bigbluebuttonbn/templates/participant_form_add.mustache b/mod/bigbluebuttonbn/templates/participant_form_add.mustache new file mode 100644 index 00000000000..4c408d892ad --- /dev/null +++ b/mod/bigbluebuttonbn/templates/participant_form_add.mustache @@ -0,0 +1,56 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/participant_form_add + + This template renders a row in the participant add form + + Example context (json): + { + "selectiontypevalue": "example", + "selectionvalue": "example", + "participanttype": "student", + "participantvalue": "", + "roles": [], + "canRemove": true + } +}} + + + {{participanttype}} + + + {{participantvalue}} + + + {{#str}}mod_form_field_participant_list_text_as, mod_bigbluebuttonbn{{/str}} + + + + + + {{#canRemove}} + + {{#pix}}t/delete, core, {{#str}}mod_form_field_participant_list_action_remove, mod_bigbluebuttonbn{{/str}}{{/pix}} + + {{/canRemove}} + + diff --git a/mod/bigbluebuttonbn/templates/participant_single_select.mustache b/mod/bigbluebuttonbn/templates/participant_single_select.mustache new file mode 100644 index 00000000000..064505c7b65 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/participant_single_select.mustache @@ -0,0 +1,120 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/participant_single_select + + Moodle template for a single select without the form and autosubmit + @see template core/single_select + + The original select form produced two embedded form tags and would produce random + bugs (https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-owner) + + + Context variables required for this template: + * name - Element name. + * method - get or post. + * action - the action url to submit to. + * classes - Element classes. + * label - Element label. + * disabled - true if this element is disabled. + * title - Element title. + * formid - optional id value for the form. + * id - id for the element. + * params - array of params with name and value attributes. + * options - Array of options for the select with value, name , slected and optgroup properites. + * labelattributes - Label attributes. + * helpicon - Help icon. + + Example context (json): + { + "name": "lang", + "method": "get", + "action": "http://localhost/stable_master/mod/scorm/player.php", + "classes": "langmenu", + "label": "Zombies are coming...", + "disabled": false, + "title": null, + "formid": "randomid", + "id": "single_select5833dd4f4b08d108", + "params": [ + { + "name": "scoid", + "value": "12" + }, + { + "name": "cm", + "value": "15" + }, + { + "name": "mode", + "value": "review" + }, + { + "name": "currentorg", + "value": "eXeMapADrive4823c6301cf72b22b72" + } + ], + "options": [ + { + "value": "en", + "name": "English ‎(en)‎", + "selected": true, + "optgroup": false + }, + { + "value": "ar", + "name": "Muhaaaa..", + "selected": false, + "optgroup": false + } + ], + "labelattributes": [], + "helpicon": false + } +}} + +
+ {{#params}} + + {{/params}} + {{#label}} + + {{/label}} + {{#helpicon}} + {{>core/help_icon}} + {{/helpicon}} + + +
\ No newline at end of file diff --git a/mod/bigbluebuttonbn/templates/recentactivity.mustache b/mod/bigbluebuttonbn/templates/recentactivity.mustache new file mode 100644 index 00000000000..d413dfd0879 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/recentactivity.mustache @@ -0,0 +1,58 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/recentactivity + + This template will render the Bigbluebutton Activity inside a recent activity report. + + Variables requirer for this template: + * userpicture - User fullname and picture + * grade - User grade + * userdate - Submission date + + Example context (json): + { + "userpicture": " ", + "eventname": "Joined meeting", + "userdate": "Wednesday, 27 January 2021, 9:54 AM", + "userurl": "http://localhost/m/stable_master/user/view.php?id=3&course=6", + "fullname": "Lukáš Svoboda" + } + +}} + + + + + + + +
{{{userpicture}}} +
+ {{#modinfo}} + {{modicon}}{{modname}} + {{/modinfo}} +
+ {{#eventname}} +
+ {{eventname}} +
+ {{/eventname}} +
+ {{fullname}} - {{#userdate}} {{submissiontimestamp}}, {{#str}} strftimedatetime {{/str}} {{/userdate}} +
+
diff --git a/mod/bigbluebuttonbn/templates/recording_row_actionbar.mustache b/mod/bigbluebuttonbn/templates/recording_row_actionbar.mustache new file mode 100644 index 00000000000..1ac51b778b4 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/recording_row_actionbar.mustache @@ -0,0 +1,53 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/recording_row_actionbar.mustache + + This template renders the mobile page. + + Example context (json): + { + "id":"1", + "recordingid":"1", + "tools":[ + { + "disabled":false, + "url":"#", + "id":"test-id", + "classes":"btn btn-link", + "attributes":[ + { + "name":"title", + "value":"Action link's title", + "class":"'btn btn-sm btn-default", + "data-action":"play" + } + ], + "text":"This is an action link" + } + ] + } +}} +
+
+ {{#tools}} +
+ {{>core/action_link}} +
+ {{/tools}} +
+
diff --git a/mod/bigbluebuttonbn/templates/recording_row_playback.mustache b/mod/bigbluebuttonbn/templates/recording_row_playback.mustache new file mode 100644 index 00000000000..3ff864f74f5 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/recording_row_playback.mustache @@ -0,0 +1,52 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/recording_row_playback.mustache + + This template renders the mobile page. + + Example context (json): + { + "id":"1", + "recordingid":"1", + "tools":[ + { + "disabled":false, + "url":"#", + "id":"test-id", + "classes":"btn btn-link", + "attributes":[ + { + "name":"title", + "value":"Action link's title", + "class":"'btn btn-sm btn-default", + "data-action":"play" + } + ], + "text":"This is an action link" + } + ] + } +}} +
+ {{#playbacks}} + + {{>core/action_link}} + + {{/playbacks}} +
diff --git a/mod/bigbluebuttonbn/templates/recording_row_preview.mustache b/mod/bigbluebuttonbn/templates/recording_row_preview.mustache new file mode 100644 index 00000000000..544a63969af --- /dev/null +++ b/mod/bigbluebuttonbn/templates/recording_row_preview.mustache @@ -0,0 +1,60 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/recording_row_preview.mustache + + This template renders the mobile page. + + Example context (json): + { + "id":"1", + "recordingid":"1", + "tools":[ + { + "disabled":false, + "url":"#", + "id":"test-id", + "classes":"btn btn-link", + "attributes":[ + { + "name":"title", + "value":"Action link's title", + "class":"'btn btn-sm btn-default", + "data-action":"play" + } + ], + "text":"This is an action link" + } + ] + } +}} +
+ {{#recordingpreviews}} +
+
+ {{#thumbnails}} + + {{/thumbnails}} +
+
+ {{#str}}view_recording_preview_help, mod_bigbluebuttonbn{{/str}} +
+
+
+
+ {{/recordingpreviews}} +
diff --git a/mod/bigbluebuttonbn/templates/recordings_session.mustache b/mod/bigbluebuttonbn/templates/recordings_session.mustache new file mode 100644 index 00000000000..ac3cebd9f68 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/recordings_session.mustache @@ -0,0 +1,51 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/recordings_session + + This template renders the mobile page. + + Example context (json): + { + "bbbid" : 3, + "has_records": true, + "search_input": {}, + "import_button": { + "method" : "get", + "url" : "#", + "primary" : 1, + "tooltip" : "This is a tooltip", + "label" : "This is a the button text", + "id": "thisismybutton", + "attributes": [{ + "name": "data-attribute", + "value": "yeah" + }] + } + } +}} + +
+ {{< mod_bigbluebuttonbn/recordings_table}} + {{$title}}{{#str}}view_section_title_recordings, mod_bigbluebuttonbn{{/str}}{{/title}} + {{/mod_bigbluebuttonbn/recordings_table}} +
+ {{#import_button}} + {{>core/single_button}} + {{/import_button}} +
+
diff --git a/mod/bigbluebuttonbn/templates/recordings_table.mustache b/mod/bigbluebuttonbn/templates/recordings_table.mustache new file mode 100644 index 00000000000..56e2420a45d --- /dev/null +++ b/mod/bigbluebuttonbn/templates/recordings_table.mustache @@ -0,0 +1,59 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/recording_table + + This template renders the mobile page. + + Example context (json): + { + "bbbid" : 3, + "has_records": true, + "search_input": {} + } +}} +
+ {{#has_recordings}} +
+

{{$title}}{{/title}}

+
+
+ {{#search}} + {{>mod_bigbluebuttonbn/search_input_recordings}} + {{/search}} +
+
+ {{/has_recordings}} + {{^has_recordings}} +
{{#str}}view_message_norecordings, mod_bigbluebuttonbn{{/str}}
+ {{/has_recordings}} +
+ +{{#has_recordings}} + {{#js}} + require(['mod_bigbluebuttonbn/recordings'], function(recordings) { + recordings.init('#bigbluebuttonbn-recording-table-{{uniqid}}', '#searchform-auto-{{uniqid}}'); + }); + require(['core/inplace_editable']); + {{/js}} +{{/has_recordings}} diff --git a/mod/bigbluebuttonbn/templates/room_view.mustache b/mod/bigbluebuttonbn/templates/room_view.mustache new file mode 100644 index 00000000000..a6465fdcf4a --- /dev/null +++ b/mod/bigbluebuttonbn/templates/room_view.mustache @@ -0,0 +1,98 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/room_view + + This template renders the mobile page. + + Example context (json): + { + "statusclosed": true, + "statusclosed": false, + "openingtime": 123456789, + "closingtime": 987654321 + } +}} + +
+ + {{statusmessage}} + + {{#openingtime}} + + {{#str}}mod_form_field_openingtime, bigbluebuttonbn{{/str}}: + {{#userdate}} {{.}}, {{#str}} strftimedaydatetime, langconfig {{/str}} {{/userdate}} + + {{/openingtime}} + {{#closingtime}} + + {{#str}}mod_form_field_closingtime, bigbluebuttonbn{{/str}}: + {{#userdate}} {{.}}, {{#str}} strftimedaydatetime, langconfig {{/str}} {{/userdate}} + + {{/closingtime}} + + {{#statusrunning}} +
+ {{#str}}view_message_session_started_at, mod_bigbluebuttonbn{{/str}} + {{#userdate}} {{startedat}}, {{#str}} strftimetime, core_langconfig{{/str}} {{/userdate}} + + {{#moderatorplural}}{{#str}}view_message_moderators, mod_bigbluebuttonbn{{/str}}{{/moderatorplural}} + {{^moderatorplural}}{{#str}}view_message_moderator, mod_bigbluebuttonbn{{/str}}{{/moderatorplural}} + {{moderatorcount}} + + + {{#viewerplural}}{{#str}}view_message_viewers, mod_bigbluebuttonbn{{/str}}{{/viewerplural}} + {{^viewerplural}}{{#str}}view_message_viewer, mod_bigbluebuttonbn{{/str}}{{/viewerplural}} + {{participantcount}}. + +
+ {{/statusrunning}} +
+ +
+ {{#haspresentations}} +

{{#str}}view_section_title_presentation, mod_bigbluebuttonbn{{/str}}

+
+ {{#presentations}} + + {{#pix}}{{iconname}},core, {{icondesc}}{{/pix}} + {{name}} + + {{/presentations}} +
+ {{/haspresentations}} +
+
+
+ {{#canjoin}} + + {{#str}}view_conference_action_join, mod_bigbluebuttonbn{{/str}} + + {{/canjoin}} + + {{#ismoderator}} + {{> mod_bigbluebuttonbn/end_session_button}} + {{/ismoderator}} +
+
+
diff --git a/mod/bigbluebuttonbn/templates/search_input_recordings.mustache b/mod/bigbluebuttonbn/templates/search_input_recordings.mustache new file mode 100644 index 00000000000..789f1fd507b --- /dev/null +++ b/mod/bigbluebuttonbn/templates/search_input_recordings.mustache @@ -0,0 +1,62 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/search_input_recordings + + Search input that auto searches. Inspired from the core/search_input_auto but + does not use a unique id for the search input. + Add a submit button and clear button. + + Example context (json): + { + "action": "https://moodle.local/admin/search.php", + "extraclasses": "my-2", + "inputname": "search", + "searchstring": "Search settings", + "sesskey": "sesskey", + "value": "policy", + "btnclass": "primary", + "hiddenfields": [ + { + "name": "course", + "value": "11" + } + ] + } +}} +
+ +
diff --git a/mod/bigbluebuttonbn/templates/view_page.mustache b/mod/bigbluebuttonbn/templates/view_page.mustache new file mode 100644 index 00000000000..0f4b10252c3 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/view_page.mustache @@ -0,0 +1,73 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_bigbluebuttonbn/view_page + + This template renders the /view.php page. + + Example context (json): + { + "instanceid": 1234, + "meetingname": "My first meeting", + "meetingdescription": "This is my first meeting!", + "serverwarning": true, + "sitenotification": { + "message": "Unlock premium feature", + "actions": [ + { + "url": "https://example.com/", + "name": "action", + "value": "buy", + "title": "Upgrade now" + } + ] + } + } +}} +{{{groupselector}}} + +{{#serverwarning}} + {{>core/notification}} +{{/serverwarning}} + +{{#sitenotification}} + {{>core/local/notification/cta}} +{{/sitenotification}} + +

{{meetingname}}

+
{{{meetingdescription}}}
+ +{{#room}} + {{>mod_bigbluebuttonbn/room_view}} +{{/room}} + +{{#recordings}} + {{>mod_bigbluebuttonbn/recordings_session}} +{{/recordings}} + +{{#recordingwarning}} + {{>core/notification}} +{{/recordingwarning}} + +{{#js}} +require(['mod_bigbluebuttonbn/rooms'], function(rooms) { + // Register action on all buttons. + rooms.init({ + bigbluebuttonbnid: {{{instanceid}}}, + }); +}); +{{/js}} diff --git a/mod/bigbluebuttonbn/tests/backup_restore_test.php b/mod/bigbluebuttonbn/tests/backup_restore_test.php new file mode 100644 index 00000000000..4de3f563530 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/backup_restore_test.php @@ -0,0 +1,196 @@ +. + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +namespace mod_bigbluebuttonbn; + +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use restore_date_testcase; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php"); + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +class backup_restore_test extends restore_date_testcase { + use testcase_helper_trait; + + /** + * Setup + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + } + + /** + * All instance types + */ + const ALL_TYPES = [ + 'Instance Type ALL' => instance::TYPE_ALL, + 'Instance Type Recording Only' => instance::TYPE_RECORDING_ONLY, + 'Instance Room Only' => instance::TYPE_ROOM_ONLY + ]; + + /** + * Test backup restore (basic) + */ + public function test_backup_restore(): void { + global $DB; + $this->resetAfterTest(); + $bbactivity = []; + foreach (self::ALL_TYPES as $key => $type) { + list($bbactivitycontext, $bbactivitycm, $bbactivity[$type]) + = $this->create_instance($this->get_course(), ['type' => $type]); + } + + $newcourseid = $this->backup_and_restore($this->get_course()); + + foreach (self::ALL_TYPES as $key => $type) { + $newbbb = + $DB->get_record('bigbluebuttonbn', ['course' => $newcourseid, 'type' => $type], '*', MUST_EXIST); + // One record. + $this->assert_bbb_activities_same($bbactivity[$type], $newbbb); + } + } + + /** + * @var $RECORDINGS_DATA array fake recording data. + */ + const RECORDINGS_DATA = [ + ['name' => 'Recording 1'], + ['name' => 'Recording 2'], + ]; + + /** + * Check backup restore with recordings + * + */ + public function test_backup_restore_with_recordings(): void { + global $DB; + $this->resetAfterTest(); + set_config('bigbluebuttonbn_importrecordings_enabled', 1); + // This is for imported recording. + $generator = $this->getDataGenerator(); + $othercourse = $generator->create_course(); + $otherbbbactivities = []; + $recordingstoimport = []; + + list('activity' => $otherbbbactivities[], 'recordings' => $recordings) = + $this->create_activity_with_recordings($othercourse, + instance::TYPE_ALL, self::RECORDINGS_DATA + ); + $recordingstoimport = array_merge($recordingstoimport, $recordings); + list('activity' => $otherbbbactivities[], 'recordings' => $recordings) = + $this->create_activity_with_recordings($this->get_course(), + instance::TYPE_ALL, self::RECORDINGS_DATA + ); + $recordingstoimport = array_merge($recordingstoimport, $recordings); + // Create a set of recordings and imported recordings. + // We have nbrecording per bb activity, except for roomonly recordings which have the imported recordings. + $bbactivity = []; + foreach (self::ALL_TYPES as $key => $type) { + $bbactivity[$type] = $this->getDataGenerator()->create_module( + 'bigbluebuttonbn', + ['course' => $this->get_course()->id, 'type' => $type, 'name' => 'BBB Activity:' . $key], + ['visible' => true] + ); + $instance = instance::get_from_instanceid($bbactivity[$type]->id); + // Create recording except for TYPE_RECORDING_ONLY Only. + if ($instance->is_feature_enabled('showroom')) { + $this->create_recordings_for_instance(instance::get_from_instanceid($bbactivity[$type]->id), + self::RECORDINGS_DATA); + } + // Then import the recordings into the instance. + if ($instance->is_feature_enabled('importrecordings')) { + foreach ($recordingstoimport as $rec) { + $rentity = recording::get_record(['id' => $rec->id]); + if ($rentity->get('bigbluebuttonbnid') != $instance->get_instance_id()) { + $rentity->create_imported_recording($instance); + } + } + } + } + + // Backup and restore steps. + $nbrecordings = count(self::RECORDINGS_DATA); + $newcourseid = $this->backup_and_restore($this->get_course()); + + // Now checks. + foreach (self::ALL_TYPES as $key => $type) { + $newbbb = + $DB->get_record('bigbluebuttonbn', + ['course' => $newcourseid, 'type' => $type, 'name' => 'BBB Activity:' . $key], + '*', + MUST_EXIST); // One record. + $this->assert_bbb_activities_same($bbactivity[$type], $newbbb); + $newinstance = instance::get_from_instanceid($newbbb->id); + + $instancerecordings = $newinstance->get_recordings(); + // Type ROOM_ONLY & TYPE_ALL : all assigned recordings (NB_RECORDINGS). + // Type TYPE_RECORDING_ONLY: all recordings from this course (i.e. + // existing recording (NB_RECORDING) + ROOM_ONLY(NB_RECORDING) + TYPE_ALL (NB_RECORDING)). + $expectedcount = $type == instance::TYPE_RECORDING_ONLY ? $nbrecordings * 3 : $nbrecordings; + // Type ROOM_ONLY & TYPE_ALL : The imported recording (NB_RECORDING*2 here) + // Type TYPE_RECORDING_ONLY: imported recordings we add the imported recording from the other activity (TYPE_ALL). + $expectedcount += $type == instance::TYPE_RECORDING_ONLY ? count($recordingstoimport) : 0; + // We managed to import recording in this activity, so let's add them. + $expectedcount += $newinstance->is_feature_enabled('importrecordings') ? count($recordingstoimport) : 0; + $this->assertCount($expectedcount, + $instancerecordings, 'Wrong count for instance Type:' . $key); + // Then check imported recordings. + foreach ($instancerecordings as $rec) { + if ($rec->get('imported')) { + $importeddata = json_decode($rec->get('importeddata')); + $this->assertNotEmpty($importeddata); + } + } + } + } + + /** + * Check two bbb activities are the same + * + * @param stdClass $bbboriginal + * @param stdClass $bbbdest + */ + protected function assert_bbb_activities_same(stdClass $bbboriginal, stdClass $bbbdest) { + $this->assertNotFalse($bbbdest); + $filterfunction = function($key) { + return !in_array($key, ['course', 'cmid', 'id', 'course']); + }; + $this->assertEquals( + array_filter((array) $bbboriginal, $filterfunction, ARRAY_FILTER_USE_KEY), + array_filter((array) $bbbdest, $filterfunction, ARRAY_FILTER_USE_KEY) + ); + } +} diff --git a/mod/bigbluebuttonbn/tests/behat/add_instance.feature b/mod/bigbluebuttonbn/tests/behat/add_instance.feature new file mode 100644 index 00000000000..a0dd51dfd5e --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/add_instance.feature @@ -0,0 +1,64 @@ +@mod @mod_bigbluebuttonbn @javascript +Feature: bigbluebuttonbn instance + In order to create a room activity with recordings + As a user + I need to add three room activities to an existent course + + Background: Make sure that a course is created + Given a BigBlueButton mock server is configured + And the following "courses" exist: + | fullname | shortname | category | + | Test course | Test course | 0 | + + Scenario: Add a mod_bigbluebuttonbn instance with Room/Activity with recordings + Given I am on the "Test course" "course" page logged in as "admin" + And I am on "Test course" course homepage with editing mode on + When I add a "BigBlueButton" to section "1" and I fill the form with: + | name | BBB Instance name | + | Instance type | Room/Activity with recordings | + | Virtual classroom name | BBB Instance name | + And I am on the "Test course" course page + Then I should see "BBB Instance name" + And I am on the "BBB Instance name" "bigbluebuttonbn activity" page + And I should see "This conference room is ready. You can join the session now." + And I should see "Join session" + And I should see "Recordings" + + Scenario: Add a mod_bigbluebuttonbn instance with Room/Activity only + Given I am on the "Test course" "course" page logged in as "admin" + And I am on "Test course" course homepage with editing mode on + When I add a "BigBlueButton" to section "1" and I fill the form with: + | Instance type | Room/Activity only | + | Virtual classroom name | BBB Instance name | + And I am on the "Test course" course page + Then I should see "BBB Instance name" + And I am on the "BBB Instance name" "bigbluebuttonbn activity" page + And I should see "This conference room is ready. You can join the session now." + And I should see "Join session" + And I should not see "Recordings" + + Scenario: Add a mod_bigbluebuttonbn instance with Recordings only + Given I am on the "Test course" "course" page logged in as "admin" + And I am on "Test course" course homepage with editing mode on + When I add a "BigBlueButton" to section "1" and I fill the form with: + | Instance type | Recordings only | + | Virtual classroom name | BBB Instance name | + And I am on the "Test course" course page + Then I should see "BBB Instance name" + And I am on the "BBB Instance name" "bigbluebuttonbn activity" page + And I should not see "This conference room is ready. You can join the session now." + And I should not see "Join session" + And I should see "Recordings" + + Scenario Outline: Add an activity and check that required settings are available for the three types of instance types + Given I am on the "Test course" "course" page logged in as "admin" + And I am on "Test course" course homepage with editing mode on + And I add a "BigBlueButton" to section "1" + When I select "" from the "Instance type" singleselect + Then I should see "Restrict access" + + Examples: + | type | + | Room/Activity with recordings | + | Room/Activity only | + | Recordings only | diff --git a/mod/bigbluebuttonbn/tests/behat/behat_mod_bigbluebuttonbn.php b/mod/bigbluebuttonbn/tests/behat/behat_mod_bigbluebuttonbn.php new file mode 100644 index 00000000000..08649f34c48 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/behat_mod_bigbluebuttonbn.php @@ -0,0 +1,179 @@ +. + +/** + * Behat custom steps and configuration for mod_bigbluebuttonbn. + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Moodle\BehatExtension\Exception\SkippedException; + +/** + * Behat custom steps and configuration for mod_bigbluebuttonbn. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_mod_bigbluebuttonbn extends behat_base { + + /** + * BeforeScenario hook to reset the remote testpoint. + * + * @BeforeScenario @mod_bigbluebuttonbn + * + * @param BeforeScenarioScope $scope + */ + public function before_scenario(BeforeScenarioScope $scope) { + if (defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { + $this->send_mock_request('backoffice/reset'); + } + } + + /** + * Check that the TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER is defined, so we can connect to the mock server. + * + * @Given /^a BigBlueButton mock server is configured$/ + */ + public function mock_is_configured(): void { + if (!defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { + throw new SkippedException( + 'The TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER constant must be defined to run mod_bigbluebuttonbn tests' + ); + } + + set_config('bigbluebuttonbn_server_url', TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER); + } + + /** + * Return the list of exact named selectors. + * + * @return array + */ + public static function get_exact_named_selectors(): array { + return [ + new behat_component_named_selector('Meeting field', [ + <<get_mocked_server_url($endpoint); + + $curl = new \curl(); + $curl->get($url->out_omit_querystring(), $url->params()); + } + + /** + * Convert page names to URLs for steps like 'When I am on the "[page name]" page'. + * + * Recognised page names are: + * | None so far! | | + * + * @param string $page name of the page, with the component name removed e.g. 'Admin notification'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_url(string $page): moodle_url { + throw new Exception("Unrecognised page type '{$page}'."); + } + + /** + * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. + * + * Recognised page names are: + * | pagetype | name meaning | description | + * | Index | BBB Course Index | The bbb index page (index.php) | + * + * @param string $type identifies which type of page this is, e.g. 'Indez'. + * @param string $identifier identifies the particular page, e.g. 'Mathematics 101'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_instance_url(string $type, string $identifier): moodle_url { + switch ($type) { + case 'Index': + $this->get_course_id($identifier); + return new moodle_url('/mod/bigbluebuttonbn/index.php', [ + 'id' => $this->get_course_id($identifier), + ]); + + default: + throw new Exception("Unrecognised page type '{$type}'."); + } + } + + /** + * Get course id from its identifier (shortname or fullname or idnumber) + * + * @param string $identifier + * @return int + * @throws dml_exception + */ + protected function get_course_id(string $identifier): int { + global $DB; + + return $DB->get_field_select( + 'course', + 'id', + "shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber", + [ + 'shortname' => $identifier, + 'fullname' => $identifier, + 'idnumber' => $identifier, + ], + MUST_EXIST + ); + } + + /** + * Trigger a recording ready notification on BBB side + * + * @Given the BigBlueButtonBN server has sent recording ready notifications + */ + public function trigger_recording_ready_notification(): void { + $this->send_mock_request('backoffice/sendNotifications'); + } + +} diff --git a/mod/bigbluebuttonbn/tests/behat/end_meeting.feature b/mod/bigbluebuttonbn/tests/behat/end_meeting.feature new file mode 100644 index 00000000000..28a1324b09d --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/end_meeting.feature @@ -0,0 +1,102 @@ +@mod @mod_bigbluebuttonbn +Feature: Test the ability to end a meeting + In order to ensure that end meeting capabilities are respected + As a teacher + I need to control who can end a meeting + + Background: + Given a BigBlueButton mock server is configured + + Scenario Outline: Only a BigBlueButton moderator can end a session + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | traverst | Terry | Travers | t.travers@example.com | + And the following "course enrolments" exist: + | user | course | role | + | traverst | C1 | | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + | moderators | | + And the following "mod_bigbluebuttonbn > meeting" exists: + | activity | Room recordings | + When I am on the "Room recordings" Activity page logged in as traverst + Then "End session" "link" exist + + Examples: + # Note: If the teacher is not listed as a moderator in the activity roles, then will not have permission to end the + # session. + | moderators | role | existence | + | | editingteacher | should not | + | | teacher | should not | + | | student | should not | + | role:teacher | student | should not | + | role:teacher | teacher | should | + | role:student | student | should | + | user:traverst | student | should | + + Scenario: An administrator can always end a meeting + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + And the following "mod_bigbluebuttonbn > meeting" exists: + | activity | Room recordings | + When I am on the "Room recordings" Activity page logged in as admin + Then "End session" "link" should exist + + Scenario Outline: Only a BigBlueButton moderator can end a session from the index page + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | traverst | Terry | Travers | t.travers@example.com | + And the following "course enrolments" exist: + | user | course | role | + | traverst | C1 | | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + | moderators | | + And the following "mod_bigbluebuttonbn > meeting" exists: + | activity | Room recordings | + When I am on the C1 "mod_bigbluebuttonbn > Index" page logged in as traverst + Then "End session" "link" exist + + Examples: + # Note: If the teacher is not listed as a moderator in the activity roles, then will not have permission to end the + # session. + | moderators | role | existence | + | | editingteacher | should not | + | | teacher | should not | + | | student | should not | + | role:teacher | student | should not | + | role:teacher | teacher | should | + | role:student | student | should | + | user:traverst | student | should | + + Scenario: An administrator can always end a meeting on the index page + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + And the following "mod_bigbluebuttonbn > meeting" exists: + | activity | Room recordings | + When I am on the C1 "mod_bigbluebuttonbn > Index" page logged in as admin + Then "End session" "link" should exist diff --git a/mod/bigbluebuttonbn/tests/behat/group_mode.feature b/mod/bigbluebuttonbn/tests/behat/group_mode.feature new file mode 100644 index 00000000000..3e54c9be11b --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/group_mode.feature @@ -0,0 +1,77 @@ +@mod @mod_bigbluebuttonbn @course +Feature: Test the module in group mode. + + Background: + # 1 = separate groups, we force the group + Given a BigBlueButton mock server is configured + And the following "courses" exist: + | fullname | shortname | category | groupmode | groupmodeforce | + | Test Course 1 | C1 | 0 | 1 | 1 | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | TeacherG1 | 1 | teacher1@example.com | + | teacher2 | TeacherG2 | 2 | teacher2@example.com | + | user1 | User1G1 | 1 | user1@example.com | + | user2 | User2G2 | 2 | user2@example.com | + | user3 | User3NoG2 | 3 | user3@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + | user1 | C1 | student | + | user2 | C1 | student | + | user3 | C1 | student | + And the following "group members" exist: + | user | group | + | teacher1 | G1 | + | teacher2 | G2 | + | user1 | G1 | + | user2 | G2 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | type | recordings_imported | + | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | + And the following "mod_bigbluebuttonbn > meeting" exists: + | activity | RoomRecordings | + And the following "mod_bigbluebuttonbn > meetings" exist: + | activity | group | + | RoomRecordings | G1 | + | RoomRecordings | G2 | + And the following "mod_bigbluebuttonbn > recordings" exist: + | bigbluebuttonbn | name | group | + | RoomRecordings | Recording G1 | G1 | + | RoomRecordings | Recording G2 | G2 | + And the following "mod_bigbluebuttonbn > recordings" exist: + | bigbluebuttonbn | name | + | RoomRecordings | Recording No group | + + @javascript + Scenario: When I create a BBB activity as a teacher who cannot access all groups, + I should only be able to select the group I belong on the main bigblue button page. + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/site:accessallgroups | Prevent | editingteacher | Course | C1 | + When I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as "teacher1" + Then I should see "Separate groups: Group 1" + + @javascript + Scenario: When I create a BBB activity as a teacher, I should only be able to specify individual "User" participants with whom I share a group with (or can view on the course participants screen). + When I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as "teacher1" + Then I should see "Group 1" in the "select[name='group']" "css_element" + And I should see "Group 2" in the "select[name='group']" "css_element" + + @javascript + Scenario Outline: When I view a BBB activity as a student, I should only be able to see Recordings from my group + When I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as "" + Then I "Recording G1" + And I "Recording G2" + And I "Recording No group" + + Examples: + | user | G1 | G2 | NO | + | user1 | should see | should not see | should not see | + | user2 | should not see | should see | should not see | + | user3 | should not see | should not see | should see | diff --git a/mod/bigbluebuttonbn/tests/behat/join_meeting.feature b/mod/bigbluebuttonbn/tests/behat/join_meeting.feature new file mode 100644 index 00000000000..96b5357cfc3 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/join_meeting.feature @@ -0,0 +1,38 @@ +@mod @mod_bigbluebuttonbn @javascript +Feature: Test the ability to run the full meeting lifecycle (start to end) + I can start a meeting then end it + + Background: + Given a BigBlueButton mock server is configured + + Scenario: Users should be able to join a meeting then end the meeting for themselves and + return to the meeting page to join again. + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | traverst | Terry | Travers | t.travers@example.com | + And the following "course enrolments" exist: + | user | course | role | + | traverst | C1 | student | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + | moderators | role:editingteacher | + | wait | 0 | + When I am on the "Room recordings" Activity page logged in as traverst + Then "Join session" "link" should exist + When I click on "Join session" "link" + And I switch to "bigbluebutton_conference" window + And I click on "End Meeting" "link" + # Selenium driver does not like the click action to be done before we + # automatically close the window so we need to make sure that the window + # is closed before. + And I close all opened windows + And I switch to the main window + And I reload the page + Then I should see "Room recordings" + And I should see "This conference room is ready. You can join the session now." diff --git a/mod/bigbluebuttonbn/tests/behat/meeting_roles.feature b/mod/bigbluebuttonbn/tests/behat/meeting_roles.feature new file mode 100644 index 00000000000..3a5ba0f3c33 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/meeting_roles.feature @@ -0,0 +1,39 @@ +@mod @mod_bigbluebuttonbn +Feature: Test that meeting roles are sent to the server + In order to ensure that start meeting capabilities are respected + As a teacher + I need meeting roles to be sent to the meeting + + Background: + Given a BigBlueButton mock server is configured + + @javascript + Scenario Outline: Users should receive the appropriate role when joining the meeting + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | traverst | Terry | Travers | t.travers@example.com | + And the following "course enrolments" exist: + | user | course | role | + | traverst | C1 | | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + | moderators | | + | viewers | | + When I am on the "Room recordings" Activity page logged in as traverst + And I click on "Join session" "link" + And I switch to "bigbluebutton_conference" window + Then I should see "" in the "attendeeRole" "mod_bigbluebuttonbn > Meeting field" + + Examples: + | moderators | viewers | moodlerole | meetingrole | + | | | student | VIEWER | + | role:editingteacher | | editingteacher | MODERATOR | + | role:student | | editingteacher | VIEWER | + | role:student | | student | MODERATOR | + | user:traverst | | student | MODERATOR | diff --git a/mod/bigbluebuttonbn/tests/behat/recordings.feature b/mod/bigbluebuttonbn/tests/behat/recordings.feature new file mode 100644 index 00000000000..b930270aadd --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/recordings.feature @@ -0,0 +1,84 @@ +@mod @mod_bigbluebuttonbn @core_form @course +Feature: The recording can be managed through the room page + As a user I am able to see the relevant recording for a given bigbluebutton activity and modify its parameters + + Background: Make sure that import recording is enabled and course, activities and recording exists + Given a BigBlueButton mock server is configured + And the following "courses" exist: + | fullname | shortname | category | + | Test Course 1 | C1 | 0 | + | Test Course 2 | C2 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | user1 | User | 1 | user1@example.com | + And the following "activities" exist: + | activity | name | intro | course | idnumber | type | recordings_imported | + | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | + And the following "mod_bigbluebuttonbn > meeting" exists: + | activity | RoomRecordings | + And the following "mod_bigbluebuttonbn > recordings" exist: + | bigbluebuttonbn | name | description | status | + | RoomRecordings | Recording 1 | Description 1 | 2 | + | RoomRecordings | Recording 2 | Description 2 | 3 | + | RoomRecordings | Recording 3 | Description 3 | 0 | + | RoomRecordings | Recording 4 | Description 4 | 1 | + + @javascript + Scenario: Recordings are not listed until the server informs that they are available + Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin + And I should not see "Recording 3" + When the BigBlueButtonBN server has sent recording ready notifications + And I reload the page + And I should see "Recording 3" + + @javascript + Scenario: Recordings are not listed until we can fetch their metadata, then they are listed + Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin + # Recording 3 will be fetched and metadata will be present so, we will see it. + And I should not see "Recording 3" + And I should not see "Recording 4" + When the BigBlueButtonBN server has sent recording ready notifications + And I reload the page + Then I should see "Recording 3" + And I should not see "Recording 4" + + @javascript + Scenario: I can see the recordings related to an activity + Given the BigBlueButtonBN server has sent recording ready notifications + When I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin + Then "Recording 1" "table_row" should exist + And "Recording 2" "table_row" should exist + + @javascript + Scenario: I can rename the recording + Given the BigBlueButtonBN server has sent recording ready notifications + And I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin + When I set the field "Edit Name" in the "Recording 1" "table_row" to "Recording with an updated name 1" + Then I should see "Recording with an updated name 1" + And I should see "Recording 2" + And I reload the page + And I should see "Recording with an updated name 1" + And I should see "Recording 2" + + @javascript + Scenario: I can set a new description for this recording + Given the BigBlueButtonBN server has sent recording ready notifications + And I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin + When I set the field "Edit Description" in the "Recording 1" "table_row" to "This is a new recording description 1" + Then I should see "This is a new recording description 1" + And I should see "Description 2" in the "Recording 2" "table_row" + And I reload the page + And I should see "This is a new recording description 1" in the "Recording 1" "table_row" + And I should see "Description 2" in the "Recording 2" "table_row" + + @javascript + Scenario: I can delete a recording + Given the BigBlueButtonBN server has sent recording ready notifications + And I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin + When I click on "a[data-action='delete']" "css_element" in the "Recording 1" "table_row" + And I click on "OK" "button" in the "Confirm" "dialogue" + Then I should not see "Recording 1" + And I should see "Recording 2" + And I reload the page + And I should not see "Recording 1" + And I should see "Recording 2" diff --git a/mod/bigbluebuttonbn/tests/behat/recordings_import.feature b/mod/bigbluebuttonbn/tests/behat/recordings_import.feature new file mode 100644 index 00000000000..4b9c69ba273 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/recordings_import.feature @@ -0,0 +1,111 @@ +@mod @mod_bigbluebuttonbn @core_form @course +Feature: Manage and list recordings + As a user I am able to import existing recording into another bigbluebutton activity + + Background: Make sure that import recording is enabled and course, activities and recording exists + Given a BigBlueButton mock server is configured + And the following config values are set as admin: + | bigbluebuttonbn_importrecordings_enabled | 1 | + | bigbluebuttonbn_importrecordings_from_deleted_enabled | 1 | + And the following "courses" exist: + | fullname | shortname | category | + | Test Course 1 | C1 | 0 | + | Test Course 2 | C2 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | user1 | User | 1 | user1@example.com | + And the following "activities" exist: + | activity | name | intro | course | idnumber | type | recordings_imported | + | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | + | bigbluebuttonbn | RoomRecordings1 | Test Recordings description 1 | C2 | bigbluebuttonbn2 | 0 | 1 | + | bigbluebuttonbn | RecordingOnly | Test Recordings only description 1 | C2 | bigbluebuttonbn3 | 2 | 1 | + And the following "mod_bigbluebuttonbn > meeting" exists: + | activity | RoomRecordings | + And the following "mod_bigbluebuttonbn > recordings" exist: + | bigbluebuttonbn | name | status | + | RoomRecordings | Recording 1 | 3 | + | RoomRecordings | Recording 2 | 3 | + + @javascript + Scenario: I check we display the right information (Recording Name as name and Description) + When I am on the "RoomRecordings1" "bigbluebuttonbn activity" page logged in as "admin" + # We check column names regarding changes made in CONTRIB-7703. + Then I should not see "Recording" in the ".mod_bigbluebuttonbn_recordings_table thead" "css_element" + And I should not see "Meeting" in the ".mod_bigbluebuttonbn_recordings_table thead" "css_element" + And I should see "Name" in the ".mod_bigbluebuttonbn_recordings_table thead" "css_element" + + @javascript + Scenario Outline: I check that I can import recordings into the Recording activity from other activities + When I am on the "" "bigbluebuttonbn activity" page logged in as "admin" + And I click on "Import recording links" "button" + And I select "Test Course 1 (C1)" from the "sourcecourseid" singleselect + And I select "RoomRecordings" from the "sourcebn" singleselect + # add the first recording + And I click on "a.action-icon" "css_element" in the "Recording 1" "table_row" + # add the second recording + And I click on "a.action-icon" "css_element" in the "Recording 2" "table_row" + And I click on "Go back" "button" + Then "Recording 1" "table_row" + And "Recording 2" "table_row" + Examples: + | instancename | existence | + | RecordingOnly | should exist | + | RoomRecordings1 | should exist | + + @javascript + Scenario: I check that I can import recordings into the Recording activity and then if I delete them + they are back into the pool to be imported again + When I am on the "RoomRecordings1" "bigbluebuttonbn activity" page logged in as "admin" + And I click on "Import recording links" "button" + And I select "Test Course 1 (C1)" from the "sourcecourseid" singleselect + And I select "RoomRecordings" from the "sourcebn" singleselect + # add the first recording + And I click on "a.action-icon" "css_element" in the "Recording 1" "table_row" + # add the second recording + And I click on "a.action-icon" "css_element" in the "Recording 2" "table_row" + And I wait until the page is ready + And I click on "Go back" "button" + # This should be refactored with the right classes for the table element + # We use javascript here to create the table so we don't get the same structure. + Then "Recording 1" "table_row" should exist + And I click on "a[data-action='delete']" "css_element" in the "Recording 1" "table_row" + And I click on "OK" "button" in the "Confirm" "dialogue" + # There is no confirmation dialog when deleting an imported record. + And I wait until the page is ready + Then I should not see "Recording 1" + And I click on "Import recording links" "button" + And I select "Test Course 1 (C1)" from the "sourcecourseid" singleselect + And I select "RoomRecordings" from the "sourcebn" singleselect + And I should see "Recording 1" + + @javascript @runonly + Scenario: I check that I can import recordings from a deleted instance into the Recording activity and then if I delete them + they are back into the pool to be imported again + Given I log in as "admin" + When I am on "Test Course 1" course homepage with editing mode on + And I delete "RoomRecordings" activity + # The activity is deleted asynchroneously. + And I run all adhoc tasks + Then I am on the "RoomRecordings1" "bigbluebuttonbn activity" page logged in as "admin" + And I click on "Import recording links" "button" + And I select "Recordings from deleted activities" from the "sourcecourseid" singleselect + Then I should see "Recording 1" + And I should see "Recording 2" + # add the first recording + Then I click on "a.action-icon" "css_element" in the "Recording 1" "table_row" + # add the second recording + And I click on "a.action-icon" "css_element" in the "Recording 2" "table_row" + And I wait until the page is ready + And I click on "Go back" "button" + # This should be refactored with the right classes for the table element + # We use javascript here to create the table so we don't get the same structure. + Then "Recording 1" "table_row" should exist + And I click on "a[data-action='delete']" "css_element" in the "Recording 1" "table_row" + And I click on "OK" "button" in the "Confirm" "dialogue" + # There is no confirmation dialog when deleting an imported record. + And I wait until the page is ready + Then I should not see "Recording 1" + And I click on "Import recording links" "button" + And I select "Recordings from deleted activities" from the "sourcecourseid" singleselect + And I should see "Recording 1" + And I should not see "Recording 2" diff --git a/mod/bigbluebuttonbn/tests/behat/start_meeting.feature b/mod/bigbluebuttonbn/tests/behat/start_meeting.feature new file mode 100644 index 00000000000..f6d3310aa99 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/start_meeting.feature @@ -0,0 +1,113 @@ +@mod @mod_bigbluebuttonbn +Feature: Test the ability to start a meeting + In order to ensure that start meeting capabilities are respected + As a teacher + I need to control who can start a meeting + + Background: + Given a BigBlueButton mock server is configured + + Scenario Outline: Users should be able to join a session depending on the Wait for moderator to join setting + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | traverst | Terry | Travers | t.travers@example.com | + And the following "course enrolments" exist: + | user | course | role | + | traverst | C1 | | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + | moderators | role:editingteacher | + | wait | | + When I am on the "Room recordings" Activity page logged in as traverst + Then "Join session" "link" exist + + Examples: + | wait | role | existence | + | 0 | student | should | + | 0 | teacher | should | + | 0 | editingteacher | should | + | 1 | student | should not | + | 1 | teacher | should not | + | 1 | editingteacher | should | + + Scenario Outline: Users should be able to join a session if a moderator has already joined + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | traverst | Terry | Travers | t.travers@example.com | + And the following "course enrolments" exist: + | user | course | role | + | traverst | C1 | | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + | moderators | role:editingteacher | + | wait | | + And the following "mod_bigbluebuttonbn > meeting" exists: + | activity | Room recordings | + | moderatorCount | 1 | + When I am on the "Room recordings" Activity page logged in as traverst + Then "Join session" "link" should exist + + Examples: + | wait | role | + | 0 | student | + | 0 | editingteacher | + | 1 | student | + | 1 | editingteacher | + + Scenario Outline: An administrator should always be allowed to join a meeting + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + | moderators | role:editingteacher | + | wait | | + When I am on the "Room recordings" Activity page logged in as admin + Then "Join session" "link" should exist + + Examples: + | wait | + | 0 | + | 1 | + + Scenario Outline: A student moderator should be able to start a meeting + Given the following course exists: + | name | Test course | + | shortname | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | traverst | Terry | Travers | t.travers@example.com | + And the following "course enrolments" exist: + | user | course | role | + | traverst | C1 | student | + And the following "activity" exists: + | course | C1 | + | activity | bigbluebuttonbn | + | name | Room recordings | + | idnumber | Room recordings | + | moderators | | + | wait | | + When I am on the "Room recordings" Activity page logged in as traverst + Then "Join session" "link" should exist + + Examples: + | wait | moderators | + | 0 | user:traverst | + | 1 | user:traverst | + | 0 | role:student | + | 1 | role:student | diff --git a/mod/bigbluebuttonbn/tests/completion_test.php b/mod/bigbluebuttonbn/tests/completion_test.php new file mode 100644 index 00000000000..931c2b8cbea --- /dev/null +++ b/mod/bigbluebuttonbn/tests/completion_test.php @@ -0,0 +1,218 @@ +. + +namespace mod_bigbluebuttonbn; + +use completion_info; +use context_module; +use mod_bigbluebuttonbn\completion\custom_completion; +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +/** + * Tests for Big Blue Button Completion. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @covers \mod_bigbluebuttonbn\completion\custom_completion + */ +class completion_test extends \advanced_testcase { + use testcase_helper_trait; + + /** + * Setup basic + */ + public function setUp(): void { + parent::setUp(); + set_config('enablecompletion', true); // Enable completion for all tests. + } + + /** + * Completion with no rules + */ + public function test_get_completion_state_no_rules() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $completion = new custom_completion($bbactivitycm, $user->id); + $result = $completion->get_overall_completion_state(); + // No custom rules so complete by default. + $this->assertEquals(COMPLETION_INCOMPLETE, $result); + } + + /** + * Completion with no rules and join meeting + */ + public function test_get_completion_state_no_rules_and_join_meeting() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Now create a couple of logs. + $instance = instance::get_from_instanceid($bbactivity->id); + logger::log_meeting_joined_event($instance, 0); + // No custom rules and we joined once so complete. + $completion = new custom_completion($bbactivitycm, $user->id); + $result = $completion->get_overall_completion_state(); + $this->assertEquals(COMPLETION_COMPLETE, $result); + } + + /** + * With state incomplete + */ + public function test_get_completion_state_incomplete() { + $this->resetAfterTest(); + + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + + $bbactivitycm->override_customdata('customcompletionrules', [ + 'completionengagementchats' => '1', + 'completionattendance' => '1' + ]); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $completion = new custom_completion($bbactivitycm, $user->id); + $result = $completion->get_overall_completion_state(); + $this->assertEquals(COMPLETION_INCOMPLETE, $result); + } + + /** + * With state complete + */ + public function test_get_completion_state_complete() { + $this->resetAfterTest(); + + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $instance = instance::get_from_instanceid($bbactivity->id); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Add a couple of fake logs. + $overrides = ['meetingid' => $bbactivity->meetingid]; + $meta = [ + 'origin' => 0, + 'data' => [ + 'duration' => 120, + 'engagement' => [ + 'chats' => 2, + 'talks' => 2, + ], + ], + ]; + logger::log_meeting_joined_event($instance, 0); + logger::log_event_summary($instance, $overrides, $meta); + logger::log_event_summary($instance, $overrides, $meta); + + // Now 2 x 120 mins of duration. + $completion = new custom_completion($bbactivitycm, $user->id); + $result = $completion->get_overall_completion_state(); + $this->assertEquals(COMPLETION_COMPLETE, $result); + } + + /** + * No rule description but active + */ + public function test_mod_bigbluebuttonbn_get_completion_active_rule_descriptions() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't. + // Inspired from the same test in forum. + list($bbactivitycontext, $cm1, $bbactivity) = $this->create_instance($this->get_course(), + ['completion' => '2', 'completionattendance' => '1']); + list($bbactivitycontext, $cm2, $bbactivity) = $this->create_instance($this->get_course(), + ['completion' => '2', 'completionattendance' => '0']); + + // Data for the stdClass input type. + // This type of input would occur when checking the default completion rules for an activity type, where we don't have + // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info. + $moddefaults = (object) [ + 'customdata' => [ + 'customcompletionrules' => [ + 'completionsubmit' => '1', + ], + ], + 'completion' => 2, + ]; + + $completioncm1 = new custom_completion($cm1, $user->id); + // TODO: check the return value here as there might be an issue with the function compared to the forum for example. + $this->assertEquals( + [ + 'completionengagementchats' => get_string('completionengagementchats_desc', 'mod_bigbluebuttonbn', 1), + 'completionengagementtalks' => get_string('completionengagementtalks_desc', 'mod_bigbluebuttonbn', 1), + 'completionattendance' => get_string('completionattendance_desc', 'mod_bigbluebuttonbn', 1), + 'completionengagementraisehand' => get_string('completionengagementraisehand_desc', 'mod_bigbluebuttonbn', 1), + 'completionengagementpollvotes' => get_string('completionengagementpollvotes_desc', 'mod_bigbluebuttonbn', 1), + 'completionengagementemojis' => get_string('completionengagementemojis_desc', 'mod_bigbluebuttonbn', 1) + ], + $completioncm1->get_custom_rule_descriptions()); + $completioncm2 = new custom_completion($cm2, $user->id); + $this->assertEquals( + [ + 'completionengagementchats' => get_string('completionengagementchats_desc', 'mod_bigbluebuttonbn', 1), + 'completionengagementtalks' => get_string('completionengagementtalks_desc', 'mod_bigbluebuttonbn', 1), + 'completionattendance' => get_string('completionattendance_desc', 'mod_bigbluebuttonbn', 0), + 'completionengagementraisehand' => get_string('completionengagementraisehand_desc', 'mod_bigbluebuttonbn', 1), + 'completionengagementpollvotes' => get_string('completionengagementpollvotes_desc', 'mod_bigbluebuttonbn', 1), + 'completionengagementemojis' => get_string('completionengagementemojis_desc', 'mod_bigbluebuttonbn', 1) + ], $completioncm2->get_custom_rule_descriptions()); + } + + /** + * Completion View + */ + public function test_view() { + $this->resetAfterTest(); + $this->setAdminUser(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance( + null, ['completion' => 2, 'completionview' => 1] + ); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + + bigbluebuttonbn_view($bbactivity, $this->get_course(), $bbactivitycm, context_module::instance($bbactivitycm->id)); + + $events = $sink->get_events(); + $this->assertTrue(count($events) > 1); // TODO : Here we have the module completion event triggered twice. + // this might be a bug from 4.0 core and will need some further investigation. + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_bigbluebuttonbn\event\activity_viewed', $event); + $this->assertEquals($bbactivitycontext, $event->get_context()); + $url = new \moodle_url('/mod/bigbluebuttonbn/view.php', ['id' => $bbactivitycontext->instanceid]); + $this->assertEquals($url, $event->get_url()); + $this->assertEventContextNotUsed($event); + $this->assertNotEmpty($event->get_name()); + + // Check completion status. + $completion = new completion_info($this->get_course()); + $completiondata = $completion->get_data($bbactivitycm); + $this->assertEquals(1, $completiondata->viewed); + $this->assertEquals(COMPLETION_INCOMPLETE, $completiondata->completionstate); + } +} diff --git a/mod/bigbluebuttonbn/tests/coverage.php b/mod/bigbluebuttonbn/tests/coverage.php new file mode 100644 index 00000000000..3f7b39351f8 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/coverage.php @@ -0,0 +1,55 @@ +. +/** + * Coverage information for the mod_bigbluebuttonbn component. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class bbb_coverage information for the core subsystem. + * + * Note that we had to change the definition of this class due to a bug in local_moodlecheck + * https://github.com/moodlehq/moodle-local_moodlecheck/issues/50 + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +class bbb_coverage extends phpunit_coverage_info { + /** @var array The list of folders relative to the plugin root to includelist in coverage generation. */ + protected $includelistfolders = [ + 'classes', + 'backup', + ]; + + /** @var array The list of files relative to the plugin root to includelist in coverage generation. */ + protected $includelistfiles = [ + 'lib.php', + ]; + + /** @var array The list of folders relative to the plugin root to excludelist in coverage generation. */ + protected $excludelistfolders = []; + + /** @var array The list of files relative to the plugin root to excludelist in coverage generation. */ + protected $excludelistfiles = []; +} +return new bbb_coverage; diff --git a/mod/bigbluebuttonbn/tests/external/can_join_test.php b/mod/bigbluebuttonbn/tests/external/can_join_test.php new file mode 100644 index 00000000000..b27cfc80805 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/external/can_join_test.php @@ -0,0 +1,123 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the update_course class. + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_bigbluebuttonbn\external\can_join + */ +class can_join_test extends \externallib_advanced_testcase { + use testcase_helper_trait; + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + } + + /** + * Helper + * + * @param ... $params + * @return mixed + */ + protected function can_join(...$params) { + $canjoin = can_join::execute(...$params); + + return external_api::clean_returnvalue(can_join::execute_returns(), $canjoin); + } + + /** + * Test execute API CALL with no instance + */ + public function test_execute_no_instance() { + $canjoin = $this->can_join(1234, 5678); + + $this->assertIsArray($canjoin); + $this->assertArrayHasKey('can_join', $canjoin); + $this->assertEquals(false, $canjoin['can_join']); + } + + /** + * Test execute API CALL without login + */ + public function test_execute_without_login() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $this->expectException(moodle_exception::class); + $this->can_join($instance->get_cm_id()); + } + + /** + * Test execute API CALL with invalid login + */ + public function test_execute_with_invalid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_user(); + $this->setUser($user); + + $this->expectException(moodle_exception::class); + $this->can_join($instance->get_cm_id()); + } + + /** + * When login as a student + */ + public function test_execute_with_valid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_and_enrol($course, 'student'); + $this->setUser($user); + + $canjoin = $this->can_join($instance->get_cm_id()); + + $this->assertIsArray($canjoin); + $this->assertArrayHasKey('can_join', $canjoin); + $this->assertEquals(true, $canjoin['can_join']); + } +} diff --git a/mod/bigbluebuttonbn/tests/external/completion_validate_test.php b/mod/bigbluebuttonbn/tests/external/completion_validate_test.php new file mode 100644 index 00000000000..df402f082f5 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/external/completion_validate_test.php @@ -0,0 +1,154 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use require_login_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the completion_validate class. + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2021 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @coversDefaultClass \mod_bigbluebuttonbn\external\completion_validate + */ +class completion_validate_test extends \externallib_advanced_testcase { + use testcase_helper_trait; + + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + } + + /** + * Helper + * + * @param ... $params + * @return mixed + */ + protected function completion_validate(...$params) { + $returnvalue = completion_validate::execute(...$params); + + return external_api::clean_returnvalue(completion_validate::execute_returns(), $returnvalue); + } + + /** + * Test execute API CALL with no instance + */ + public function test_execute_no_instance() { + $result = $this->completion_validate(1234); + + $this->assertIsArray($result); + $this->assertArrayHasKey('warnings', $result); + $this->assertEquals([ + 'item' => 'mod_bigbluebuttonbn', + 'itemid' => 1234, + 'warningcode' => 'indexerrorbbtn', + 'message' => 'BigBlueButton ID 1234 is incorrect', + ], $result['warnings'][0]); + } + + /** + * Test execute API CALL without login + */ + public function test_execute_without_login() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + $this->expectException(require_login_exception::class); + $this->completion_validate($instance->get_instance_id()); + } + + /** + * Test execute API CALL with invalid login + */ + public function test_execute_with_invalid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_user($course); + $this->setUser($user); + $this->expectException(require_login_exception::class); + $this->completion_validate($instance->get_instance_id()); + } + + /** + * When login as a student + */ + public function test_execute_with_valid_login_but_student() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_and_enrol($course, 'student'); + $this->setUser($user); + + $returnvalue = $this->completion_validate($instance->get_instance_id()); + + $this->assertArrayHasKey('warnings', $returnvalue); + + $this->assertEquals([ + 'item' => 'mod_bigbluebuttonbn', + 'itemid' => $record->id, + 'warningcode' => 'nopermissions', + 'message' => 'Sorry, but you do not currently have permissions to do that (completion_validate).', + ], $returnvalue['warnings'][0]); + } + + /** + * When login as a student + */ + public function test_execute_with_valid_login_with_teacher() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_and_enrol($course, 'editingteacher'); + $this->setUser($user); + + $returnvalue = $this->completion_validate($instance->get_instance_id()); + + $this->assertArrayHasKey('warnings', $returnvalue); + $this->assertEmpty($returnvalue['warnings']); + } +} + diff --git a/mod/bigbluebuttonbn/tests/external/end_meeting_test.php b/mod/bigbluebuttonbn/tests/external/end_meeting_test.php new file mode 100644 index 00000000000..ce776aa5c0a --- /dev/null +++ b/mod/bigbluebuttonbn/tests/external/end_meeting_test.php @@ -0,0 +1,171 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\meeting; +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use moodle_exception; +use require_login_exception; +use restricted_context_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the update_course class. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_bigbluebuttonbn\external\end_meeting + */ +class end_meeting_test extends \externallib_advanced_testcase { + use testcase_helper_trait; + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + } + + /** + * Helper + * + * @param ... $params + * @return array|bool|mixed + */ + protected function end_meeting(...$params) { + $returnvalue = end_meeting::execute(...$params); + + return external_api::clean_returnvalue(end_meeting::execute_returns(), $returnvalue); + } + + /** + * Test execute API CALL with no instance + */ + public function test_execute_no_instance() { + $this->expectException(moodle_exception::class); + $endmeeting = $this->end_meeting(1234, 5678); + } + + /** + * Test execute API CALL without login + */ + public function test_execute_without_login() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $this->expectException(require_login_exception::class); + $this->end_meeting($instance->get_instance_id(), 0); + } + + /** + * Test execute API CALL with invalid login + */ + public function test_execute_with_invalid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_user(); + $this->setUser($user); + + $this->expectException(require_login_exception::class); + $this->end_meeting($instance->get_instance_id(), 0); + } + + /** + * When login as a student + */ + public function test_execute_with_student_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_and_enrol($course, 'student'); + $this->setUser($user); + + $this->expectException(restricted_context_exception::class); + $this->end_meeting($instance->get_instance_id(), 0); + } + + /** + * Test execute admin logic + */ + public function test_execute_with_admin_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $plugingenerator = $generator->get_plugin_generator('mod_bigbluebuttonbn'); + $plugingenerator->create_meeting([ + 'instanceid' => $instance->get_instance_id(), + ]); + + $this->setAdminUser(); + + $result = $this->end_meeting($instance->get_instance_id(), 0); + $this->assertIsArray($result); + + // TODO Check that the meeting was ended on the remote. + } + /** + * Test execute admin logic + */ + public function test_execute_end_meeting_already_ended() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $plugingenerator = $generator->get_plugin_generator('mod_bigbluebuttonbn'); + $plugingenerator->create_meeting([ + 'instanceid' => $instance->get_instance_id(), + ]); + + // Then end the meeting. + // Execute the end command. + $meeting = new meeting($instance); + $meeting->end_meeting(); + + $this->setAdminUser(); + + $result = $this->end_meeting($instance->get_instance_id(), 0); + $this->assertIsArray($result); + + // TODO Check that the meeting was ended on the remote. + } +} diff --git a/mod/bigbluebuttonbn/tests/external/get_bigbluebuttons_by_courses_test.php b/mod/bigbluebuttonbn/tests/external/get_bigbluebuttons_by_courses_test.php new file mode 100644 index 00000000000..d0f048e2df1 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/external/get_bigbluebuttons_by_courses_test.php @@ -0,0 +1,179 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the get_bigbluebuttons_by_courses class. + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2021 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @coversDefaultClass \mod_bigbluebuttonbn\external\get_bigbluebuttonbns_by_courses + */ +class get_bigbluebuttons_by_courses_test extends \externallib_advanced_testcase { + use testcase_helper_trait; + + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + } + + /** + * Helper + * + * @param ... $params + * @return mixed + */ + protected function get_bigbluebuttons_by_courses(...$params) { + $returnvalue = get_bigbluebuttonbns_by_courses::execute(...$params); + + return external_api::clean_returnvalue(get_bigbluebuttonbns_by_courses::execute_returns(), $returnvalue); + } + + /** + * Test execute API CALL with no instance + */ + public function test_execute_no_instance() { + $bbbactivities = $this->get_bigbluebuttons_by_courses([1234, 5678]); + + $this->assertIsArray($bbbactivities); + $this->assertArrayHasKey('bigbluebuttonbns', $bbbactivities); + $this->assertArrayHasKey('warnings', $bbbactivities); + $this->assertEmpty($bbbactivities['bigbluebuttonbns']); + } + + /** + * Test execute API CALL without login + */ + public function test_execute_without_login() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $this->expectException(moodle_exception::class); + $this->get_bigbluebuttons_by_courses($instance->get_cm_id()); + } + + /** + * Test execute API CALL with invalid login + */ + public function test_execute_with_invalid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_user(); + $this->setUser($user); + + $this->expectException(moodle_exception::class); + $this->get_bigbluebuttons_by_courses($instance->get_cm_id()); + } + + /** + * Test get bbbactivities + */ + public function test_execute_with_valid_login() { + $this->resetAfterTest(true); + $generator = $this->getDataGenerator(); + + // Create a user. + $user = $generator->create_user(); + + // Create courses to add the modules. + $course1 = $generator->create_course(); + $course2 = $generator->create_course(); + + $bbbs = []; + // First bbb activity. + $bbbs[] = $generator->create_module('bigbluebuttonbn', ['course' => $course1->id]); + + // Second bbb activity. + $bbbs[] = $generator->create_module('bigbluebuttonbn', ['course' => $course2->id]); + + $generator->enrol_user($user->id, $course1->id, null, 'manual'); + $generator->enrol_user($user->id, $course2->id, null, 'manual'); + // Set to the user. + self::setUser($user); + + // Call the external function passing course ids. + $bbbactivities = $this->get_bigbluebuttons_by_courses([$course1->id, $course2->id]); + $this->assert_same_bbb_activities($bbbs, $bbbactivities['bigbluebuttonbns']); + + // Call the external function without passing course id. + $bbbactivities = $this->get_bigbluebuttons_by_courses(); + $this->assert_same_bbb_activities($bbbs, $bbbactivities['bigbluebuttonbns']); + + // Unenrol user from second course and alter expected bbb activity. + $enrol = enrol_get_plugin('manual'); + $enrolinstances = enrol_get_instances($course2->id, true); + foreach ($enrolinstances as $courseenrolinstance) { + if ($courseenrolinstance->enrol == "manual") { + $instance2 = $courseenrolinstance; + break; + } + } + $enrol->unenrol_user($instance2, $user->id); + + // Call the external function without passing course id. + $bbbactivities = $this->get_bigbluebuttons_by_courses(); + $this->assertCount(1, $bbbactivities['bigbluebuttonbns']); + $this->assertEquals($bbbs[0]->id, $bbbactivities['bigbluebuttonbns'][0]['id']); + + // Call for the second course we unenrolled the user from. + $bbbactivities = $this->get_bigbluebuttons_by_courses([$course2->id]); + $this->assertCount(0, $bbbactivities['bigbluebuttonbns']); + } + + /** + * Check if the two arrays containing the activities are the same. + * + * @param mixed $expected + * @param mixed $actual + */ + protected function assert_same_bbb_activities($expected, $actual) { + $this->assertCount(count($expected), $actual); + $getids = function($bbb) { + return is_array($bbb) ? $bbb['id'] : $bbb->id; + }; + $expectedids = array_map($getids, $expected); + $actualid = array_map($getids, $actual); + sort($expectedids); + sort($actualid); + $this->assertEquals($expectedids, $actualid); + } +} + diff --git a/mod/bigbluebuttonbn/tests/external/get_join_url_test.php b/mod/bigbluebuttonbn/tests/external/get_join_url_test.php new file mode 100644 index 00000000000..93889a272f0 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/external/get_join_url_test.php @@ -0,0 +1,234 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\config; +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the get_join_url class. + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2021 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @coversDefaultClass \mod_bigbluebuttonbn\external\get_join_url + */ +class get_join_url_test extends \externallib_advanced_testcase { + use testcase_helper_trait; + + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + } + + /** + * Helper + * + * @param ... $params + * @return mixed + */ + protected function get_join_url(...$params) { + $getjoinurl = get_join_url::execute(...$params); + + return external_api::clean_returnvalue(get_join_url::execute_returns(), $getjoinurl); + } + + /** + * Test execute API CALL with no instance + */ + public function test_execute_no_instance() { + $this->expectExceptionMessageMatches('/No such an instance.*/'); + $joinurl = $this->get_join_url(1234, 5678); + + $this->assertIsArray($joinurl); + $this->assertArrayNotHasKey('join_url', $joinurl); + $this->assertEquals(false, $joinurl['join_url']); + } + + /** + * Test execute API CALL without login + */ + public function test_execute_without_login() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $this->expectException(moodle_exception::class); + $this->get_join_url($instance->get_cm_id()); + } + + /** + * Test execute API CALL with invalid login + */ + public function test_execute_with_invalid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_user(); + $this->setUser($user); + + $this->expectException(moodle_exception::class); + $this->get_join_url($instance->get_cm_id()); + } + + /** + * When login as a student + */ + public function test_execute_with_valid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_and_enrol($course, 'student'); + $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + // Make sure the meeting is running (this is not the default with the mock server). + $bbbgenerator->create_meeting([ + 'instanceid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id() + ]); + + $this->setUser($user); + + $joinurl = $this->get_join_url($instance->get_cm_id()); + + $this->assertIsArray($joinurl); + $this->assertArrayHasKey('join_url', $joinurl); + $this->assertEmpty($joinurl['warnings']); + $this->assertNotNull($joinurl['join_url']); + } + + /** + * Check that URL are different depending on the group. + */ + public function test_execute_with_group() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $g1 = $generator->create_group(['courseid' => $course->id]); + $g2 = $generator->create_group(['courseid' => $course->id]); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_and_enrol($course, 'student'); + $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + // Make sure the meeting is running (this is not the default with the mock server). + $bbbgenerator->create_meeting([ + 'instanceid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id() + ]); + + $this->setUser($user); + + $joinurlnogroup = $this->get_join_url($instance->get_cm_id()); + $joinurlnogroupv2 = $this->get_join_url($instance->get_cm_id()); + $joinurlg1 = $this->get_join_url($instance->get_cm_id(), $g1->id); + $joinurlg2 = $this->get_join_url($instance->get_cm_id(), $g2->id); + + foreach ([$joinurlnogroup, $joinurlnogroupv2, $joinurlg1, $joinurlg2] as $join) { + $this->assertIsArray($join); + $this->assertArrayHasKey('join_url', $join); + $this->assertEmpty($join['warnings']); + $this->assertNotNull($join['join_url']); + } + $this->assertNotEquals($joinurlnogroup['join_url'], $joinurlg1['join_url']); + $this->assertNotEquals($joinurlg2['join_url'], $joinurlg1['join_url']); + } + + /** + * Check that we return the same URL once meeting is started. + */ + public function test_execute_with_same_url() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_and_enrol($course, 'student'); + + $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + // Make sure the meeting is running (this is not the default with the mock server). + $bbbgenerator->create_meeting([ + 'instanceid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id() + ]); + + $this->setUser($user); + + $joinurl = $this->get_join_url($instance->get_cm_id()); + $joinurlv2 = $this->get_join_url($instance->get_cm_id()); + + foreach ([$joinurl, $joinurlv2] as $join) { + $this->assertIsArray($join); + $this->assertArrayHasKey('join_url', $join); + $this->assertEmpty($join['warnings']); + $this->assertNotNull($join['join_url']); + } + $this->assertEquals($joinurl['join_url'], $joinurlv2['join_url']); + } + + /** + * Check that we return the same URL once meeting is started. + */ + public function test_user_limit() { + $this->resetAfterTest(); + set_config('bigbluebuttonbn_userlimit_editable', true); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id, 'userlimit' => 2]); + + $user = $generator->create_and_enrol($course, 'student'); + $instance = instance::get_from_instanceid($record->id); + + $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + // Make sure the meeting is running (this is not the default with the mock server). + $bbbgenerator->create_meeting([ + 'instanceid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id(), + 'participants' => 2 + ]); + $this->setUser($user); + $joinurl = $this->get_join_url($instance->get_cm_id()); + $this->assertNotNull($joinurl['warnings']); + $this->assertEquals('userlimitreached', $joinurl['warnings'][0]['warningcode']); + } +} + diff --git a/mod/bigbluebuttonbn/tests/external/get_recordings_test.php b/mod/bigbluebuttonbn/tests/external/get_recordings_test.php new file mode 100644 index 00000000000..d6cd2b5fd85 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/external/get_recordings_test.php @@ -0,0 +1,358 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use require_login_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the update_course class. + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2021 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @coversDefaultClass \mod_bigbluebuttonbn\external\get_recordings + */ +class get_recordings_test extends \externallib_advanced_testcase { + use testcase_helper_trait; + + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + } + + /** + * Helper + * + * @param ... $params + * @return array|bool|mixed + */ + protected function get_recordings(...$params) { + $recordings = get_recordings::execute(...$params); + + return external_api::clean_returnvalue(get_recordings::execute_returns(), $recordings); + } + + /** + * Test execute API CALL with no instance + */ + public function test_execute_wrong_instance() { + $getrecordings = $this->get_recordings(1234); + + $this->assertIsArray($getrecordings); + $this->assertArrayHasKey('status', $getrecordings); + $this->assertEquals(false, $getrecordings['status']); + $this->assertStringContainsString('nosuchinstance', $getrecordings['warnings'][0]['warningcode']); + } + + /** + * Test execute API CALL without login + */ + public function test_execute_without_login() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $this->expectException(require_login_exception::class); + $this->get_recordings($instance->get_instance_id()); + } + + /** + * Test execute API CALL with invalid login + */ + public function test_execute_with_invalid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_user(); + $this->setUser($user); + + $this->expectException(require_login_exception::class); + $this->get_recordings($instance->get_instance_id()); + } + + /** + * When login as a student + */ + public function test_execute_with_valid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_and_enrol($course, 'student'); + $this->setUser($user); + + $getrecordings = $this->get_recordings($instance->get_instance_id()); + + $this->assertIsArray($getrecordings); + $this->assertArrayHasKey('status', $getrecordings); + $this->assertEquals(true, $getrecordings['status']); + $this->assertNotEmpty($getrecordings['tabledata']); + $this->assertEquals($getrecordings['tabledata']['data'], '[]'); + } + + /** + * Check if tools are present for teacher/moderator + */ + public function test_get_recordings_tools() { + $this->resetAfterTest(); + $dataset = [ + 'type' => instance::TYPE_ALL, + 'groups' => null, + 'users' => [['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student']], + 'recordingsdata' => [ + [['name' => 'Recording1']], + [['name' => 'Recording2']] + ], + ]; + $activityid = $this->create_from_dataset($dataset); + $instance = instance::get_from_instanceid($activityid); + + $context = \context_course::instance($instance->get_course_id()); + foreach ($dataset['users'] as $userdef) { + $user = \core_user::get_user_by_username($userdef['username']); + $this->setUser($user); + $getrecordings = $this->get_recordings($instance->get_instance_id()); + // Check users see or do not see recording dependings on their groups. + foreach ($dataset['recordingsdata'] as $recordingdata) { + foreach ($recordingdata as $recording) { + if (has_capability('moodle/course:update', $context)) { + $this->assertStringContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], + "User $user->username, should be able to delete the recording {$recording['name']}"); + $this->assertStringContainsString('data-action=\"publish\"', $getrecordings['tabledata']['data'], + "User $user->username, should be able to publish the recording {$recording['name']}"); + } else { + $this->assertStringNotContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], + "User $user->username, should not be able to delete the recording {$recording['name']}"); + } + } + } + } + // Now without delete. + foreach ($dataset['users'] as $userdef) { + $user = \core_user::get_user_by_username($userdef['username']); + $this->setUser($user); + $getrecordings = $this->get_recordings($instance->get_instance_id(), 'protect'); + // Check users see or do not see recording dependings on their groups. + foreach ($dataset['recordingsdata'] as $recordingdata) { + foreach ($recordingdata as $recording) { + $this->assertStringNotContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], + "User $user->username, should not be able to delete the recording {$recording['name']}"); + } + } + } + } + + /** + * Check preview is present and displayed + */ + public function test_get_recordings_preview() { + $this->resetAfterTest(); + $dataset = [ + 'type' => instance::TYPE_ALL, + 'additionalsettings' => [ + 'recordings_preview' => 1 + ], + 'groups' => null, + 'users' => [['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student']], + 'recordingsdata' => [ + [['name' => 'Recording1']], + [['name' => 'Recording2']] + ], + ]; + $activityid = $this->create_from_dataset($dataset); + $instance = instance::get_from_instanceid($activityid); + + $context = \context_course::instance($instance->get_course_id()); + foreach ($dataset['users'] as $userdef) { + $user = \core_user::get_user_by_username($userdef['username']); + $this->setUser($user); + $getrecordings = $this->get_recordings($instance->get_instance_id()); + $this->assertNotEmpty($getrecordings['tabledata']['columns']['3']); + $this->assertEquals('preview', $getrecordings['tabledata']['columns']['3']['key']); + } + } + + /** + * Check we can see all recording from a course in a room only instance + */ + public function test_get_recordings_room_only() { + $this->resetAfterTest(); + set_config('bigbluebuttonbn_importrecordings_enabled', 1); + $dataset = [ + 'type' => instance::TYPE_ALL, + 'groups' => null, + 'users' => [['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student']], + 'recordingsdata' => [ + [['name' => 'Recording1']], + [['name' => 'Recording2']] + ], + ]; + $activityid = $this->create_from_dataset($dataset); + $instance = instance::get_from_instanceid($activityid); + + // Now create a recording only activity. + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + // Now create a new activity and import the first record. + $newactivity = $plugingenerator->create_instance([ + 'course' => $instance->get_course_id(), + 'type' => instance::TYPE_RECORDING_ONLY, + 'name' => 'Example 2' + ]); + $plugingenerator->create_meeting([ + 'instanceid' => $newactivity->id, + ]); // We need to have a meeting created in order to import recordings. + $newinstance = instance::get_from_instanceid($newactivity->id); + $this->create_recordings_for_instance($newinstance, [['name' => 'Recording3']]); + + foreach ($dataset['users'] as $userdef) { + $user = \core_user::get_user_by_username($userdef['username']); + $this->setUser($user); + $getrecordings = $this->get_recordings($newinstance->get_instance_id()); + // Check users see or do not see recording dependings on their groups. + $data = json_decode($getrecordings['tabledata']['data']); + $this->assertCount(3, $data); + } + } + + /** + * Check if we can see the imported recording in a new instance + */ + public function test_get_recordings_imported() { + $this->resetAfterTest(); + set_config('bigbluebuttonbn_importrecordings_enabled', 1); + $dataset = [ + 'type' => instance::TYPE_ALL, + 'groups' => null, + 'users' => [['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student']], + 'recordingsdata' => [ + [['name' => 'Recording1']], + [['name' => 'Recording2']] + ], + ]; + + $activityid = $this->create_from_dataset($dataset); + $instance = instance::get_from_instanceid($activityid); + + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + // Now create a new activity and import the first record. + $newactivity = $plugingenerator->create_instance([ + 'course' => $instance->get_course_id(), + 'type' => instance::TYPE_ALL, + 'name' => 'Example 2' + ]); + $plugingenerator->create_meeting([ + 'instanceid' => $newactivity->id, + ]); // We need to have a meeting created in order to import recordings. + $newinstance = instance::get_from_instanceid($newactivity->id); + $recordings = $instance->get_recordings(); + foreach ($recordings as $recording) { + if ($recording->get('name') == 'Recording1') { + $recording->create_imported_recording($newinstance); + } + } + + foreach ($dataset['users'] as $userdef) { + $user = \core_user::get_user_by_username($userdef['username']); + $this->setUser($user); + $getrecordings = $this->get_recordings($newinstance->get_instance_id()); + // Check users see or do not see recording dependings on their groups. + foreach ($dataset['recordingsdata'] as $index => $recordingdata) { + foreach ($recordingdata as $recording) { + if ($instance->can_manage_recordings()) { + $this->assertStringContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], + "User $user->username, should be able to delete the recording {$recording['name']}"); + } else { + $this->assertStringNotContainsString('data-action=\"delete\"', $getrecordings['tabledata']['data'], + "User $user->username, should not be able to delete the recording {$recording['name']}"); + } + if ($index === 0) { + $this->assertStringContainsString($recording['name'], $getrecordings['tabledata']['data']); + } else { + $this->assertStringNotContainsString($recording['name'], $getrecordings['tabledata']['data']); + } + } + } + + } + } + + /** + * Check if recording are visible/invisible depending on the group. + */ + public function test_get_recordings_groups_visible() { + $this->resetAfterTest(); + $dataset = [ + 'type' => instance::TYPE_ALL, + 'groups' => ['G1' => ['s1'], 'G2' => ['s2']], + 'users' => [['username' => 't1', 'role' => 'editingteacher'], ['username' => 's1', 'role' => 'student'], + ['username' => 's2', 'role' => 'student']], + 'recordingsdata' => [ + 'G1' => [['name' => 'Recording1']], + 'G2' => [['name' => 'Recording2']] + ], + ]; + $activityid = $this->create_from_dataset($dataset); + $instance = instance::get_from_instanceid($activityid); + + foreach ($dataset['users'] as $userdef) { + $user = \core_user::get_user_by_username($userdef['username']); + $this->setUser($user); + $groups = array_values(groups_get_my_groups()); + $usergroupnames = array_map(function($g) { + return $g->name; + }, $groups); + $mygroup = !empty($groups) ? end($groups) : null; + + $getrecordings = $this->get_recordings( + $instance->get_instance_id(), null, !empty($mygroup) ? $mygroup->id : null); + // Check users see or do not see recording dependings on their groups. + foreach ($dataset['recordingsdata'] as $groupname => $recordingdata) { + foreach ($recordingdata as $recording) { + if (in_array($groupname, $usergroupnames) || $instance->can_manage_recordings()) { + $this->assertStringContainsString($recording['name'], $getrecordings['tabledata']['data'], + "User $user->username, should see recording {$recording['name']}"); + } else { + $this->assertStringNotContainsString($recording['name'], $getrecordings['tabledata']['data'], + "User $user->username, should not see recording {$recording['name']}"); + } + } + } + } + } +} diff --git a/mod/bigbluebuttonbn/tests/external/view_bigbluebuttonbn_test.php b/mod/bigbluebuttonbn/tests/external/view_bigbluebuttonbn_test.php new file mode 100644 index 00000000000..3f33b4cf16c --- /dev/null +++ b/mod/bigbluebuttonbn/tests/external/view_bigbluebuttonbn_test.php @@ -0,0 +1,126 @@ +. + +namespace mod_bigbluebuttonbn\external; + +use external_api; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use moodle_exception; +use require_login_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the view_bigbluebuttonbn class. + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2021 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @coversDefaultClass \mod_bigbluebuttonbn\external\view_bigbluebuttonbn + */ +class view_bigbluebuttonbn_test extends \externallib_advanced_testcase { + use testcase_helper_trait; + + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + } + + /** + * Helper + * + * @param ... $params + * @return mixed + */ + protected function view_bigbluebuttonbn(...$params) { + $returnvalue = view_bigbluebuttonbn::execute(...$params); + + return external_api::clean_returnvalue(view_bigbluebuttonbn::execute_returns(), $returnvalue); + } + + /** + * Test execute API CALL with no instance + */ + public function test_execute_no_instance() { + $bbbactivities = $this->view_bigbluebuttonbn(1234); + + $this->assertIsArray($bbbactivities); + $this->assertArrayHasKey('status', $bbbactivities); + $this->assertArrayHasKey('warnings', $bbbactivities); + $this->assertFalse($bbbactivities['status']); + } + + /** + * Test execute API CALL without login + */ + public function test_execute_without_login() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + $this->expectException(require_login_exception::class); + $this->view_bigbluebuttonbn($instance->get_instance_id()); + } + + /** + * Test execute API CALL with invalid login + */ + public function test_execute_with_invalid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_user($course); + $this->setUser($user); + $this->expectException(require_login_exception::class); + $this->view_bigbluebuttonbn($instance->get_instance_id()); + } + + /** + * When login as a student + */ + public function test_execute_with_valid_login() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id]); + $instance = instance::get_from_instanceid($record->id); + + $user = $generator->create_and_enrol($course, 'student'); + $this->setUser($user); + + $returnvalue = $this->view_bigbluebuttonbn($instance->get_instance_id()); + + $this->assertArrayHasKey('status', $returnvalue); + $this->assertArrayHasKey('warnings', $returnvalue); + $this->assertTrue($returnvalue['status']); + } +} + diff --git a/mod/bigbluebuttonbn/tests/fixtures/bbpresentation.pptx b/mod/bigbluebuttonbn/tests/fixtures/bbpresentation.pptx new file mode 100644 index 0000000000000000000000000000000000000000..73e008eee9e84adbd97f532d750b1e8df4ea67e8 GIT binary patch literal 40373 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�$OstEuEdjABRDF#$gw2S(#>`N^~ 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 T*B8*o2>fRY>_xiQr$7B4BszaP literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/tests/generator/behat_mod_bigbluebuttonbn_generator.php b/mod/bigbluebuttonbn/tests/generator/behat_mod_bigbluebuttonbn_generator.php new file mode 100644 index 00000000000..44469c2e737 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/generator/behat_mod_bigbluebuttonbn_generator.php @@ -0,0 +1,118 @@ +. + +/** + * Behat data generator for mod_bigbluebuttonbn. + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Behat data generator for behat_mod_bigbluebuttonbn_generator. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +class behat_mod_bigbluebuttonbn_generator extends behat_generator_base { + + /** + * Get all entities that can be create through this behat_generator + * + * @return array + */ + protected function get_creatable_entities(): array { + return [ + 'recordings' => [ + 'singular' => 'recording', + 'datagenerator' => 'recording', + 'required' => ['bigbluebuttonbn', 'name'], + 'switchids' => [ + 'bigbluebuttonbn' => 'bigbluebuttonbnid', + 'group' => 'groupid', + ], + ], + 'logs' => [ + 'singular' => 'log', + 'datagenerator' => 'override', + 'required' => ['bigbluebuttonbn', 'user'], + 'switchids' => [ + 'bigbluebuttonbn' => 'bigbluebuttonbnid', + 'user' => 'userid', + ], + ], + 'meetings' => [ + 'singular' => 'meeting', + 'datagenerator' => 'meeting', + 'required' => [ + 'activity', + ], + 'switchids' => [ + 'activity' => 'instanceid', + 'group' => 'groupid', + ], + ], + ]; + } + + /** + * Look up the id of a bigbluebutton activity from its name. + * + * @param string $bbactivityname the bigbluebutton activity name, for example 'Test meeting'. + * @return int corresponding id. + * @throws Exception + */ + protected function get_bigbluebuttonbn_id(string $bbactivityname): int { + global $DB; + + if (!$id = $DB->get_field('bigbluebuttonbn', 'id', ['name' => $bbactivityname])) { + throw new Exception('There is no bigbluebuttonbn with name "' . $bbactivityname . '" does not exist'); + } + return $id; + } + + /** + * Get the activity id from its name + * + * @param string $activityname + * @return int + * @throws Exception + */ + protected function get_activity_id(string $activityname): int { + global $DB; + + $sql = <<get_field_sql($sql, ['idnumber' => $activityname, 'name' => $activityname]); + if (empty($id)) { + throw new Exception("There is no bigbluebuttonbn with name '{$activityname}' does not exist"); + } + + return $id; + } +} diff --git a/mod/bigbluebuttonbn/tests/generator/lib.php b/mod/bigbluebuttonbn/tests/generator/lib.php new file mode 100644 index 00000000000..b11cc56425e --- /dev/null +++ b/mod/bigbluebuttonbn/tests/generator/lib.php @@ -0,0 +1,374 @@ +. + +/** + * mod_bigbluebuttonbn data generator + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\logger; +use mod_bigbluebuttonbn\recording; +use mod_bigbluebuttonbn\testing\generator\mockedserver; + +/** + * bigbluebuttonbn module data generator + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class mod_bigbluebuttonbn_generator extends \testing_module_generator { + + /** + * Creates an instance of bigbluebuttonbn for testing purposes. + * + * @param array|stdClass $record data for module being generated. + * @param null|array $options general options for course module. + * @return stdClass record from module-defined table with additional field cmid + */ + public function create_instance($record = null, array $options = null) { + $now = time(); + $defaults = [ + "type" => 0, + "meetingid" => sha1(rand()), + "record" => true, + "moderatorpass" => "mp", + "viewerpass" => "ap", + "participants" => "{}", + "timecreated" => $now, + "timemodified" => $now, + "presentation" => null, + "recordings_preview" => 0 + ]; + + $record = (array) $record; + + $record['participants'] = json_encode($this->get_participants_from_record($record)); + + foreach ($defaults as $key => $value) { + if (!isset($record[$key])) { + $record[$key] = $value; + } + } + if ($record['presentation']) { + global $USER; + // Here we replace the original presentation file with a draft area in which we store this file. + $draftareaid = file_get_unused_draft_itemid(); + $bbbfilerecord['contextid'] = context_user::instance($USER->id)->id; + $bbbfilerecord['component'] = 'user'; + $bbbfilerecord['filearea'] = 'draft'; + $bbbfilerecord['itemid'] = $draftareaid; + $bbbfilerecord['filepath'] = '/'; + $bbbfilerecord['filename'] = basename($record['presentation']); + $fs = get_file_storage(); + + $fs->create_file_from_pathname($bbbfilerecord, $record['presentation']); + // Now the $record['presentation'] must contain the draftareaid. + $record['presentation'] = $draftareaid; + } + return parent::create_instance((object) $record, (array) $options); + } + + /** + * Create the participants field data from create_instance data. + * + * @param array $record + * @return array + */ + protected function get_participants_from_record(array $record): array { + $roles = []; + if (array_key_exists('moderators', $record) && !empty($record['moderators'])) { + $roles = array_merge( + $roles, + $this->get_participant_configuration($record['moderators'], 'moderator') + ); + unset($record['moderators']); + } + + if (array_key_exists('viewers', $record) && !empty($record['viewers'])) { + $roles = array_merge( + $roles, + $this->get_participant_configuration($record['viewers'], 'viewer') + ); + unset($record['viewers']); + } + + if (!empty($roles)) { + array_unshift($roles, (object) [ + 'selectiontype' => 'all', + 'selectionid' => 'all', + 'role' => 'viewer', + ]); + } + + return $roles; + } + + /** + * Get the participant configuration for a field and role for use in get_participants_from_record. + * + * @param string $field + * @param string $role + * @return array + */ + protected function get_participant_configuration(string $field, string $role): array { + global $DB; + + $values = explode(',', $field); + + $roles = $DB->get_records_menu('role', [], '', 'shortname, id'); + + $configuration = []; + foreach ($values as $value) { + if (empty($value)) { + // Empty value. + continue; + } + [$type, $name] = explode(':', $value); + + $participant = (object) [ + 'selectiontype' => $type, + 'role' => $role, + ]; + switch ($type) { + case 'role': + if (!array_key_exists($name, $roles)) { + throw new \coding_exception("Unknown role '{$name}'"); + } + $participant->selectionid = $roles[$name]; + + break; + case 'user': + $participant->selectionid = $DB->get_field('user', 'id', ['username' => $name], MUST_EXIST); + break; + default: + throw new \coding_exception("Unknown participant type: '{$type}'"); + } + + $configuration[] = $participant; + } + + return $configuration; + } + + /** + * Create a recording for the given bbb activity. + * + * The recording is created both locally, and a recording record is created on the mocked BBB server. + * + * @param array $data + * @return stdClass the recording object + */ + public function create_recording(array $data): stdClass { + $instance = instance::get_from_instanceid($data['bigbluebuttonbnid']); + + if (isset($data['imported']) && filter_var($data['imported'], FILTER_VALIDATE_BOOLEAN)) { + if (empty($data['importedid'])) { + throw new moodle_exception('error'); + } + $recording = recording::get_record(['recordingid' => $data['importedid']]); + $recording->imported = true; + } else { + $recording = (object) [ + 'headless' => false, + 'imported' => false, + 'status' => $data['status'] ?? recording::RECORDING_STATUS_NOTIFIED, + ]; + } + + if (!empty($data['groupid'])) { + $instance->set_group_id($data['groupid']); + $recording->groupid = $data['groupid']; + } + + $recording->bigbluebuttonbnid = $instance->get_instance_id(); + $recording->courseid = $instance->get_course_id(); + if (isset($options['imported']) && $options['imported']) { + $precording = $recording->create_imported_recording($instance); + } else { + if ($recording->status == recording::RECORDING_STATUS_DISMISSED) { + $recording->recordingid = sprintf( + "%s-%s", + md5($instance->get_meeting_id()), + time() + rand(1, 100000) + ); + } else { + $recording->recordingid = $this->create_mockserver_recording($instance, $recording, $data); + } + $precording = new recording(0, $recording); + $precording->create(); + } + return $precording->to_record(); + } + + /** + * Add a recording in the mock server + * + * @param instance $instance + * @param stdClass $recordingdata + * @param array $data + * @return string + * @throws moodle_exception + */ + protected function create_mockserver_recording(instance $instance, stdClass $recordingdata, array $data): string { + $mockdata = array_merge((array) $recordingdata, [ + 'meetingID' => $instance->get_meeting_id(), + 'meta' => [ + 'isBreakout' => 'false', + 'bn-presenter-name' => $data['presentername'] ?? 'Fake presenter', + 'bn-recording-ready-url' => new moodle_url('/mod/bigbluebuttonbn/bbb_broker.php', [ + 'action' => 'recording_ready', + 'bigbluebuttonbn' => $instance->get_instance_id() + ]), + 'bbb-recording-description' => $data['description'] ?? '', + 'bbb-recording-name' => $data['name'] ?? '', + 'bbb-recording-tags' => $data['tags'] ?? '', + ], + ]); + + $result = $this->send_mock_request('backoffice/createRecording', [], $mockdata); + + return (string) $result->recordID; + } + + /** + * Mock an in-progress meeting on the remote server. + * + * @param array $data + * @return stdClass + */ + public function create_meeting(array $data): stdClass { + $instance = instance::get_from_instanceid($data['instanceid']); + + if (array_key_exists('groupid', $data)) { + $instance = instance::get_group_instance_from_instance($instance, $data['groupid']); + } + + $meetingid = $instance->get_meeting_id(); + + // Default room configuration. + $roomconfig = array_merge($data, [ + 'meetingID' => $meetingid, + 'meetingName' => $instance->get_meeting_name(), + 'attendeePW' => $instance->get_viewer_password(), + 'moderatorPW' => $instance->get_moderator_password(), + 'voiceBridge' => $instance->get_voice_bridge(), + 'meta' => [ + 'bbb-context' => $instance->get_course()->fullname, + 'bbb-context-id' => $instance->get_course()->id, + 'bbb-context-label' => $instance->get_course()->shortname, + 'bbb-context-name' => $instance->get_course()->fullname, + 'bbb-origin' => 'Moodle', + 'bbb-origin-tag' => 'moodle-mod_bigbluebuttonbn (TODO version)', + 'bbb-recording-description' => $instance->get_meeting_description(), + 'bbb-recording-name' => $instance->get_meeting_name(), + ], + ]); + + $this->send_mock_request('backoffice/createMeeting', [], $roomconfig); + + return (object) $roomconfig; + } + + /** + * Create a log record + * + * @param mixed $record + * @param array|null $options + */ + public function create_log($record, array $options = null) { + $instance = instance::get_from_instanceid($record['bigbluebuttonbnid']); + + $record = array_merge([ + 'meetingid' => $instance->get_meeting_id(), + ], (array) $record); + + $testlogclass = new class extends logger { + /** + * Log test event + * + * @param instance $instance + * @param array $record + */ + public static function log_test_event(instance $instance, array $record): void { + self::log( + $instance, + logger::EVENT_CREATE, + $record + ); + } + }; + + $testlogclass::log_test_event($instance, $record); + } + + /** + * Get a URL for a mocked BBB server endpoint. + * + * @param string $endpoint + * @param array $params + * @return moodle_url + */ + protected function get_mocked_server_url(string $endpoint = '', array $params = []): moodle_url { + return new moodle_url(TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER . '/' . $endpoint, $params); + } + + /** + * Utility to send a request to the mock server + * + * @param string $endpoint + * @param array $params + * @param array $mockdata + * @return SimpleXMLElement + * @throws coding_exception + */ + protected function send_mock_request(string $endpoint, array $params = [], array $mockdata = []): SimpleXMLElement { + $url = $this->get_mocked_server_url($endpoint, $params); + + foreach ($mockdata as $key => $value) { + if (is_array($value)) { + foreach ($value as $subkey => $subvalue) { + $paramname = "{$key}_{$subkey}"; + $url->param($paramname, $subvalue); + } + } else { + $url->param($key, $value); + } + } + + $curl = new \curl(); + $result = $curl->get($url->out_omit_querystring(), $url->params()); + + return simplexml_load_string($result, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOBLANKS); + } + + /** + * Reset the mock server + */ + public function reset_mock(): void { + if (defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) { + $this->send_mock_request('backoffice/reset'); + } + } +} diff --git a/mod/bigbluebuttonbn/tests/instance_test.php b/mod/bigbluebuttonbn/tests/instance_test.php new file mode 100644 index 00000000000..e3c966d5b36 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/instance_test.php @@ -0,0 +1,498 @@ +. +/** + * Tests for the Big Blue Button Instance. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_bigbluebuttonbn; + +use advanced_testcase; +use moodle_exception; + +/** + * Tests for the Big Blue Button Instance. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_bigbluebuttonbn\instance + */ +class instance_test extends advanced_testcase { + + /** + * Test get from + * + * @param string $function + * @param string $field + * @dataProvider get_from_location_provider + * @covers ::get_from_instanceid + * @covers ::get_from_cmid + */ + public function test_get_from(string $function, string $field): void { + $this->resetAfterTest(); + + [ + 'record' => $record, + ] = $this->get_test_instance(); + + $instance = call_user_func("mod_bigbluebuttonbn\instance::{$function}", $record->{$field}); + + $this->assertInstanceOf(instance::class, $instance); + $this->assertEquals($record->id, $instance->get_instance_id()); + $this->assertEquals($record->cmid, $instance->get_cm_id()); + $this->assertEquals($record->cmid, $instance->get_cm()->id); + } + + /** + * Get from location provider + * + * @return string[][] + */ + public function get_from_location_provider(): array { + return [ + ['get_from_instanceid', 'id'], + ['get_from_cmid', 'cmid'], + ]; + } + + /** + * Get an instance from a cmid. + * @covers ::get_from_cmid + */ + public function test_get_from_cmid(): void { + $this->resetAfterTest(); + + [ + 'record' => $record, + 'cm' => $cm, + ] = $this->get_test_instance(); + + $instance = instance::get_from_cmid($cm->id); + + $this->assertInstanceOf(instance::class, $instance); + $this->assertEquals($record->id, $instance->get_instance_id()); + $this->assertEquals($cm->id, $instance->get_cm()->id); + } + + /** + * If the instance was not found, and exception should be thrown. + * @covers ::get_from_cmid + */ + public function test_get_from_cmid_not_found(): void { + $this->assertNull(instance::get_from_cmid(100)); + } + + /** + * If the instance was not found, and exception should be thrown. + */ + public function test_get_from_instance_not_found(): void { + $this->assertNull(instance::get_from_instanceid(100)); + } + + /** + * Get from meeting id + * + * @covers ::get_from_meetingid + */ + public function test_get_from_meetingid(): void { + $this->resetAfterTest(); + + [ + 'record' => $record, + ] = $this->get_test_instance(); + + // The meetingid is confusingly made up of a meetingid field, courseid, instanceid, and groupid. + $instance = instance::get_from_meetingid(sprintf( + "%s-%s-%s", + $record->meetingid, + $record->course, + $record->id + )); + + $this->assertInstanceOf(instance::class, $instance); + $this->assertEquals($record->id, $instance->get_instance_id()); + $this->assertEquals($record->cmid, $instance->get_cm_id()); + $this->assertEquals($record->cmid, $instance->get_cm()->id); + } + + /** + * Get the get_from_meetingid() function where the meetingid includes a groupid. + * + * @covers ::get_from_meetingid + */ + public function test_get_from_meetingid_group(): void { + $this->resetAfterTest(); + + [ + 'record' => $record, + 'course' => $course, + 'cm' => $cm, + ] = $this->get_test_instance(); + + $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]); + + $instance = instance::get_from_meetingid( + sprintf("%s-%s-%s[0]", $record->meetingid, $record->course, $record->id) + ); + + $this->assertEquals($cm->instance, $instance->get_instance_id()); + $this->assertEquals($cm->id, $instance->get_cm_id()); + } + + /** + * Ensure that invalid meetingids throw an appropriate exception. + * + * @dataProvider invalid_meetingid_provider + * @param string $meetingid + * @covers ::get_from_meetingid + */ + public function test_get_from_meetingid_invalid(string $meetingid): void { + $this->expectException(moodle_exception::class); + instance::get_from_meetingid($meetingid); + } + + /** + * Provide invalid meeting examples + * + * @return \string[][] + */ + public function invalid_meetingid_provider(): array { + // Meeting IDs are in the formats: + // - -- + // - --[] + // Note: deducing the group from meeting id will soon be deprecated. + return [ + 'Non-numeric instanceid' => ['aaa-123-aaa'], + ]; + } + + /** + * Test the get_all_instances_in_course function. + * + * @covers ::get_all_instances_in_course + */ + public function test_get_all_instances_in_course(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $records = []; + for ($i = 0; $i < 5; $i++) { + $records[] = $this->getDataGenerator()->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + } + + $instances = instance::get_all_instances_in_course($course->id); + $this->assertCount(5, $instances); + foreach ($instances as $instance) { + $this->assertInstanceOf(instance::class, $instance); + } + } + + /** + * Get test instance from data + * + * @param array $data + * @return array + */ + protected function get_test_instance(array $data = []): array { + $course = $this->getDataGenerator()->create_course(); + $record = $this->getDataGenerator()->create_module('bigbluebuttonbn', array_merge([ + 'course' => $course->id, + ], $data)); + $cm = get_fast_modinfo($course)->instances['bigbluebuttonbn'][$record->id]; + + return [ + 'course' => $course, + 'record' => $record, + 'cm' => $cm, + ]; + } + + /** + * Test the get_meeting_id function for a meeting configured for a group. + * + * @covers ::get_meeting_id + */ + public function test_get_meeting_id_with_groups(): void { + $this->resetAfterTest(); + + [ + 'record' => $record, + 'course' => $course, + ] = $this->get_test_instance(); + + $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]); + + $instance = instance::get_from_instanceid($record->id); + + // No group. + $this->assertEquals( + sprintf("%s-%s-%s[0]", $record->meetingid, $record->course, $record->id), + $instance->get_meeting_id(0) + ); + + // Specified group. + $this->assertEquals( + sprintf("%s-%s-%s[%d]", $record->meetingid, $record->course, $record->id, $group->id), + $instance->get_meeting_id($group->id) + ); + } + + /** + * Test the get_meeting_id function for a meeting configured for a group. + * + * @covers ::get_meeting_id + */ + public function test_get_meeting_id_without_groups(): void { + $this->resetAfterTest(); + + [ + 'record' => $record, + 'course' => $course, + ] = $this->get_test_instance(); + + $instance = instance::get_from_instanceid($record->id); + + // No group. + $this->assertEquals( + sprintf("%s-%s-%s[0]", $record->meetingid, $record->course, $record->id), + $instance->get_meeting_id(null) + ); + } + + /** + * Data provider to check the various room_available scenarios' + * + * @return array + */ + public function is_currently_open_provider(): array { + return [ + 'No opening or closing time set: Is open' => [null, null, true], + 'Opening time set in the past, no closing: Is open' => [-DAYSECS, null, true], + 'Opening time set in the future, no closing: Is closed' => [+DAYSECS, null, false], + 'Closing time set in the past, no opening: Is closed' => [null, -DAYSECS, false], + 'Closing time set in the future, no opening: Is open' => [null, +DAYSECS, true], + 'Opening and closing in the past: Is closed' => [-WEEKSECS, -DAYSECS, false], + 'Opening and closing in the future: Is closed' => [+DAYSECS, +WEEKSECS, false], + 'Opening in the past, Closing in the future: Is open' => [-DAYSECS, +DAYSECS, true], + ]; + } + + /** + * Check instance currently open + * + * @dataProvider is_currently_open_provider + * @param null|int $openingtime + * @param null|int $closingtime + * @param bool $expected + */ + public function test_is_currently_open(?int $openingtime, ?int $closingtime, bool $expected): void { + $stub = $this->getMockBuilder(instance::class) + ->onlyMethods(['get_instance_var']) + ->disableOriginalConstructor() + ->getMock(); + + if ($openingtime) { + $openingtime = $openingtime + time(); + } + + if ($closingtime) { + $closingtime = $closingtime + time(); + } + + $stub->method('get_instance_var') + ->willReturnCallback(function($var) use ($openingtime, $closingtime) { + if ($var === 'openingtime') { + return $openingtime; + } + + return $closingtime; + }); + $this->assertEquals($expected, $stub->is_currently_open()); + } + + /** + * Ensure that the user_must_wait_to_join function works as expectd. + * + * @dataProvider user_must_wait_to_join_provider + * @param bool $isadmin + * @param bool $ismoderator + * @param bool $haswaitingroom + * @param bool $expected + */ + public function test_user_must_wait_to_join(bool $isadmin, bool $ismoderator, bool $haswaitingroom, bool $expected): void { + $stub = $this->getMockBuilder(instance::class) + ->setMethods([ + 'get_instance_var', + 'is_admin', + 'is_moderator', + ]) + ->disableOriginalConstructor() + ->getMock(); + + $stub->method('is_admin')->willReturn($isadmin); + $stub->method('is_moderator')->willReturn($ismoderator); + $stub->method('get_instance_var')->willReturn($haswaitingroom); + + $this->assertEquals($expected, $stub->user_must_wait_to_join()); + } + + /** + * Data provider for the user_must_wait_to_join function. + * + * @return array + */ + public function user_must_wait_to_join_provider(): array { + return [ + 'Admins must never wait to join (waiting disabled)' => [true, false, false, false], + 'Admins must never wait to join (waiting enabled)' => [true, false, true, false], + 'Moderators must never wait to join (waiting disabled)' => [false, true, false, false], + 'Moderators must never wait to join (waiting enabled)' => [false, true, true, false], + 'Other users must wait to join if waiting enabled' => [false, false, true, true], + 'Other users cannot wait to join if waiting disabled' => [false, false, false, false], + ]; + } + + /** + * Ensure that the does_current_user_count_towards_user_limit function works as expectd. + * + * @dataProvider does_current_user_count_towards_user_limit_provider + * @param bool $isadmin + * @param bool $ismoderator + * @param bool $expected + */ + public function test_does_current_user_count_towards_user_limit( + bool $isadmin, + bool $ismoderator, + bool $expected + ): void { + $stub = $this->getMockBuilder(instance::class) + ->setMethods([ + 'is_admin', + 'is_moderator', + ]) + ->disableOriginalConstructor() + ->getMock(); + + $stub->method('is_admin')->willReturn($isadmin); + $stub->method('is_moderator')->willReturn($ismoderator); + + $this->assertEquals($expected, $stub->does_current_user_count_towards_user_limit()); + } + + /** + * Data provider for the does_current_user_count_towards_user_limit function. + * + * @return array + */ + public function does_current_user_count_towards_user_limit_provider(): array { + return [ + 'Admin does not count' => [true, false, false], + 'Moderator does not count' => [false, true, false], + 'Other users do count' => [false, false, true], + ]; + } + + /** + * Ensure that the does_current_user_count_towards_user_limit function works as expectd. + * + * @dataProvider get_current_user_password_provider + * @param bool $isadmin + * @param bool $ismoderator + * @param bool $expectedmodpassword + */ + public function test_get_current_user_password(bool $isadmin, bool $ismoderator, bool $expectedmodpassword): void { + $stub = $this->getMockBuilder(instance::class) + ->setMethods([ + 'is_admin', + 'is_moderator', + 'get_moderator_password', + 'get_viewer_password', + ]) + ->disableOriginalConstructor() + ->getMock(); + + $stub->method('is_admin')->willReturn($isadmin); + $stub->method('is_moderator')->willReturn($ismoderator); + $stub->method('get_moderator_password')->willReturn('Moderator Password'); + $stub->method('get_viewer_password')->willReturn('Viewer Password'); + + if ($expectedmodpassword) { + $this->assertEquals('Moderator Password', $stub->get_current_user_password()); + } else { + $this->assertEquals('Viewer Password', $stub->get_current_user_password()); + } + } + + /** + * Data provider for the get_current_user_password function. + * + * @return array + */ + public function get_current_user_password_provider(): array { + return [ + 'Admin is a moderator' => [true, false, true], + 'Moderator is a moderator' => [false, true, true], + 'Others are a viewer' => [false, false, false], + ]; + } + + /** + * Tests for the allow_recording_start_stop function. + * + * @dataProvider allow_recording_start_stop_provider + * @param bool $isrecorded + * @param bool $showbuttons + * @param bool $expected + */ + public function test_allow_recording_start_stop( + bool $isrecorded, + bool $showbuttons, + bool $expected + ): void { + $stub = $this->getMockBuilder(instance::class) + ->setMethods([ + 'is_recorded', + 'should_show_recording_button', + ]) + ->disableOriginalConstructor() + ->getMock(); + + $stub->method('is_recorded')->willReturn($isrecorded); + $stub->method('should_show_recording_button')->willReturn($showbuttons); + + $this->assertEquals($expected, $stub->allow_recording_start_stop()); + } + + /** + * Data provider for the allow_recording_start_stop function. + * + * @return array + */ + public function allow_recording_start_stop_provider(): array { + return [ + 'Meeting is not recorded: No start/stop' => [false, false, false], + 'Meeting recorded, Buttons shown: Allow' => [true, true, true], + 'Meeting recorded, Buttons not shown: Deny' => [true, false, false], + ]; + } + +} diff --git a/mod/bigbluebuttonbn/tests/lib_test.php b/mod/bigbluebuttonbn/tests/lib_test.php new file mode 100644 index 00000000000..7f5e7e4e1fe --- /dev/null +++ b/mod/bigbluebuttonbn/tests/lib_test.php @@ -0,0 +1,527 @@ +. + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +namespace mod_bigbluebuttonbn; + +use calendar_event; +use context_module; +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use mod_bigbluebuttonbn_mod_form; +use MoodleQuickForm; +use navigation_node; +use ReflectionClass; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/lib.php'); + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +class lib_test extends \advanced_testcase { + use testcase_helper_trait; + + /** + * Check support + * + * @covers ::bigbluebuttonbn_supports + */ + public function test_bigbluebuttonbn_supports() { + $this->resetAfterTest(); + $this->assertTrue(bigbluebuttonbn_supports(FEATURE_IDNUMBER)); + $this->assertTrue(bigbluebuttonbn_supports(FEATURE_MOD_INTRO)); + $this->assertFalse(bigbluebuttonbn_supports(FEATURE_GRADE_HAS_GRADE)); + } + + /** + * Check add instance + * + * @covers ::bigbluebuttonbn_add_instance + */ + public function test_bigbluebuttonbn_add_instance() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $id = bigbluebuttonbn_add_instance($bbformdata); + $this->assertNotNull($id); + } + + /** + * Check update instance + * + * @covers ::bigbluebuttonbn_update_instance + */ + public function test_bigbluebuttonbn_update_instance() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $result = bigbluebuttonbn_update_instance($bbformdata); + $this->assertTrue($result); + } + + /** + * Check delete instance + * + * @covers ::bigbluebuttonbn_delete_instance + */ + public function test_bigbluebuttonbn_delete_instance() { + $this->resetAfterTest(); + $this->initialise_mock_server(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $result = bigbluebuttonbn_delete_instance($bbactivity->id); + $this->assertTrue($result); + } + + /** + * Check user outline page + * + * @covers ::bigbluebuttonbn_user_outline + */ + public function test_bigbluebuttonbn_user_outline() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $this->setUser($user); + + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + + $result = bigbluebuttonbn_user_outline($this->get_course(), $user, $bbactivitycm, $bbactivity); + $this->assertEquals((object) [], $result); + + // Now create a couple of logs. + $instance = instance::get_from_instanceid($bbactivity->id); + logger::log_meeting_joined_event($instance, 0); + logger::log_recording_played_event($instance, 1); + + $result = bigbluebuttonbn_user_outline($this->get_course(), $user, $bbactivitycm, $bbactivity); + $this->assertStringContainsString('Has joined the meeting or played a recording 2 time(s)', $result->info); + } + + /** + * Check user completion + * + * @covers ::bigbluebuttonbn_user_complete + */ + public function test_bigbluebuttonbn_user_complete() { + $this->initialise_mock_server(); + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_and_enrol($this->get_course()); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $this->setUser($user); + + // Now create a couple of logs. + $instance = instance::get_from_instanceid($bbactivity->id); + $recordings = $this->create_recordings_for_instance($instance, [['name' => "Pre-Recording 1"]]); + logger::log_meeting_joined_event($instance, 0); + logger::log_recording_played_event($instance, $recordings[0]->id); + ob_start(); + bigbluebuttonbn_user_complete($this->get_course(), $user, $bbactivitycm, $bbactivity); + $output = ob_get_contents(); + ob_end_clean(); + $this->assertStringContainsString('Has joined the meeting or played a recording', $output); + } + + /** + * Check get recent activity + * + * @covers ::bigbluebuttonbn_get_recent_mod_activity + */ + public function test_bigbluebuttonbn_get_recent_mod_activity() { + $this->initialise_mock_server(); + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_and_enrol($this->get_course()); + $this->setUser($user); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + // Now create a couple of logs. + $instance = instance::get_from_instanceid($bbactivity->id); + $recordings = $this->create_recordings_for_instance($instance, [['name' => "Pre-Recording 1"]]); + logger::log_meeting_joined_event($instance, 0); + logger::log_meeting_joined_event($instance, 0); + logger::log_recording_played_event($instance, $recordings[0]->id); + + $activities = $this->prepare_for_recent_activity_array(0, $user->id, 0); + $this->assertCount(4, $activities); + $this->assertEquals( + ["Meeting joined", "Meeting joined", "Recording viewed"], + array_values( + array_filter( + array_map(function($activity) { + return $activity->eventname ?? ""; + }, $activities), + function($e) { + return !empty($e); + } + ) + ) + ); + $this->assertEquals("Pre-Recording 1", $activities[3]->content); // The recording view event should contain the name + // of the activity. + } + + /** + * Prepare the list of activities as is done in course recent activity + * + * @param int $date + * @param int $user + * @param int $group + * @return array|void + * @throws \moodle_exception + */ + protected function prepare_for_recent_activity_array($date, $user, $group) { + // Same algorithm as in cource/recent.php, but stops at the first bbb activity. + $course = $this->get_course(); + $modinfo = get_fast_modinfo($course->id); + $sections = array(); + $index = 0; + foreach ($modinfo->get_section_info_all() as $i => $section) { + if (!empty($section->uservisible)) { + $sections[$i] = $section; + } + } + foreach ($sections as $sectionnum => $section) { + + $activity = new stdClass(); + $activity->type = 'section'; + if ($section->section > 0) { + $activity->name = get_section_name($this->get_course(), $section); + } else { + $activity->name = ''; + } + + $activity->visible = $section->visible; + $activities[$index++] = $activity; + + if (empty($modinfo->sections[$sectionnum])) { + continue; + } + + foreach ($modinfo->sections[$sectionnum] as $cmid) { + $cm = $modinfo->cms[$cmid]; + + if (!$cm->uservisible) { + continue; + } + + if (!empty($filter) and $cm->modname != $filter) { + continue; + } + + if (!empty($filtermodid) and $cmid != $filtermodid) { + continue; + } + + if ($cm->modname == 'bigbluebuttonbn') { + return bigbluebuttonbn_get_recent_mod_activity($activities, + $index, + $date, + $course->id, $cmid, + $user, + $group); + } + } + } + + } + + /** + * Check user recent activity + * + * @covers ::bigbluebuttonbn_print_recent_mod_activity + */ + public function test_bigbluebuttonbn_print_recent_mod_activity() { + $this->initialise_mock_server(); + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $user = $generator->create_and_enrol($this->get_course()); + $this->setUser($user); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + // Now create a couple of logs. + $instance = instance::get_from_instanceid($bbactivity->id); + $recordings = $this->create_recordings_for_instance($instance, [['name' => "Pre-Recording 1"]]); + logger::log_meeting_joined_event($instance, 0); + logger::log_meeting_joined_event($instance, 0); + logger::log_recording_played_event($instance, $recordings[0]->id); + + $activities = $this->prepare_for_recent_activity_array(0, $user->id, 0); + ob_start(); + bigbluebuttonbn_print_recent_mod_activity($activities[1], $this->get_course()->id, false, [], false); + $output = ob_get_contents(); + ob_end_clean(); + $this->assertStringContainsString('Meeting joined', $output); + } + + /** + * Check extra capabilities return value + * + * @covers ::bigbluebuttonbn_get_extra_capabilities + */ + public function test_bigbluebuttonbn_get_extra_capabilities() { + $this->resetAfterTest(); + $this->assertEquals(['moodle/site:accessallgroups'], bigbluebuttonbn_get_extra_capabilities()); + } + + /** + * Check form definition + * + * @covers ::bigbluebuttonbn_reset_course_form_definition + */ + public function test_bigbluebuttonbn_reset_course_form_definition() { + global $CFG, $PAGE; + $this->initialise_mock_server(); + + $PAGE->set_course($this->get_course()); + $this->setAdminUser(); + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + include_once($CFG->dirroot . '/mod/bigbluebuttonbn/mod_form.php'); + $data = new stdClass(); + $data->instance = $bbactivity; + $data->id = $bbactivity->id; + $data->course = $bbactivity->course; + + $form = new mod_bigbluebuttonbn_mod_form($data, 1, $bbactivitycm, $this->get_course()); + $refclass = new ReflectionClass("mod_bigbluebuttonbn_mod_form"); + $formprop = $refclass->getProperty('_form'); + $formprop->setAccessible(true); + + /* @var $mform MoodleQuickForm quickform object definition */ + $mform = $formprop->getValue($form); + bigbluebuttonbn_reset_course_form_definition($mform); + $this->assertNotNull($mform->getElement('bigbluebuttonbnheader')); + } + + /** + * Check defaults for form + * + * @covers ::bigbluebuttonbn_reset_course_form_defaults + */ + public function test_bigbluebuttonbn_reset_course_form_defaults() { + global $CFG; + $this->resetAfterTest(); + $results = bigbluebuttonbn_reset_course_form_defaults($this->get_course()); + $this->assertEquals([ + 'reset_bigbluebuttonbn_events' => 0, + 'reset_bigbluebuttonbn_tags' => 0, + 'reset_bigbluebuttonbn_logs' => 0, + 'reset_bigbluebuttonbn_recordings' => 0, + ], $results); + } + + /** + * Check user data + * + * @covers ::bigbluebuttonbn_reset_userdata + */ + public function test_bigbluebuttonbn_reset_userdata() { + $this->resetAfterTest(); + $data = new stdClass(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $data->courseid = $this->get_course()->id; + $data->reset_bigbluebuttonbn_tags = true; + $data->course = $bbactivity->course; + $results = bigbluebuttonbn_reset_userdata($data); + $this->assertEquals([ + 'component' => 'BigBlueButton', + 'item' => 'Deleted tags', + 'error' => false + ], + $results[0] + ); + } + + /** + * Check course module + * + * @covers ::bigbluebuttonbn_get_coursemodule_info + */ + public function test_bigbluebuttonbn_get_coursemodule_info() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $info = bigbluebuttonbn_get_coursemodule_info($bbactivitycm); + $this->assertEquals($info->name, $bbactivity->name); + } + + /** + * Check update since + * + * @covers ::bigbluebuttonbn_check_updates_since + */ + public function test_bigbluebuttonbn_check_updates_since() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $result = bigbluebuttonbn_check_updates_since($bbactivitycm, 0); + $this->assertEquals( + '{"configuration":{"updated":false},"contentfiles":{"updated":false},"introfiles":' . + '{"updated":false},"completion":{"updated":false}}', + json_encode($result) + ); + } + + /** + * Check font awesome icon map + * + * @covers ::mod_bigbluebuttonbn_get_fontawesome_icon_map + */ + public function test_mod_bigbluebuttonbn_get_fontawesome_icon_map() { + $this->resetAfterTest(); + $this->assertEquals(['mod_bigbluebuttonbn:icon' => 'icon-bigbluebutton'], + mod_bigbluebuttonbn_get_fontawesome_icon_map()); + } + + /** + * Check event action (calendar) + * + * @covers ::mod_bigbluebuttonbn_core_calendar_provide_event_action + */ + public function test_mod_bigbluebuttonbn_core_calendar_provide_event_action() { + global $DB; + $this->initialise_mock_server(); + $this->resetAfterTest(); + $this->setAdminUser(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + + // Standard use case, the meeting start and we want add an action event to join the meeting. + $event = $this->create_action_event($this->get_course(), $bbactivity, logger::EVENT_MEETING_START); + $factory = new \core_calendar\action_factory(); + $actionevent = mod_bigbluebuttonbn_core_calendar_provide_event_action($event, $factory); + $this->assertEquals("Join session", $actionevent->get_name()); + + // User has already joined the meeting (there is log event EVENT_JOIN already for this user). + $instance = instance::get_from_instanceid($bbactivity->id); + logger::log_meeting_joined_event($instance, 0); + + $bbactivity->closingtime = time() - 1000; + $bbactivity->openingtime = time() - 2000; + $DB->update_record('bigbluebuttonbn', $bbactivity); + $event = $this->create_action_event($this->get_course(), $bbactivity, logger::EVENT_MEETING_START); + $actionevent = mod_bigbluebuttonbn_core_calendar_provide_event_action($event, $factory); + $this->assertNull($actionevent); + } + + /** + * Creates an action event. + * + * @param stdClass $course The course the bigbluebutton activity is in + * @param stdClass $bbbactivity The bigbluebutton activity to create an event for + * @param string $eventtype The event type. eg. ASSIGN_EVENT_TYPE_DUE. + * @return bool|calendar_event + */ + private function create_action_event(stdClass $course, stdClass $bbbactivity, string $eventtype) { + $event = new stdClass(); + $event->name = 'Calendar event'; + $event->modulename = 'bigbluebuttonbn'; + $event->courseid = $course->id; + $event->instance = $bbbactivity->id; + $event->type = CALENDAR_EVENT_TYPE_ACTION; + $event->priority = CALENDAR_EVENT_USER_OVERRIDE_PRIORITY; + $event->eventtype = $eventtype; + $event->timestart = time(); + + return calendar_event::create($event); + } + + /** + * Test setting navigation admin menu + * + * @covers ::bigbluebuttonbn_extend_settings_navigation + */ + public function test_bigbluebuttonbn_extend_settings_navigation_admin() { + global $PAGE, $CFG; + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $CFG->bigbluebuttonbn_meetingevents_enabled = true; + + $PAGE->set_cm($bbactivitycm); + $PAGE->set_context(context_module::instance($bbactivitycm->id)); + $PAGE->set_url('/mod/bigbluebuttonbn/view.php', ['id' => $bbactivitycm->id]); + $settingnav = $PAGE->settingsnav; + + $this->setAdminUser(); + $node = navigation_node::create('testnavigationnode'); + bigbluebuttonbn_extend_settings_navigation($settingnav, $node); + $this->assertCount(1, $node->get_children_key_list()); + } + + /** + * Check additional setting menu + * + * @covers ::bigbluebuttonbn_extend_settings_navigation + */ + public function test_bigbluebuttonbn_extend_settings_navigation_user() { + global $PAGE, $CFG; + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $user = $generator->create_user(); + $this->setUser($user); + list($course, $bbactivitycmuser) = get_course_and_cm_from_instance($bbactivity->id, 'bigbluebuttonbn'); + + $CFG->bigbluebuttonbn_meetingevents_enabled = true; + + $PAGE->set_cm($bbactivitycmuser); + $PAGE->set_context(context_module::instance($bbactivitycm->id)); + $PAGE->set_url('/mod/bigbluebuttonbn/view.php', ['id' => $bbactivitycm->id]); + + $settingnav = $PAGE->settingsnav; + $node = navigation_node::create('testnavigationnode'); + bigbluebuttonbn_extend_settings_navigation($settingnav, $node); + $this->assertCount(0, $node->get_children_key_list()); + } + + /** + * Check the visibility on calendar + * @covers ::mod_bigbluebuttonbn_core_calendar_is_event_visible + */ + public function test_mod_bigbluebuttonbn_core_calendar_is_event_visible() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbactivity->closingtime = time() - 1000; + $bbactivity->openingtime = time() - 2000; + $DB->update_record('bigbluebuttonbn', $bbactivity); + $event = $this->create_action_event($this->get_course(), $bbactivity, logger::EVENT_MEETING_START); + $this->assertFalse(mod_bigbluebuttonbn_core_calendar_is_event_visible($event)); + $bbactivity->closingtime = time() + 1000; + $DB->update_record('bigbluebuttonbn', $bbactivity); + $event = $this->create_action_event($this->get_course(), $bbactivity, logger::EVENT_MEETING_START); + $this->assertTrue(mod_bigbluebuttonbn_core_calendar_is_event_visible($event)); + $event->instance = 0; + $this->assertFalse(mod_bigbluebuttonbn_core_calendar_is_event_visible($event)); + } +} diff --git a/mod/bigbluebuttonbn/tests/local/bigbluebutton/recordings/recording_data_test.php b/mod/bigbluebuttonbn/tests/local/bigbluebutton/recordings/recording_data_test.php new file mode 100644 index 00000000000..542cf724401 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/local/bigbluebutton/recordings/recording_data_test.php @@ -0,0 +1,65 @@ +. + +/** + * Privacy provider tests. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\local\bigbluebutton\recordings; + +/** + * Privacy provider tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @coversDefaultClass \mod_bigbluebuttonbn\local\bigbluebutton\recordings\recording_data + */ +class recording_data_test extends \advanced_testcase { + + /** + * Test for the type_text provider. + * + * @covers ::type_text + * @dataProvider type_text_provider + * @param string $name + * @param string $type + */ + public function test_get_recording_type_text(string $name, string $type) { + $this->assertEquals($name, recording_data::type_text($type)); + } + + /** + * Type of recordings (dataprovider) + * + * @return \string[][] + */ + public function type_text_provider(): array { + return [ + ['Presentation', 'presentation'], + ['Video', 'video'], + ['Videos', 'videos'], + ['Whatever', 'whatever'], + ['Whatever It Can Be', 'whatever it can be'], + ]; + } +} diff --git a/mod/bigbluebuttonbn/tests/local/helpers/files_test.php b/mod/bigbluebuttonbn/tests/local/helpers/files_test.php new file mode 100644 index 00000000000..d7daf970952 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/local/helpers/files_test.php @@ -0,0 +1,235 @@ +. + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +namespace mod_bigbluebuttonbn\local\helpers; + +use context_course; +use context_module; +use context_system; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\test\testcase_helper_trait; +use stdClass; +use stored_file; + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @covers \mod_bigbluebuttonbn\local\helpers\files + * @coversDefaultClass \mod_bigbluebuttonbn\local\helpers\files + */ +class files_test extends \advanced_testcase { + use testcase_helper_trait; + + /** @var string Filename used for the presentation */ + const PRESENTATION_FILENAME = 'bbpresentation.pptx'; + + /** @var string Filename used for the presentation */ + const PRESENTATION_FILEPATH = '/mod/bigbluebuttonbn/tests/fixtures/bbpresentation.pptx'; + + /** + * Plugin valid test case + */ + public function test_pluginfile_valid() { + $this->resetAfterTest(); + $this->assertFalse(files::pluginfile_valid(context_course::instance($this->get_course()->id), 'presentation')); + $this->assertTrue(files::pluginfile_valid(context_system::instance(), 'presentation')); + $this->assertFalse(files::pluginfile_valid(context_system::instance(), 'otherfilearea')); + } + + /** + * Plugin file test case + */ + public function test_pluginfile_file() { + global $CFG; + $this->resetAfterTest(); + + list($user, $bbactivity) = $this->create_user_and_activity($CFG->dirroot . self::PRESENTATION_FILEPATH); + $this->setUser($user); + $instance = instance::get_from_instanceid($bbactivity->id); + /** @var stored_file $mediafile */ + $mediafile = + files::pluginfile_file($this->get_course(), $instance->get_cm(), $instance->get_context(), + 'presentation', [self::PRESENTATION_FILENAME]); + $this->assertEquals(self::PRESENTATION_FILENAME, $mediafile->get_filename()); + } + + /** + * Get presentation file + */ + public function test_default_presentation_get_file() { + $this->resetAfterTest(); + + list($user, $bbactivity) = $this->create_user_and_activity(); + $this->setUser($user); + + $instance = instance::get_from_instanceid($bbactivity->id); + $mediafilename = + files::get_plugin_filename($this->get_course(), $instance->get_cm(), $instance->get_context(), ['presentation.pptx']); + $this->assertEquals('presentation.pptx', $mediafilename); + } + + /** + * Test that file is accessible only once. + */ + public function test_presentation_file_accessible_twice() { + global $CFG; + $this->resetAfterTest(); + + list($user, $bbactivity) = $this->create_user_and_activity($CFG->dirroot . self::PRESENTATION_FILEPATH); + $this->setUser($user); + + $instance = instance::get_from_instanceid($bbactivity->id); + $presentation = $instance->get_presentation_for_bigbluebutton_upload(); + $fulldirset = explode('/', $presentation['url']); + $filename = array_pop($fulldirset); + $nonce = array_pop($fulldirset); + // The link should be valid twice. + for ($i = 0; $i < 2; $i++) { + $mediafile = + files::pluginfile_file($this->get_course(), $instance->get_cm(), $instance->get_context(), 'presentation', + [$nonce, $filename]); + $this->assertEquals($filename, $mediafile->get_filename()); + } + // Third time is a charm, this should be false. + $mediafile = + files::pluginfile_file($this->get_course(), $instance->get_cm(), $instance->get_context(), 'presentation', + [$nonce, $filename]); + $this->assertFalse($mediafile); + } + + /** + * Test that file is accessible only once. + */ + public function test_presentation_file_not_accessible_externally() { + global $CFG; + $this->resetAfterTest(); + + list($user, $bbactivity) = $this->create_user_and_activity($CFG->dirroot . self::PRESENTATION_FILEPATH); + $this->setUser($user); + + $instance = instance::get_from_instanceid($bbactivity->id); + $presentation = $instance->get_presentation(); + $fulldirset = explode('/', $presentation['url']); + $filename = array_pop($fulldirset); + $this->setGuestUser(); + $this->expectException(\require_login_exception::class); + files::pluginfile_file($this->get_course(), $instance->get_cm(), $instance->get_context(), 'presentation', + [$filename]); + + $this->setUser($user); + $mediafile = + files::pluginfile_file($this->get_course(), $instance->get_cm(), $instance->get_context(), 'presentation', + [$filename]); + $this->assertNotNull($mediafile); + } + + /** + * Get filename test + */ + public function test_pluginfile_filename() { + $this->resetAfterTest(); + + list($user, $bbactivity, $bbactivitycm, $bbactivitycontext) = $this->create_user_and_activity(); + $this->setUser($user); + $this->create_sample_file(self::PRESENTATION_FILENAME, $bbactivitycontext->id); + $presentationdef = files::get_presentation($bbactivitycontext, self::PRESENTATION_FILENAME, $bbactivity->id, true); + $pathparts = explode('/', $presentationdef['url']); + $filename = array_pop($pathparts); + $salt = array_pop($pathparts); + $filename = files::get_plugin_filename($this->get_course(), $bbactivitycm, $bbactivitycontext, + [$salt, $filename]); + $this->assertEquals(self::PRESENTATION_FILENAME, $filename); + } + + /** + * Get media files + */ + public function test_get_media_file() { + $this->resetAfterTest(); + + list($user, $bbactivity) = $this->create_user_and_activity(); + $this->setUser($user); + + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $mediafilepath = files::save_media_file($bbformdata); + $this->assertEmpty($mediafilepath); + + // From test_delete_original_file_from_draft (lib/test/filelib_test.php) + // Create a bbb private file. + $this->create_sample_file(self::PRESENTATION_FILENAME, context_module::instance($bbformdata->coursemodule)->id); + file_prepare_draft_area($bbformdata->presentation, + context_module::instance($bbformdata->coursemodule)->id, + 'mod_bigbluebuttonbn', + 'presentation', 0); + + $mediafilepath = files::save_media_file($bbformdata); + $this->assertEquals('/' . self::PRESENTATION_FILENAME, $mediafilepath); + } + + /** + * Create a user and an activity + * + * @param null $presentationfilename + * @param bool $closed + * @return array + */ + protected function create_user_and_activity($presentationpath = null, $closed = false): array { + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $this->setAdminUser(); + $activitydata = ['closingtime' => time() + ($closed ? -3600 : +3600)]; + if (!empty($presentationpath)) { + $activitydata['presentation'] = $presentationpath; + } + list($bbactivitycontext, $bbactivitycm, $bbactivity) = + $this->create_instance(null, $activitydata); + $generator->enrol_user($user->id, $this->get_course()->id, 'editingteacher'); + return [$user, $bbactivity, $bbactivitycm, $bbactivitycontext]; + } + + /** + * Helper to create sample file for tests + * + * @param string $filename + * @param int $contextid + * @return stored_file + */ + protected function create_sample_file($filename, $contextid) { + $bbbfilerecord = new stdClass; + $bbbfilerecord->contextid = $contextid; + $bbbfilerecord->component = 'mod_bigbluebuttonbn'; + $bbbfilerecord->filearea = 'presentation'; + $bbbfilerecord->itemid = 0; + $bbbfilerecord->filepath = '/'; + $bbbfilerecord->filename = $filename; + $bbbfilerecord->source = 'test'; + $fs = get_file_storage(); + return $fs->create_file_from_string($bbbfilerecord, 'Presentation file content'); + } +} diff --git a/mod/bigbluebuttonbn/tests/local/helpers/mod_helper_trait_test.php b/mod/bigbluebuttonbn/tests/local/helpers/mod_helper_trait_test.php new file mode 100644 index 00000000000..7294efd708b --- /dev/null +++ b/mod/bigbluebuttonbn/tests/local/helpers/mod_helper_trait_test.php @@ -0,0 +1,189 @@ +. + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +namespace mod_bigbluebuttonbn\local\helpers; + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @covers \mod_bigbluebuttonbn\local\helpers\mod_helper + * @coversDefaultClass \mod_bigbluebuttonbn\local\helpers\mod_helper + */ +class mod_helper_trait_test extends \advanced_testcase { + use testcase_helper_trait; + + /** + * Presave test + */ + public function test_process_pre_save() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $bbformdata->participants = '

this -> "

\n'; + $bbformdata->timemodified = time(); + mod_helper::process_pre_save($bbformdata); + $this->assertTrue($bbformdata->timemodified != 0); + $this->assertEquals('

this -> "

\n', $bbformdata->participants); + } + + /** + * Presave instance + */ + public function test_process_pre_save_instance() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $bbformdata->instance = 0; + $bbformdata->timemodified = time(); + mod_helper::process_pre_save($bbformdata); + $this->assertTrue($bbformdata->timemodified == 0); + } + + /** + * Presave checkboxes + */ + public function test_process_pre_save_checkboxes() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + unset($bbformdata->wait); + unset($bbformdata->recordallfromstart); + mod_helper::process_pre_save($bbformdata); + $this->assertTrue(isset($bbformdata->wait)); + $this->assertTrue(isset($bbformdata->recordallfromstart)); + } + + /** + * Presave common + */ + public function test_process_pre_save_common() { + global $CFG; + $this->resetAfterTest(); + + list($bbactivitycontext, $bbactivitycm, $bbactivity) = + $this->create_instance(null, ['type' => instance::TYPE_RECORDING_ONLY]); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + + $bbformdata->groupmode = '1'; + mod_helper::process_pre_save($bbformdata); + $this->assertEquals(0, $bbformdata->groupmode); + } + + /** + * Post save + */ + public function test_process_post_save() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = + $this->create_instance(null, ['type' => instance::TYPE_RECORDING_ONLY]); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + + // Enrol users in a course so he will receive the message. + $teacher = $generator->create_user(['role' => 'editingteacher']); + $generator->enrol_user($teacher->id, $this->get_course()->id); + + // Mark the form to trigger notification. + $bbformdata->notification = true; + $messagesink = $this->redirectMessages(); + mod_helper::process_post_save($bbformdata); + // Now run cron. + ob_start(); + $this->runAdhocTasks(); + ob_get_clean(); // Suppress output as it can fail the test. + $this->assertEquals(1, $messagesink->count()); + } + + /** + * Post save notification + */ + public function test_process_post_save_with_add() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = + $this->create_instance(null, ['type' => instance::TYPE_RECORDING_ONLY]); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + + $bbformdata->add = "1"; + + $messagesink = $this->redirectMessages(); + // Enrol users in a course so he will receive the message. + $teacher = $generator->create_user(['role' => 'editingteacher']); + $generator->enrol_user($teacher->id, $this->get_course()->id); + $bbformdata->notification = true; + mod_helper::process_post_save($bbformdata); + // Now run cron. + ob_start(); + $this->runAdhocTasks(); + ob_get_clean(); // Suppress output as it can fail the test. + $this->assertEquals(1, $messagesink->count()); + } + + /** + * Post save + * + * There was an issue when both the opening time and completion were set + * and the form was saved twice. + */ + public function test_process_post_save_twice_with_completion() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = + $this->create_instance(null, ['type' => instance::TYPE_RECORDING_ONLY]); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $bbformdata->completionunlocked = 0; + $bbformdata->completion = COMPLETION_AGGREGATION_ANY; + $bbformdata->completionview = COMPLETION_VIEWED; + $bbformdata->completionexpected = time(); + $bbformdata->openingtime = time() - 1000; + $bbformdata->closing = time() + 1000; + // Enrol users in a course so he will receive the message. + $teacher = $generator->create_user(['role' => 'editingteacher']); + $generator->enrol_user($teacher->id, $this->get_course()->id); + + // Mark the form to trigger notification. + $bbformdata->notification = true; + $messagesink = $this->redirectMessages(); + mod_helper::process_post_save($bbformdata); + // Now run cron. + ob_start(); + $this->runAdhocTasks(); + ob_get_clean(); // Suppress output as it can fail the test. + $this->assertEquals(1, $messagesink->count()); + // Do it a again, so we check we still have one event of type EVENT_MEETING_START. + mod_helper::process_post_save($bbformdata); + $this->assertEquals(1, $messagesink->count()); + } +} diff --git a/mod/bigbluebuttonbn/tests/local/helpers/reset_test.php b/mod/bigbluebuttonbn/tests/local/helpers/reset_test.php new file mode 100644 index 00000000000..56badc085fc --- /dev/null +++ b/mod/bigbluebuttonbn/tests/local/helpers/reset_test.php @@ -0,0 +1,128 @@ +. + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +namespace mod_bigbluebuttonbn\local\helpers; + +use core_tag_tag; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @coversDefaultClass \mod_bigbluebuttonbn\local\helpers\reset + * @covers \mod_bigbluebuttonbn\local\helpers\reset + */ +class reset_test extends \advanced_testcase { + use testcase_helper_trait; + /** + * Reset course item test + */ + public function test_reset_course_items() { + global $CFG; + $this->resetAfterTest(); + $CFG->bigbluebuttonbn_recordings_enabled = false; + $results = reset::reset_course_items(); + $this->assertEquals(["events" => 0, "tags" => 0, "logs" => 0], $results); + $CFG->bigbluebuttonbn_recordings_enabled = true; + $results = reset::reset_course_items(); + $this->assertEquals(["events" => 0, "tags" => 0, "logs" => 0, "recordings" => 0], $results); + } + + /** + * Reset get_status test + */ + public function test_reset_getstatus() { + $this->resetAfterTest(); + $result = reset::reset_getstatus('events'); + $this->assertEquals([ + 'component' => 'BigBlueButton', + 'item' => 'Deleted events', + 'error' => false, + ], $result); + } + + /** + * Reset event test + */ + public function test_reset_events() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance( + null, + ['openingtime' => time()] + ); + $formdata = $this->get_form_data_from_instance($bbactivity); + \mod_bigbluebuttonbn\local\helpers\mod_helper::process_post_save($formdata); + $this->assertEquals(1, $DB->count_records( + 'event', + ['modulename' => 'bigbluebuttonbn', 'courseid' => $this->get_course()->id])); + reset::reset_events($this->get_course()->id); + $this->assertEquals(0, $DB->count_records( + 'event', + ['modulename' => 'bigbluebuttonbn', 'courseid' => $this->get_course()->id])); + } + + /** + * Reset tags test + */ + public function test_reset_tags() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, + ['course' => $this->get_course()->id], + ['visible' => true] + ); + core_tag_tag::add_item_tag('mod_bigbluebuttonbn', 'bbitem', $bbactivity->id, $bbactivitycontext, 'newtag'); + $alltags = core_tag_tag::get_item_tags('mod_bigbluebuttonbn', 'bbitem', $bbactivity->id); + $this->assertCount(1, $alltags); + reset::reset_tags($this->get_course()->id); + $alltags = core_tag_tag::get_item_tags('mod_bigbluebuttonbn', 'bbitem', $bbactivity->id); + $this->assertCount(0, $alltags); + } + + /** + * Reset recordings test + */ + public function test_reset_recordings() { + $this->initialise_mock_server(); + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, + ['course' => $this->get_course()->id], + ['visible' => true] + ); + $instance = instance::get_from_instanceid($bbactivity->id); + $this->create_recordings_for_instance($instance, [ + ['name' => 'Recording 1'], + ['name' => 'Recording 2'], + ]); + $this->assertCount(2, $instance->get_recordings()); + reset::reset_recordings($this->get_course()->id); + $this->assertCount(0, $instance->get_recordings()); + } +} diff --git a/mod/bigbluebuttonbn/tests/local/helpers/roles_test.php b/mod/bigbluebuttonbn/tests/local/helpers/roles_test.php new file mode 100644 index 00000000000..cd447e6eb70 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/local/helpers/roles_test.php @@ -0,0 +1,94 @@ +. + +namespace mod_bigbluebuttonbn\local\helpers; + +use context_course; +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @covers \mod_bigbluebuttonbn\local\helpers\roles + * @coversDefaultClass \mod_bigbluebuttonbn\local\helpers\roles + */ +class roles_test extends \advanced_testcase { + use testcase_helper_trait; + + /** + * Test select separate group prevent all + * + */ + public function test_get_users_select_separate_groups_prevent_all() { + $this->resetAfterTest(); + $numstudents = 12; + $numteachers = 3; + $groupsnum = 3; + list($course, $groups, $students, $teachers, $bbactivity, $roleids) = + $this->setup_course_students_teachers( + (object) ['enablecompletion' => true, 'groupmode' => strval(SEPARATEGROUPS), 'groupmodeforce' => 1], + $numstudents, $numteachers, $groupsnum); + $context = context_course::instance($course->id); + // Prevent access all groups. + role_change_permission($roleids['teacher'], $context, 'moodle/site:accessallgroups', CAP_PREVENT); + $this->setUser($teachers[0]); + $users = roles::get_users_array($context, $bbactivity); + $this->assertCount(($numstudents + $numteachers) / $groupsnum, $users); + $this->setUser($teachers[1]); + $users = roles::get_users_array($context, $bbactivity); + $this->assertCount(($numstudents + $numteachers) / $groupsnum, $users); + $this->setUser($teachers[2]); + $users = roles::get_users_array($context, $bbactivity); + $this->assertCount(($numstudents + $numteachers) / $groupsnum, $users); + $course->groupmode = strval(SEPARATEGROUPS); + $course->groupmodeforce = "0"; + update_course($course); + $this->setUser($teachers[2]); + $users = roles::get_users_array($context, $bbactivity); + $this->assertCount($numstudents + $numteachers, $users); + + } + + /** + * Test select separate groups + * + */ + public function test_get_users_select_separate_groups() { + $this->resetAfterTest(); + $numstudents = 12; + $numteachers = 3; + $groupsnum = 3; + list($course, $groups, $students, $teachers, $bbactivity, $roleids) = + $this->setup_course_students_teachers( + (object) ['enablecompletion' => true, 'groupmode' => strval(VISIBLEGROUPS), 'groupmodeforce' => 1], + $numstudents, $numteachers, $groupsnum); + + $context = context_course::instance($course->id); + $this->setUser($teachers[0]); + $users = roles::get_users_array($context, $bbactivity); + $this->assertCount($numstudents + $numteachers, $users); + $this->setUser($teachers[1]); + $users = roles::get_users_array($context, $bbactivity); + $this->assertCount($numstudents + $numteachers, $users); + $this->setUser($teachers[1]); + $users = roles::get_users_array($context, $bbactivity); + $this->assertCount($numstudents + $numteachers, $users); + } +} diff --git a/mod/bigbluebuttonbn/tests/logger_test.php b/mod/bigbluebuttonbn/tests/logger_test.php new file mode 100644 index 00000000000..51c8c63335e --- /dev/null +++ b/mod/bigbluebuttonbn/tests/logger_test.php @@ -0,0 +1,72 @@ +. + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +namespace mod_bigbluebuttonbn; + +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +/** + * Tests for the logger class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + * @covers \mod_bigbluebuttonbn\logger + * @coversDefaultClass \mod_bigbluebuttonbn\logger + */ +class logger_test extends \advanced_testcase { + use testcase_helper_trait; + + /** + * Test delete instance logs + */ + public function test_log_instance_deleted() { + global $DB; + + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $instance = instance::get_from_instanceid($bbactivity->id); + logger::log_instance_deleted($instance); + + $this->assertTrue($DB->record_exists('bigbluebuttonbn_logs', [ + 'bigbluebuttonbnid' => $bbactivity->id, + 'log' => logger::EVENT_DELETE, + ])); + } + + /** + * Test log method + */ + public function test_log_recording_played_event() { + global $DB; + + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $instance = instance::get_from_instanceid($bbactivity->id); + + logger::log_recording_played_event($instance, 1); + $this->assertTrue($DB->record_exists('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instance->get_instance_id()])); + } +} diff --git a/mod/bigbluebuttonbn/tests/meeting_test.php b/mod/bigbluebuttonbn/tests/meeting_test.php new file mode 100644 index 00000000000..47b9d5b1964 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/meeting_test.php @@ -0,0 +1,341 @@ +. + +/** + * Meeting test. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn; + +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +/** + * Meeting tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @covers \mod_bigbluebuttonbn\meeting + * @coversDefaultClass \mod_bigbluebuttonbn\meeting + */ +class meeting_test extends \advanced_testcase { + use testcase_helper_trait; + + /** + * Setup Test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + // We do not force the group mode so we can change the activity group mode during test. + $this->course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS]); + $this->getDataGenerator()->create_group(['name' => 'G1', 'courseid' => $this->course->id]); + $this->getDataGenerator()->create_group(['name' => 'G2', 'courseid' => $this->course->id]); + } + + /** + * Get a list of possible test (dataprovider) + * + * @return array[] + */ + public function get_instance_types_meeting_info(): array { + return [ + 'Instance Type ALL - No Group' => [ + 'type' => instance::TYPE_ALL, + 'groupname' => null, + 'groupmode' => NOGROUPS, + 'canjoin' => ['useringroup' => true, 'usernotingroup' => true], + ], + 'Instance Type ALL - Group 1 - Visible groups' => [ + 'type' => instance::TYPE_ALL, + 'groupname' => 'G1', + 'groupmode' => VISIBLEGROUPS, + 'canjoin' => ['useringroup' => true, 'usernotingroup' => true], + ], + 'Instance Type ALL - Group 1 - Separate groups' => [ + 'type' => instance::TYPE_ALL, + 'groupname' => 'G1', + 'groupmode' => SEPARATEGROUPS, + 'canjoin' => ['useringroup' => true, 'usernotingroup' => false], + ], + 'Instance Type ROOM Only - No Group' => [ + 'type' => instance::TYPE_ROOM_ONLY, + 'groupname' => null, + 'groupmode' => NOGROUPS, + 'canjoin' => ['useringroup' => true, 'usernotingroup' => true], + ], + 'Instance Type ROOM Only - Group 1 - Visible groups' => [ + 'type' => instance::TYPE_ROOM_ONLY, + 'groupname' => 'G1', + 'groupmode' => VISIBLEGROUPS, + 'canjoin' => ['useringroup' => true, 'usernotingroup' => true], + ], + 'Instance Type ROOM Only - Group 1 - Separate groups' => [ + 'type' => instance::TYPE_ROOM_ONLY, + 'groupname' => 'G1', + 'groupmode' => SEPARATEGROUPS, + 'canjoin' => ['useringroup' => true, 'usernotingroup' => false], + ], + 'Instance Type Recording Only - No Group' => [ + 'type' => instance::TYPE_RECORDING_ONLY, + 'groupname' => null, + 'groupmode' => NOGROUPS, + 'canjoin' => ['useringroup' => false, 'usernotingroup' => false] + ], + 'Instance Type Recording Only - Group 1' => [ + 'type' => instance::TYPE_RECORDING_ONLY, + 'groupname' => 'G1', + 'groupmode' => VISIBLEGROUPS, + 'canjoin' => ['useringroup' => false, 'usernotingroup' => false] + ] + ]; + } + + /** + * Test that create meeing is working for all types. + * + * @dataProvider get_instance_types_meeting_info + * @param int $type + * @param string|null $groupname + * @covers ::create_meeting + * @covers ::create_meeting_data + * @covers ::create_meeting_metadata + */ + public function test_create_meeting(int $type, ?string $groupname) { + $this->resetAfterTest(); + [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = + $this->prepare_meeting($type, $groupname, SEPARATEGROUPS, false); + $meeting->create_meeting(); + $meetinginfo = $meeting->get_meeting_info(); + $this->assertNotNull($meetinginfo); + $this->assertEquals($activity->id, $meetinginfo->bigbluebuttonbnid); + $this->assertFalse($meetinginfo->statusrunning); + $this->assertStringContainsString("is ready", $meetinginfo->statusmessage); + $this->assertEquals($groupid, $meetinginfo->groupid); + } + + /** + * Test for get meeting info for all types + * + * @param int $type + * @param string|null $groupname + * @dataProvider get_instance_types_meeting_info + * @covers ::get_meeting_info + * @covers ::do_get_meeting_info + */ + public function test_get_meeting_info(int $type, ?string $groupname) { + $this->resetAfterTest(); + [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting($type, $groupname); + $meetinginfo = $meeting->get_meeting_info(); + $this->assertNotNull($meetinginfo); + $this->assertEquals($activity->id, $meetinginfo->bigbluebuttonbnid); + $this->assertTrue($meetinginfo->statusrunning); + $this->assertStringContainsString("in progress", $meetinginfo->statusmessage); + $this->assertEquals($groupid, $meetinginfo->groupid); + $meeting->end_meeting(); + $meeting->update_cache(); + $meetinginfo = $meeting->get_meeting_info(); + $this->assertFalse($meetinginfo->statusrunning); + } + + /** + * Test can join is working for all types + * + * @param int $type + * @param string|null $groupname + * @param int $groupmode + * @param array $canjoin + * @throws \coding_exception + * @dataProvider get_instance_types_meeting_info + * @covers ::can_join + */ + public function test_can_join(int $type, ?string $groupname, int $groupmode, array $canjoin) { + $this->resetAfterTest(); + [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = $this->prepare_meeting($type, $groupname, $groupmode); + $this->setUser($useringroup); + $meeting->update_cache(); + $this->assertEquals($canjoin['useringroup'], $meeting->can_join()); + if ($meeting->can_join()) { + $meetinginfo = $meeting->get_meeting_info(); + $this->assertStringContainsString("This conference is in progress", $meetinginfo->statusmessage); + } + if ($groupname) { + $this->setUser($usernotingroup); + $meeting->update_cache(); + $this->assertEquals($canjoin['usernotingroup'], $meeting->can_join()); + } + } + + /** + * Test can join is working if opening/closing time are set + * + * @param int $type + * @param string|null $groupname + * @param int $groupmode + * @param array $canjoin + * @param array $dates + * @throws \coding_exception + * @dataProvider get_data_can_join_with_dates + * @covers ::can_join + */ + public function test_can_join_with_dates(int $type, ?string $groupname, int $groupmode, array $canjoin, array $dates) { + $this->resetAfterTest(); + [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = + $this->prepare_meeting($type, $groupname, $groupmode, true, $dates); + $this->setUser($useringroup); + $meeting->update_cache(); + $this->assertEquals($canjoin['useringroup'], $meeting->can_join()); + if ($groupname) { + $this->setUser($usernotingroup); + $meeting->update_cache(); + $this->assertEquals($canjoin['usernotingroup'], $meeting->can_join()); + } + } + + /** + * Test that attendees returns the right list of attendees + * + * @covers ::get_attendees + */ + public function test_get_attendees() { + $this->resetAfterTest(); + [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = + $this->prepare_meeting(instance::TYPE_ALL, null, NOGROUPS, true); + $this->setUser($useringroup); + $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); + $meeting->update_cache(); + $this->assertCount(1, $meeting->get_attendees()); + $otheruser = $this->getDataGenerator()->create_and_enrol($this->get_course()); + $this->setUser($otheruser); + $meeting->update_cache(); + $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); + $meeting->update_cache(); + $this->assertCount(2, $meeting->get_attendees()); + } + + /** + * Test that attendees returns the right list of attendees + * + * @covers ::get_attendees + */ + public function test_participant_count() { + $this->resetAfterTest(); + [$meeting, $useringroup, $usernotingroup, $groupid, $activity] = + $this->prepare_meeting(instance::TYPE_ALL, null, NOGROUPS, true); + $this->setUser($useringroup); + $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); + $meeting->update_cache(); + $meetinginfo = $meeting->get_meeting_info(); + $this->assertEquals(1, $meetinginfo->participantcount); + $this->assertEquals(0, $meetinginfo->moderatorcount); + $this->setUser($usernotingroup); + $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); + $meeting->update_cache(); + $meetinginfo = $meeting->get_meeting_info(); + $this->assertEquals(2, $meetinginfo->participantcount); + $this->assertEquals(0, $meetinginfo->moderatorcount); + $this->setAdminUser(); + $this->join_meeting($meeting->join(logger::ORIGIN_BASE)); + $meeting->update_cache(); + $meetinginfo = $meeting->get_meeting_info(); + $this->assertEquals(2, $meetinginfo->participantcount); + $this->assertEquals(1, $meetinginfo->moderatorcount); + } + /** + * Send a join meeting API CALL + * + * @param string $url + */ + protected function join_meeting(string $url) { + $curl = new \curl(); + $url = new \moodle_url($url); + $curl->get($url->out_omit_querystring(), $url->params()); + } + + /** + * Get a list of possible test (dataprovider) + * + * @return array[] + */ + public function get_data_can_join_with_dates(): array { + return [ + 'Instance Type ALL - No Group - Closed in past' => [ + 'type' => instance::TYPE_ALL, + 'groupname' => null, + 'groupmode' => NOGROUPS, + 'canjoin' => ['useringroup' => false, 'usernotingroup' => false], + 'dates' => ['openingtime' => time() - 7200, 'closingtime' => time() - 3600] + ], + 'Instance Type ALL - No Group - Open in future' => [ + 'type' => instance::TYPE_ALL, + 'groupname' => null, + 'groupmode' => NOGROUPS, + 'canjoin' => ['useringroup' => false, 'usernotingroup' => false], + 'dates' => ['openingtime' => time() + 3600, 'closingtime' => time() + 7200] + ], + ]; + } + + /** + * Helper to prepare for a meeting + * + * @param int $type + * @param string|null $groupname + * @param int $groupmode + * @param bool $createmeeting + * @param array $dates + * @return array + */ + protected function prepare_meeting(int $type, ?string $groupname, int $groupmode = SEPARATEGROUPS, bool $createmeeting = true, + array $dates = []) { + $this->setAdminUser(); + $bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + $groupid = 0; + $useringroup = $this->getDataGenerator()->create_and_enrol($this->get_course()); + $usernotingroup = $this->getDataGenerator()->create_and_enrol($this->get_course()); + if (!empty($groupname)) { + $groupid = groups_get_group_by_name($this->get_course()->id, $groupname); + $this->getDataGenerator()->create_group_member(['groupid' => $groupid, 'userid' => $useringroup->id]); + } + $meetinginfo = [ + 'course' => $this->get_course()->id, + 'type' => $type + ]; + if ($dates) { + $meetinginfo = array_merge($meetinginfo, $dates); + }; + $activity = $bbbgenerator->create_instance($meetinginfo, ['groupmode' => $groupmode]); + $instance = instance::get_from_instanceid($activity->id); + if ($groupid) { + $instance->set_group_id($groupid); + } + if ($createmeeting) { + // Create the meetings on the mock server, so we can join it as a simple user. + $bbbgenerator->create_meeting([ + 'instanceid' => $instance->get_instance_id(), + 'groupid' => $instance->get_group_id() + ]); + } + $meeting = new meeting($instance); + return [$meeting, $useringroup, $usernotingroup, $groupid, $activity]; + } +} diff --git a/mod/bigbluebuttonbn/tests/privacy/provider_test.php b/mod/bigbluebuttonbn/tests/privacy/provider_test.php new file mode 100644 index 00000000000..ead70ad2930 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/privacy/provider_test.php @@ -0,0 +1,291 @@ +. + +/** + * Privacy provider tests. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +namespace mod_bigbluebuttonbn\privacy; + +use context_module; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\userlist; + +/** + * Privacy provider tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @covers \mod_bigbluebuttonbn\privacy\provider + */ +class provider_test extends \core_privacy\tests\provider_testcase { + + /** + * Test for provider::get_metadata(). + */ + public function test_get_metadata() { + $this->resetAfterTest(true); + + $collection = new collection('mod_bigbluebuttonbn'); + $newcollection = provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(4, $itemcollection); + + $instancetable = array_shift($itemcollection); + $this->assertEquals('bigbluebuttonbn', $instancetable->get_name()); + + $instancelogstable = array_shift($itemcollection); + $this->assertEquals('bigbluebuttonbn_logs', $instancelogstable->get_name()); + + $recordings = array_shift($itemcollection); + $this->assertEquals('bigbluebuttonbn_recordings', $recordings->get_name()); + + $bigbluebuttonserver = array_shift($itemcollection); + $this->assertEquals('bigbluebutton', $bigbluebuttonserver->get_name()); + + $privacyfields = $instancetable->get_privacy_fields(); + $this->assertArrayHasKey('participants', $privacyfields); + $this->assertEquals('privacy:metadata:bigbluebuttonbn', $instancetable->get_summary()); + + $privacyfields = $instancelogstable->get_privacy_fields(); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('timecreated', $privacyfields); + $this->assertArrayHasKey('meetingid', $privacyfields); + $this->assertArrayHasKey('log', $privacyfields); + $this->assertArrayHasKey('meta', $privacyfields); + $this->assertEquals('privacy:metadata:bigbluebuttonbn_logs', $instancelogstable->get_summary()); + + $privacyfields = $bigbluebuttonserver->get_privacy_fields(); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('fullname', $privacyfields); + $this->assertEquals('privacy:metadata:bigbluebutton', $bigbluebuttonserver->get_summary()); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + $bigbluebuttonbn = $e['instance']; + $course = $e['course']; + + // Another bigbluebuttonbn activity that has no user activity. + $this->getDataGenerator()->create_module('bigbluebuttonbn', ['course' => $course]); + + // Create a user which will make a submission. + $user = $this->getDataGenerator()->create_user(); + + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user->id]); + + // Check the contexts supplied are correct. + $contextlist = provider::get_contexts_for_userid($user->id); + $this->assertCount(1, $contextlist); + + $contextformodule = $contextlist->current(); + $cmcontext = context_module::instance($bigbluebuttonbn->cmid); + $this->assertEquals($cmcontext->id, $contextformodule->id); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context_logs() { + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + $bigbluebuttonbn = $e['instance']; + + // Create users which will make submissions. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user2->id]); + + // Export all of the data for the context for user 1. + $cmcontext = context_module::instance($bigbluebuttonbn->cmid); + $this->export_context_data_for_user($user1->id, $cmcontext, 'mod_bigbluebuttonbn'); + $writer = \core_privacy\local\request\writer::with_context($cmcontext); + + $this->assertTrue($writer->has_any_data()); + + $data = $writer->get_data(); + $this->assertCount(2, $data->logs); + } + + /** + * Test that only users with relevant contexts are fetched. + */ + public function test_get_users_in_context() { + // For backward compatibility with old versions of Moodle. + if (!class_exists('\core_privacy\local\request\userlist')) { + return; + } + + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + $bigbluebuttonbn = $e['instance']; + + // Users which will make submissions. + $user1 = $e['users'][0]; + $user2 = $e['users'][1]; + + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user2->id]); + + // Export all of the data for the context for user 1. + $cmcontext = context_module::instance($bigbluebuttonbn->cmid); + + $userlist = new userlist($cmcontext, 'mod_bigbluebuttonbn'); + provider::get_users_in_context($userlist); + + // Ensure correct users are found in relevant contexts. + $this->assertCount(2, $userlist); + $expected = [intval($user1->id), intval($user2->id)]; + $actual = $userlist->get_userids(); + $this->assertEquals(sort($expected), sort($actual)); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + + // Before deletion, we should have 3 responses, 1 Add event and 2 Create events (1 per user). + $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id]); + $this->assertEquals(3, $count); + + // Delete data based on context. + $cmcontext = context_module::instance($e['instance']->cmid); + provider::delete_data_for_all_users_in_context($cmcontext); + + // After deletion, the bigbluebuttonbn logs for that activity should have been deleted. + $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id]); + $this->assertEquals(0, $count); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + + // Delete data for the first user. + $context = \context_module::instance($e['instance']->cmid); + $contextlist = new approved_contextlist($e['users'][0], 'bigbluebuttonbn', + [$context->id]); + provider::delete_data_for_user($contextlist); + + // After deletion the bigbluebuttonbn logs for the first user should have been deleted. + $count = $DB->count_records('bigbluebuttonbn_logs', + ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][0]->id]); + $this->assertEquals(0, $count); + + // Check the logs for the other user is still there. + $count = $DB->count_records('bigbluebuttonbn_logs', + ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][1]->id]); + $this->assertEquals(1, $count); + } + + /** + * Test that data for users in approved userlist is deleted. + */ + public function test_delete_data_for_users() { + global $DB; + + // For backward compatibility with old versions of Moodle. + if (!class_exists('\core_privacy\local\request\approved_userlist')) { + return; + } + + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + + // Delete user 1 and 2 data from chat 1 context only. + $context = \context_module::instance($e['instance']->cmid); + $approveduserids = [$e['users'][0]->id]; + $approvedlist = new approved_userlist($context, 'mod_bigbluebuttonbn', $approveduserids); + provider::delete_data_for_users($approvedlist); + + // After deletion the bigbluebuttonbn logs for the first user should have been deleted. + $count = $DB->count_records('bigbluebuttonbn_logs', + ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][0]->id]); + $this->assertEquals(0, $count); + + // Check the logs for the other user is still there. + $count = $DB->count_records('bigbluebuttonbn_logs', + ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][1]->id]); + $this->assertEquals(1, $count); + } + + /** + * Prepares the environment for testing. + * + * @return array $e + */ + protected function get_bigbluebuttonbn_environemnt() { + $e = []; + + // Create a course. + $e['course'] = $this->getDataGenerator()->create_course(); + + // Create a bigbluebuttonbn instance. + $e['instance'] = $this->getDataGenerator()->create_module('bigbluebuttonbn', + ['course' => $e['course']->id]); + + // Create users that will use the bigbluebuttonbn instance. + $e['users'][] = $this->getDataGenerator()->create_user(); + $e['users'][] = $this->getDataGenerator()->create_user(); + + // Create the bigbluebuttonbn logs. + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][0]->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][1]->id]); + + return $e; + } +} diff --git a/mod/bigbluebuttonbn/tests/recording_test.php b/mod/bigbluebuttonbn/tests/recording_test.php new file mode 100644 index 00000000000..c5722dbb5f0 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/recording_test.php @@ -0,0 +1,256 @@ +. + +/** + * Recording tests. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn; + +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +/** + * Recording test class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @covers \mod_bigbluebuttonbn\recording + * @coversDefaultClass \mod_bigbluebuttonbn\recording + */ +class recording_test extends \advanced_testcase { + use testcase_helper_trait; + + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + } + + /** + * Test for bigbluebuttonbn_get_allrecordings status refresh. + * + * @param int $status + * @dataProvider get_status_provider + * @covers ::get + */ + public function test_get_allrecordings_status_refresh(int $status) { + $this->resetAfterTest(); + ['recordings' => $recordings] = $this->create_activity_with_recordings($this->get_course(), + instance::TYPE_ALL, [['status' => $status]]); + + $this->assertEquals($status, (new recording($recordings[0]->id))->get('status')); + } + + /** + * Get name + * + * @covers ::get_name + */ + public function test_get_name(): void { + $this->resetAfterTest(); + ['recordings' => $recordings] = $this->create_activity_with_recordings($this->get_course(), + instance::TYPE_ALL, [['name' => 'Example name']]); + + $this->assertEquals('Example name', (new recording($recordings[0]->id))->get('name')); + } + + /** + * Test get description + * + * @covers ::get_description + */ + public function test_get_description(): void { + $this->resetAfterTest(); + ['recordings' => $recordings] = $this->create_activity_with_recordings($this->get_course(), + instance::TYPE_ALL, [[ + 'description' => 'Example description', + ]]); + + $this->assertEquals('Example description', (new recording($recordings[0]->id))->get('description')); + } + + /** + * Get possible status + * + * @return array[] + */ + public function get_status_provider(): array { + return [ + [recording::RECORDING_STATUS_PROCESSED], + [recording::RECORDING_STATUS_DISMISSED], + ]; + } + + /** + * Test for bigbluebuttonbn_get_allrecordings() + * + * @param int $type The activity type + * @dataProvider get_allrecordings_types_provider + * @covers ::get_recordings_for_instance + */ + public function test_get_allrecordings(int $type): void { + $this->resetAfterTest(); + $recordingcount = 2; // Two recordings only. + list('activity' => $activity) = + $this->create_activity_with_recordings($this->get_course(), + $type, array_pad([], $recordingcount, [])); + + // Fetch the recordings for the instance. + // The count shoudl match the input count. + $recordings = recording::get_recordings_for_instance(instance::get_from_instanceid($activity->id)); + $this->assertCount($recordingcount, $recordings); + } + + /** + * Get possible type for recording / tests + * + * @return array[] + */ + public function get_allrecordings_types_provider(): array { + return [ + 'Instance Type ALL' => [ + 'type' => instance::TYPE_ALL + ], + 'Instance Type ROOM Only' => [ + 'type' => instance::TYPE_ROOM_ONLY, + ], + 'Instance Type Recording only' => [ + 'type' => instance::TYPE_RECORDING_ONLY + ], + ]; + } + + /** + * Test for bigbluebuttonbn_get_allrecordings(). + * + * @param int $type + * @dataProvider get_allrecordings_types_provider + */ + public function test_get_recording_for_group($type) { + $this->resetAfterTest(); + + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + + $testcourse = $this->getDataGenerator()->create_course(['groupmodeforce' => true, 'groupmode' => VISIBLEGROUPS]); + $teacher = $this->getDataGenerator()->create_and_enrol($testcourse, 'editingteacher'); + + $group1 = $this->getDataGenerator()->create_group(['G1', 'courseid' => $testcourse->id]); + $student1 = $this->getDataGenerator()->create_and_enrol($testcourse); + $this->getDataGenerator()->create_group_member(['userid' => $student1, 'groupid' => $group1->id]); + + $group2 = $this->getDataGenerator()->create_group(['G2', 'courseid' => $testcourse->id]); + $student2 = $this->getDataGenerator()->create_and_enrol($testcourse); + $this->getDataGenerator()->create_group_member(['userid' => $student2, 'groupid' => $group2->id]); + + // No group. + $student3 = $this->getDataGenerator()->create_and_enrol($testcourse); + + $activity = $plugingenerator->create_instance([ + 'course' => $testcourse->id, + 'type' => $type, + 'name' => 'Example' + ]); + $instance = instance::get_from_instanceid($activity->id); + $instance->set_group_id(0); + $this->create_recordings_for_instance($instance, [['name' => "Pre-Recording 1"], ['name' => "Pre-Recording 2"]]); + $instance->set_group_id($group1->id); + $this->create_recordings_for_instance($instance, [['name' => "Group 1 Recording 1"]]); + $instance->set_group_id($group2->id); + $this->create_recordings_for_instance($instance, [['name' => "Group 2 Recording 1"]]); + + $this->setUser($student1); + $instance1 = instance::get_from_instanceid($activity->id); + $instance1->set_group_id($group1->id); + $recordings = recording::get_recordings_for_instance($instance1); + $this->assertCount(1, $recordings); + $this->assert_has_recording_by_name('Group 1 Recording 1', $recordings); + + $this->setUser($student2); + $instance2 = instance::get_from_instanceid($activity->id); + $instance2->set_group_id($group2->id); + $recordings = recording::get_recordings_for_instance($instance2); + $this->assertCount(1, $recordings); + $this->assert_has_recording_by_name('Group 2 Recording 1', $recordings); + + $this->setUser($student3); + $instance3 = instance::get_from_instanceid($activity->id); + $recordings = recording::get_recordings_for_instance($instance3); + $this->assertIsArray($recordings); + $this->assertCount(4, $recordings); + $this->assert_has_recording_by_name('Pre-Recording 1', $recordings); + $this->assert_has_recording_by_name('Pre-Recording 2', $recordings); + + } + + /** + * Test that we can get recordings from a deleted activity + * + * @param int $type + * @dataProvider get_allrecordings_types_provider + */ + public function test_get_recordings_from_deleted_activity($type) { + $this->resetAfterTest(true); + $this->initialise_mock_server(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + + $testcourse = $this->getDataGenerator()->create_course(); + + $activity = $plugingenerator->create_instance([ + 'course' => $testcourse->id, + 'type' => $type, + 'name' => 'Example' + ]); + $instance = instance::get_from_instanceid($activity->id); + $this->create_recordings_for_instance($instance, [['name' => "Deleted Recording 1"]]); + $activity2 = $plugingenerator->create_instance([ + 'course' => $testcourse->id, + 'type' => $type, + 'name' => 'Example' + ]); + $instance2 = instance::get_from_instanceid($activity2->id); + $this->create_recordings_for_instance($instance2, [['name' => "Recording 1"]]); + + bigbluebuttonbn_delete_instance($activity->id); + $recordings = recording::get_recordings_for_course($testcourse->id, [], false, false, true); + $this->assertCount(2, $recordings); + $this->assert_has_recording_by_name('Deleted Recording 1', $recordings); + $this->assert_has_recording_by_name('Recording 1', $recordings); + $recordings = recording::get_recordings_for_course($testcourse->id, [], false, false, false); + $this->assertCount(1, $recordings); + } + + /** + * Check that a recording exist in the list of recordings + * + * @param string $recordingname + * @param array $recordings + */ + public function assert_has_recording_by_name($recordingname, $recordings) { + $recordingnames = array_map(function($r) { + return $r->get('name'); + }, $recordings); + $this->assertContains($recordingname, $recordingnames); + } +} diff --git a/mod/bigbluebuttonbn/tests/search_test.php b/mod/bigbluebuttonbn/tests/search_test.php new file mode 100644 index 00000000000..7e5c2d1fd24 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/search_test.php @@ -0,0 +1,74 @@ +. + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); + +/** + * Provides the unit tests for forum search. + * + * @package mod_bigbluebuttonbn + * @copyright 2021 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +class search_test extends advanced_testcase { + use testcase_helper_trait; + /** + * @var string Area id + */ + protected $bbbactivtyarea = null; + + public function setUp(): void { + set_config('enableglobalsearch', true); + $this->bbbactivtyarea = \core_search\manager::get_search_area('mod_bigbluebuttonbn-activity'); + } + + /** + * Test for indexing + * + * @return void + */ + public function test_indexing() { + $this->resetAfterTest(); + + // Setup test data. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $bbactivity1 = $generator->create_module('bigbluebuttonbn', ['course' => $course->id, 'name' => 'BBB 1']); + $bbactivity2 = $generator->create_module('bigbluebuttonbn', ['course' => $course->id, 'name' => 'BBB 2']); + + // Get all surveys for indexing - note that there are special entries in the table with + // course zero which should not be returned. + $rs = $this->bbbactivtyarea->get_document_recordset(); + $this->assertEquals(2, iterator_count($rs)); + $rs->close(); + + // Test specific context and course context. + $rs = $this->bbbactivtyarea->get_document_recordset(0, context_module::instance($bbactivity1->cmid)); + $this->assertEquals(1, iterator_count($rs)); + $rs->close(); + $rs = $this->bbbactivtyarea->get_document_recordset(0, context_course::instance($course->id)); + $documents = iterator_to_array($rs); + $this->assertEquals(2, count($documents)); + $rs->close(); + } +} diff --git a/mod/bigbluebuttonbn/tests/task/send_instance_update_notification_test.php b/mod/bigbluebuttonbn/tests/task/send_instance_update_notification_test.php new file mode 100644 index 00000000000..77ce8c9af88 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/task/send_instance_update_notification_test.php @@ -0,0 +1,106 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use advanced_testcase; + +/** + * Class containing the scheduled task for lti module. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_bigbluebuttonbn\task\send_notification + * @covers \mod_bigbluebuttonbn\task\send_notification + * @covers \mod_bigbluebuttonbn\task\send_instance_update_notification + */ +class send_instance_update_notification_test extends advanced_testcase { + /** + * Test the sending of messages. + * + * @dataProvider update_type_provider + * @param int $type + */ + public function test_recipients(int $type): void { + $this->resetAfterTest(); + $this->preventResetByRollback(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $instancedata = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + + // Create some users in the course, and some not. + $editingteacher = $generator->create_and_enrol($course, 'editingteacher'); + $teacher = $generator->create_and_enrol($course, 'teacher'); + $students = [ + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + ]; + + $recipients = array_map(function($user) { + return $user->id; + }, $students); + $recipients[] = $editingteacher->id; + $recipients[] = $teacher->id; + + $unrelateduser = $generator->create_user(); + + $stub = $this->getMockBuilder(send_instance_update_notification::class) + ->onlyMethods([]) + ->getMock(); + $stub->set_update_type($type); + + $instancedata = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + + $stub->set_instance_id($instancedata->id); + + // Capture events. + $sink = $this->redirectMessages(); + + // Now execute. + $stub->execute(); + + // Check the events. + $messages = $sink->get_messages(); + $this->assertCount(7, $messages); + + foreach ($messages as $message) { + $this->assertNotFalse(array_search($message->useridto, $recipients)); + $this->assertNotEquals($unrelateduser->id, $message->useridto); + $this->assertEquals($editingteacher->id, $message->useridfrom); + } + } + + /** + * Provider to provide a list of update types. + * + * @return array + */ + public function update_type_provider(): array { + return [ + [send_instance_update_notification::TYPE_CREATED], + [send_instance_update_notification::TYPE_UPDATED], + ]; + } +} diff --git a/mod/bigbluebuttonbn/tests/task/send_notification_test.php b/mod/bigbluebuttonbn/tests/task/send_notification_test.php new file mode 100644 index 00000000000..915e3077b34 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/task/send_notification_test.php @@ -0,0 +1,62 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use advanced_testcase; + +/** + * Class containing the scheduled task for lti module. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \mod_bigbluebuttonbn\task\send_notification + * @coversDefaultClass \mod_bigbluebuttonbn\task\send_notification + */ +class send_notification_test extends advanced_testcase { + /** + * Check if set instance ID works correctly + * + */ + public function test_set_instance_id(): void { + $this->resetAfterTest(); + $stub = $this->getMockForAbstractClass( + send_notification::class, + [], + '', + true, + true, + true, + [] + ); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $instancedata = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + + $stub->set_instance_id($instancedata->id); + + $rc = new \ReflectionClass(send_notification::class); + $rcm = $rc->getMethod('get_instance'); + $rcm->setAccessible(true); + $instance = $rcm->invoke($stub); + + $this->assertEquals($instancedata->id, $instance->get_instance_id()); + } +} diff --git a/mod/bigbluebuttonbn/tests/task/send_recording_ready_notification_test.php b/mod/bigbluebuttonbn/tests/task/send_recording_ready_notification_test.php new file mode 100644 index 00000000000..7c9e497ebb8 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/task/send_recording_ready_notification_test.php @@ -0,0 +1,194 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use advanced_testcase; + +/** + * Class containing the scheduled task for lti module. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_bigbluebuttonbn\task\send_notification + * @covers \mod_bigbluebuttonbn\task\send_notification + * @covers \mod_bigbluebuttonbn\task\send_recording_ready_notification + */ +class send_recording_ready_notification_test extends advanced_testcase { + /** + * Test the sending of messages. + */ + public function test_recipients(): void { + $this->resetAfterTest(); + $this->preventResetByRollback(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $instancedata = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + + // Create some users in the course, and some not. + $editingteacher = $generator->create_and_enrol($course, 'editingteacher'); + $teacher = $generator->create_and_enrol($course, 'teacher'); + $students = [ + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + ]; + + $recipients = array_map(function($user) { + return $user->id; + }, $students); + $recipients[] = $editingteacher->id; + $recipients[] = $teacher->id; + + $unrelateduser = $generator->create_user(); + + $instancedata = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + + $stub = $this->getMockBuilder(send_recording_ready_notification::class) + ->onlyMethods([]) + ->getMock(); + + $stub->set_instance_id($instancedata->id); + + // Capture events. + $sink = $this->redirectMessages(); + + // Now execute. + $stub->execute(); + + // Check the events. + $messages = $sink->get_messages(); + $this->assertCount(7, $messages); + + foreach ($messages as $message) { + $this->assertNotFalse(array_search($message->useridto, $recipients)); + $this->assertNotEquals($unrelateduser->id, $message->useridto); + $this->assertEquals($editingteacher->id, $message->useridfrom); + } + } + + /** + * Test the sending of messages. + */ + public function test_recipients_no_teacher(): void { + $this->resetAfterTest(); + $this->preventResetByRollback(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $instancedata = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + + // Create some users in the course, and some not. + $students = [ + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + $generator->create_and_enrol($course, 'student'), + ]; + + $recipients = array_map(function($user) { + return $user->id; + }, $students); + + $unrelateduser = $generator->create_user(); + + $instancedata = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + + $stub = $this->getMockBuilder(send_recording_ready_notification::class) + ->onlyMethods([]) + ->getMock(); + + $stub->set_instance_id($instancedata->id); + + // Capture events. + $sink = $this->redirectMessages(); + + // Now execute. + $stub->execute(); + + // Check the events. + $messages = $sink->get_messages(); + $this->assertCount(5, $messages); + + $noreplyuser = \core_user::get_noreply_user(); + foreach ($messages as $message) { + $this->assertNotFalse(array_search($message->useridto, $recipients)); + $this->assertNotEquals($unrelateduser->id, $message->useridto); + $this->assertEquals($noreplyuser->id, $message->useridfrom); + } + } + + /** + * Test that messages are not sent to a suspended user. + */ + public function test_messages_sent_suspended_user(): void { + global $DB; + + $this->resetAfterTest(); + $this->preventResetByRollback(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $instancedata = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + + // Create some users in the course, and some not. + $student = $generator->create_and_enrol($course, 'student'); + $suspendedstudent = $generator->create_and_enrol($course, 'student'); + $DB->set_field('user', 'suspended', 1, ['id' => $suspendedstudent->id]); + + $instancedata = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + + $stub = $this->getMockBuilder(send_recording_ready_notification::class) + ->onlyMethods([]) + ->getMock(); + + $stub->set_instance_id($instancedata->id); + + // Capture events. + $sink = $this->redirectMessages(); + + // Now execute. + $stub->execute(); + + // Check the events. + $messages = $sink->get_messages(); + $this->assertCount(1, $messages); + + $noreplyuser = \core_user::get_noreply_user(); + foreach ($messages as $message) { + $this->assertEquals($student->id, $message->useridto); + $this->assertNotEquals($suspendedstudent->id, $message->useridto); + $this->assertEquals($noreplyuser->id, $message->useridfrom); + } + } +} diff --git a/mod/bigbluebuttonbn/tests/task/upgrade_recordings_imported_test.php b/mod/bigbluebuttonbn/tests/task/upgrade_recordings_imported_test.php new file mode 100644 index 00000000000..6ee30fb4c24 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/task/upgrade_recordings_imported_test.php @@ -0,0 +1,123 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use advanced_testcase; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\proxy\recording_proxy; +use mod_bigbluebuttonbn\recording; +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +/** + * Class containing the scheduled task for lti module. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \mod_bigbluebuttonbn\task\upgrade_imported_recordings + * @covers \mod_bigbluebuttonbn\task\upgrade_recording_base_task + */ +class upgrade_recordings_imported_test extends advanced_testcase { + use testcase_helper_trait; + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->initialise_mock_server(); + } + /** + * Upgrade task test + */ + public function test_upgrade_recordings(): void { + global $DB; + $generator = $this->getDataGenerator(); + + // Create a course with student and teacher, and two groups. + $course = $generator->create_course(); + $groupa = $generator->create_group(['courseid' => $course->id]); + $groupb = $generator->create_group(['courseid' => $course->id]); + + $teacher = $generator->create_and_enrol($course, 'editingteacher'); + $generator->create_and_enrol($course, 'student'); + + // Create an ungrouped activity. + $activity = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + $instance = instance::get_from_instanceid($activity->id); + $this->create_legacy_log_entries($instance, $teacher->id, 30, true); + + // Create an grouped activity. + $activity = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + 'groupmode' => SEPARATEGROUPS, + ]); + $groupedinstance = instance::get_from_instanceid($activity->id); + + $groupainstance = instance::get_group_instance_from_instance($groupedinstance, $groupa->id); + $this->create_legacy_log_entries($groupainstance, $teacher->id, 15, true); + + $groupbinstance = instance::get_group_instance_from_instance($groupedinstance, $groupb->id); + $this->create_legacy_log_entries($groupbinstance, $teacher->id, 15, true); + + // Create logs for an activity which no longer exists (because we deleted it). + $activity = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + $oldinstance = instance::get_from_instanceid($activity->id); + $this->create_legacy_log_entries($oldinstance, $teacher->id, 15, true); + course_delete_module($oldinstance->get_cm_id()); + + // Truncate the recordings table to reflect what it would have looked like before this version. + $DB->delete_records('bigbluebuttonbn_recordings'); + + $upgraderecording = new upgrade_imported_recordings(); + $rc = new \ReflectionClass(upgrade_imported_recordings::class); + $rcm = $rc->getMethod('process_bigbluebuttonbn_logs'); + $rcm->setAccessible(true); + + // The first run will lead to all of them being processed, and none left over. + // A new job is always queued on a successful run. + $returnvalue = $rcm->invoke($upgraderecording); + $this->assertTrue($returnvalue); + + $this->assertEquals(0, $DB->count_records('bigbluebuttonbn_logs', ['log' => 'Import'])); + $this->assertEquals(75, recording::count_records(['imported' => '1'])); + + $this->assertEquals(15, recording::count_records(['groupid' => $groupa->id, 'imported' => '1'])); + $this->assertEquals(15, recording::count_records(['groupid' => $groupb->id, 'imported' => '1'])); + $this->assertEquals(45, recording::count_records(['groupid' => 0, 'imported' => '1'])); + + // The second run will lead to no change in the number of logs, but no further jobs will be queued. + $returnvalue = $rcm->invoke($upgraderecording); + $this->assertFalse($returnvalue); + + // Ensure that logs match. + $matches = [ + 'Fetching logs for conversion', + "Creating new recording records", + "Unable to find an activity for .*. This recording is headless", + 'Migrated 75 recordings', + 'Deleting migrated log records', + 'Fetching logs for conversion', + 'No logs were found', + ]; + $this->expectOutputRegex('/' . implode('.*', $matches) . '/s'); + } +} diff --git a/mod/bigbluebuttonbn/tests/task/upgrade_recordings_test.php b/mod/bigbluebuttonbn/tests/task/upgrade_recordings_test.php new file mode 100644 index 00000000000..843e2536bbf --- /dev/null +++ b/mod/bigbluebuttonbn/tests/task/upgrade_recordings_test.php @@ -0,0 +1,124 @@ +. + +namespace mod_bigbluebuttonbn\task; + +use advanced_testcase; +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\recording; +use mod_bigbluebuttonbn\test\testcase_helper_trait; + +/** + * Class containing the scheduled task for lti module. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \mod_bigbluebuttonbn\task\upgrade_recordings + * @covers \mod_bigbluebuttonbn\task\upgrade_recording_base_task + */ +class upgrade_recordings_test extends advanced_testcase { + use testcase_helper_trait; + /** + * Setup for test + */ + public function setUp(): void { + parent::setUp(); + $this->initialise_mock_server(); + $this->resetAfterTest(); + } + /** + * Upgrade task test + */ + public function test_upgrade_recordings(): void { + global $DB; + $generator = $this->getDataGenerator(); + + // Create a course with student and teacher, and two groups. + $course = $generator->create_course(); + $groupa = $generator->create_group(['courseid' => $course->id]); + $groupb = $generator->create_group(['courseid' => $course->id]); + + $teacher = $generator->create_and_enrol($course, 'editingteacher'); + $generator->create_and_enrol($course, 'student'); + + // Create an ungrouped activity. + $activity = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + $instance = instance::get_from_instanceid($activity->id); + $this->create_legacy_log_entries($instance, $teacher->id, 30); + + // Create an grouped activity. + $activity = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + 'groupmode' => SEPARATEGROUPS, + ]); + $groupedinstance = instance::get_from_instanceid($activity->id); + + $groupainstance = instance::get_group_instance_from_instance($groupedinstance, $groupa->id); + $this->create_legacy_log_entries($groupainstance, $teacher->id, 15); + + $groupbinstance = instance::get_group_instance_from_instance($groupedinstance, $groupb->id); + $this->create_legacy_log_entries($groupbinstance, $teacher->id, 15); + + // Create logs for an activity which no longer exists (because we deleted it). + $activity = $generator->create_module('bigbluebuttonbn', [ + 'course' => $course->id, + ]); + $oldinstance = instance::get_from_instanceid($activity->id); + $this->create_legacy_log_entries($oldinstance, $teacher->id, 15); + course_delete_module($oldinstance->get_cm_id()); + + // Truncate the recordings table to reflect what it would have looked like before this version. + $DB->delete_records('bigbluebuttonbn_recordings'); + + $upgraderecording = new upgrade_recordings(); + $rc = new \ReflectionClass(upgrade_recordings::class); + $rcm = $rc->getMethod('process_bigbluebuttonbn_logs'); + $rcm->setAccessible(true); + + // The first run will lead to all of them being processed, and none left over. + // A new job is always queued on a successful run. + $returnvalue = $rcm->invoke($upgraderecording); + $this->assertTrue($returnvalue); + + $this->assertEquals(0, $DB->count_records('bigbluebuttonbn_logs', ['log' => 'Create'])); + $this->assertEquals(75, recording::count_records(['imported' => '0'])); + + $this->assertEquals(15, recording::count_records(['groupid' => $groupa->id, 'imported' => '0'])); + $this->assertEquals(15, recording::count_records(['groupid' => $groupb->id, 'imported' => '0'])); + $this->assertEquals(45, recording::count_records(['groupid' => 0, 'imported' => '0'])); + + // The second run will lead to no change in the number of logs, but no further jobs will be queued. + $returnvalue = $rcm->invoke($upgraderecording); + $this->assertFalse($returnvalue); + + // Ensure that logs match. + $matches = [ + 'Fetching logs for conversion', + "Creating new recording records", + "Unable to find an activity for .*. This recording is headless", + 'Migrated 75 recordings', + 'Deleting migrated log records', + 'Fetching logs for conversion', + 'No logs were found', + ]; + $this->expectOutputRegex('/' . implode('.*', $matches) . '/s'); + } + + +} diff --git a/mod/bigbluebuttonbn/version.php b/mod/bigbluebuttonbn/version.php new file mode 100644 index 00000000000..b081fdc7701 --- /dev/null +++ b/mod/bigbluebuttonbn/version.php @@ -0,0 +1,32 @@ +. + +/** + * Version for BigBlueButtonBN Moodle Activity Module. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; + + +$plugin->version = 2021091407; +$plugin->requires = 2020061500; +$plugin->component = 'mod_bigbluebuttonbn'; diff --git a/mod/bigbluebuttonbn/view.php b/mod/bigbluebuttonbn/view.php new file mode 100644 index 00000000000..b345024a528 --- /dev/null +++ b/mod/bigbluebuttonbn/view.php @@ -0,0 +1,101 @@ +. + +/** + * View a BigBlueButton room. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ + +use mod_bigbluebuttonbn\instance; +use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy; +use mod_bigbluebuttonbn\logger; +use mod_bigbluebuttonbn\output\view_page; +use mod_bigbluebuttonbn\plugin; + +require(__DIR__ . '/../../config.php'); +global $OUTPUT, $PAGE; + +// Get the bbb instance from either the cmid (id), or the instanceid (bn). +$id = optional_param('id', 0, PARAM_INT); +if ($id) { + $instance = instance::get_from_cmid($id); +} else { + $bn = optional_param('bn', 0, PARAM_INT); + if ($bn) { + $instance = instance::get_from_instanceid($bn); + } +} + +if (!$instance) { + throw new moodle_exception('view_error_url_missing_parameters', plugin::COMPONENT); +} + +$cm = $instance->get_cm(); +$course = $instance->get_course(); +$bigbluebuttonbn = $instance->get_instance_data(); + +require_login($course, true, $cm); + +$groupid = groups_get_activity_group($cm, true) ?: null; +if ($groupid) { + $instance->set_group_id($groupid); +} + +logger::log_instance_viewed($instance); + +// Require a working server. +bigbluebutton_proxy::require_working_server($instance); + +// Mark viewed by user (if required). +$completion = new completion_info($course); +$completion->set_module_viewed($cm); + +// Print the page header. +$PAGE->set_url($instance->get_view_url()); +$PAGE->set_title($cm->name); +$PAGE->set_cacheable(false); +$PAGE->set_heading($course->fullname); + +// Output starts. +$renderer = $PAGE->get_renderer('mod_bigbluebuttonbn'); + +echo $OUTPUT->header(); + +// Validate if the user is in a role allowed to join. +if (!$instance->can_join() && $instance->get_type() != instance::TYPE_RECORDING_ONLY) { + if (isguestuser()) { + notice(get_string('view_noguests', plugin::COMPONENT), get_login_url()); + } else { + notice( + get_string('view_nojoin', plugin::COMPONENT), + new moodle_url('/course/view.php', ['id' => $course->id]) + ); + } +} + +echo $renderer->render(new view_page($instance)); + +// Output finishes. +echo $OUTPUT->footer(); + +// Shows version as a comment. +echo '' . "\n";