diff --git a/.github/workflows/learninghub-moodle_Deploy_dev.yml b/.github/workflows/learninghub-moodle_Deploy_dev.yml index 55ce50f78d0..d53d91f60c6 100644 --- a/.github/workflows/learninghub-moodle_Deploy_dev.yml +++ b/.github/workflows/learninghub-moodle_Deploy_dev.yml @@ -104,6 +104,9 @@ jobs: - name: Create PersistentVolume for theme run: kubectl apply -f kubectl/pv-definition-theme-dev.yml + + - name: Create PersistentVolume for uploads + run: kubectl apply -f kubectl/pv-definition-uploads-dev.yml - name: Create PersistentVolumeClaim run: kubectl apply -f kubectl/pvc-definition-dev.yml @@ -111,6 +114,9 @@ jobs: - name: Create PersistentVolumeClaim for theme run: kubectl apply -f kubectl/pvc-definition-theme-dev.yml + - name: Create PersistentVolumeClaim for uploads + run: kubectl apply -f kubectl/pvc-definition-uploads-dev.yml + - name: Attach ACR to cluster run: az aks update -n ${{ vars.AZURE_CLUSTER_NAME }} -g ${{ vars.AZURE_RESOURCE_GROUP_NAME }} --attach-acr ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} continue-on-error: true @@ -188,6 +194,9 @@ jobs: rm -f $(basename $zipfile); \ done + - name: Ensure uploads folder has correct permissions applied + run: chmod ugo+rwx ./webservice/rest/uploads + - name: Build and push Docker image run: | docker build \ diff --git a/.github/workflows/learninghub-moodle_Deploy_pgvle.yml b/.github/workflows/learninghub-moodle_Deploy_pgvle.yml index fab0acacaaf..aa5c15e4af8 100644 --- a/.github/workflows/learninghub-moodle_Deploy_pgvle.yml +++ b/.github/workflows/learninghub-moodle_Deploy_pgvle.yml @@ -104,6 +104,9 @@ jobs: - name: Create PersistentVolume for theme run: kubectl apply -f kubectl/pv-definition-theme-${{ vars.AZURE_ENVIRONMENT }}.yml + + - name: Create PersistentVolume for uploads + run: kubectl apply -f kubectl/pv-definition-uploads-${{ vars.AZURE_ENVIRONMENT }}.yml - name: Create PersistentVolumeClaim run: kubectl apply -f kubectl/pvc-definition-${{ vars.AZURE_ENVIRONMENT }}.yml @@ -111,6 +114,9 @@ jobs: - name: Create PersistentVolumeClaim for theme run: kubectl apply -f kubectl/pvc-definition-theme-${{ vars.AZURE_ENVIRONMENT }}.yml + - name: Create PersistentVolumeClaim for uploads + run: kubectl apply -f kubectl/pvc-definition-uploads-${{ vars.AZURE_ENVIRONMENT }}.yml + - name: Attach ACR to cluster run: az aks update -n ${{ vars.AZURE_CLUSTER_NAME }} -g ${{ vars.AZURE_RESOURCE_GROUP_NAME }} --attach-acr ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} continue-on-error: true @@ -188,6 +194,9 @@ jobs: rm -f $(basename $zipfile); \ done + - name: Ensure uploads folder has correct permissions applied + run: chmod ugo+rwx ./webservice/rest/uploads + - name: Build and push Docker image run: | docker build -t ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }}.azurecr.io/${{ vars.DOCKER_IMAGE_NAME }}:latest . diff --git a/.github/workflows/learninghub-moodle_Deploy_prod.yml b/.github/workflows/learninghub-moodle_Deploy_prod.yml index 176182d3c98..b58d5c3a2d4 100644 --- a/.github/workflows/learninghub-moodle_Deploy_prod.yml +++ b/.github/workflows/learninghub-moodle_Deploy_prod.yml @@ -149,6 +149,9 @@ jobs: - name: Create PersistentVolume for theme run: kubectl apply -f kubectl/pv-definition-theme-prod.yml + + - name: Create PersistentVolume for uploads + run: kubectl apply -f kubectl/pv-definition-uploads-prod.yml - name: Create PersistentVolumeClaim run: kubectl apply -f kubectl/pvc-definition-prod.yml @@ -156,6 +159,9 @@ jobs: - name: Create PersistentVolumeClaim for theme run: kubectl apply -f kubectl/pvc-definition-theme-prod.yml + - name: Create PersistentVolumeClaim for uploads + run: kubectl apply -f kubectl/pvc-definition-uploads-prod.yml + - name: Attach ACR to cluster run: az aks update -n ${{ vars.AZURE_CLUSTER_NAME }} -g ${{ vars.AZURE_RESOURCE_GROUP_NAME }} --attach-acr ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} continue-on-error: true @@ -233,6 +239,9 @@ jobs: rm -f $(basename $zipfile); \ done + - name: Ensure uploads folder has correct permissions applied + run: chmod ugo+rwx ./webservice/rest/uploads + - name: Build and push Docker image run: | docker build \ diff --git a/.github/workflows/learninghub-moodle_Deploy_test.yml b/.github/workflows/learninghub-moodle_Deploy_test.yml index 785f2758336..3bcba262dd7 100644 --- a/.github/workflows/learninghub-moodle_Deploy_test.yml +++ b/.github/workflows/learninghub-moodle_Deploy_test.yml @@ -104,6 +104,9 @@ jobs: - name: Create PersistentVolume for theme run: kubectl apply -f kubectl/pv-definition-theme-test.yml + + - name: Create PersistentVolume for uploads + run: kubectl apply -f kubectl/pv-definition-uploads-test.yml - name: Create PersistentVolumeClaim run: kubectl apply -f kubectl/pvc-definition-test.yml @@ -111,6 +114,9 @@ jobs: - name: Create PersistentVolumeClaim for theme run: kubectl apply -f kubectl/pvc-definition-theme-test.yml + - name: Create PersistentVolumeClaim for uploads + run: kubectl apply -f kubectl/pvc-definition-uploads-test.yml + - name: Attach ACR to cluster run: az aks update -n ${{ vars.AZURE_CLUSTER_NAME }} -g ${{ vars.AZURE_RESOURCE_GROUP_NAME }} --attach-acr ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} continue-on-error: true @@ -188,6 +194,9 @@ jobs: rm -f $(basename $zipfile); \ done + - name: Ensure uploads folder has correct permissions applied + run: chmod ugo+rwx ./webservice/rest/uploads + - name: Build and push Docker image run: | docker build -t ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }}.azurecr.io/${{ vars.DOCKER_IMAGE_NAME }}:latest . diff --git a/.github/workflows/learninghub-moodle_Test.yml b/.github/workflows/learninghub-moodle_Test.yml index 194348ce29b..b47a1457552 100644 --- a/.github/workflows/learninghub-moodle_Test.yml +++ b/.github/workflows/learninghub-moodle_Test.yml @@ -1,6 +1,6 @@ name: Run tests for LearningHub-Moodle on: - pull_request: + workflow_dispatch: permissions: id-token: write diff --git a/Terraform/dev/main.tf b/Terraform/dev/main.tf index 0d19d3e8b3a..6a193db45af 100644 --- a/Terraform/dev/main.tf +++ b/Terraform/dev/main.tf @@ -23,6 +23,12 @@ resource "azurerm_storage_share" "storage_share_theme" { quota = var.StorageQuota } +resource "azurerm_storage_share" "storage_share_uploads" { + name = "moodlerestuploads" + storage_account_name = azurerm_storage_account.storage_account.name + quota = var.StorageQuota +} + resource "azurerm_storage_container" "assessment_container" { name = "assessmentstoragecontainer" storage_account_name = azurerm_storage_account.storage_account.name diff --git a/Terraform/pgvle/main.tf b/Terraform/pgvle/main.tf index df432d5908a..203c7512e9f 100644 --- a/Terraform/pgvle/main.tf +++ b/Terraform/pgvle/main.tf @@ -23,6 +23,12 @@ resource "azurerm_storage_share" "storage_share_theme" { quota = var.StorageQuota } +resource "azurerm_storage_share" "storage_share_uploads" { + name = "moodlerestuploads" + storage_account_name = azurerm_storage_account.storage_account.name + quota = var.StorageQuota +} + resource "azurerm_storage_container" "assessment_container" { name = "assessmentstoragecontainer" storage_account_name = azurerm_storage_account.storage_account.name diff --git a/Terraform/prod/main.tf b/Terraform/prod/main.tf index 36a0d1d0a88..459b7110efb 100644 --- a/Terraform/prod/main.tf +++ b/Terraform/prod/main.tf @@ -23,6 +23,12 @@ resource "azurerm_storage_share" "storage_share_theme" { quota = var.StorageQuota } +resource "azurerm_storage_share" "storage_share_uploads" { + name = "moodlerestuploads" + storage_account_name = azurerm_storage_account.storage_account.name + quota = var.StorageQuota +} + resource "azurerm_storage_container" "assessment_container" { name = "assessmentstoragecontainer" storage_account_name = azurerm_storage_account.storage_account.name diff --git a/Terraform/test/main.tf b/Terraform/test/main.tf index 40507a17f27..5b5df04a98a 100644 --- a/Terraform/test/main.tf +++ b/Terraform/test/main.tf @@ -23,6 +23,12 @@ resource "azurerm_storage_share" "storage_share_theme" { quota = var.StorageQuota } +resource "azurerm_storage_share" "storage_share_uploads" { + name = "moodlerestuploads" + storage_account_name = azurerm_storage_account.storage_account.name + quota = var.StorageQuota +} + resource "azurerm_storage_container" "assessment_container" { name = "assessmentstoragecontainer" storage_account_name = azurerm_storage_account.storage_account.name diff --git a/auth/oidc/classes/privacy/provider.php b/auth/oidc/classes/privacy/provider.php index 4a6beb724ea..59ebe3b2d0c 100644 --- a/auth/oidc/classes/privacy/provider.php +++ b/auth/oidc/classes/privacy/provider.php @@ -76,6 +76,11 @@ public static function get_metadata(collection $collection): collection { 'refreshtoken', 'idtoken', ], + 'auth_oidc_sid' => [ // ✅ Add this block + 'userid', + 'sid', + 'timecreated', + ], ]; foreach ($tables as $table => $fields) { diff --git a/auth/oidc/lang/en/auth_oidc.php b/auth/oidc/lang/en/auth_oidc.php index 75409756fdd..da5c7daf059 100644 --- a/auth/oidc/lang/en/auth_oidc.php +++ b/auth/oidc/lang/en/auth_oidc.php @@ -494,6 +494,10 @@ $string['update_username_results'] = 'Update username results'; $string['new_username'] = 'New username'; $string['missing_idp_type'] = 'This configuration is only available if an IdP type is configured.'; +$string['privacy:metadata:auth_oidc_sid'] = 'Stores OIDC session identifiers linked to users.'; +$string['privacy:metadata:auth_oidc_sid:userid'] = 'The ID of the user associated with the session.'; +$string['privacy:metadata:auth_oidc_sid:sid'] = 'The session ID from the OIDC provider.'; +$string['privacy:metadata:auth_oidc_sid:timecreated'] = 'The time the session ID was created.'; // phpcs:enable moodle.Files.LangFilesOrdering.IncorrectOrder // phpcs:enable moodle.Files.LangFilesOrdering.UnexpectedComment \ No newline at end of file diff --git a/config.php b/config.php index 00f2576171e..fc78a545c86 100644 --- a/config.php +++ b/config.php @@ -32,6 +32,9 @@ $CFG->phpunit_prefix = 'phpu_'; $CFG->phpunit_dataroot = 'phpu_moodledata'; +$CFG->recordrtc_audio = false; +$CFG->recordrtc_video = false; + require_once(__DIR__ . '/lib/setup.php'); // There is no php closing tag in this file, diff --git a/kubectl/deployment-dev.yml b/kubectl/deployment-dev.yml index bb04de4cd29..496e45cfe4a 100644 --- a/kubectl/deployment-dev.yml +++ b/kubectl/deployment-dev.yml @@ -32,10 +32,15 @@ spec: readOnly: false - name: moodletheme mountPath: /var/www/html/theme + - name: moodlerestuploads + mountPath: /var/www/html/webservice/rest/uploads volumes: - name: moodledata persistentVolumeClaim: claimName: moodledataclaim - name: moodletheme persistentVolumeClaim: - claimName: moodlethemeclaim \ No newline at end of file + claimName: moodlethemeclaim + - name: moodlerestuploads + persistentVolumeClaim: + claimName: moodleuploadsclaim \ No newline at end of file diff --git a/kubectl/pv-definition-prod.yml b/kubectl/pv-definition-prod.yml index b93608f45be..80e36120dd0 100644 --- a/kubectl/pv-definition-prod.yml +++ b/kubectl/pv-definition-prod.yml @@ -16,6 +16,7 @@ spec: volumeHandle: "moodleCluster#learninghubmoodleprod#moodledata" # make sure this volumeid is unique for every identical share in the cluster volumeAttributes: shareName: moodledata + server: learninghubmoodleprod.privatelink.file.core.windows.net nodeStageSecretRef: name: azure-secret namespace: learninghubmoodle diff --git a/kubectl/pv-definition-uploads-dev.yml b/kubectl/pv-definition-uploads-dev.yml new file mode 100644 index 00000000000..ea892cdd72c --- /dev/null +++ b/kubectl/pv-definition-uploads-dev.yml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + annotations: + pv.kubernetes.io/provisioned-by: file.csi.azure.com + name: moodlerestuploads +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-csi + csi: + driver: file.csi.azure.com + volumeHandle: "moodleCluster#learninghubmoodledev#moodlerestuploads" # make sure this volumeid is unique for every identical share in the cluster + volumeAttributes: + shareName: moodlerestuploads + server: learninghubmoodledev.privatelink.file.core.windows.net + nodeStageSecretRef: + name: azure-secret + namespace: learninghubmoodle + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - uid=0 + - gid=0 + - mfsymlinks + - cache=strict + - nosharesock + - nobrl # disable sending byte range lock requests to the server and for applications which have challenges with posix locks \ No newline at end of file diff --git a/kubectl/pv-definition-uploads-pgvle.yml b/kubectl/pv-definition-uploads-pgvle.yml new file mode 100644 index 00000000000..66fee38d210 --- /dev/null +++ b/kubectl/pv-definition-uploads-pgvle.yml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + annotations: + pv.kubernetes.io/provisioned-by: file.csi.azure.com + name: moodlerestuploads +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-csi + csi: + driver: file.csi.azure.com + volumeHandle: "moodleCluster#learninghubmoodlepgvle#moodlerestuploads" # make sure this volumeid is unique for every identical share in the cluster + volumeAttributes: + shareName: moodlerestuploads + server: learninghubmoodledev.privatelink.file.core.windows.net + nodeStageSecretRef: + name: azure-secret + namespace: learninghubmoodle + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - uid=0 + - gid=0 + - mfsymlinks + - cache=strict + - nosharesock + - nobrl # disable sending byte range lock requests to the server and for applications which have challenges with posix locks \ No newline at end of file diff --git a/kubectl/pv-definition-uploads-prod.yml b/kubectl/pv-definition-uploads-prod.yml new file mode 100644 index 00000000000..e8e1b977eae --- /dev/null +++ b/kubectl/pv-definition-uploads-prod.yml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + annotations: + pv.kubernetes.io/provisioned-by: file.csi.azure.com + name: moodlerestuploads +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-csi + csi: + driver: file.csi.azure.com + volumeHandle: "moodleCluster#learninghubmoodleprod#moodlerestuploads" # make sure this volumeid is unique for every identical share in the cluster + volumeAttributes: + shareName: moodlerestuploads + server: learninghubmoodleprod.privatelink.file.core.windows.net + nodeStageSecretRef: + name: azure-secret + namespace: learninghubmoodle + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - uid=0 + - gid=0 + - mfsymlinks + - cache=strict + - nosharesock + - nobrl # disable sending byte range lock requests to the server and for applications which have challenges with posix locks \ No newline at end of file diff --git a/kubectl/pv-definition-uploads-test.yml b/kubectl/pv-definition-uploads-test.yml new file mode 100644 index 00000000000..2a3577a19b9 --- /dev/null +++ b/kubectl/pv-definition-uploads-test.yml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + annotations: + pv.kubernetes.io/provisioned-by: file.csi.azure.com + name: moodlerestuploads +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-csi + csi: + driver: file.csi.azure.com + volumeHandle: "moodleCluster#learninghubmoodletest#moodlerestuploads" # make sure this volumeid is unique for every identical share in the cluster + volumeAttributes: + shareName: moodlerestuploads + server: learninghubmoodletest.privatelink.file.core.windows.net + nodeStageSecretRef: + name: azure-secret + namespace: learninghubmoodle + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - uid=0 + - gid=0 + - mfsymlinks + - cache=strict + - nosharesock + - nobrl # disable sending byte range lock requests to the server and for applications which have challenges with posix locks \ No newline at end of file diff --git a/kubectl/pvc-definition-uploads-dev.yml b/kubectl/pvc-definition-uploads-dev.yml new file mode 100644 index 00000000000..b511922874e --- /dev/null +++ b/kubectl/pvc-definition-uploads-dev.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: moodleuploadsclaim + namespace: learninghubmoodle +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: azurefile-csi + volumeName: moodlerestuploads \ No newline at end of file diff --git a/kubectl/pvc-definition-uploads-pgvle.yml b/kubectl/pvc-definition-uploads-pgvle.yml new file mode 100644 index 00000000000..b511922874e --- /dev/null +++ b/kubectl/pvc-definition-uploads-pgvle.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: moodleuploadsclaim + namespace: learninghubmoodle +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: azurefile-csi + volumeName: moodlerestuploads \ No newline at end of file diff --git a/kubectl/pvc-definition-uploads-prod.yml b/kubectl/pvc-definition-uploads-prod.yml new file mode 100644 index 00000000000..b511922874e --- /dev/null +++ b/kubectl/pvc-definition-uploads-prod.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: moodleuploadsclaim + namespace: learninghubmoodle +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: azurefile-csi + volumeName: moodlerestuploads \ No newline at end of file diff --git a/kubectl/pvc-definition-uploads-test.yml b/kubectl/pvc-definition-uploads-test.yml new file mode 100644 index 00000000000..b511922874e --- /dev/null +++ b/kubectl/pvc-definition-uploads-test.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: moodleuploadsclaim + namespace: learninghubmoodle +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: azurefile-csi + volumeName: moodlerestuploads \ No newline at end of file diff --git a/lib/classes/task/send_new_user_passwords_task.php b/lib/classes/task/send_new_user_passwords_task.php index 678b29a40c7..6c27a97728f 100644 --- a/lib/classes/task/send_new_user_passwords_task.php +++ b/lib/classes/task/send_new_user_passwords_task.php @@ -44,8 +44,12 @@ public function get_name() { public function execute() { global $DB; + $select = $DB->sql_compare_text('name') . ' = ? AND ' . $DB->sql_compare_text('value') . ' = ?'; + $params = array('create_password', '1'); + // Generate new password emails for users - ppl expect these generated asap. - if ($DB->count_records('user_preferences', array('name' => 'create_password', 'value' => '1'))) { + //if ($DB->count_records('user_preferences', array('name' => 'create_password', 'value' => '1'))) { + if ($DB->count_records_select('user_preferences', $select, $params)) { mtrace('Creating passwords for new users...'); $userfieldsapi = \core_user\fields::for_name(); $usernamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; diff --git a/local/custom_service/classes/privacy/provider.php b/local/custom_service/classes/privacy/provider.php new file mode 100644 index 00000000000..6e0fc064f09 --- /dev/null +++ b/local/custom_service/classes/privacy/provider.php @@ -0,0 +1,11 @@ + array( + 'classname' => 'insert_scorm_resource', + 'methodname' => 'insert_scorm_resource', + 'classpath' => 'local/custom_service/externallib.php', + 'description' => 'Create a scorm resource under a course', + 'type' => 'write', + 'ajax' => true, + ), + +); + +$services = array( + 'Insert Scorm resource' => array( + 'functions' => array( + + 'mod_scorm_insert_scorm_resource' + ), + 'restrictedusers' => 0, + 'enabled' => 1, + // This field os optional, but requried if the `restrictedusers` value is + // set, so as to allow configuration via the Web UI. + 'shortname' => 'InsertScorm', + + // Whether to allow file downloads. + 'downloadfiles' => 0, + + // Whether to allow file uploads. + 'uploadfiles' => 0, + ) +); \ No newline at end of file diff --git a/local/custom_service/externallib.php b/local/custom_service/externallib.php new file mode 100644 index 00000000000..b6a995d3852 --- /dev/null +++ b/local/custom_service/externallib.php @@ -0,0 +1,186 @@ +libdir.'/externallib.php'); +require_once($CFG->dirroot.'/user/lib.php'); +require_once($CFG->dirroot.'/course/lib.php'); +require_once($CFG->dirroot.'/mod/scorm/lib.php'); +require_once($CFG->dirroot.'/mod/scorm/locallib.php'); +require_once(__DIR__.'/../../config.php'); +require_once($CFG->libdir . '/filestorage/file_storage.php'); +require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php"); +class insert_scorm_resource extends external_api { + + + + public static function insert_scorm_resource_parameters() { + return new external_function_parameters( + array( + 'courseid' => new external_value(PARAM_TEXT, 'Course Id'), + 'section' => new external_value(PARAM_TEXT, 'section'), + 'scormname' => new external_value(PARAM_TEXT, 'scorm name'), + 'foldername' => new external_value(PARAM_TEXT, 'foldername'), + 'base64Zip' => new external_value(PARAM_RAW, 'Base64-encoded ZIP file content') + + ) + ); + } + + public static function insert_scorm_resource($courseids,$section,$scormname,$foldername,$base64Zip) { + global $DB,$CFG; + require_once($CFG->libdir . '/filelib.php'); + require_once($CFG->dirroot . '/course/lib.php'); + require_once($CFG->libdir . '/formslib.php'); + + require_login(); + // zip file + $savedPath = self::saveBase64ToZip($base64Zip, $foldername . '.zip'); + + //zip end here + $courseid = $courseids; // Course ID where the SCORM package will be uploaded + $scormfile =$savedPath;//$path . '' . $file. '.zip'; // Path to the SCORM .zip file + + $zip = new ZipArchive; + + if ($zip->open($scormfile) === TRUE) { + // Check if imsmanifest.xml exists in the ZIP archive + if ($zip->locateName('imsmanifest.xml', ZipArchive::FL_NODIR) !== false) { + $zip->close(); + } else { + $zip->close(); + echo'imsmanifest.xml is missing from SCORM package.'; + return; + } + } else { + echo 'Failed to open SCORM package.'; + return; + } + + // Get course and context + try{ + $course = get_course($courseid); + } + catch(exception $ex) + { + echo 'Course not found'; + return; + } + + $context = context_course::instance($courseid); + + // Check permissions + require_capability('mod/scorm:addinstance', $context); + + // Create SCORM instance (if needed) + $scorm = new stdClass(); + $scorm->course = $courseid; + $scorm->name = $scormname; + $scorm->reference='Test Ref.zip'; + $scorm->intro = 'Intro to SCORM'; + $scorm->maxattempt=3; + $scorm->introformat = FORMAT_HTML; + $scorm->timemodified = time(); + + // Insert the SCORM instance into the database and get the instance ID + $scorm->id = $DB->insert_record('scorm', $scorm); + + // Create a new course module record + $cm = new stdClass(); + $cm->course = $courseid; + $cm->module = $DB->get_field('modules', 'id', array('name' => 'scorm')); + $cm->instance = $scorm->id; + $cm->visible = 1; + $cm->section = $section; // You can set the section if needed + + // Insert the course module + $cm->id = add_course_module($cm); //$DB->insert_record('course_modules', $cm); + + $sectionid=course_add_cm_to_section($courseid,$cm->id,$cm->section); + + // Update the record + $data = new stdClass(); + $data->id = $cm->id; // The ID of the course module to update + $data->section = $sectionid; // The new section value + + // // Update the record in the course_modules table + $DB->update_record('course_modules', $data); + // Upload the SCORM package to Moodle file storage + $fs = get_file_storage(); + $context = context_module::instance($cm->id); + + // Add the SCORM .zip package to the file area + $fileinfo = array( + 'contextid' => $context->id, + 'component' => 'mod_scorm', + 'filearea' => 'package', + 'itemid' => 0, // Item ID (could be used to reference a specific instance of the package) + 'filepath' => '/', + 'filename' => $file. '.zip' + ); + + $file = $fs->create_file_from_pathname($fileinfo, $scormfile); + + $packer = get_file_packer('application/zip'); + ; + if ($file) { + $extracted_files = $file->extract_to_storage($packer,$context->id, 'mod_scorm', 'content', 0, '/'); + } else { + } + + //new code for reading imsmanifest.xml + $fs = get_file_storage(); + + // Locate the extracted directory in Moodle file storage (adjust as needed) + $contextid = $context->id; // The course/module context ID + $component = 'mod_scorm'; // Change this to match your module (e.g., mod_scorm, mod_lti, etc.) + $filearea = 'content'; // File area for SCORM or Common Cartridge + $itemid = 0; // Usually 0 unless specified + $filename = 'imsmanifest.xml'; + + // Get the manifest file + $file = $fs->get_file($contextid, $component, $filearea, $itemid, '/', $filename); + $manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml'); + + + scorm_parse_scorm($scorm, $manifest); + + + $lti_updated[] = array( + 'id' => $scorm->id, + 'name' => $scormname, + ); + return $lti_updated; + } + public static function insert_scorm_resource_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'new scorm id'), + 'name' => new external_value(PARAM_RAW, 'new scorm name'), + ) + ) + ); + } + public static function saveBase64ToZip($base64Data, $filename, $saveDir = 'uploads/') + { + // Make sure directory exists + if (!file_exists($saveDir)) { + mkdir($saveDir, 0777, true); + } + + // Sanitize filename + $safeFilename = preg_replace('/[^a-zA-Z0-9_\.-]/', '_', $filename); + $zipPath = rtrim($saveDir, '/') . '/' . $safeFilename; + + // Decode base64 + $binaryData = base64_decode($base64Data); + + // Save the file + if (file_put_contents($zipPath, $binaryData) !== false) { + return realpath($zipPath); // return full path + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/local/custom_service/lang/en/local_custom_service.php b/local/custom_service/lang/en/local_custom_service.php new file mode 100644 index 00000000000..6fdc0b44600 --- /dev/null +++ b/local/custom_service/lang/en/local_custom_service.php @@ -0,0 +1,3 @@ +dirroot . '/local/custom_service/classes/privacy/provider.php'); + +use core_privacy\tests\provider_testcase; +use local_custom_service\privacy\provider; + +class local_custom_service_privacy_test extends provider_testcase { + + public function test_privacy_provider() { + $this->assertInstanceOf( + \core_privacy\local\metadata\null_provider::class, + new provider() + ); + } +} diff --git a/local/custom_service/version.php b/local/custom_service/version.php new file mode 100644 index 00000000000..11103b82a41 --- /dev/null +++ b/local/custom_service/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version details + * + * @package block_calendar_upcoming + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2024100990; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2024100290; // Requires this Moodle version. +$plugin->component = 'local_custom_service'; // Full name of the plugin (used for diagnostics) +$plugin->privacy = ['provider' => 'local_custom_service\privacy\provider']; \ No newline at end of file diff --git a/local/telconfig/classes/api_client.php b/local/telconfig/classes/api_client.php new file mode 100644 index 00000000000..a909b7ba667 --- /dev/null +++ b/local/telconfig/classes/api_client.php @@ -0,0 +1,31 @@ + [ + 'header' => "Content-type: application/json\r\n", + 'method' => 'POST', + 'content' => json_encode($data), + 'timeout' => 5, + ] + ]; + $context = stream_context_create($options); + return @file_get_contents($url, false, $context); + } + + public function delete(string $url): string|false { + $options = [ + 'http' => [ + 'method' => 'DELETE', + 'timeout' => 5, + ] + ]; + $context = stream_context_create($options); + return @file_get_contents($url, false, $context); + } +} \ No newline at end of file diff --git a/local/telconfig/classes/config_exception.php b/local/telconfig/classes/config_exception.php new file mode 100644 index 00000000000..bae551496ec --- /dev/null +++ b/local/telconfig/classes/config_exception.php @@ -0,0 +1,29 @@ +. + +/** + * Member does not exist exception. + * + * @package local_telconfig + */ + +namespace local_telconfig; + +class config_exception extends \moodle_exception { + public function __construct($message = 'Missing API config for Findwise') { + parent::__construct('error', 'local_telconfig', '', null, $message); + } +} \ No newline at end of file diff --git a/local/telconfig/classes/course_data_builder.php b/local/telconfig/classes/course_data_builder.php new file mode 100644 index 00000000000..f37e27a4bb4 --- /dev/null +++ b/local/telconfig/classes/course_data_builder.php @@ -0,0 +1,142 @@ +. + +/** + * Helper class locally used. + * + * @package local_telconfig + * @copyright + * @license + */ + +namespace local_telconfig; + +defined('MOODLE_INTERNAL') || die(); + +class course_data_builder { + public static function build_course_metadata($course): array { + global $DB, $CFG; + + try { + + // Get course context and teachers (authors) + $context = \context_course::instance($course->id); + $teachers = get_role_users(3, $context); // 3 = editingteacher by default + $authors = array_values(array_map(fn($u) => fullname($u), $teachers)); + + // Extract tags + require_once($CFG->dirroot . '/tag/lib.php'); + $tags = \core_tag_tag::get_item_tags('core', 'course', $course->id); + $keywords = array_reduce($tags, function ($carry, $tag) { + return array_merge($carry, self::tokenise_keywords($tag->rawname)); + }, []); + + + // Merge in section and resource keywords + $keywords = array_merge( + $keywords, + self::get_section_keywords($course), + self::get_resource_keywords($course) + ); + + $keywords = array_values(array_unique($keywords)); + + // Prepare data + $data = [ + '_id' => 'M' . $course->id, + 'course_id' => $course->id, + 'authored_date' => date('Y-m-d', $course->startdate), + 'authors' => $authors, + 'catalogue_ids' => ['1'],//[$course->category], + 'description' => format_text($course->summary, FORMAT_HTML), + 'keywords' => array_values($keywords), + 'location_paths' => [], // category hierarchy if needed + 'publication_date' => date('Y-m-d', $course->startdate), + 'rating' => 0, + 'resource_reference_id' => 0, + 'resource_type' => 'Course', + 'title' => $course->fullname, + ]; + + return $data; + + } catch (\Throwable $e) { + debugging('Error in course_data_builder: ' . $e->getMessage(), DEBUG_DEVELOPER); + return []; // Always return an array + } + } + + private static function get_section_keywords($course): array { + $keywords = []; + $modinfo = get_fast_modinfo($course); + $sections = $modinfo->get_section_info_all(); + + foreach ($sections as $section) { + // Skip hidden sections + if (!$section->uservisible || !$section->visible || empty($section->name)) { + continue; + } + + // Tokenise and merge keywords + $keywords = array_merge($keywords, self::tokenise_keywords($section->name)); + } + + return $keywords; + } + + private static function get_resource_keywords($course): array { + global $DB; + $keywords = []; + + // Now fetch all course modules from DB + $coursemodules = $DB->get_records('course_modules', ['course' => $course->id]); + + foreach ($coursemodules as $cm) { + // Skip deleted or hidden modules + if (!empty($cm->deletioninprogress) || empty($cm->visible)) { + continue; + } + + // Get module type (e.g., 'resource', 'quiz', etc.) + $module = $DB->get_record('modules', ['id' => $cm->module], '*', IGNORE_MISSING); + if (!$module) { + continue; + } + + // Dynamically get the module instance (e.g., from 'resource', 'quiz', etc.) + $instancetable = $module->name; + $instance = $DB->get_record($instancetable, ['id' => $cm->instance], '*', IGNORE_MISSING); + if ($instance && !empty($instance->name)) { + $keywords = array_merge($keywords, self::tokenise_keywords($instance->name)); + } + } + + return $keywords; + } + + + private static function tokenise_keywords(string $input): array { + $input = strtolower(trim($input)); + if (empty($input)) { + return []; + } + + $tokens = preg_split('/\s+/', $input); // split on spaces + $keywords = array_merge([$input], $tokens); // include full phrase and individual words + + return array_unique($keywords); // remove duplicates + } +} diff --git a/local/telconfig/classes/helper.php b/local/telconfig/classes/helper.php new file mode 100644 index 00000000000..22d42b95f2a --- /dev/null +++ b/local/telconfig/classes/helper.php @@ -0,0 +1,74 @@ +. + +/** + * Helper class locally used. + * + * @package local_telconfig + * @copyright + * @license + */ + +namespace local_telconfig; +use local_telconfig\api_client; + +defined('MOODLE_INTERNAL') || die(); + +class helper { + + /** + * Sends structured data to an external API endpoint. + * + * @param array $data + * @return void + */ + public static function send_findwise_api(array $data, string $method = 'POST', ?api_client $client = null): void { + $indexurl = get_config('local_telconfig', 'findwiseindexurl'); + $indexmethod = get_config('local_telconfig', 'findwiseindexmethod'); + $collection = get_config('local_telconfig', 'findwisecollection'); + $apitoken = get_config('local_telconfig', 'findwiseapitoken'); + + if (empty($indexurl) || empty($apitoken)) { + return; + } + + $indexurl = rtrim($indexurl, '/') . '/' . $indexmethod . '?token=' . urlencode($apitoken); + $apiurl = str_replace('{0}', $collection, $indexurl); + + $client ??= new api_client(); + + try { + if ($method === 'DELETE') { + // Add logic to construct a deletion URL with course ID + if (isset($data['course_id'])) { + $deleteurl = rtrim($apiurl, '/') . '&id=M' . $data['course_id']; + $response = $client->delete($deleteurl); + } else { + debugging('send_findwise_api: Cannot perform DELETE without course_id in $data.', DEBUG_DEVELOPER); + return; + } + } else { + $response = $client->post($apiurl, $data); // POST or PUT + } + + if ($response === false) { + debugging('send_findwise_api: Failed to send data to findwise API.', DEBUG_DEVELOPER); + } + } catch (\Exception $e) { + debugging('send_findwise_api: Exception occurred while sending data: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + } +} diff --git a/local/telconfig/classes/observer.php b/local/telconfig/classes/observer.php new file mode 100644 index 00000000000..98f13d006ba --- /dev/null +++ b/local/telconfig/classes/observer.php @@ -0,0 +1,137 @@ +get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); + + // Only act if it's for 'self' enrolment. + if (!isset($event->other['enrol']) || $event->other['enrol'] !== 'self') { + return; + } + + // Get course info + $course = $DB->get_record('course', ['id' => $event->courseid], '*', MUST_EXIST); + + if ((int)$enrol->status === ENROL_INSTANCE_ENABLED) { + // Fetch the enrolment instance data. + $data = course_data_builder::build_course_metadata($course); + helper::send_findwise_api($data); + } else { + // Delete from external API when disabled. + $data = ['course_id' => $course->id]; + helper::send_findwise_api($data,'DELETE'); + } + + } catch (\dml_exception $e) { + debugging("Failed to fetch course/enrol data: " . $e->getMessage(), DEBUG_DEVELOPER); + } + } + + /** + * Triggered when a course is updated. + * + * @param \core\event\base $event + * @return void + */ + public static function local_course_updated(\core\event\base $event): void { + global $DB; + + try { + $course = $DB->get_record('course', ['id' => $event->objectid], '*', MUST_EXIST); + + // Only proceed if the course has self enrolment enabled + if (!self::is_course_self_enrollable($course->id)) { + return; + } + + // Rebuild and send metadata to API (as an update). + $data = course_data_builder::build_course_metadata($course); + helper::send_findwise_api($data); + + } catch (\dml_exception $e) { + debugging("Failed to process local course update: " . $e->getMessage(), DEBUG_DEVELOPER); + } + } + + /** + * Triggered when a section is updated. + * + * @param \core\event\base $event + * @return void + */ + public static function local_section_changed(\core\event\base $event): void { + global $DB; + + try { + $course = $DB->get_record('course', ['id' => $event->courseid], '*', MUST_EXIST); + + // Only proceed if the course has self enrolment enabled + if (!self::is_course_self_enrollable($course->id)) { + return; + } + + // Handle the update, e.g., send new metadata + $data = course_data_builder::build_course_metadata($course); // or enrich this with section title + helper::send_findwise_api($data); + + } catch (\Throwable $e) { + debugging('Error handling local section change: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + } + + /** + * Triggered when a module is updated. + * + * @param \core\event\base $event + * @return void + */ + public static function local_module_changed(\core\event\base $event): void { + global $DB; + + try { + $courseid = $event->courseid; + $course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST); + + // Only proceed if the course has self enrolment enabled + if (!self::is_course_self_enrollable($course->id)) { + return; + } + + // Build metadata + $data = course_data_builder::build_course_metadata($course); // or enrich with mod/section name + helper::send_findwise_api($data); + + } catch (\Throwable $e) { + debugging('Error handling local module change: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + } + private static function is_course_self_enrollable(int $courseid): bool { + global $DB; + + return $DB->record_exists('enrol', [ + 'courseid' => $courseid, + 'enrol' => 'self', + 'status' => ENROL_INSTANCE_ENABLED, + ]); + } +} diff --git a/local/telconfig/classes/privacy/provider.php b/local/telconfig/classes/privacy/provider.php new file mode 100644 index 00000000000..1ab6b341f9e --- /dev/null +++ b/local/telconfig/classes/privacy/provider.php @@ -0,0 +1,11 @@ + '\core\event\enrol_instance_updated', + 'callback' => '\local_telconfig\observer::enrol_instance_changed', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_updated', + 'callback' => '\local_telconfig\observer::local_course_updated', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_section_created', + 'callback' => '\local_telconfig\observer::local_section_changed', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_section_updated', + 'callback' => '\local_telconfig\observer::local_section_changed', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_section_deleted', + 'callback' => '\local_telconfig\observer::local_section_changed', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_module_created', + 'callback' => '\local_telconfig\observer::local_module_changed', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_module_updated', + 'callback' => '\local_telconfig\observer::local_module_changed', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_module_deleted', + 'callback' => '\local_telconfig\observer::local_module_changed', + 'priority' => 9999, + 'internal' => false, + ], +]; diff --git a/local/telconfig/lang/en/local_telconfig.php b/local/telconfig/lang/en/local_telconfig.php new file mode 100644 index 00000000000..6047a5b9139 --- /dev/null +++ b/local/telconfig/lang/en/local_telconfig.php @@ -0,0 +1,32 @@ +. + +/** + * Meta enrolment plugin settings and presets. + * + * @package telconfig + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + $settings = new admin_settingpage('localtelconfigsettings', get_string('pluginname', 'local_telconfig')); + + // Findwisesettings + $settings->add(new admin_setting_heading('Findwisesettings', + get_string('Findwisesettings', 'local_telconfig'), get_string('Findwisesettings_desc', 'local_telconfig'))); + + $settings->add(new admin_setting_configtext('local_telconfig/findwiseindexurl', + get_string('findwiseindexurl', 'local_telconfig'), get_string('findwiseindexurl_desc', 'local_telconfig'), '', PARAM_TEXT)); + $settings->add(new admin_setting_configtext('local_telconfig/findwiseindexmethod', + get_string('findwiseindexmethod', 'local_telconfig'), get_string('findwiseindexmethod_desc', 'local_telconfig'), '', PARAM_TEXT)); + $settings->add(new admin_setting_configtext('local_telconfig/findwisecollection', + get_string('findwisecollection', 'local_telconfig'), get_string('findwisecollection_desc', 'local_telconfig'), '', PARAM_TEXT)); + $settings->add(new admin_setting_configpasswordunmask('local_telconfig/findwiseapitoken', + get_string('findwiseapitoken', 'local_telconfig'), get_string('findwiseapitoken_desc', 'local_telconfig'), '', PARAM_TEXT)); + + //Mustache template settings + $settings->add(new admin_setting_heading('mustachetemplatessettings', + get_string('mustachetemplatessettings', 'local_telconfig'), get_string('mustachetemplatessettings_desc', 'local_telconfig'))); + + $settings->add(new admin_setting_configtext('local_telconfig/mustachetemplatesurl', + get_string('mustachetemplatesurl', 'local_telconfig'), get_string('mustachetemplatesurl_desc', 'local_telconfig'), '', PARAM_TEXT)); + + // content server settings + $settings->add(new admin_setting_heading('contentserversettings', + get_string('contentserversettings', 'local_telconfig'), get_string('contentserversettings_desc', 'local_telconfig'))); + + $settings->add(new admin_setting_configtext('local_telconfig/contentserverurl', + get_string('contentserverurl', 'local_telconfig'), get_string('contentserverurl_desc', 'local_telconfig'), '', PARAM_TEXT)); + + + $ADMIN->add('localplugins', $settings); +} diff --git a/local/telconfig/tests/course_data_builder_test.php b/local/telconfig/tests/course_data_builder_test.php new file mode 100644 index 00000000000..820bb1e1ed5 --- /dev/null +++ b/local/telconfig/tests/course_data_builder_test.php @@ -0,0 +1,71 @@ +. + +// File: local/telconfig/tests/course_data_builder_test.php + +namespace local_telconfig\tests; + +use advanced_testcase; +use context_course; +use core_tag_tag; +use local_telconfig\course_data_builder; + +defined('MOODLE_INTERNAL') || die(); + +class course_data_builder_test extends advanced_testcase { + + + public function test_build_course_metadata_returns_expected_array() { + global $DB; + + $this->resetAfterTest(true); + + // Create a course using Moodle's generator + $course = $this->getDataGenerator()->create_course([ + 'fullname' => 'Test Course', + 'summary' => 'Test summary', + 'startdate' => strtotime('2022-01-01') + ]); + + // Enrol a teacher in the course + $teacher = $this->getDataGenerator()->create_user(['firstname' => 'Alice', 'lastname' => 'Teacher']); + $roleid = 3; // editingteacher + $context = context_course::instance($course->id); + role_assign($roleid, $teacher->id, $context->id); + + // Add tags to course + core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), ['tag1', 'tag2']); + + // Call the method + $result = course_data_builder::build_course_metadata($course); + + // Assertions + $this->assertIsArray($result); + $this->assertSame('M' . $course->id, $result['_id']); + $this->assertSame($course->id, $result['course_id']); + $this->assertSame(date('Y-m-d', $course->startdate), $result['authored_date']); + $this->assertContains(fullname($teacher), $result['authors']); + $this->assertEquals(['1'], $result['catalogue_ids']); + $this->assertSame(format_text($course->summary, FORMAT_HTML), $result['description']); + $this->assertEquals(['tag1', 'tag2'], $result['keywords']); + $this->assertIsArray($result['location_paths']); + $this->assertSame(date('Y-m-d', $course->startdate), $result['publication_date']); + $this->assertSame(0, $result['rating']); + $this->assertSame(0, $result['resource_reference_id']); + $this->assertSame('Course', $result['resource_type']); + $this->assertSame($course->fullname, $result['title']); + } +} diff --git a/local/telconfig/tests/helper_test.php b/local/telconfig/tests/helper_test.php new file mode 100644 index 00000000000..0e60cafbe3a --- /dev/null +++ b/local/telconfig/tests/helper_test.php @@ -0,0 +1,46 @@ +. + +use advanced_testcase; +use local_telconfig\helper; +use local_telconfig\api_client; + +class telconfig_helper_test extends advanced_testcase { + + public function test_send_findwise_api_makes_api_call() { + define('LOCAL_TELCONFIG_DEV_TEST', true); // Flag to enable strict config check + $this->resetAfterTest(); + + // Set fake plugin config values. + set_config('findwiseindexurl', 'http://fake.local/api', 'local_telconfig'); + set_config('findwiseindexmethod', 'index', 'local_telconfig'); + set_config('findwisecollection', 'courses', 'local_telconfig'); + set_config('findwiseapitoken', 'faketoken', 'local_telconfig'); + + // Create a mock API client. + $mock = $this->createMock(api_client::class); + $mock->expects($this->once()) + ->method('post') + ->with( + $this->stringContains('http://fake.local/api/index?token='), + $this->equalTo(['courseid' => 123]) + ) + ->willReturn('{"status":"ok"}'); + + // Call the method with the mock client. + helper::send_findwise_api(['courseid' => 123], 'POST', $mock); + } +} diff --git a/local/telconfig/tests/privacy_test.php b/local/telconfig/tests/privacy_test.php new file mode 100644 index 00000000000..cb9d2cbee3c --- /dev/null +++ b/local/telconfig/tests/privacy_test.php @@ -0,0 +1,17 @@ +dirroot . '/local/telconfig/classes/privacy/provider.php'); + +use core_privacy\tests\provider_testcase; +use local_telconfig\privacy\provider; + +class local_telconfig_privacy_test extends provider_testcase { + + public function test_privacy_provider() { + $this->assertInstanceOf( + \core_privacy\local\metadata\null_provider::class, + new provider() + ); + } +} diff --git a/local/telconfig/version.php b/local/telconfig/version.php new file mode 100644 index 00000000000..a087a0b0b75 --- /dev/null +++ b/local/telconfig/version.php @@ -0,0 +1,28 @@ +. + +/** + * Version details + * + * @package local_telconfig + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2024100993; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2024100290; // Requires this Moodle version. +$plugin->component = 'local_telconfig'; // Full name of the plugin (used for diagnostics) +$plugin->privacy = ['provider' => 'local_telconfig\privacy\provider']; \ No newline at end of file diff --git a/singlesignout.php b/singlesignout.php new file mode 100644 index 00000000000..f8c91b87c2e --- /dev/null +++ b/singlesignout.php @@ -0,0 +1,40 @@ +libdir . '/filelib.php'); + +// Accept logout_token (no signature verification) +$logout_token = $_POST['logout_token'] ?? null; +if (!$logout_token) { + http_response_code(400); + exit("Missing logout_token"); +} + +// Decode JWT payload (2nd part) +$jwt_parts = explode('.', $logout_token); +if (count($jwt_parts) !== 3) { + http_response_code(400); + exit("Invalid JWT"); +} + +$payload_json = base64_decode(strtr($jwt_parts[1], '-_', '+/')); +$payload = json_decode($payload_json, true); +$sub = $payload['sub'] ?? null; + +if (!$sub) { + http_response_code(400); + exit("Missing sub claim"); +} + +// Lookup user by sub (assumed to match idnumber or username) +$user = $DB->get_record('user', ['auth' => 'oidc', 'username' => $sub]); +if (!$user) { + http_response_code(404); + exit("User not found"); +} + +// Kill the user’s sessions +\core\session\manager::kill_user_sessions($user->id); + +http_response_code(200); +echo "User logged out"; diff --git a/webservice/rest/uploads/null.txt b/webservice/rest/uploads/null.txt new file mode 100644 index 00000000000..e69de29bb2d