diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index 7515255..aaec0c7 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -8,4 +8,7 @@ on: [ push, pull_request ] jobs: call-moodle-ci-workflow: - uses: Opencast-Moodle/moodle-workflows-opencast/.github/workflows/moodle-ci.yml@main \ No newline at end of file + uses: Opencast-Moodle/moodle-workflows-opencast/.github/workflows/moodle-ci.yml@main + with: + requires-block-plugin: true + branch-block-plugin: main diff --git a/amd/build/maintenance.min.js b/amd/build/maintenance.min.js new file mode 100644 index 0000000..75f393a --- /dev/null +++ b/amd/build/maintenance.min.js @@ -0,0 +1,10 @@ +define("tool_opencast/maintenance",["exports","core/ajax","core/notification","core/str","core/toast"],(function(_exports,Ajax,Notification,Str,Toast){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +/** + * Javascript to handle maintenance mode in tool opencast. + * + * @module tool_opencast/maintenance + * @copyright 2024 Farbod Zamani Boroujeni (zamani@elan-ev.de) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notification=_exports.init=void 0,Ajax=_interopRequireWildcard(Ajax),Notification=_interopRequireWildcard(Notification),Str=_interopRequireWildcard(Str),Toast=_interopRequireWildcard(Toast);_exports.init=()=>{Str.get_strings([{key:"maintenancemode_modal_sync_confirmation_title",component:"tool_opencast"},{key:"maintenancemode_modal_sync_confirmation_text",component:"tool_opencast"},{key:"maintenancemode_modal_sync_confirmation_btn",component:"tool_opencast"},{key:"maintenancemode_modal_sync_error_title",component:"tool_opencast"},{key:"maintenancemode_modal_sync_error_noinstance_message",component:"tool_opencast"},{key:"maintenancemode_modal_sync_failed",component:"tool_opencast"},{key:"maintenancemode_modal_sync_succeeded",component:"tool_opencast"}]).then((function(jsstrings){document.querySelectorAll(".form-setting .opencast_config_dt_selector").forEach((dtblock=>{var _dtblock$dataset;if(null!=dtblock&&null!==(_dtblock$dataset=dtblock.dataset)&&void 0!==_dtblock$dataset&&_dtblock$dataset.isoptional){var _enablingelement$chec;const enablingelement=document.getElementById("".concat(dtblock.dataset.settingid,"_enabled")),initialvalue=null!==(_enablingelement$chec=null==enablingelement?void 0:enablingelement.checked)&&void 0!==_enablingelement$chec&&_enablingelement$chec,selects=dtblock.querySelectorAll(".opencast-config-dt-select");selects.forEach((select=>{select.disabled=!initialvalue})),enablingelement.addEventListener("change",(event=>{selects.forEach((select=>{select.disabled=!event.target.checked}))}))}}));document.querySelectorAll(".maintenance-sync-btn").forEach((btn=>{btn.addEventListener("click",(e=>{var _e$target,_e$target$dataset;e.preventDefault();const ocinstanceid=null===(_e$target=e.target)||void 0===_e$target||null===(_e$target$dataset=_e$target.dataset)||void 0===_e$target$dataset?void 0:_e$target$dataset.ocinstanceid;ocinstanceid?Notification.confirm(jsstrings[0],jsstrings[1],jsstrings[2],null,(()=>performSync(ocinstanceid,jsstrings))):Notification.alert(jsstrings[3],jsstrings[4])})),btn.removeAttribute("disabled"),btn.removeAttribute("title"),btn.classList.remove("disabled"),btn.classList.remove("btn-warning"),btn.classList.add("btn-primary")}))})).catch(Notification.exception)};const performSync=(ocinstanceid,jsstrings)=>{ocinstanceid&&Ajax.call([{methodname:"tool_opencast_maintenance_sync",args:{ocinstanceid:ocinstanceid}}])[0].then((data=>{null!=data&&data.status?(Toast.add(jsstrings[6],{type:"success"}),reloadWithDelay()):Toast.add(jsstrings[5],{type:"danger"})})).catch((error=>Notification.exception(error)))},reloadWithDelay=function(){let delay=arguments.length>0&&void 0!==arguments[0]?arguments[0]:3e3;setTimeout((()=>{window.location.reload()}),delay)};_exports.notification=(message,level,notify)=>{var _window;null!==(_window=window)&&void 0!==_window&&_window.ocMaintenanceNotified||!notify||(Notification.addNotification({message:message,type:level}),window.ocMaintenanceNotified=!0)}})); + +//# sourceMappingURL=maintenance.min.js.map \ No newline at end of file diff --git a/amd/build/maintenance.min.js.map b/amd/build/maintenance.min.js.map new file mode 100644 index 0000000..4aa4f2a --- /dev/null +++ b/amd/build/maintenance.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"maintenance.min.js","sources":["../src/maintenance.js"],"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 * Javascript to handle maintenance mode in tool opencast.\n *\n * @module tool_opencast/maintenance\n * @copyright 2024 Farbod Zamani Boroujeni (zamani@elan-ev.de)\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Ajax from 'core/ajax';\nimport * as Notification from 'core/notification';\nimport * as Str from 'core/str';\nimport * as Toast from 'core/toast';\n\n/**\n * Initializes the tool maintenance js module.\n */\nexport const init = () => {\n // Load strings\n var strings = [\n {key: 'maintenancemode_modal_sync_confirmation_title', component: 'tool_opencast'},\n {key: 'maintenancemode_modal_sync_confirmation_text', component: 'tool_opencast'},\n {key: 'maintenancemode_modal_sync_confirmation_btn', component: 'tool_opencast'},\n {key: 'maintenancemode_modal_sync_error_title', component: 'tool_opencast'},\n {key: 'maintenancemode_modal_sync_error_noinstance_message', component: 'tool_opencast'},\n {key: 'maintenancemode_modal_sync_failed', component: 'tool_opencast'},\n {key: 'maintenancemode_modal_sync_succeeded', component: 'tool_opencast'},\n ];\n Str.get_strings(strings).then(function(jsstrings) {\n // Required functionality for admin_setting_configdatetimeselector.\n const datetimeselectors = document.querySelectorAll('.form-setting .opencast_config_dt_selector');\n datetimeselectors.forEach((dtblock) => {\n if (dtblock?.dataset?.isoptional) {\n const enablingelement = document.getElementById(`${dtblock.dataset.settingid}_enabled`);\n const initialvalue = enablingelement?.checked ?? false;\n const selects = dtblock.querySelectorAll(`.opencast-config-dt-select`);\n selects.forEach((select) => {\n select.disabled = !initialvalue;\n });\n enablingelement.addEventListener('change', (event) => {\n selects.forEach((select) => {\n select.disabled = !event.target.checked;\n });\n });\n }\n });\n\n // Sync Button.\n const syncbtns = document.querySelectorAll('.maintenance-sync-btn');\n syncbtns.forEach((btn) => {\n btn.addEventListener('click', (e) => {\n e.preventDefault();\n const ocinstanceid = e.target?.dataset?.ocinstanceid;\n if (!ocinstanceid) {\n Notification.alert(jsstrings[3], jsstrings[4]);\n return;\n }\n\n Notification.confirm(\n jsstrings[0], jsstrings[1], jsstrings[2], null,\n () => performSync(ocinstanceid, jsstrings)\n );\n });\n // Make the button accessible to use after the listener is added.\n btn.removeAttribute('disabled');\n btn.removeAttribute('title');\n btn.classList.remove('disabled');\n btn.classList.remove('btn-warning');\n btn.classList.add('btn-primary');\n });\n\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Perform sync request via Ajax call.\n * @param {int} ocinstanceid\n * @param {array} jsstrings\n */\nconst performSync = (ocinstanceid, jsstrings) => {\n if (!ocinstanceid) {\n return;\n }\n Ajax.call([{\n methodname: 'tool_opencast_maintenance_sync',\n args: {ocinstanceid: ocinstanceid},\n }])[0]\n .then((data) => {\n if (!data?.status) {\n Toast.add(jsstrings[5], {type: 'danger'});\n return;\n }\n Toast.add(jsstrings[6], {type: 'success'});\n reloadWithDelay();\n return;\n })\n .catch((error) => Notification.exception(error));\n};\n\n/**\n * Reloads the current page with a delay.\n * @param {int} delay default 3000 ms\n */\nconst reloadWithDelay = (delay = 3000) => {\n setTimeout(() => {\n window.location.reload();\n }, delay);\n};\n\n/**\n * Opencast Tool maintenance notification handler.\n *\n * It is used to make sure that there is only one maintenance notification printed at a time.\n *\n * @param {string} message\n * @param {string} level\n * @param {bool} notify\n */\nexport const notification = (message, level, notify) => {\n if (!window?.ocMaintenanceNotified && notify) {\n Notification.addNotification({\n message: message,\n type: level\n });\n window.ocMaintenanceNotified = true;\n }\n};\n"],"names":["Str","get_strings","key","component","then","jsstrings","document","querySelectorAll","forEach","dtblock","dataset","_dtblock$dataset","isoptional","enablingelement","getElementById","settingid","initialvalue","checked","selects","select","disabled","addEventListener","event","target","btn","e","preventDefault","ocinstanceid","_e$target","_e$target$dataset","Notification","confirm","performSync","alert","removeAttribute","classList","remove","add","catch","exception","Ajax","call","methodname","args","data","status","Toast","type","reloadWithDelay","error","delay","setTimeout","window","location","reload","message","level","notify","_window","ocMaintenanceNotified","addNotification"],"mappings":";;;;;;;kRA+BoB,KAWhBA,IAAIC,YATU,CACV,CAACC,IAAK,gDAAiDC,UAAW,iBAClE,CAACD,IAAK,+CAAgDC,UAAW,iBACjE,CAACD,IAAK,8CAA+CC,UAAW,iBAChE,CAACD,IAAK,yCAA0CC,UAAW,iBAC3D,CAACD,IAAK,sDAAuDC,UAAW,iBACxE,CAACD,IAAK,oCAAqCC,UAAW,iBACtD,CAACD,IAAK,uCAAwCC,UAAW,mBAEpCC,MAAK,SAASC,WAETC,SAASC,iBAAiB,8CAClCC,SAASC,kCACnBA,MAAAA,kCAAAA,QAASC,qCAATC,iBAAkBC,WAAY,iCACxBC,gBAAkBP,SAASQ,yBAAkBL,QAAQC,QAAQK,uBAC7DC,2CAAeH,MAAAA,uBAAAA,gBAAiBI,gEAChCC,QAAUT,QAAQF,+CACxBW,QAAQV,SAASW,SACbA,OAAOC,UAAYJ,gBAEvBH,gBAAgBQ,iBAAiB,UAAWC,QACxCJ,QAAQV,SAASW,SACbA,OAAOC,UAAYE,MAAMC,OAAON,kBAO/BX,SAASC,iBAAiB,yBAClCC,SAASgB,MACdA,IAAIH,iBAAiB,SAAUI,oCAC3BA,EAAEC,uBACIC,+BAAeF,EAAEF,uDAAFK,UAAUlB,4CAAVmB,kBAAmBF,aACnCA,aAKLG,aAAaC,QACT1B,UAAU,GAAIA,UAAU,GAAIA,UAAU,GAAI,MAC1C,IAAM2B,YAAYL,aAActB,aANhCyB,aAAaG,MAAM5B,UAAU,GAAIA,UAAU,OAUnDmB,IAAIU,gBAAgB,YACpBV,IAAIU,gBAAgB,SACpBV,IAAIW,UAAUC,OAAO,YACrBZ,IAAIW,UAAUC,OAAO,eACrBZ,IAAIW,UAAUE,IAAI,qBAIvBC,MAAMR,aAAaS,kBAQpBP,YAAc,CAACL,aAActB,aAC1BsB,cAGLa,KAAKC,KAAK,CAAC,CACPC,WAAY,iCACZC,KAAM,CAAChB,aAAcA,iBACrB,GACHvB,MAAMwC,OACEA,MAAAA,MAAAA,KAAMC,QAIXC,MAAMT,IAAIhC,UAAU,GAAI,CAAC0C,KAAM,YAC/BC,mBAJIF,MAAMT,IAAIhC,UAAU,GAAI,CAAC0C,KAAM,cAOtCT,OAAOW,OAAUnB,aAAaS,UAAUU,UAOvCD,gBAAkB,eAACE,6DAAQ,IAC7BC,YAAW,KACPC,OAAOC,SAASC,WACjBJ,8BAYqB,CAACK,QAASC,MAAOC,sCACpCL,2BAAAM,QAAQC,wBAAyBF,SAClC3B,aAAa8B,gBAAgB,CACzBL,QAASA,QACTR,KAAMS,QAEVJ,OAAOO,uBAAwB"} \ No newline at end of file diff --git a/amd/src/maintenance.js b/amd/src/maintenance.js new file mode 100644 index 0000000..ab74a14 --- /dev/null +++ b/amd/src/maintenance.js @@ -0,0 +1,142 @@ +// 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 . + +/** + * Javascript to handle maintenance mode in tool opencast. + * + * @module tool_opencast/maintenance + * @copyright 2024 Farbod Zamani Boroujeni (zamani@elan-ev.de) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import * as Ajax from 'core/ajax'; +import * as Notification from 'core/notification'; +import * as Str from 'core/str'; +import * as Toast from 'core/toast'; + +/** + * Initializes the tool maintenance js module. + */ +export const init = () => { + // Load strings + var strings = [ + {key: 'maintenancemode_modal_sync_confirmation_title', component: 'tool_opencast'}, + {key: 'maintenancemode_modal_sync_confirmation_text', component: 'tool_opencast'}, + {key: 'maintenancemode_modal_sync_confirmation_btn', component: 'tool_opencast'}, + {key: 'maintenancemode_modal_sync_error_title', component: 'tool_opencast'}, + {key: 'maintenancemode_modal_sync_error_noinstance_message', component: 'tool_opencast'}, + {key: 'maintenancemode_modal_sync_failed', component: 'tool_opencast'}, + {key: 'maintenancemode_modal_sync_succeeded', component: 'tool_opencast'}, + ]; + Str.get_strings(strings).then(function(jsstrings) { + // Required functionality for admin_setting_configdatetimeselector. + const datetimeselectors = document.querySelectorAll('.form-setting .opencast_config_dt_selector'); + datetimeselectors.forEach((dtblock) => { + if (dtblock?.dataset?.isoptional) { + const enablingelement = document.getElementById(`${dtblock.dataset.settingid}_enabled`); + const initialvalue = enablingelement?.checked ?? false; + const selects = dtblock.querySelectorAll(`.opencast-config-dt-select`); + selects.forEach((select) => { + select.disabled = !initialvalue; + }); + enablingelement.addEventListener('change', (event) => { + selects.forEach((select) => { + select.disabled = !event.target.checked; + }); + }); + } + }); + + // Sync Button. + const syncbtns = document.querySelectorAll('.maintenance-sync-btn'); + syncbtns.forEach((btn) => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + const ocinstanceid = e.target?.dataset?.ocinstanceid; + if (!ocinstanceid) { + Notification.alert(jsstrings[3], jsstrings[4]); + return; + } + + Notification.confirm( + jsstrings[0], jsstrings[1], jsstrings[2], null, + () => performSync(ocinstanceid, jsstrings) + ); + }); + // Make the button accessible to use after the listener is added. + btn.removeAttribute('disabled'); + btn.removeAttribute('title'); + btn.classList.remove('disabled'); + btn.classList.remove('btn-warning'); + btn.classList.add('btn-primary'); + }); + + return; + }).catch(Notification.exception); +}; + +/** + * Perform sync request via Ajax call. + * @param {int} ocinstanceid + * @param {array} jsstrings + */ +const performSync = (ocinstanceid, jsstrings) => { + if (!ocinstanceid) { + return; + } + Ajax.call([{ + methodname: 'tool_opencast_maintenance_sync', + args: {ocinstanceid: ocinstanceid}, + }])[0] + .then((data) => { + if (!data?.status) { + Toast.add(jsstrings[5], {type: 'danger'}); + return; + } + Toast.add(jsstrings[6], {type: 'success'}); + reloadWithDelay(); + return; + }) + .catch((error) => Notification.exception(error)); +}; + +/** + * Reloads the current page with a delay. + * @param {int} delay default 3000 ms + */ +const reloadWithDelay = (delay = 3000) => { + setTimeout(() => { + window.location.reload(); + }, delay); +}; + +/** + * Opencast Tool maintenance notification handler. + * + * It is used to make sure that there is only one maintenance notification printed at a time. + * + * @param {string} message + * @param {string} level + * @param {bool} notify + */ +export const notification = (message, level, notify) => { + if (!window?.ocMaintenanceNotified && notify) { + Notification.addNotification({ + message: message, + type: level + }); + window.ocMaintenanceNotified = true; + } +}; diff --git a/classes/local/api.php b/classes/local/api.php index acf1e70..762df10 100644 --- a/classes/local/api.php +++ b/classes/local/api.php @@ -58,6 +58,8 @@ class api extends \curl { public $opencastapi; /** @var \OpencastApi\Rest\OcRestClient the opencast REST Client instance */ public $opencastrestclient; + /** @var \tool_opencast\local\maintenance_class the maintenance class instance */ + public $maintenance; /** @var array array of supported api levels */ private static $supportedapilevel; @@ -227,6 +229,7 @@ public function __construct($instanceid = null, $this->timeout = settings_api::get_apitimeout($storedconfigocinstanceid); $this->connecttimeout = settings_api::get_apiconnecttimeout($storedconfigocinstanceid); $this->baseurl = settings_api::get_apiurl($storedconfigocinstanceid); + $this->maintenance = new maintenance_class($storedconfigocinstanceid); if (empty($this->username)) { throw new empty_configuration_exception('apiusernameempty', 'tool_opencast'); @@ -279,8 +282,56 @@ public function __construct($instanceid = null, 'timeout' => (intval($this->timeout) / 1000), 'connect_timeout' => (intval($this->connecttimeout) / 1000), ]; - $this->opencastapi = new \OpencastApi\Opencast($config, [], $enableingest); - $this->opencastrestclient = new \OpencastApi\Rest\OcRestClient($config); + $this->opencastapi = $this->decorate_opencast_api_services($config, [], $enableingest); + $this->opencastrestclient = new \tool_opencast\proxy\decorated_opencastapi_rest_client($config, $this->maintenance); + + // We notify the maintenance directly in constructor, to cover almost every external use of this class. + $this->notify_maintenance(); + } + + /** + * Decorates the Opencast API services with maintenance-aware proxy. + * + * This function creates a new Opencast API instance and wraps each of its services + * with a decorated proxy that is aware of the maintenance status. + * + * @param array $config The configuration array for the Opencast API. + * @param array $engageconfig Optional. The engage configuration array for the Opencast API. Default is an empty array. + * @param bool $enableingest Optional. Whether to enable ingest functionality. Default is false. + * + * @return \OpencastApi\Opencast A decorated instance of the Opencast API with maintenance-aware service proxy. + */ + private function decorate_opencast_api_services( + array $config, + array $engageconfig = [], + bool $enableingest = false + ): \OpencastApi\Opencast { + $decoratedopencastapi = new \OpencastApi\Opencast($config, $engageconfig, $enableingest); + $classvars = get_object_vars($decoratedopencastapi); + foreach (array_keys($classvars) as $name) { + $decoratedopencastapi->{$name} = + new \tool_opencast\proxy\decorated_opencastapi_service($decoratedopencastapi->{$name}, $this->maintenance); + } + return $decoratedopencastapi; + } + + /** + * Notifies about maintenance status and handles maintenance message display. + * + * This function checks if maintenance is set and, if so, handles the display + * of maintenance notification messages. + * + * @return void + */ + private function notify_maintenance() { + + // When the maintenance is not set, we do nothing! + if (empty($this->maintenance)) { + return; + } + + // We now handle maintenance messages notification display. + $this->maintenance->handle_notification_message_display(); } /** @@ -363,6 +414,12 @@ private function get_authentication_header($runwithroles = []) { * @throws \moodle_exception */ public function oc_get($resource, $runwithroles = []) { + + // Check for maintenance first. + if (!empty($this->maintenance) && !$this->maintenance->can_access(__FUNCTION__)) { + return $this->maintenance->decide_access_bounce(); + } + $url = $this->baseurl . $resource; $this->resetHeader(); @@ -462,6 +519,11 @@ private function add_postname_chunkupload($file, $key) { */ public function oc_post($resource, $params = [], $runwithroles = []) { + // Check for maintenance first. + if (!empty($this->maintenance) && !$this->maintenance->can_access(__FUNCTION__)) { + return $this->maintenance->decide_access_bounce(); + } + $url = $this->baseurl . $resource; $this->resetHeader(); @@ -507,6 +569,11 @@ public function oc_post($resource, $params = [], $runwithroles = []) { */ public function oc_put($resource, $params = [], $runwithroles = []) { + // Check for maintenance first. + if (!empty($this->maintenance) && !$this->maintenance->can_access(__FUNCTION__)) { + return $this->maintenance->decide_access_bounce(); + } + $url = $this->baseurl . $resource; $this->resetHeader(); @@ -542,6 +609,11 @@ public function oc_put($resource, $params = [], $runwithroles = []) { */ public function oc_delete($resource, $params = [], $runwithroles = []) { + // Check for maintenance first. + if (!empty($this->maintenance) && !$this->maintenance->can_access(__FUNCTION__)) { + return $this->maintenance->decide_access_bounce(); + } + $url = $this->baseurl . $resource; $this->resetHeader(); @@ -634,4 +706,35 @@ public function connection_test_credentials() { return true; } + + /** + * Synchronizes the maintenance status with Opencast. + * + * This function attempts to retrieve the maintenance status from Opencast + * and update the local maintenance mode accordingly. It is an experimental + * feature as the corresponding functionality may not yet exist in Opencast. + * + * @return bool Returns true if the maintenance mode was successfully updated, + * false if the update failed or the required properties/methods + * are not available. + */ + public function sync_maintenance_with_opencast() { + // This an experimental feature, because the feature does not exist in Opencast yet. + if ($this->maintenance && $this->opencastapi && property_exists($this->opencastapi->baseApi, 'getMaintenance')) { + $response = $this->opencastapi->baseApi->getMaintenance(); + if ($response['code'] != 200) { + return false; + } + $maintenanceobj = $response['body']; + $inmaintenance = isset($maintenanceobj->in_maintenance) ? (bool) $maintenanceobj->in_maintenance : false; + $readonly = isset($maintenanceobj->read_only) ? (bool) $maintenanceobj->read_only : false; + $maintenancemode = $inmaintenance ? maintenance_class::MODE_ENABLE : maintenance_class::MODE_DISABLE; + if ($inmaintenance && $readonly) { + $maintenancemode = maintenance_class::MODE_READONLY; + } + return $this->maintenance->update_mode_from_opencast($maintenancemode); + } + + return false; + } } diff --git a/classes/local/maintenance_class.php b/classes/local/maintenance_class.php new file mode 100644 index 0000000..e89ef0c --- /dev/null +++ b/classes/local/maintenance_class.php @@ -0,0 +1,681 @@ +. + +namespace tool_opencast\local; + +/** + * Maintenance Helper class + * + * It is the brain of the maintenance system for Opencast Moodle plugins. + * + * @package tool_opencast + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class maintenance_class { + + /** @var int Disable mode flag id. */ + const MODE_DISABLE = 0; + + /** @var int Read-only mode flag id. */ + const MODE_READONLY = 1; + + /** @var int Enable mode flag id. */ + const MODE_ENABLE = 2; + + /** @var string Mode config id. */ + const CONFIG_ID_MODE = 'maintenancemode'; + + /** @var string Nofitication level config id. */ + const CONFIG_ID_NOTIFLEVEL = 'maintenancemode_notification_level'; + + /** @var string Message config id. */ + const CONFIG_ID_MESSAGE = 'maintenancemode_message'; + + /** @var string Start date config id. */ + const CONFIG_ID_STARTDATE = 'maintenancemode_startdate'; + + /** @var string End date config id. */ + const CONFIG_ID_ENDDATE = 'maintenancemode_enddate'; + + /** @var string The name of the plugin tool_opencast. */ + private const PLUGINNAME = 'tool_opencast'; + + /** @var int The maintenance mode value. */ + private int $mode; + + /** @var string The notification level value. */ + private string $notiflevel; + + /** @var string The maintenance message value. */ + private string $message; + + /** @var \stdClass The start date value. */ + private \stdClass $startdate; + + /** @var \stdClass The end date value. */ + private \stdClass $enddate; + + /** @var int The id of the Opencast instance. */ + private int $ocinstanceid; + + /** + * Constructor. + * + * @param int|null $ocinstanceid The id of the Opencast instance. + * If null, the default Opencast instance will be used. + */ + public function __construct(?int $ocinstanceid) { + $this->ocinstanceid = $ocinstanceid ?? settings_api::get_default_ocinstance(); + $this->init(); + } + + /** + * Initializes the requirements for the class. + * This function contains all the setters of the class. + * + * @return void + */ + private function init() { + $this->set_mode(); + $this->set_notiflevel(); + $this->set_message(); + $this->set_startdate(); + $this->set_enddate(); + } + + /** + * Sets the value of the maintenance mode. + * Pulls the value from configs or sets a default value if not found. + * + * @return void + */ + private function set_mode() { + $this->mode = (int) (settings_api::get_maintenancemode($this->ocinstanceid) ?? self::MODE_DISABLE); + } + + /** + * Sets the value of the maintenance notification level. + * Pulls the value from configs or sets a default value if not found. + * + * @return void + */ + private function set_notiflevel() { + $this->notiflevel = + settings_api::get_maintenancenotiflevel($this->ocinstanceid) ?? \core\output\notification::NOTIFY_WARNING; + } + + /** + * Sets the value of the maintenance message. + * Pulls the value from configs or sets a default value if not found. + * + * @return void + */ + private function set_message() { + $this->message = (string) settings_api::get_maintenancemessage($this->ocinstanceid) ?? ''; + } + + /** + * Sets the value of the maintenance start date. + * Pulls the value from configs or sets a default value if not found. + * + * @return void + */ + private function set_startdate() { + $startdate = new \stdClass(); + $configstr = settings_api::get_maintenancestartdate($this->ocinstanceid) ?? null; + if (!empty($configstr)) { + $startdate = json_decode($configstr); + } + $this->startdate = $startdate; + } + + /** + * Sets the value of the maintenance end date. + * Pulls the value from configs or sets a default value if not found. + * + * @return void + */ + private function set_enddate() { + $enddate = new \stdClass(); + $configstr = settings_api::get_maintenancenddate($this->ocinstanceid) ?? null; + if (!empty($configstr)) { + $enddate = json_decode($configstr); + } + $this->enddate = $enddate; + } + + /** + * Retrieves maintenance mode value. + * + * @return int maintenance mode value + */ + public function get_mode() { + return $this->mode; + } + + /** + * Retrieves maintenance notification level value. + * + * @return string maintenance notification level value + */ + public function get_notiflevel() { + return $this->notiflevel; + } + + /** + * Retrieves maintenance message value. + * + * @return string maintenance message value + */ + public function get_message() { + return $this->message; + } + + /** + * Retrieves maintenance formatted message. + * + * @param string $format message format (default FORMAT_HTML) + * + * @return string maintenance message value + */ + public function get_formatted_message($format = FORMAT_HTML) { + return format_text($this->get_message(), $format); + } + + /** + * Retrieves maintenance start date value. + * + * @return \stdClass|null maintenance start date value or null if not set. + */ + public function get_startdate() { + return $this->startdate; + } + + /** + * Retrieves maintenance end date value. + * + * @return \stdClass|null maintenance end date value or null if not set. + */ + public function get_enddate() { + return $this->enddate; + } + + /** + * Checks if the maintenance mode is activated. + * + * The function checks the current mode and time range to determine if the maintenance mode is activated. + * It considers the following conditions: + * - If the mode is disabled, it immediately returns false. + * - If there is no time range specified, it returns true, + * indicating that the maintenance mode is activated until further notice. + * - If the current time falls within the specified time range, it returns true. + * - If the current time is outside the specified time range, it returns false. + * + * @return bool True if maintenance mode is activated, false otherwise. + */ + public function is_activated() { + // If disabled, immediately return false. + if ($this->mode === self::MODE_DISABLE) { + return false; + } + + // Decide whether to check for date and time range. + + $hastimerange = isset($this->startdate) && $this->startdate->enabled || isset($this->enddate) && $this->enddate->enabled; + + // If no time range specified, that means it is activated until further notice! + if (!$hastimerange) { + return true; + } + + // Get the now time with correct timezone! + $nowdatetime = new \DateTime('now', \core_date::get_user_timezone_object()); + $nowdtimestamp = $nowdatetime->getTimestamp(); + + // Check if two sides of time range is enabled. + if ($this->startdate->enabled && $this->enddate->enabled && + $nowdtimestamp >= $this->startdate->timestamp && $nowdtimestamp <= $this->enddate->timestamp) { + return true; + } + + // If only the start date is enabled, we check whether the current time is after the start date. + if ($this->startdate->enabled && !$this->enddate->enabled && $nowdtimestamp >= $this->startdate->timestamp) { + return true; + } + + // If only the end date is enabled, we check whether the current time is before the end date. + if (!$this->startdate->enabled && $this->enddate->enabled && $nowdtimestamp <= $this->enddate->timestamp) { + return true; + } + + // If none of the above conditions are met, then it is not activated! + return false; + } + + /** + * Evaluates whether the access to resources is possible. + * + * This function is meant to be used either in decorated proxies or in the top level methods before performing call to Opencast. + * It considers the following conditions: + * - If the maintenance is activated, if not everything is accessible. + * - Checks if the mode is Read-Only and the method that has been called is eligible to perform the operation. + * + * @param string $method The name of the method that is being called + * + * @return bool True if access is granted, false otherwise in case conditions are not satisfied. + */ + public function can_access(string $method) { + // Of course, if deactivated, we allow access. + if (!$this->is_activated()) { + return true; + } + + // Landing here means, it is activated, and we now have to check for the Read-Only mode. + // Read-Only mode means, only to perform methods that have word "get" in their name. + if ($this->mode === self::MODE_READONLY && strpos(strtolower($method), 'get') !== false) { + return true; + } + + // In any case. we return false, meaning it is not accessible. + return false; + } + + /** + * Decides how to bounce the access restriction. + * + * This function is meant to be used in the top level methods before performing call to Opencast. + * + * It simply checks the referer and/or initiator url path against the whitelist and/or blacklist in web calls, + * and decides what to do with the call, either by redirecting, closing window, throwing exceptions or doing nothing! + * + * It considers the following conditions: + * - Web calls (checks if the call is from web): + * - Admins are not restricted except the admin/cron.php call! + * - When the call is in top level course area, it means that the system is loading the plugins, so we let it pass. + * - By ajax calls, e.g. from Repository or H5P extension plugins, we simply throw "access_denied_exception" exception. + * - In case no condition from above could be met, we either redirect the call back from where it came, + * or to the course view page, or simply close the current window in case we could not decide what to do! + * + * - System calls (CLI): + * - If a call comes in from system level e.g CLI, we only throw exception. + * + * @return void + * @throws moodle_exception|access_denied_exception + * Redirection could happen. + */ + public function decide_access_bounce() { + global $CFG, $COURSE, $SITE; + + $isbahat = defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING; + $iscli = defined('CLI_SCRIPT') && CLI_SCRIPT; + $isphpunit = defined('PHPUNIT_TEST') && PHPUNIT_TEST; + + $wwwroot = $CFG->wwwroot; + + // Hanlde behat wwwroot. + if ($isbahat && empty($wwwroot)) { + $wwwroot = $CFG->behat_wwwroot; + } + + $wwwrootparsed = parse_url($wwwroot); + + // Make sure path exists in wwwrootparsed. + if (empty($wwwrootparsed) || !isset($wwwrootparsed['path'])) { + $wwwrootparsed['path'] = ''; + } + + $iswebrequest = isset($_SERVER['REMOTE_ADDR']); // It is a web call when the REMOTE_ADDR is set in $_SERVER! + + // If it is a web request. + if ($iswebrequest && !$iscli && !$isphpunit) { + // We have to carefully decide whether to redirect back to the referer or the course page. + // The reason for this check is to avoid redirecting loops! + $referer = get_local_referer(false); + $requestedfrom = parse_url($referer); + $frompath = !empty($requestedfrom['path']) ? rtrim($requestedfrom['path'], '/') : ''; + + $initiator = qualified_me(); + $requesttarget = parse_url($initiator); + $tagetpath = !empty($requesttarget['path']) ? rtrim($requesttarget['path'], '/') : ''; + + $whitelist = []; + $whitelist[] = $wwwrootparsed['path']; + $whitelist[] = $wwwrootparsed['path'] . '/course/view.php'; + $whitelist[] = $wwwrootparsed['path'] . '/my'; + $whitelist[] = $wwwrootparsed['path'] . '/my/courses.php'; + $whitelist[] = $wwwrootparsed['path'] . '/course'; + + $blacklist = []; + $blacklist['block_opencast'] = $wwwrootparsed['path'] . '/blocks/opencast'; // Match for block_opencast plugin. + $blacklist['mod_opencast'] = $wwwrootparsed['path'] . '/mod/opencast'; // Match for mod_opencast plugin. + $blacklist['modedit'] = $wwwrootparsed['path'] . '/course/modedit'; // Match for mod_opencast plugin. + $blacklist['repository_opencast'] = $wwwrootparsed['path'] . '/repository'; // Match for repository_opencast plugin. + $blacklist['admin_cron'] = $wwwrootparsed['path'] . '/admin/cron'; // Match for admin cron. + $blacklist['admin_cron'] = $wwwrootparsed['path'] . '/local'; // Match for local plugins like och5p and och5pcore. + + // If admin and it is not admin cron page, + // we let it pass to avoid interrupting any installation, configuration or upgrade. + if (is_siteadmin() && strpos($frompath, $blacklist['admin_cron']) === false) { + return ['code' => 404]; + } + + $fromblacklisted = $this->is_path_blacklisted($frompath, $blacklist); + $targetblacklisted = $this->is_path_blacklisted($tagetpath, $blacklist); + + // Exception: Calls going up to course from blacklist, or nothing to do with blacklist, we do nothing! + if ((!$fromblacklisted && !$targetblacklisted) || // Outside reaching or loading opencast. + (in_array($tagetpath, $whitelist) && $fromblacklisted)) { // Going back from plugin to course or somewhere else + return ['code' => 404]; + } + + // Is ajax or popup, we throw error to make sure the user gets the correct form of notification. + if (is_in_popup() || (defined('AJAX_SCRIPT') && AJAX_SCRIPT) || + isset($initiator['path']) && strpos($requesttarget['path'], 'ajax') !== false) { + throw new \core\exception\access_denied_exception('maintenance_exception_message', self::PLUGINNAME); + } + + // We now have to decide whether to redirect back or close the window! + + // If the requested action is from any of the white lists, we will redirect back to the same url. + if (in_array($frompath, $whitelist) && $targetblacklisted) { + redirect($referer); + } + + if ($COURSE && $SITE && $SITE->id != $COURSE->id) { + $courseurl = new \moodle_url('/course/view.php', ['id' => $COURSE->id]); + redirect($courseurl); + } + + // In case any of the above conditions are not met, we close the window. + close_window(0, true); + } + + // If it is not yet returned, it has to throw an error, this should also cover requests coming from unit tests. + throw new \moodle_exception('maintenance_exception_message', self::PLUGINNAME); + } + + + /** + * Checks if a given path is blacklisted. + * + * This function determines whether the provided path matches any of the entries + * in the blacklist array. It uses a case-sensitive partial string match. + * + * @param string $path The path to check against the blacklist. + * @param array $blacklist An array of blacklisted path patterns. + * + * @return bool Returns true if the path matches any blacklist entry, false otherwise. + */ + private function is_path_blacklisted(string $path, array $blacklist) { + if (empty($path) || empty($blacklist)) { + return false; + } + $filterred = array_filter($blacklist, function ($v, $k) use ($path) { + return strpos($path, $v) !== false; + }, ARRAY_FILTER_USE_BOTH); + return !empty($filterred); + } + + + /** + * Displays a notification message based on the maintenance mode settings. + * + * This function retrieves the formatted maintenance message, the notification level, + * and checks if the maintenance mode is activated. It then uses the Moodle Page API + * to load the 'tool_opencast/maintenance' JavaScript module and passes the necessary + * parameters to display the notification. + * + * @global moodle_page $PAGE The global Moodle page object. + * @return void + */ + public function handle_notification_message_display() { + global $PAGE; + + // Get the formatted message. + $message = $this->get_formatted_message(); + // Fallback to a default maintenance message if the configured message somehow does not exist. + if (empty($message)) { + $message = get_string('maintenance_default_notification_message', self::PLUGINNAME); + } + + // Get the level. + $level = $this->get_notiflevel(); + + // We notify only when it is activated! + $notify = $this->is_activated(); + + // Make sure that the $PAGE is ready for notifications js module load. + if ($PAGE) { + $PAGE->requires->js_call_amd('tool_opencast/maintenance', 'notification', [$message, $level, $notify]); + } + } + + /** + * Updates the maintenance mode status fetched from opencast known as Synchronization! + * + * @param int $mode the mode to update + * + * @return bool whether the update was successful + */ + public function update_mode_from_opencast(int $mode) { + $result = false; + if (in_array($mode, [self::MODE_DISABLE, self::MODE_ENABLE, self::MODE_READONLY])) { + $result = set_config(self::CONFIG_ID_MODE . '_' . $this->ocinstanceid, $mode, self::PLUGINNAME); + } + return $result; + } + + // STATIC HELPER METHODS! + + /** + * Returns mode choice options for the selection. + * + * @return array choice options + */ + public static function get_admin_settings_mode_choices() { + return [ + self::MODE_DISABLE => get_string('maintenancemode_disable', self::PLUGINNAME), + self::MODE_READONLY => get_string('maintenancemode_readonly', self::PLUGINNAME), + self::MODE_ENABLE => get_string('maintenancemode_enable', self::PLUGINNAME), + ]; + } + + /** + * Returns notification level choice options for the selection. + * + * @return array choice options + */ + public static function get_admin_settings_notiflevel_choices() { + return [ + \core\output\notification::NOTIFY_WARNING => get_string('maintenancemode_notiflevel_warning', self::PLUGINNAME), + \core\output\notification::NOTIFY_ERROR => get_string('maintenancemode_notiflevel_error', self::PLUGINNAME), + \core\output\notification::NOTIFY_INFO => get_string('maintenancemode_notiflevel_info', self::PLUGINNAME), + \core\output\notification::NOTIFY_SUCCESS => get_string('maintenancemode_notiflevel_success', self::PLUGINNAME), + ]; + } + + /** + * Gets the full configuration id of maintenance mode setting. + * + * @param int $ocintanceid the opencast instance id + * @param bool $withpluginname flag to determine whether to prepend plugin name. + * + * @return string the configuration id + */ + public static function get_mode_full_config_id(int $ocintanceid, bool $withpluginname = false) { + return self::generate_config_id(self::CONFIG_ID_MODE, $ocintanceid, $withpluginname); + } + + /** + * Gets the full configuration id of maintenance notification level setting. + * + * @param int $ocintanceid the opencast instance id + * @param bool $withpluginname flag to determine whether to prepend plugin name. + * + * @return string the configuration id + */ + public static function get_notificationlevel_full_config_id(int $ocintanceid, bool $withpluginname = false) { + return self::generate_config_id(self::CONFIG_ID_NOTIFLEVEL, $ocintanceid, $withpluginname); + } + + /** + * Gets the full configuration id of maintenance message setting. + * + * @param int $ocintanceid the opencast instance id + * @param bool $withpluginname flag to determine whether to prepend plugin name. + * + * @return string the configuration id + */ + public static function get_message_full_config_id(int $ocintanceid, bool $withpluginname = false) { + return self::generate_config_id(self::CONFIG_ID_MESSAGE, $ocintanceid, $withpluginname); + } + + /** + * Gets the full configuration id of maintenance start date setting. + * + * @param int $ocintanceid the opencast instance id + * @param bool $withpluginname flag to determine whether to prepend plugin name. + * + * @return string the configuration id + */ + public static function get_startdate_full_config_id(int $ocintanceid, bool $withpluginname = false) { + return self::generate_config_id(self::CONFIG_ID_STARTDATE, $ocintanceid, $withpluginname); + } + + /** + * Gets the full configuration id of maintenance end date setting. + * + * @param int $ocintanceid the opencast instance id + * @param bool $withpluginname flag to determine whether to prepend plugin name. + * + * @return string the configuration id + */ + public static function get_enddate_full_config_id(int $ocintanceid, bool $withpluginname = false) { + return self::generate_config_id(self::CONFIG_ID_ENDDATE, $ocintanceid, $withpluginname); + } + + /** + * Generates the full configuration id by combining the id and the instance id and if requested appending plugin name. + * + * @param string $configid the configuration id + * @param int $ocintanceid the opencast instance id + * @param bool $withpluginname flag to determine whether to prepend plugin name. + * + * @return string the generated configuration id + */ + private static function generate_config_id(string $configid, int $ocintanceid, bool $withpluginname) { + $id = $configid . '_'. $ocintanceid; + if ($withpluginname) { + $id = self::PLUGINNAME. '/'. $id; + } + return $id; + } + + /** + * An auxiliary method to be used by the admin settings in order to validate the start and end dates. + * + * It gets the some requirements, prepares and returns the validation callable function. + * + * @param string $currentid the current setting id to compare with. + * @param string $compsettingid the setting id to compare against. + * @param string $compsettingstringname the string name of the setting to compare against. + * @param string $compopr the comparision operator. currently used ">=" or "<=" + * + * @return callable the validation callback function + */ + public static function maintenance_datetime_validation(string $currentid, string $compsettingid, + string $compsettingstringname, string $compopr= '>=') { + // Preparing various parameters to be used in the validation callback function. + $compsettinglabel = get_string($compsettingstringname, self::PLUGINNAME); + $errorstrings = [ + '>=' => get_string('maintenancemode_datetime_ge_error', self::PLUGINNAME, $compsettinglabel), + '<=' => get_string('maintenancemode_datetime_le_error', self::PLUGINNAME, $compsettinglabel), + 'expired' => get_string('maintenancemode_datetime_expired_error', self::PLUGINNAME), + ]; + $domain = [ + 'startdate' => strpos($currentid, self::CONFIG_ID_STARTDATE) !== false, + 'enddate' => strpos($currentid, self::CONFIG_ID_ENDDATE) !== false, + ]; + return function (array $data, array $datasubmitted) use ($compsettingid, $compopr, $errorstrings, $domain): string { + // Get the current timestamp. + $thistimestamp = (int) $data['timestamp']; + + // Regulate the expiration: + // - If is it start date we allow the time in the past, by not checking any further! + // - If is it end date we allow only the time in the future. + if ($domain['enddate'] === true && $thistimestamp < time()) { + return $errorstrings['expired']; + } + + // Extract the data time of the other side to compare against from submitted data. + $compsubmitteddata = array_filter($datasubmitted, function ($value, $key) use ($compsettingid) { + return strpos($key, $compsettingid) !== false; + }, ARRAY_FILTER_USE_BOTH); + + // If found, we proceed. + if (!empty($compsubmitteddata)) { + $compfiltereddata = []; + // Extract only the datetime parameters. + foreach ($compsubmitteddata[array_key_first($compsubmitteddata)] as $key => $value) { + if ($key === 'enabled' || $key === 'oldvalue') { + continue; + } + switch ($key) { + case 'year': + $compfiltereddata['year'] = intval($value); + break; + case 'month': + $compfiltereddata['month'] = intval($value); + break; + case 'day': + $compfiltereddata['day'] = intval($value); + break; + case 'hour': + $compfiltereddata['hour'] = intval($value); + break; + case 'minute': + $compfiltereddata['minute'] = intval($value); + break; + default: + break; + } + } + // If the compiled filtered array is not empty, we do the comparison. + if (!empty($compfiltereddata)) { + $configtimestamp = make_timestamp( + $compfiltereddata['year'], + $compfiltereddata['month'], + $compfiltereddata['day'], + $compfiltereddata['hour'], + $compfiltereddata['minute'] + ); + + // Compare the timestamps of both sides based on provided comparison operator. + if ($compopr === '>=' && $thistimestamp >= $configtimestamp) { + // Returning error message, if the condition is met. + return $errorstrings[$compopr]; + } else if ($compopr === '<=' && $thistimestamp <= $configtimestamp) { + // Returning error message, if the condition is met. + return $errorstrings[$compopr]; + } + } + } + return ''; + }; + } +} diff --git a/classes/local/settings_api.php b/classes/local/settings_api.php index 4386950..80c0952 100644 --- a/classes/local/settings_api.php +++ b/classes/local/settings_api.php @@ -168,6 +168,86 @@ public static function get_lticonsumersecret(int $ocinstanceid) { return get_config('tool_opencast', 'lticonsumersecret_' . $ocinstanceid); } + /** + * Get the maintenance mode for a specific Opencast instance. + * + * This function retrieves the maintenance mode setting for the given Opencast instance. + * + * @param int $ocinstanceid The ID of the Opencast instance to check the maintenance mode for. + * + * @return mixed The maintenance mode setting for the specified Opencast instance. + * Returns false if the setting is not found. + * + * @throws \dml_exception If there's an error retrieving the configuration. + */ + public static function get_maintenancemode(int $ocinstanceid) { + return get_config('tool_opencast', maintenance_class::get_mode_full_config_id($ocinstanceid)); + } + + /** + * Get the maintenance notification level for a specific Opencast instance. + * + * This function retrieves the maintenance notification level setting for the given Opencast instance. + * + * @param int $ocinstanceid The ID of the Opencast instance to check the maintenance notification level for. + * + * @return mixed The maintenance notification level setting for the specified Opencast instance. + * Returns false if the setting is not found. + * + * @throws \dml_exception If there's an error retrieving the configuration. + */ + public static function get_maintenancenotiflevel(int $ocinstanceid) { + return get_config('tool_opencast', maintenance_class::get_notificationlevel_full_config_id($ocinstanceid)); + } + + /** + * Get the maintenance message for a specific Opencast instance. + * + * This function retrieves the maintenance message setting for the given Opencast instance. + * + * @param int $ocinstanceid The ID of the Opencast instance to retrieve the maintenance message for. + * + * @return mixed The maintenance message setting for the specified Opencast instance. + * Returns false if the setting is not found. + * + * @throws \dml_exception If there's an error retrieving the configuration. + */ + public static function get_maintenancemessage(int $ocinstanceid) { + return get_config('tool_opencast', maintenance_class::get_message_full_config_id($ocinstanceid)); + } + + /** + * Get the maintenance start date json string for a specific Opencast instance. + * + * This function retrieves the maintenance start date json string setting for the given Opencast instance. + * + * @param int $ocinstanceid The ID of the Opencast instance to retrieve the maintenance start date json string for. + * + * @return mixed The maintenance start date json string setting for the specified Opencast instance. + * Returns false if the setting is not found. + * + * @throws \dml_exception If there's an error retrieving the configuration. + */ + public static function get_maintenancestartdate(int $ocinstanceid) { + return get_config('tool_opencast', maintenance_class::get_startdate_full_config_id($ocinstanceid)); + } + + /** + * Get the maintenance end date json string for a specific Opencast instance. + * + * This function retrieves the maintenance end date json string setting for the given Opencast instance. + * + * @param int $ocinstanceid The ID of the Opencast instance to retrieve the maintenance end date json string for. + * + * @return mixed The maintenance end date json string setting for the specified Opencast instance. + * Returns false if the setting is not found. + * + * @throws \dml_exception If there's an error retrieving the configuration. + */ + public static function get_maintenancenddate(int $ocinstanceid) { + return get_config('tool_opencast', maintenance_class::get_enddate_full_config_id($ocinstanceid)); + } + /** * Return the Opencast instance for the passed Opencast instance id, if any. * If no Opencast instance with this id is configured, null is returned. diff --git a/classes/proxy/decorated_opencastapi_rest_client.php b/classes/proxy/decorated_opencastapi_rest_client.php new file mode 100644 index 0000000..dd4966c --- /dev/null +++ b/classes/proxy/decorated_opencastapi_rest_client.php @@ -0,0 +1,71 @@ +. + +namespace tool_opencast\proxy; + +use OpencastApi\Rest\OcRestClient; +use tool_opencast\local\maintenance_class; + +/** + * A decorated proxy class to wrap around the Opencast API Rest Client class. + * + * This proxy class is meant to have more local control over the overall system app interaction with Opencast API Library. + * Its main purpose is to apply a top layer controller such as maintenance checkers. + * + * @package tool_opencast + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class decorated_opencastapi_rest_client { + + /** @var OcRestClient The Opencast API Rest Client */ + private OcRestClient $restclient; + + /** @var maintenance_class|null The maintenance class */ + private ?maintenance_class $maintenance; + + /** + * Constructor + * @param array $config The Opencast API configuration + * @param maintenance_class|null $maintenance The maintenance class + */ + public function __construct(array $config, ?maintenance_class $maintenance = null) { + $this->restclient = new OcRestClient($config); + $this->maintenance = $maintenance; + } + + /** + * Magic method to handle method calls on the decorated proxy object. + * + * If the maintenance class is set and the current method is not allowed, it will restrict access. + * Otherwise, it will call the actual Opencast API Rest Client method. + * @param string $method The method name to be called + * @param array $args An array of arguments passed to the method + * + * @return mixed|void The result of the method call, or void if it is in maintenance mode. + */ + public function __call(string $method, array $args) { + if (!empty($this->maintenance) && !$this->maintenance->can_access($method)) { + return $this->maintenance->decide_access_bounce(); + } + $returedresult = call_user_func_array([$this->restclient, $method], $args); + if ($returedresult === $this->restclient) { + return $this; + } + return $returedresult; + } +} diff --git a/classes/proxy/decorated_opencastapi_service.php b/classes/proxy/decorated_opencastapi_service.php new file mode 100644 index 0000000..ac74044 --- /dev/null +++ b/classes/proxy/decorated_opencastapi_service.php @@ -0,0 +1,77 @@ +. + +namespace tool_opencast\proxy; + +use OpencastApi\Rest\OcRest; +use tool_opencast\local\maintenance_class; + +/** + * A decorated proxy class to wrap around the Opencast API services. + * + * This proxy class is meant to have more local control over the overall system app interaction with Opencast API Library. + * Its main purpose is to apply a top layer controller such as maintenance checkers. + * + * @package tool_opencast + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class decorated_opencastapi_service { + + /** @var OcRest|null The Opencast API service */ + public ?OcRest $apiservice; + + /** @var maintenance_class|null The maintenance class */ + private ?maintenance_class $maintenance; + + /** + * Constructor + * @param OcRest|null $apiservice The Opencast API service + * @param maintenance_class|null $maintenance The maintenance class + */ + public function __construct(?OcRest $apiservice = null, ?maintenance_class $maintenance = null) { + $this->apiservice = $apiservice; + $this->maintenance = $maintenance; + } + + /** + * Magic method to handle method calls on the decorated proxy object. + * + * If the maintenance class is set and the current method is not allowed, it will restrict access. + * Otherwise, it will call the actual Opencast API service method. + * + * @param string $method The name of the method + * @param array $args The arguments for the method + * + * @return mixed|void The decorated proxy object or the response object obtained from original service. + * or void if it is in maintenance mode. + */ + public function __call(string $method, array $args) { + // Maintenance feature checker. + if (!empty($this->maintenance) && !$this->maintenance->can_access($method)) { + return $this->maintenance->decide_access_bounce(); + } + $response = call_user_func_array([$this->apiservice, $method], $args); + + // Handle recursive. + if ($response === $this->apiservice) { + return $this; + } + + return $response; + } +} diff --git a/classes/settings/admin_setting_configdatetimeselector.php b/classes/settings/admin_setting_configdatetimeselector.php new file mode 100644 index 0000000..f0401cc --- /dev/null +++ b/classes/settings/admin_setting_configdatetimeselector.php @@ -0,0 +1,403 @@ +. + +namespace tool_opencast\settings; + +/** + * Admin setting class which is used to create a date time selector. + * + * @package tool_opencast + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_setting_configdatetimeselector extends \admin_setting { + + /** @var bool Flag to determine whether it is optional */ + private $optional; + + /** @var callable|null Validation function */ + protected $validatefunction = null; + + /** + * Constructor + * @param string $name setting unique ascii name + * @param string $visiblename localised + * @param string $description long localised info + * @param int $defaultsetting the default timestamp + * @param bool $optional whether the setting need to be optionally selected by an enable checkbox. Defaults to false + */ + public function __construct($name, $visiblename, $description, $defaultsetting = 0, $optional = false) { + parent::__construct($name, $visiblename, $description, $defaultsetting); + $this->optional = $optional; + } + + + /** + * Retrieves the current setting value for the date and time selector. + * + * This function reads the configuration, parses it, and returns an array + * containing the date and time components, along with additional settings. + * + * @return array|null An array containing the following keys: + * - year: The year (YYYY format) + * - month: The month (01-12 format) + * - day: The day of the month (1-31 format) + * - hour: The hour (0-23 format) + * - minute: The minute (0-59 format) + * - timestamp: The Unix timestamp of the date and time + * - optional: Whether the setting is optional + * - enabled: Whether the setting is enabled (for optional settings) + * Returns null if the configuration is empty. + */ + public function get_setting() { + $config = $this->config_read($this->name); + if (empty($config)) { + return null; + } + + $config = json_decode($config); + + $configtimestamp = !empty($config->timestamp) ? (int) $config->timestamp : time(); + + $configdatetime = usergetdate($configtimestamp); + + $settings = [ + 'year' => $configdatetime['year'], + 'month' => $configdatetime['mon'], + 'day' => $configdatetime['mday'], + 'hour' => $configdatetime['hours'], + 'minute' => $configdatetime['minutes'], + 'timestamp' => $configdatetime[0], + 'optional' => $this->optional, + 'enabled' => (bool) $config->enabled, + ]; + + return $settings; + } + + + /** + * Writes the setting to the configuration. + * + * This function processes the input data, make timestamp out of the given date info, + * and saves the setting in a JSON-encoded format. + * + * @param array|mixed $data The input data to be processed and saved. + * Expected to be an array containing date and time components. + * + * @return string Returns an empty string on success, or an error message on failure. + * If $data is not an array, an empty string is returned. + */ + public function write_setting($data) { + + if (!is_array($data)) { + return ''; + } + + $oldvalue = json_decode($data['oldvalue'], true); + + // In case the setting is optional and disabled, we only receive "oldvalue" parameter here. + if (count($data) === 1 && !empty($data['oldvalue'])) { + $data = $oldvalue; + unset($data['enabled']); // When enabled is unset, that means it is disabled, so we force it here. + } + + // Make timestamp out of data with make_timestamp method to ensure its integrity. + $configtimestamp = make_timestamp($data['year'], $data['month'], $data['day'], $data['hour'], $data['minute']); + + $additionalsettings = [ + 'timestamp' => $configtimestamp, + 'optional' => $this->optional, + ]; + + // Make sure that enabled setting is correctly recorded. + if (isset($data['enabled'])) { + $data['enabled'] = true; + } else { + $data['enabled'] = false; + } + + // Here, before merge, we make sure that "oldvalues" parameter is not going to be stored. + if (isset($data['oldvalue'])) { + unset($data['oldvalue']); + } + + $settings = array_merge($data, $additionalsettings); + + // Validate the new setting, if it is enabled. + if ($settings['enabled'] == true) { + $error = $this->validate_setting($settings); + if (!empty($error)) { + return $error; + } + } + + $result = $this->config_write($this->name, json_encode($settings)); + + return ($result ? '' : get_string('errorsetting', 'admin')); + } + + /** + * Validate the setting. This uses the callback function if provided; subclasses could override + * to carry out validation directly in the class. + * + * @param array $data New values being set + * @return string Empty string if valid, or error message text + */ + protected function validate_setting(array $data): string { + // If validation function is specified, call it now. + if ($this->validatefunction) { + // For more accurate dependency validation, we pass the submitted form data to the validation function. + $datasubmitted = (array) data_submitted(); + return call_user_func($this->validatefunction, $data, $datasubmitted); + } else { + return ''; + } + } + + /** + * Sets a validate function. + * + * The callback will be passed one parameter, the new setting value, and should return either + * an empty string '' if the value is OK, or an error message if not. + * + * @param callable|null $validatefunction Validate function or null to clear + */ + public function set_validate_function(?callable $validatefunction = null) { + $this->validatefunction = $validatefunction; + } + + /** + * Returns XHTML time select fields + * + * @param array $data the current setting + * @param string $query + * @return string XHTML time select fields and wrapping div(s) + */ + public function output_html($data, $query = '') { + global $OUTPUT; + + // We have to get the setting from the get_setting function, otherwise the pass $data variable is insufficient. + $setting = $this->get_setting(); + $default = $this->get_defaultsetting(); + if (is_array($default)) { + $defaultinfo = userdate(intval($default), get_string('strftimedatetime', 'langconfig')); + } else { + $defaultinfo = null; + } + + // Support internationalised calendars. + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + + // Set the now datetime as default. + $savedtime = time(); + if (!empty($setting)) { + $savedtime = intval($setting['timestamp']); + } + + $dt = new \DateTime('@' . $savedtime, \core_date::get_user_timezone_object()); + $dttimestamp = $dt->getTimestamp(); + + $getdatefields = $calendartype->timestamp_to_date_array($dttimestamp); + $current = [ + 'year' => $getdatefields['year'], + 'month' => $getdatefields['mon'], + 'day' => $getdatefields['mday'], + 'hour' => $getdatefields['hours'], + 'minute' => $getdatefields['minutes'], + ]; + + // To prevent bad data when it is a very fresh config setting. + if (empty($setting)) { + $setting = $current; + $setting['enabled'] = false; + $setting['optional'] = $this->optional; + } + + // Time part is handled the same everywhere. + $hours = []; + for ($i = 0; $i <= 23; $i++) { + $hours[$i] = sprintf("%02d", $i); + } + $minutes = []; + for ($i = 0; $i < 60; $i += 5) { + $minutes[$i] = sprintf("%02d", $i); + } + + // List date fields. + $fields = $calendartype->get_date_order($current['year'], $calendartype->get_max_year()); + + // Add time fields - in RTL mode these are switched. + $fields['split'] = '/'; + if (right_to_left()) { + $fields['minute'] = $minutes; + $fields['colon'] = ':'; + $fields['hour'] = $hours; + } else { + $fields['hour'] = $hours; + $fields['colon'] = ':'; + $fields['minute'] = $minutes; + } + + // Output all date fields. + $spanattrs = [ + 'class' => 'fdate_time_selector opencast_config_dt_selector', + 'data-settingid' => $this->get_id(), + ]; + if ($this->optional) { + $spanattrs['data-isoptional'] = true; + } + $html = \html_writer::start_tag('span', $spanattrs); + + // We record old value in a hidden input element, to avoid getting ignored when the config is optional but disabled. + $html .= \html_writer::empty_tag('input', + ['type' => 'hidden', 'name' => $this->get_element_name('oldvalue'), 'value' => json_encode($setting)]); + + // Now, we try to add (enabled/disabled) checkbox if the setting is optional. + $html .= $this->add_optional_checkbox((bool) $setting['enabled']); + + // We then continue with rendering the date time select fields as well as calendar button. + foreach ($fields as $field => $options) { + if ($options === '/') { + $html = rtrim($html); + + // In Gregorian calendar mode only, we support a date selector popup, reusing + // code from form to ensure consistency. + if ($calendartype->get_name() === 'gregorian') { + $image = $OUTPUT->pix_icon('i/calendar', get_string('calendar', 'calendar'), 'moodle'); + $html .= ' ' . \html_writer::link('#', $image, + [ + 'name' => $this->get_element_name('calendar'), + 'id' => $this->get_element_id('calendar'), + ] + ); + } + continue; + } + if ($options === ':') { + $html .= ': '; + continue; + } + $html .= \html_writer::start_tag('label', ['for' => $this->get_element_id($field)]); + $html .= \html_writer::span(get_string($field) . ' ', 'accesshide'); + $html .= \html_writer::start_tag('select', + [ + 'class' => 'custom-select opencast-config-dt-select', + 'name' => $this->get_element_name($field), + 'id' => $this->get_element_id($field), + ] + ); + foreach ($options as $key => $value) { + $params = ['value' => $key]; + if ($current[$field] == $key) { + $params['selected'] = 'selected'; + } + $html .= \html_writer::tag('option', s($value), $params); + } + $html .= \html_writer::end_tag('select'); + $html .= \html_writer::end_tag('label'); + $html .= ' '; + } + $html = rtrim($html) . \html_writer::end_tag('span'); + + return format_admin_setting($this, $this->visiblename, $html, $this->description, + $this->get_id(), '', $defaultinfo, $query); + } + + /** + * Adds an optional checkbox to enable/disable the date time selector. + * + * This function generates HTML for a checkbox that allows users to enable or disable + * the date time selector when it's set as optional. If the setting is not optional, + * an empty string is returned. + * + * @param bool $configvalue The current enabled/disabled state of the checkbox + * @return string HTML markup for the optional checkbox, or an empty string if not optional + */ + private function add_optional_checkbox(bool $configvalue) { + // If it is not optional, we don't show the checkbox, and consider it as always enabled. + if (!$this->optional) { + return ''; + } + $html = \html_writer::start_tag('label', + ['class' => 'form-check d-inline-block pr-2'] + ); + + $checkboxattrs = [ + 'id' => $this->get_enabled_element_id(), + 'class' => 'form-check-input', + ]; + $checkboxlabelattrs = ['class' => 'mr-2']; + $checkboxhtml = \html_writer::checkbox( $this->get_enabled_element_name(), + '', $configvalue, '', $checkboxattrs, $checkboxlabelattrs); + $checkboxhtml .= ' ' . get_string('enable'); + $html .= $checkboxhtml; + $html .= \html_writer::end_tag('label'); + + return $html; + } + + /** + * Get the name attribute for the enabled checkbox element. + * + * This method generates the name attribute for the checkbox that enables or disables + * the date time selector when it's set as optional. + * + * @return string The name attribute for the enabled checkbox element + */ + private function get_enabled_element_name() { + return $this->get_element_name('enabled'); + } + + /** + * Get the id attribute for the enabled checkbox element. + * + * This method generates the id attribute for the checkbox that enables or disables + * the date time selector when it's set as optional. + * + * @return string The name attribute for the enabled checkbox element + */ + private function get_enabled_element_id() { + return $this->get_element_id('enabled'); + } + + /** + * Generates a unique element ID by appending a suffix to the base ID. + * + * This method creates a unique identifier for HTML elements by combining + * the base ID of the setting with a provided suffix. + * + * @param string $suffix The suffix to append to the base ID. + * @return string The generated element ID. + */ + private function get_element_id(string $suffix) { + return $this->get_id() . '_' . $suffix; + } + + /** + * Generates a name for the element by appending a suffix to the full name of the setting. + * + * This method creates a name for HTML elements by combining + * the full name of the setting with a provided suffix. + * + * @param string $suffix The suffix to append to the full setting name. + * @return string The generated element name. + */ + private function get_element_name(string $suffix) { + return $this->get_full_name() . '[' . $suffix . ']'; + } +} diff --git a/classes/settings/admin_settings_builder.php b/classes/settings/admin_settings_builder.php index 1ec838f..2c9b4ec 100644 --- a/classes/settings/admin_settings_builder.php +++ b/classes/settings/admin_settings_builder.php @@ -17,6 +17,7 @@ namespace tool_opencast\settings; use tool_opencast\local\settings_api; +use tool_opencast\local\maintenance_class; /** * Static admin setting builder class, which is used, to create and to add admin settings for tool_opencast. @@ -111,6 +112,7 @@ private static function create_settings_fulltree($instances): void { self::add_notification_banner_for_demo_instance($settings, $instanceid); self::add_config_settings_fulltree($settings, $instanceid); + self::add_maintenance_mode_block($settings, $instanceid); self::add_connection_test_tool($settings, $instanceid); self::include_admin_settingpage($settings); @@ -241,11 +243,15 @@ private static function require_amds(string $pluginnameid): void { return; } + // Important for maintenance start and end date calendar js. + form_init_date_js(); $PAGE->requires->jquery(); $PAGE->requires->js_call_amd('tool_opencast/tool_testtool', 'init'); $PAGE->requires->js_call_amd('tool_opencast/tool_settings', 'init', [$pluginnameid]); + $PAGE->requires->js_call_amd('tool_opencast/maintenance', 'init'); $PAGE->requires->css('/admin/tool/opencast/css/tabulator.min.css'); $PAGE->requires->css('/admin/tool/opencast/css/tabulator_bootstrap4.min.css'); + $PAGE->requires->css('/admin/tool/opencast/css/styles.css'); } /** @@ -413,6 +419,180 @@ private static function add_admin_setting_configpasswordunmask(\admin_settingpag $settings->add($settingconfigpasswordunmask); } + /** + * Adds an admin setting configselect to the passed admin settingpage. + * + * @param \admin_settingpage $settings + * The admin settingpage, the configselect is added to. + * + * @param string $name + * The internal name for the configselect. + * + * @param string $visiblenameidentifier + * The identifier for the string, that is used for the visible name of the configselect. + * + * @param string $descriptionidentifier + * The identifier for the string, that is used for the visible description of the configselect. + * + * @param string $defaultsetting + * The default setting for the configselect. + * + * @param array $choices + * The choices options of the configselect. + * + * @return void + */ + private static function add_admin_setting_configselect(\admin_settingpage $settings, + string $name, + string $visiblenameidentifier, + string $descriptionidentifier, + string $defaultsetting, + array $choices): void { + $settingconfigselect = new \admin_setting_configselect( + $name, + get_string($visiblenameidentifier, self::PLUGINNAME), + get_string($descriptionidentifier, self::PLUGINNAME), + $defaultsetting, + $choices + ); + $settings->add($settingconfigselect); + } + + /** + * Adds an admin setting configtextarea to the passed admin settingpage. + * + * @param \admin_settingpage $settings + * The admin settingpage, the configtextarea is added to. + * + * @param string $name + * The internal name for the configtextarea. + * + * @param string $visiblenameidentifier + * The identifier for the string, that is used for the visible name of the configtextarea. + * + * @param string $descriptionidentifier + * The identifier for the string, that is used for the visible description of the configtextarea. + * + * @param string $defaultsetting + * The default setting for the configtextarea. + * + * @param mixed $paramtype + * The parameter type of the configtext. + * + * @param string $cols + * The number of columns to make the editor. + * + * @param string $rows + * The number of rows to make the editor. + * + * @return void + */ + private static function add_admin_setting_configtextarea(\admin_settingpage $settings, + string $name, + string $visiblenameidentifier, + string $descriptionidentifier, + string $defaultsetting, + $paramtype = PARAM_RAW, + string $cols='60', + string $rows='8'): void { + $settingconfigtextarea = new admin_setting_configtextarea( + $name, + get_string($visiblenameidentifier, self::PLUGINNAME), + get_string($descriptionidentifier, self::PLUGINNAME), + $defaultsetting, $paramtype, $cols, $rows + ); + $settings->add($settingconfigtextarea); + } + + + /** + * Adds an admin setting confightmleditor to the passed admin settingpage. + * + * @param \admin_settingpage $settings + * The admin settingpage, the confightmleditor is added to. + * + * @param string $name + * The internal name for the confightmleditor. + * + * @param string $visiblenameidentifier + * The identifier for the string, that is used for the visible name of the confightmleditor. + * + * @param string $descriptionidentifier + * The identifier for the string, that is used for the visible description of the confightmleditor. + * + * @param string $defaultsetting + * The default setting for the confightmleditor. + * + * @param mixed $paramtype + * The parameter type of the configtext. + * + * @param string $cols + * The number of columns to make the editor. + * + * @param string $rows + * The number of rows to make the editor. + * + * @return void + */ + private static function add_admin_setting_confightmleditor(\admin_settingpage $settings, + string $name, + string $visiblenameidentifier, + string $descriptionidentifier, + string $defaultsetting, + $paramtype = PARAM_RAW, + string $cols='60', + string $rows='8'): void { + $settingconfightmleditor = new \admin_setting_confightmleditor( + $name, + get_string($visiblenameidentifier, self::PLUGINNAME), + get_string($descriptionidentifier, self::PLUGINNAME), + $defaultsetting, $paramtype, $cols, $rows + ); + $settings->add($settingconfightmleditor); + } + + /** + * Adds an admin setting configdatetimeselector to the passed admin settingpage. + * + * @param \admin_settingpage $settings + * The admin settingpage, the configdatetimeselector is added to. + * + * @param string $name + * The internal name for the configdatetimeselector. + * + * @param string $visiblenameidentifier + * The identifier for the string, that is used for the visible name of the configdatetimeselector. + * + * @param string $descriptionidentifier + * The identifier for the string, that is used for the visible description of the configdatetimeselector. + * + * @param int $defaultsetting + * The default setting timestamp for the configdatetimeselector. + * + * @param bool $optional + * Flag indicating whether this config should be optional with enable checkbox to disable/enable. + * + * @param callable|null $validatefunction Validate function or null to clear + * + * @return void + */ + private static function add_admin_setting_configdatetimeselector(\admin_settingpage $settings, + string $name, + string $visiblenameidentifier, + string $descriptionidentifier, + int $defaultsetting = 0, + bool $optional = false, + ?callable $validatefunction = null): void { + $settingconfigdatetimeselector = new admin_setting_configdatetimeselector( + $name, + get_string($visiblenameidentifier, self::PLUGINNAME), + get_string($descriptionidentifier, self::PLUGINNAME), + $defaultsetting, $optional + ); + $settingconfigdatetimeselector->set_validate_function($validatefunction); + $settings->add($settingconfigdatetimeselector); + } + /** * Adds the connection test tool to the passed admin settingpage for the passed Opencast instance id, * where a button with its description is added to the passed admin settingpage, @@ -452,4 +632,104 @@ private static function add_connection_test_tool(\admin_settingpage $settings, get_string('testtoolheaderdesc', self::PLUGINNAME, $connectiontoolbutton)) ); } + + /** + * Adds the maintenance mode block to the passed admin setting page for the given Opencast instance ID. + * + * This block includes a button to sync the maintenance mode settings with the corresponding Opencast instance, + * a dropdown to select the maintenance mode, a dropdown to select the notification level, a textarea/htmleditor to enter the + * maintenance message, and two datetime selectors to set the start and end dates of the maintenance period. + * + * @param \admin_settingpage $settings The admin setting page to add the maintenance mode block to. + * @param int $instanceid The ID of the Opencast instance to add the maintenance mode block for. + * @return void + */ + private static function add_maintenance_mode_block(\admin_settingpage $settings, + int $instanceid): void { + + // Prepare the Opencast maintenance sync button. + $attributes = [ + 'class' => 'btn btn-warning disabled maintenance-sync-btn mb-3 mt-2', + 'disabled' => 'disabled', + 'title' => get_string('maintenancemode_btn_disabled', self::PLUGINNAME), + 'data-ocinstanceid' => strval($instanceid), + ]; + // Get the API fetch (sync) button HTML. + $apifetchbutton = \html_writer::tag( + 'button', + get_string('maintenancemode_btn', self::PLUGINNAME), + $attributes + ); + // Place the button inside the header description. + $settings->add(new \admin_setting_heading( + 'tool_opencast/maintenancemodesection', + get_string('maintenanceheader', self::PLUGINNAME), + get_string('maintenanceheader_desc', self::PLUGINNAME, $apifetchbutton)) + ); + + // Render the maintenance mode option. + // Record ID outside in order to apply hide_if dependency option. + $maintenancemodeid = maintenance_class::get_mode_full_config_id($instanceid, true); + self::add_admin_setting_configselect($settings, + $maintenancemodeid, + 'maintenancemode', 'maintenancemode_desc', + maintenance_class::MODE_DISABLE, + maintenance_class::get_admin_settings_mode_choices() + ); + + // Render the maintenance notify level option. + $maintenancemodenotiflevelid = maintenance_class::get_notificationlevel_full_config_id($instanceid, true); + self::add_admin_setting_configselect($settings, + $maintenancemodenotiflevelid, + 'maintenancemode_notiflevel', 'maintenancemode_notiflevel_desc', + \core\output\notification::NOTIFY_WARNING, + maintenance_class::get_admin_settings_notiflevel_choices() + ); + // Apply hide_if dependency option. + $settings->hide_if($maintenancemodenotiflevelid, $maintenancemodeid, 'eq', maintenance_class::MODE_DISABLE); + + // Render the maintenance message option. + $maintenancemessageid = maintenance_class::get_message_full_config_id($instanceid, true); + self::add_admin_setting_confightmleditor($settings, + $maintenancemessageid, + 'maintenancemode_message', 'maintenancemode_message_desc', + '', + ); + // Apply hide_if dependency option. + $settings->hide_if($maintenancemessageid, $maintenancemodeid, 'eq', maintenance_class::MODE_DISABLE); + + // Render the maintenance start date options. + $maintenancestartdateid = maintenance_class::get_startdate_full_config_id($instanceid, true); + self::add_admin_setting_configdatetimeselector($settings, + $maintenancestartdateid, + 'maintenancemode_start', 'maintenancemode_start_desc', + 0, + true, + maintenance_class::maintenance_datetime_validation( + $maintenancestartdateid, + maintenance_class::get_enddate_full_config_id($instanceid), + 'maintenancemode_end', + '>=' + ) + ); + // Apply hide_if dependency option. + $settings->hide_if($maintenancestartdateid, $maintenancemodeid, 'eq', maintenance_class::MODE_DISABLE); + + // Render the maintenance end date options. + $maintenanceenddateid = maintenance_class::get_enddate_full_config_id($instanceid, true); + self::add_admin_setting_configdatetimeselector($settings, + $maintenanceenddateid, + 'maintenancemode_end', 'maintenancemode_end_desc', + 0, + true, + maintenance_class::maintenance_datetime_validation( + $maintenanceenddateid, + maintenance_class::get_startdate_full_config_id($instanceid), + 'maintenancemode_start', + '<=' + ) + ); + // Apply hide_if dependency option. + $settings->hide_if($maintenanceenddateid, $maintenancemodeid, 'eq', maintenance_class::MODE_DISABLE); + } } diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..89083aa --- /dev/null +++ b/css/styles.css @@ -0,0 +1,4 @@ +div#dateselector-calendar-panel { + /* Set higher than the z-index of the htmleditor */ + z-index: 10 !important; /* stylelint-disable-line */ +} diff --git a/db/services.php b/db/services.php index b18c864..a40cf78 100644 --- a/db/services.php +++ b/db/services.php @@ -59,6 +59,16 @@ 'ajax' => true, 'loginrequired' => true, ], + 'tool_opencast_maintenance_sync' => [ + 'classname' => 'tool_opencast_external', + 'methodname' => 'maintenance_sync', + 'classpath' => 'admin/tool/opencast/external.php', + 'description' => 'Service to Sync Maintenance Mode with Opencast', + 'type' => 'read', + 'capabilities' => 'tool/opencast:externalapi', + 'ajax' => true, + 'loginrequired' => true, + ], ]; $services = [ diff --git a/external.php b/external.php index ddb699a..dc2927e 100644 --- a/external.php +++ b/external.php @@ -225,6 +225,19 @@ public static function connection_test_tool_returns() { ); } + /** + * Describes the maintenance_sync return value. + * + * @return external_single_structure array the result of the connection test + */ + public static function maintenance_sync_returns() { + return new external_single_structure( + [ + 'status' => new external_value(PARAM_BOOL, 'Maintenance Synchronization result status'), + ] + ); + } + /** * Describes the parameters for testing the connection. * @@ -243,6 +256,20 @@ public static function connection_test_tool_parameters() { ); } + /** + * Describes the parameters for syncing the maintenance. + * + * @return external_function_parameters + * @throws coding_exception + */ + public static function maintenance_sync_parameters() { + return new external_function_parameters( + [ + 'ocinstanceid' => new external_value(PARAM_INT, 'Opencast instance id'), + ] + ); + } + /** * Builds a html tag for the alert of the connection test tool. * @@ -324,4 +351,33 @@ public static function connection_test_tool($apiurl, $apiusername, $apipassword, 'testresult' => $resulthtml, ]; } + + /** + * Perform fetching and syncing maintenance mode data from Opencast. + * + * @param int $ocinstanceid Opencast instance id. + * @return array + * @throws coding_exception + * @throws dml_exception + * @throws invalid_parameter_exception + * @throws required_capability_exception + */ + public static function maintenance_sync($ocinstanceid) { + + // Validate the parameters. + $params = self::validate_parameters(self::maintenance_sync_parameters(), + [ + 'ocinstanceid' => $ocinstanceid, + ] + ); + + // Get a customized api instance to use. + $api = \tool_opencast\local\api::get_instance($params['ocinstanceid']); + + $result = $api->sync_maintenance_with_opencast(); + + return [ + 'status' => $result, + ]; + } } diff --git a/lang/en/tool_opencast.php b/lang/en/tool_opencast.php index 30d25ea..c800dd4 100644 --- a/lang/en/tool_opencast.php +++ b/lang/en/tool_opencast.php @@ -26,8 +26,8 @@ defined('MOODLE_INTERNAL') || die(); $string['addinstance'] = 'Add instance'; -$string['apicreadentialstestfailedshort'] = 'Opencast API User Credentials test failed with http code: {$a}'; $string['apicreadentialstestfailedlong'] = 'The given Username or Password for the Opencast API is not valid.
Please use valid Username and Password in order to avoid fatal error during tasks which use this setting.'; +$string['apicreadentialstestfailedshort'] = 'Opencast API User Credentials test failed with http code: {$a}'; $string['apicreadentialstestsuccessfulshort'] = 'Opencast API User Credentials test successful.'; $string['apipassword'] = 'Password of Opencast API user'; $string['apipassworddesc'] = 'Configure the password of the Opencast user who is used to do the Opencast API calls.'; @@ -59,6 +59,39 @@ $string['lticonsumerkey_desc'] = 'LTI Consumer key for the integration of Opencast services that require authentication such as Studio or the editor.'; $string['lticonsumersecret'] = 'Consumer secret'; $string['lticonsumersecret_desc'] = 'LTI Consumer secret for the integration of Opencast services that require authentication.'; +$string['maintenance_default_notification_message'] = '
Opencast Maintenance Notice

Please note that Opencast is currently undergoing maintenance. As a result, some or all features related to Opencast may be temporarily unavailable. Thank you for your understanding.'; +$string['maintenance_exception_message'] = 'Opencast is currently undergoing maintenance. Interactions are temporarily disabled.'; +$string['maintenanceheader'] = 'Maintenance'; +$string['maintenanceheader_desc'] = 'In this section the maintenance mode can be configured with the following settings.
Depending on Opencast feature and settings availability, is it also possible to {$a}'; +$string['maintenancemode'] = 'Maintenance mode'; +$string['maintenancemode_btn'] = 'Sync Opencast Maintenance Mode'; +$string['maintenancemode_btn_disabled'] = 'Required js modules are not loaded.'; +$string['maintenancemode_datetime_expired_error'] = 'This field should not be in the past!'; +$string['maintenancemode_datetime_ge_error'] = 'This field should be before "{$a}"'; +$string['maintenancemode_datetime_le_error'] = 'This field should be after "{$a}"'; +$string['maintenancemode_desc'] = 'Setting maintenance mode to avoid conflict during Opencast downtime.
If Read-Only mode is selected, only reading resources from Opencast will be allowed.'; +$string['maintenancemode_disable'] = 'Disable'; +$string['maintenancemode_enable'] = 'Enable'; +$string['maintenancemode_end'] = 'Maintenance ends at'; +$string['maintenancemode_end_desc'] = 'The end date and time of maintenance'; +$string['maintenancemode_message'] = 'Maintenance Message'; +$string['maintenancemode_message_desc'] = 'An error message to display during maintenance.'; +$string['maintenancemode_modal_sync_confirmation_btn'] = 'Sync'; +$string['maintenancemode_modal_sync_confirmation_text'] = 'Are you sure to sync the maintenance mode with Opencast? This wil overwrite the current configuration.'; +$string['maintenancemode_modal_sync_confirmation_title'] = 'Sync Opencast Maintenance Mode'; +$string['maintenancemode_modal_sync_error_noinstance_message'] = 'Unable to find the Opencast instance id!'; +$string['maintenancemode_modal_sync_error_title'] = 'Syncing Error'; +$string['maintenancemode_modal_sync_failed'] = 'Maintenance Synchronization Unsuccessful!'; +$string['maintenancemode_modal_sync_succeeded'] = 'Maintenance successfully synchronized. The page will refresh in 3 seconds to apply the updated changes.'; +$string['maintenancemode_notiflevel'] = 'Notification Level'; +$string['maintenancemode_notiflevel_desc'] = 'By this setting you can set the level of notification message which helps rendering it in different styles and color based on the level e.g. Error Level will print a notification in a red box.'; +$string['maintenancemode_notiflevel_error'] = 'Error'; +$string['maintenancemode_notiflevel_info'] = 'Information'; +$string['maintenancemode_notiflevel_success'] = 'Success'; +$string['maintenancemode_notiflevel_warning'] = 'Warning'; +$string['maintenancemode_readonly'] = 'Read Only'; +$string['maintenancemode_start'] = 'Maintenance starts at'; +$string['maintenancemode_start_desc'] = 'The start date and time of maintenance'; $string['name'] = 'Name'; $string['needphp55orhigher'] = 'PHP Version 5.5 or higher is needed'; $string['nomockhandler'] = 'The Opencast Api Object is unable to handle the responses for testing purposes.'; diff --git a/tests/behat/behat_tool_opencast.php b/tests/behat/behat_tool_opencast.php new file mode 100644 index 0000000..6843e4f --- /dev/null +++ b/tests/behat/behat_tool_opencast.php @@ -0,0 +1,83 @@ +. + +/** + * Behat steps definitions for tool opencast. + * + * @package tool_opencast + * @category test + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +use tool_opencast\seriesmapping; + +require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); + +/** + * Steps definitions related with the opencast tool API. + * + * @package tool_opencast + * @category test + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_tool_opencast extends behat_base { + + /** + * Setup of block block by creating series mapping. + * + * @Given /^I setup block plugin$/ + */ + public function i_setup_block_plugin() { + $courses = \core_course_category::search_courses(['search' => 'Course 1']); + + $mapping = new seriesmapping(); + $mapping->set('courseid', reset($courses)->id); + $mapping->set('series', '1234-1234-1234-1234-1234'); + $mapping->set('isdefault', '1'); + $mapping->set('ocinstanceid', 1); + $mapping->create(); + } + + /** + * adds a breakpoints in tool + * stops the execution until you hit enter in the console + * + * @Then /^breakpoint in tool/ + */ + public function breakpoint_in_tool() { + fwrite(STDOUT, "\033[s \033[93m[Breakpoint] Press \033[1;93m[RETURN]\033[0;93m to continue...\033[0m"); + while (fgets(STDIN, 1024) == '') { + continue; + } + fwrite(STDOUT, "\033[u"); + return; + } + + /** + * Adds a step to make sure the block drawer keeps opened. + * + * @Given /^I make sure the block drawer keeps opened/ + */ + public function i_make_sure_the_block_drawer_keeps_opened() { + set_user_preference('behat_keep_drawer_closed', 0); + } +} diff --git a/tests/behat/tool_opencast_maintenace.feature b/tests/behat/tool_opencast_maintenace.feature new file mode 100644 index 0000000..9e1cf3f --- /dev/null +++ b/tests/behat/tool_opencast_maintenace.feature @@ -0,0 +1,111 @@ +@tool @tool_opencast +Feature: Configure and check maintenance + In order to configure and check the maintenance period + As an admin + I need to be able to set and configure the maintenance for each instance + And check if the maintenance is properly set and displayed. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following config values are set as admin: + | config | value | plugin | + | apiurl_1 | https://stable.opencast.org | tool_opencast | + | apipassword_1 | opencast | tool_opencast | + | apiusername_1 | admin | tool_opencast | + | ocinstances | [{"id":1,"name":"Default","isvisible":true,"isdefault":true}] | tool_opencast | + | limituploadjobs_1 | 0 | block_opencast | + | group_creation_1 | 0 | block_opencast | + | group_name_1 | Moodle_course_[COURSEID] | block_opencast | + | series_name_1 | Course_Series_[COURSEID] | block_opencast | + | enablechunkupload_1 | 0 | block_opencast | + | uploadworkflow_1 | schedule-and-upload | block_opencast | + | enableuploadwfconfigpanel_1 | 1 | block_opencast | + | alloweduploadwfconfigs_1 | straightToPublishing | block_opencast | + + @javascript + Scenario: As an admin I should be able to configure the maintenance for an instance + Given I log in as "admin" + When I navigate to "Plugins > Admin tools > Opencast API > Configuration" in site administration + Then "Enable" "option" should exist in the "#id_s_tool_opencast_maintenancemode_1" "css_element" + And "Read Only" "option" should exist in the "#id_s_tool_opencast_maintenancemode_1" "css_element" + And "Disable" "option" should exist in the "#id_s_tool_opencast_maintenancemode_1" "css_element" + And I set the field "Maintenance mode" to "Enable" + And I should see "Notification Level" + And "Warning" "option" should exist in the "#id_s_tool_opencast_maintenancemode_notification_level_1" "css_element" + And "Error" "option" should exist in the "#id_s_tool_opencast_maintenancemode_notification_level_1" "css_element" + And "Information" "option" should exist in the "#id_s_tool_opencast_maintenancemode_notification_level_1" "css_element" + And "Success" "option" should exist in the "#id_s_tool_opencast_maintenancemode_notification_level_1" "css_element" + And I set the field "Notification Level" to "Error" + And I set the field "Maintenance Message" to "Opencast Maintenance Notification" + And I click on "#id_s_tool_opencast_maintenancemode_startdate_1_enabled" "css_element" + And I select "00" from the "s_tool_opencast_maintenancemode_startdate_1[hour]" singleselect + And I select "00" from the "s_tool_opencast_maintenancemode_startdate_1[minute]" singleselect + And I click on "#id_s_tool_opencast_maintenancemode_enddate_1_enabled" "css_element" + And I select "23" from the "s_tool_opencast_maintenancemode_enddate_1[hour]" singleselect + And I select "55" from the "s_tool_opencast_maintenancemode_enddate_1[minute]" singleselect + When I press "Save changes" + Then I should see "Changes saved" + + @javascript + Scenario: As an admin I should not be able to configure the maintenance in the past + Given I log in as "admin" + When I navigate to "Plugins > Admin tools > Opencast API > Configuration" in site administration + Then I set the field "Maintenance mode" to "Enable" + And I click on "#id_s_tool_opencast_maintenancemode_enddate_1_enabled" "css_element" + And I select "00" from the "s_tool_opencast_maintenancemode_enddate_1[hour]" singleselect + And I select "00" from the "s_tool_opencast_maintenancemode_enddate_1[minute]" singleselect + When I press "Save changes" + Then I should not see "Changes saved" + And I should see "This field should not be in the past!" in the "#admin-maintenancemode_enddate_1" "css_element" + + @javascript + Scenario: As an admin I should not be able to configure the false maintenance start date and end date + Given I log in as "admin" + When I navigate to "Plugins > Admin tools > Opencast API > Configuration" in site administration + Then I set the field "Maintenance mode" to "Enable" + And I click on "#id_s_tool_opencast_maintenancemode_enddate_1_enabled" "css_element" + And I select "23" from the "s_tool_opencast_maintenancemode_enddate_1[hour]" singleselect + And I select "55" from the "s_tool_opencast_maintenancemode_enddate_1[minute]" singleselect + And I click on "#id_s_tool_opencast_maintenancemode_startdate_1_enabled" "css_element" + And I select "23" from the "s_tool_opencast_maintenancemode_startdate_1[hour]" singleselect + And I select "55" from the "s_tool_opencast_maintenancemode_startdate_1[minute]" singleselect + When I press "Save changes" + Then I should not see "Changes saved" + And I should see "This field should be before \"Maintenance ends at\"" in the "#admin-maintenancemode_startdate_1" "css_element" + And I should see "This field should be after \"Maintenance starts at\"" in the "#admin-maintenancemode_enddate_1" "css_element" + + @javascript + Scenario: Teachers should not be able to access the Opencast plugin during maintenance period + Given I log in as "teacher1" + And I setup block plugin + And I make sure the block drawer keeps opened + And I am on "Course 1" course homepage with editing mode on + And I add the "Opencast Videos" block + And I wait "2" seconds + And I reload the page + And I should see "Opencast Videos" + And the following config values are set as admin: + | maintenancemode_1 | 2 | tool_opencast | + | maintenancemode_notification_level_1 | error | tool_opencast | + | maintenancemode_message_1 | Opencast Maintenance Notification | tool_opencast | + | maintenancemode_startdate_1 | {"enabled":false} | tool_opencast | + | maintenancemode_enddate_1 | {"enabled":false} | tool_opencast | + When I reload the page + And I wait "2" seconds + And I click on "Add video" "button" + Then I should see "Opencast Maintenance Notification" in the "#user-notifications" "css_element" + And I should not see "Videos available in this course" in the "#region-main" "css_element" + And the following config values are set as admin: + | maintenancemode_1 | 0 | tool_opencast | + And I reload the page + When I click on "Add video" "button" + Then I should not see "Opencast Maintenance Notification" in the "#user-notifications" "css_element" + And I should see "Opencast Videos" in the "#page-header" "css_element"