From 5efc1f9e645ae65595c75b777d2d0e0afece16d9 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Sun, 8 Apr 2018 18:04:59 +0200 Subject: [PATCH] MDL-61899 tool_dataprivacy: Subject access requests tool This commit is composed of different issues, most of them under MDL-61306: - MDL-61888, MDL-61785, MDL-61830, MDL-61874, MDL-59720, MDL-61486, MDL-61697, MDL-61798, MDL-61685, MDL-61682, MDL-61684, MDL-61618, MDL-61681, MDL-61680, MDL-61487, MDL-61499, MDL-61500, MDL-61362 and MDL-59718 Contributions to this patch by: - Jun Pataleta - David Monllao - Andrew Nicols - Adrian B. Greeve - Jake Dallimore - Sara Arjona --- .../dataprivacy/amd/build/add_category.min.js | 1 + .../dataprivacy/amd/build/add_purpose.min.js | 1 + .../amd/build/categoriesactions.min.js | 1 + .../amd/build/data_deletion.min.js | 1 + .../amd/build/data_registry.min.js | 1 + .../amd/build/data_request_modal.min.js | 1 + .../build/effective_retention_period.min.js | 1 + .../tool/dataprivacy/amd/build/events.min.js | 1 + .../amd/build/form-user-selector.min.js | 1 + .../amd/build/myrequestactions.min.js | 1 + .../amd/build/purposesactions.min.js | 1 + .../amd/build/requestactions.min.js | 1 + .../tool/dataprivacy/amd/src/add_category.js | 172 +++ admin/tool/dataprivacy/amd/src/add_purpose.js | 173 +++ .../dataprivacy/amd/src/categoriesactions.js | 129 ++ .../tool/dataprivacy/amd/src/data_deletion.js | 156 +++ .../tool/dataprivacy/amd/src/data_registry.js | 317 +++++ .../dataprivacy/amd/src/data_request_modal.js | 93 ++ .../amd/src/effective_retention_period.js | 89 ++ admin/tool/dataprivacy/amd/src/events.js | 30 + .../dataprivacy/amd/src/form-user-selector.js | 76 ++ .../dataprivacy/amd/src/myrequestactions.js | 221 ++++ .../dataprivacy/amd/src/purposesactions.js | 129 ++ .../dataprivacy/amd/src/requestactions.js | 227 ++++ admin/tool/dataprivacy/categories.php | 39 + admin/tool/dataprivacy/classes/api.php | 756 ++++++++++++ admin/tool/dataprivacy/classes/category.php | 91 ++ .../dataprivacy/classes/context_instance.php | 116 ++ .../tool/dataprivacy/classes/contextlevel.php | 140 +++ .../dataprivacy/classes/data_registry.php | 356 ++++++ .../tool/dataprivacy/classes/data_request.php | 93 ++ .../dataprivacy/classes/expired_context.php | 162 +++ .../classes/expired_contexts_manager.php | 132 ++ .../expired_course_related_contexts.php | 126 ++ .../classes/expired_user_contexts.php | 98 ++ admin/tool/dataprivacy/classes/external.php | 1037 ++++++++++++++++ .../classes/external/category_exporter.php | 56 + .../external/context_instance_exporter.php | 45 + .../external/contextlevel_exporter.php | 45 + .../external/data_request_exporter.php | 189 +++ .../classes/external/purpose_exporter.php | 98 ++ .../dataprivacy/classes/form/category.php | 67 ++ .../classes/form/context_instance.php | 215 ++++ .../dataprivacy/classes/form/contextlevel.php | 124 ++ .../dataprivacy/classes/form/defaults.php | 78 ++ .../tool/dataprivacy/classes/form/purpose.php | 113 ++ .../dataprivacy/classes/output/categories.php | 88 ++ .../classes/output/crud_element.php | 92 ++ .../classes/output/data_deletion_page.php | 97 ++ .../classes/output/data_registry_page.php | 483 ++++++++ .../classes/output/data_requests_page.php | 115 ++ .../classes/output/expired_contexts_table.php | 284 +++++ .../classes/output/my_data_requests_page.php | 124 ++ .../dataprivacy/classes/output/purposes.php | 88 ++ .../dataprivacy/classes/output/renderer.php | 129 ++ .../tool/dataprivacy/classes/page_helper.php | 85 ++ .../dataprivacy/classes/privacy/provider.php | 92 ++ admin/tool/dataprivacy/classes/purpose.php | 167 +++ .../classes/task/delete_expired_contexts.php | 69 ++ .../classes/task/expired_retention_period.php | 68 ++ .../task/initiate_data_request_task.php | 95 ++ .../task/process_data_request_task.php | 207 ++++ admin/tool/dataprivacy/createdatarequest.php | 84 ++ .../dataprivacy/createdatarequest_form.php | 140 +++ admin/tool/dataprivacy/datadeletion.php | 47 + admin/tool/dataprivacy/dataregistry.php | 42 + admin/tool/dataprivacy/datarequests.php | 42 + admin/tool/dataprivacy/db/access.php | 52 + admin/tool/dataprivacy/db/caches.php | 43 + admin/tool/dataprivacy/db/install.xml | 108 ++ admin/tool/dataprivacy/db/messages.php | 44 + admin/tool/dataprivacy/db/services.php | 166 +++ admin/tool/dataprivacy/db/tasks.php | 46 + admin/tool/dataprivacy/db/upgrade.php | 44 + admin/tool/dataprivacy/defaults.php | 76 ++ admin/tool/dataprivacy/editcategory.php | 58 + admin/tool/dataprivacy/editpurpose.php | 59 + .../dataprivacy/lang/en/tool_dataprivacy.php | 171 +++ admin/tool/dataprivacy/lib.php | 197 +++ admin/tool/dataprivacy/mydatarequests.php | 63 + admin/tool/dataprivacy/purposes.php | 39 + admin/tool/dataprivacy/settings.php | 72 ++ admin/tool/dataprivacy/styles.css | 14 + .../dataprivacy/templates/categories.mustache | 88 ++ .../templates/contact_dpo.mustache | 58 + .../templates/context_tree_branches.mustache | 38 + .../templates/context_tree_node.mustache | 50 + .../templates/data_deletion.mustache | 88 ++ .../templates/data_registry.mustache | 61 + .../templates/data_request_email.mustache | 105 ++ .../templates/data_request_modal.mustache | 40 + .../data_request_results_email.mustache | 54 + .../templates/data_requests.mustache | 152 +++ .../form-user-selector-suggestion.mustache | 41 + .../templates/my_data_requests.mustache | 130 ++ .../dataprivacy/templates/purposes.mustache | 106 ++ .../templates/request_details.mustache | 85 ++ admin/tool/dataprivacy/tests/api_test.php | 1060 +++++++++++++++++ .../tests/expired_contexts_test.php | 209 ++++ .../tool/dataprivacy/tests/external_test.php | 285 +++++ admin/tool/dataprivacy/version.php | 29 + 101 files changed, 12569 insertions(+) create mode 100644 admin/tool/dataprivacy/amd/build/add_category.min.js create mode 100644 admin/tool/dataprivacy/amd/build/add_purpose.min.js create mode 100644 admin/tool/dataprivacy/amd/build/categoriesactions.min.js create mode 100644 admin/tool/dataprivacy/amd/build/data_deletion.min.js create mode 100644 admin/tool/dataprivacy/amd/build/data_registry.min.js create mode 100644 admin/tool/dataprivacy/amd/build/data_request_modal.min.js create mode 100644 admin/tool/dataprivacy/amd/build/effective_retention_period.min.js create mode 100644 admin/tool/dataprivacy/amd/build/events.min.js create mode 100644 admin/tool/dataprivacy/amd/build/form-user-selector.min.js create mode 100644 admin/tool/dataprivacy/amd/build/myrequestactions.min.js create mode 100644 admin/tool/dataprivacy/amd/build/purposesactions.min.js create mode 100644 admin/tool/dataprivacy/amd/build/requestactions.min.js create mode 100644 admin/tool/dataprivacy/amd/src/add_category.js create mode 100644 admin/tool/dataprivacy/amd/src/add_purpose.js create mode 100644 admin/tool/dataprivacy/amd/src/categoriesactions.js create mode 100644 admin/tool/dataprivacy/amd/src/data_deletion.js create mode 100644 admin/tool/dataprivacy/amd/src/data_registry.js create mode 100644 admin/tool/dataprivacy/amd/src/data_request_modal.js create mode 100644 admin/tool/dataprivacy/amd/src/effective_retention_period.js create mode 100644 admin/tool/dataprivacy/amd/src/events.js create mode 100644 admin/tool/dataprivacy/amd/src/form-user-selector.js create mode 100644 admin/tool/dataprivacy/amd/src/myrequestactions.js create mode 100644 admin/tool/dataprivacy/amd/src/purposesactions.js create mode 100644 admin/tool/dataprivacy/amd/src/requestactions.js create mode 100644 admin/tool/dataprivacy/categories.php create mode 100644 admin/tool/dataprivacy/classes/api.php create mode 100644 admin/tool/dataprivacy/classes/category.php create mode 100644 admin/tool/dataprivacy/classes/context_instance.php create mode 100644 admin/tool/dataprivacy/classes/contextlevel.php create mode 100644 admin/tool/dataprivacy/classes/data_registry.php create mode 100644 admin/tool/dataprivacy/classes/data_request.php create mode 100644 admin/tool/dataprivacy/classes/expired_context.php create mode 100644 admin/tool/dataprivacy/classes/expired_contexts_manager.php create mode 100644 admin/tool/dataprivacy/classes/expired_course_related_contexts.php create mode 100644 admin/tool/dataprivacy/classes/expired_user_contexts.php create mode 100644 admin/tool/dataprivacy/classes/external.php create mode 100644 admin/tool/dataprivacy/classes/external/category_exporter.php create mode 100644 admin/tool/dataprivacy/classes/external/context_instance_exporter.php create mode 100644 admin/tool/dataprivacy/classes/external/contextlevel_exporter.php create mode 100644 admin/tool/dataprivacy/classes/external/data_request_exporter.php create mode 100644 admin/tool/dataprivacy/classes/external/purpose_exporter.php create mode 100644 admin/tool/dataprivacy/classes/form/category.php create mode 100644 admin/tool/dataprivacy/classes/form/context_instance.php create mode 100644 admin/tool/dataprivacy/classes/form/contextlevel.php create mode 100644 admin/tool/dataprivacy/classes/form/defaults.php create mode 100644 admin/tool/dataprivacy/classes/form/purpose.php create mode 100644 admin/tool/dataprivacy/classes/output/categories.php create mode 100644 admin/tool/dataprivacy/classes/output/crud_element.php create mode 100644 admin/tool/dataprivacy/classes/output/data_deletion_page.php create mode 100644 admin/tool/dataprivacy/classes/output/data_registry_page.php create mode 100644 admin/tool/dataprivacy/classes/output/data_requests_page.php create mode 100644 admin/tool/dataprivacy/classes/output/expired_contexts_table.php create mode 100644 admin/tool/dataprivacy/classes/output/my_data_requests_page.php create mode 100644 admin/tool/dataprivacy/classes/output/purposes.php create mode 100644 admin/tool/dataprivacy/classes/output/renderer.php create mode 100644 admin/tool/dataprivacy/classes/page_helper.php create mode 100644 admin/tool/dataprivacy/classes/privacy/provider.php create mode 100644 admin/tool/dataprivacy/classes/purpose.php create mode 100644 admin/tool/dataprivacy/classes/task/delete_expired_contexts.php create mode 100644 admin/tool/dataprivacy/classes/task/expired_retention_period.php create mode 100644 admin/tool/dataprivacy/classes/task/initiate_data_request_task.php create mode 100644 admin/tool/dataprivacy/classes/task/process_data_request_task.php create mode 100644 admin/tool/dataprivacy/createdatarequest.php create mode 100644 admin/tool/dataprivacy/createdatarequest_form.php create mode 100644 admin/tool/dataprivacy/datadeletion.php create mode 100644 admin/tool/dataprivacy/dataregistry.php create mode 100644 admin/tool/dataprivacy/datarequests.php create mode 100644 admin/tool/dataprivacy/db/access.php create mode 100644 admin/tool/dataprivacy/db/caches.php create mode 100644 admin/tool/dataprivacy/db/install.xml create mode 100644 admin/tool/dataprivacy/db/messages.php create mode 100644 admin/tool/dataprivacy/db/services.php create mode 100644 admin/tool/dataprivacy/db/tasks.php create mode 100644 admin/tool/dataprivacy/db/upgrade.php create mode 100644 admin/tool/dataprivacy/defaults.php create mode 100644 admin/tool/dataprivacy/editcategory.php create mode 100644 admin/tool/dataprivacy/editpurpose.php create mode 100644 admin/tool/dataprivacy/lang/en/tool_dataprivacy.php create mode 100644 admin/tool/dataprivacy/lib.php create mode 100644 admin/tool/dataprivacy/mydatarequests.php create mode 100644 admin/tool/dataprivacy/purposes.php create mode 100644 admin/tool/dataprivacy/settings.php create mode 100644 admin/tool/dataprivacy/styles.css create mode 100644 admin/tool/dataprivacy/templates/categories.mustache create mode 100644 admin/tool/dataprivacy/templates/contact_dpo.mustache create mode 100644 admin/tool/dataprivacy/templates/context_tree_branches.mustache create mode 100644 admin/tool/dataprivacy/templates/context_tree_node.mustache create mode 100644 admin/tool/dataprivacy/templates/data_deletion.mustache create mode 100644 admin/tool/dataprivacy/templates/data_registry.mustache create mode 100644 admin/tool/dataprivacy/templates/data_request_email.mustache create mode 100644 admin/tool/dataprivacy/templates/data_request_modal.mustache create mode 100644 admin/tool/dataprivacy/templates/data_request_results_email.mustache create mode 100644 admin/tool/dataprivacy/templates/data_requests.mustache create mode 100644 admin/tool/dataprivacy/templates/form-user-selector-suggestion.mustache create mode 100644 admin/tool/dataprivacy/templates/my_data_requests.mustache create mode 100644 admin/tool/dataprivacy/templates/purposes.mustache create mode 100644 admin/tool/dataprivacy/templates/request_details.mustache create mode 100644 admin/tool/dataprivacy/tests/api_test.php create mode 100644 admin/tool/dataprivacy/tests/expired_contexts_test.php create mode 100644 admin/tool/dataprivacy/tests/external_test.php create mode 100644 admin/tool/dataprivacy/version.php diff --git a/admin/tool/dataprivacy/amd/build/add_category.min.js b/admin/tool/dataprivacy/amd/build/add_category.min.js new file mode 100644 index 00000000000..92cd4ee8455 --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/add_category.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","core/ajax","core/notification","core/modal_factory","core/modal_events","core/fragment"],function(a,b,c,d,e,f,g){var h={CATEGORY_LINK:'[data-add-element="category"]'},i=function(a){this.contextId=a;var c=[{key:"addcategory",component:"tool_dataprivacy"},{key:"save",component:"admin"}];this.strings=b.get_strings(c),this.registerEventListeners()};return i.prototype.contextId=0,i.prototype.strings=0,i.prototype.registerEventListeners=function(){var b=a(h.CATEGORY_LINK);b.on("click",function(){return this.strings.then(function(a){e.create({type:e.types.SAVE_CANCEL,title:a[0],body:""},b).done(function(b){this.setupFormModal(b,a[1])}.bind(this))}.bind(this)).fail(d.exception)}.bind(this))},i.prototype.getBody=function(a){var b=null;return"undefined"!=typeof a&&(b={jsonformdata:JSON.stringify(a)}),g.loadFragment("tool_dataprivacy","addcategory_form",this.contextId,b)},i.prototype.setupFormModal=function(a,b){a.setLarge(),a.setSaveButtonText(b),a.getRoot().on(f.hidden,this.destroy.bind(this)),a.setBody(this.getBody()),a.getRoot().on(f.save,this.submitForm.bind(this)),a.getRoot().on("submit","form",this.submitFormAjax.bind(this)),this.modal=a,a.show()},i.prototype.submitForm=function(a){a.preventDefault(),this.modal.getRoot().find("form").submit()},i.prototype.submitFormAjax=function(a){a.preventDefault();var b=this.modal.getRoot().find("form").serialize();c.call([{methodname:"tool_dataprivacy_create_category_form",args:{jsonformdata:JSON.stringify(b)},done:function(a){a.validationerrors?this.modal.setBody(this.getBody(b)):this.close()}.bind(this),fail:d.exception}])},i.prototype.close=function(){this.destroy(),document.location.reload()},i.prototype.destroy=function(){Y.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()}),this.modal.destroy()},i.prototype.removeListeners=function(){a(h.CATEGORY_LINK).off("click")},{getInstance:function(a){return new i(a)}}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/add_purpose.min.js b/admin/tool/dataprivacy/amd/build/add_purpose.min.js new file mode 100644 index 00000000000..dec27cc14aa --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/add_purpose.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","core/ajax","core/notification","core/modal_factory","core/modal_events","core/fragment"],function(a,b,c,d,e,f,g){var h={PURPOSE_LINK:'[data-add-element="purpose"]'},i=function(a){this.contextId=a;var c=[{key:"addpurpose",component:"tool_dataprivacy"},{key:"save",component:"admin"}];this.strings=b.get_strings(c),this.registerEventListeners()};return i.prototype.contextId=0,i.prototype.strings=0,i.prototype.registerEventListeners=function(){var b=a(h.PURPOSE_LINK);b.on("click",function(){return this.strings.then(function(a){e.create({type:e.types.SAVE_CANCEL,title:a[0],body:""},b).done(function(b){this.setupFormModal(b,a[1])}.bind(this))}.bind(this)).fail(d.exception)}.bind(this))},i.prototype.getBody=function(a){var b=null;return"undefined"!=typeof a&&(b={jsonformdata:JSON.stringify(a)}),g.loadFragment("tool_dataprivacy","addpurpose_form",this.contextId,b)},i.prototype.setupFormModal=function(a,b){a.setLarge(),a.setSaveButtonText(b),a.getRoot().on(f.hidden,this.destroy.bind(this)),a.setBody(this.getBody()),a.getRoot().on(f.save,this.submitForm.bind(this)),a.getRoot().on("submit","form",this.submitFormAjax.bind(this)),this.modal=a,a.show()},i.prototype.submitForm=function(a){a.preventDefault(),this.modal.getRoot().find("form").submit()},i.prototype.submitFormAjax=function(a){a.preventDefault();var b=this.modal.getRoot().find("form").serialize();c.call([{methodname:"tool_dataprivacy_create_purpose_form",args:{jsonformdata:JSON.stringify(b)},done:function(a){a.validationerrors?this.modal.setBody(this.getBody(b)):this.close()}.bind(this),fail:d.exception}])},i.prototype.close=function(){this.destroy(),document.location.reload()},i.prototype.destroy=function(){Y.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()}),this.modal.destroy()},i.prototype.removeListeners=function(){a(h.PURPOSE_LINK).off("click")},{getInstance:function(a){return new i(a)}}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/categoriesactions.min.js b/admin/tool/dataprivacy/amd/build/categoriesactions.min.js new file mode 100644 index 00000000000..66e3b0a458b --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/categoriesactions.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletecategory"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletecategory",component:"tool_dataprivacy",param:i},{key:"deletecategorytext",component:"tool_dataprivacy",param:i}];d.get_strings(j).then(function(d){var g=d[0],i=d[1];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(g),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_category",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-categoryid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/data_deletion.min.js b/admin/tool/dataprivacy/amd/build/data_deletion.min.js new file mode 100644 index 00000000000..5aa1e48e6a0 --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/data_deletion.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){function g(a){var g=[{key:"confirm",component:"moodle"},{key:"confirmcontextdeletion",component:"tool_dataprivacy"}],h="tool_dataprivacy_confirm_contexts_for_deletion",i="";d.get_strings(g).then(function(a){i=a[0];var b=a[1];return e.create({title:i,body:b,type:e.types.SAVE_CANCEL})}).then(function(d){return d.setSaveButtonText(i),d.getRoot().on(f.save,function(){var d={ids:a},e={methodname:h,args:d};b.call([e])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d}).done(function(a){a.show()}).fail(c.exception)}var h={MARK_FOR_DELETION:'[data-action="markfordeletion"]',SELECT_ALL:'[data-action="selectall"]'},i={SELECTCONTEXT:".selectcontext"},j=function(){this.registerEvents()};return j.prototype.registerEvents=function(){a(h.MARK_FOR_DELETION).click(function(b){b.preventDefault();var c=[];a(i.SELECTCONTEXT).each(function(){var b=a(this);b.is(":checked")&&c.push(b.val())}),g(c)}),a(h.SELECT_ALL).change(function(b){b.preventDefault();var c=a(this);c.is(":checked")?a(i.SELECTCONTEXT).attr("checked","checked"):a(i.SELECTCONTEXT).removeAttr("checked")})},j}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/data_registry.min.js b/admin/tool/dataprivacy/amd/build/data_registry.min.js new file mode 100644 index 00000000000..149a339686d --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/data_registry.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","core/ajax","core/notification","core/templates","core/modal_factory","core/modal_events","core/fragment","tool_dataprivacy/add_purpose","tool_dataprivacy/add_category"],function(a,b,c,d,e,f,g,h,i,j){var k={TREE_NODES:"[data-context-tree-node=1]",FORM_CONTAINER:"#context-form-container"},l=function(a,b,c){this.systemContextId=a,this.currentContextLevel=b,this.currentContextId=c,this.init()};return l.prototype.systemContextId=0,l.prototype.currentContextLevel=0,l.prototype.currentContextId=0,l.prototype.addpurpose=null,l.prototype.addcategory=null,l.prototype.init=function(){this.addpurpose=i.getInstance(this.systemContextId),this.addcategory=j.getInstance(this.systemContextId);var a=[{key:"changessaved",component:"moodle"},{key:"contextpurposecategorysaved",component:"tool_dataprivacy"},{key:"noblockstoload",component:"tool_dataprivacy"},{key:"noactivitiestoload",component:"tool_dataprivacy"},{key:"nocoursestoload",component:"tool_dataprivacy"}];this.strings=b.get_strings(a),this.registerEventListeners(),this.currentContextId?this.loadForm("context_form",[this.currentContextId],this.submitContextFormAjax.bind(this)):this.loadForm("contextlevel_form",[this.currentContextLevel],this.submitContextLevelFormAjax.bind(this))},l.prototype.registerEventListeners=function(){a(k.TREE_NODES).on("click",function(b){b.preventDefault();var c=a(b.currentTarget);a(k.TREE_NODES).removeClass("active"),c.addClass("active");var d=c.data("contextlevel"),e=c.data("contextid");if(d)window.history.pushState({},null,"?contextlevel="+d),this.addpurpose.removeListeners(),this.addcategory.removeListeners(),this.currentContextLevel=d,this.loadForm("contextlevel_form",[this.currentContextLevel],this.submitContextLevelFormAjax.bind(this));else if(e)window.history.pushState({},null,"?contextid="+e),this.addpurpose.removeListeners(),this.addcategory.removeListeners(),this.currentContextId=e,this.loadForm("context_form",[this.currentContextId],this.submitContextFormAjax.bind(this));else{var f=c.data("expandcontextid"),g=c.data("expandelement"),h=c.data("expanded");g&&(h?this.collapse(c):!c.data("loaded")&&f&&g?(c.find("> i").removeClass("fa-plus"),c.find("> i").addClass("fa-circle-o-notch fa-spin"),this.loadExtra(c,f,g)):this.expand(c))}}.bind(this))},l.prototype.removeListeners=function(){a(k.TREE_NODES).off("click")},l.prototype.loadForm=function(b,c,f){this.clearForm();var g=h.loadFragment("tool_dataprivacy",b,this.systemContextId,c);g.done(function(b,c){a(k.FORM_CONTAINER).html(b),e.runTemplateJS(c),this.addpurpose.registerEventListeners(),this.addcategory.registerEventListeners(),a(k.FORM_CONTAINER).on("submit","form",f)}.bind(this)).fail(d.exception)},l.prototype.clearForm=function(){Y.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()}),a(k.FORM_CONTAINER).off("submit","form")},l.prototype.submitForm=function(b){b.preventDefault(),a(k.FORM_CONTAINER).find("form").submit()},l.prototype.submitContextLevelFormAjax=function(a){this.submitFormAjax(a,"tool_dataprivacy_set_contextlevel_form")},l.prototype.submitContextFormAjax=function(a){this.submitFormAjax(a,"tool_dataprivacy_set_context_form")},l.prototype.submitFormAjax=function(b,e){b.preventDefault();var f=a(k.FORM_CONTAINER).find("form").serialize();return this.strings.then(function(a){c.call([{methodname:e,args:{jsonformdata:JSON.stringify(f)},done:function(){d.alert(a[0],a[1])},fail:d.exception}])}).fail(d.exception)},l.prototype.loadExtra=function(a,b,f){c.call([{methodname:"tool_dataprivacy_tree_extra_branches",args:{contextid:b,element:f},done:function(b){return 0==b.branches.length?void this.noElements(a,f):void e.render("tool_dataprivacy/context_tree_branches",b).then(function(b){a.after(b),this.removeListeners(),this.registerEventListeners(),this.expand(a),a.data("loaded",1)}.bind(this)).fail(d.exception)}.bind(this),fail:d.exception}])},l.prototype.noElements=function(a,b){a.data("expandcontextid",""),a.data("expandelement",""),this.strings.then(function(c){var d=2;"module"==b?d=3:"course"==b&&(d=4),a.text(c[d])}).fail(d.exception)},l.prototype.collapse=function(a){a.data("expanded",0),a.siblings("nav").addClass("hidden"),a.find("> i").removeClass("fa-minus"),a.find("> i").addClass("fa-plus")},l.prototype.expand=function(a){a.data("expanded",1),a.siblings("nav").removeClass("hidden"),a.find("> i").removeClass("fa-plus"),a.find("> i").removeClass("fa-circle-o-notch fa-spin"),a.find("> i").addClass("fa-minus")},{init:function(a,b,c){return new l(a,b,c)}}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/data_request_modal.min.js b/admin/tool/dataprivacy/amd/build/data_request_modal.min.js new file mode 100644 index 00000000000..7e9bda8ba60 --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/data_request_modal.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","tool_dataprivacy/events"],function(a,b,c,d,e,f){var g=!1,h={APPROVE_BUTTON:'[data-action="approve"]',DENY_BUTTON:'[data-action="deny"]'},i=function(a){d.call(this,a),this.getFooter().find(h.APPROVE_BUTTON).length||b.exception({message:"No approve button found"}),this.getFooter().find(h.DENY_BUTTON).length||b.exception({message:"No deny button found"})};return i.TYPE="tool_dataprivacy-data_request",i.prototype=Object.create(d.prototype),i.prototype.constructor=i,i.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this),this.getModal().on(c.events.activate,h.APPROVE_BUTTON,function(b,c){var d=a.Event(f.approve);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this)),this.getModal().on(c.events.activate,h.DENY_BUTTON,function(b,c){var d=a.Event(f.deny);this.getRoot().trigger(d,this),d.isDefaultPrevented()||(this.hide(),c.originalEvent.preventDefault())}.bind(this))},g||(e.register(i.TYPE,i,"tool_dataprivacy/data_request_modal"),g=!0),i}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js b/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js new file mode 100644 index 00000000000..fd43e12f671 --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){var b={PURPOSE_SELECT:"#id_purposeid",RETENTION_FIELD:".form-control-static"},c=function(a){this.purposeRetentionPeriods=a,this.registerEventListeners()},d=function(){a(b.PURPOSE_SELECT).off("change")};return c.prototype.purposeRetentionPeriods=[],c.prototype.registerEventListeners=function(){a(b.PURPOSE_SELECT).on("change",function(c){var d=a(c.currentTarget).val(),e=this.purposeRetentionPeriods[d];a(b.RETENTION_FIELD).each(function(){var b=a(this),c=b.siblings("#id_error_retention_current");c.length>0&&b.text(e)})}.bind(this))},{init:function(a){return d(),new c(a)}}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/events.min.js b/admin/tool/dataprivacy/amd/build/events.min.js new file mode 100644 index 00000000000..1d4e97399b5 --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/events.min.js @@ -0,0 +1 @@ +define([],function(){return{approve:"tool_dataprivacy-data_request:approve",deny:"tool_dataprivacy-data_request:deny"}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/form-user-selector.min.js b/admin/tool/dataprivacy/amd/build/form-user-selector.min.js new file mode 100644 index 00000000000..faa073f568c --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/form-user-selector.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/templates"],function(a,b,c){return{processResults:function(b,c){var d=[];return a.each(c,function(a,b){d.push({value:b.id,label:b._label})}),d},transport:function(d,e,f,g){var h;h=b.call([{methodname:"tool_dataprivacy_get_users",args:{query:e}}]),h[0].then(function(b){var d=[],e=0;return a.each(b,function(a,b){d.push(c.render("tool_dataprivacy/form-user-selector-suggestion",b))}),a.when.apply(a.when,d).then(function(){var c=arguments;a.each(b,function(a,b){b._label=c[e],e++}),f(b)})}).fail(g)}}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js new file mode 100644 index 00000000000..937367c6fe9 --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates"],function(a,b,c,d,e,f,g){function h(a){var e={methodname:"tool_dataprivacy_contact_dpo",args:{message:a}},f="success";b.call([e])[0].then(function(a){return a.result?d.get_string("requestsubmitted","tool_dataprivacy"):(f="error",a.warnings.join("
"))}).done(function(a){c.addNotification({message:a,type:f})}).fail(c.exception)}var i={CANCEL_REQUEST:'[data-action="cancel"]',CONTACT_DPO:'[data-action="contactdpo"]'},j=function(){this.registerEvents()};return j.prototype.registerEvents=function(){a(i.CANCEL_REQUEST).click(function(g){g.preventDefault();var h=a(this).data("requestid"),i=[{key:"cancelrequest",component:"tool_dataprivacy"},{key:"cancelrequestconfirmation",component:"tool_dataprivacy"}];d.get_strings(i).then(function(a){var d=a[0],g=a[1];return e.create({title:d,body:g,type:e.types.SAVE_CANCEL}).then(function(a){return a.setSaveButtonText(d),a.getRoot().on(f.save,function(){var a={requestid:h},d={methodname:"tool_dataprivacy_cancel_data_request",args:a};b.call([d])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a})}).done(function(a){a.show()}).fail(c.exception)}),a(i.CONTACT_DPO).click(function(b){b.preventDefault();var i=a(this).data("replytoemail"),j=[{key:"contactdataprotectionofficer",component:"tool_dataprivacy"},{key:"send",component:"tool_dataprivacy"}],k="";d.get_strings(j).then(function(a){var b=a[0];k=a[1];var c={replytoemail:i};return e.create({title:b,body:g.render("tool_dataprivacy/contact_dpo",c),type:e.types.SAVE_CANCEL,large:!0})}).done(function(b){b.setSaveButtonText(k),b.getRoot().on(f.save,function(b){var c=a("#message").val().trim();0===c.length?(b.preventDefault(),a('[data-region="messageinput"]').addClass("has-danger notifyproblem"),a("#id_error_message").removeAttr("hidden")):h(c)}),b.getRoot().on(f.hidden,function(){b.destroy()}),b.show()}).fail(c.exception)})},{init:function(){return new j}}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/purposesactions.min.js b/admin/tool/dataprivacy/amd/build/purposesactions.min.js new file mode 100644 index 00000000000..0b159814b52 --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/purposesactions.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={DELETE:'[data-action="deletepurpose"]'},h=function(){this.registerEvents()};return h.prototype.registerEvents=function(){a(g.DELETE).click(function(g){g.preventDefault();var h=a(this).data("id"),i=a(this).data("name"),j=[{key:"deletepurpose",component:"tool_dataprivacy",param:i},{key:"deletepurposetext",component:"tool_dataprivacy",param:i}];d.get_strings(j).then(function(d){var g=d[0],i=d[1];return e.create({title:g,body:i,type:e.types.SAVE_CANCEL}).then(function(d){return d.setSaveButtonText(g),d.getRoot().on(f.save,function(){var d={methodname:"tool_dataprivacy_delete_purpose",args:{id:h}};b.call([d])[0].done(function(b){b.result?a('tr[data-purposeid="'+h+'"]').remove():c.addNotification({message:b.warnings[0].message,type:"error"})}).fail(c.exception)}),d.getRoot().on(f.hidden,function(){d.destroy()}),d})}).done(function(a){a.show()}).fail(c.exception)})},{init:function(){return new h}}}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/build/requestactions.min.js b/admin/tool/dataprivacy/amd/build/requestactions.min.js new file mode 100644 index 00000000000..586a2da2888 --- /dev/null +++ b/admin/tool/dataprivacy/amd/build/requestactions.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],function(a,b,c,d,e,f,g,h,i){function j(a,g){var h=[],j="";switch(a){case i.approve:h=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}],j="tool_dataprivacy_approve_data_request";break;case i.deny:h=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}],j="tool_dataprivacy_deny_data_request"}var k="";d.get_strings(h).then(function(a){k=a[0];var b=a[1];return e.create({title:k,body:b,type:e.types.SAVE_CANCEL})}).then(function(a){return a.setSaveButtonText(k),a.getRoot().on(f.save,function(){var a={requestid:g},d={methodname:j,args:a};b.call([d])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a}).done(function(a){a.show()}).fail(c.exception)}var k={APPROVE_REQUEST:'[data-action="approve"]',DENY_REQUEST:'[data-action="deny"]',VIEW_REQUEST:'[data-action="view"]'},l=function(){this.registerEvents()};return l.prototype.registerEvents=function(){a(k.VIEW_REQUEST).click(function(d){d.preventDefault();var k=a(this).data("requestid"),l={requestid:k},m={methodname:"tool_dataprivacy_get_data_request",args:l},n=b.call([m]),o="",p=e.types.DEFAULT;a.when(n[0]).then(function(a){return a.result?(2==a.result.status&&(p=h.TYPE),o=a.result.typename,g.render("tool_dataprivacy/request_details",a.result)):(c.addNotification({message:a.warnings[0].message,type:"error"}),!1)}).then(function(a){return e.create({title:o,body:a,type:p,large:!0}).then(function(a){return a.getRoot().on(i.approve,function(){j(i.approve,k)}),a.getRoot().on(i.deny,function(){j(i.deny,k)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a})}).done(function(a){a.show()}).fail(c.exception)}),a(k.APPROVE_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.approve,c)}),a(k.DENY_REQUEST).click(function(b){b.preventDefault();var c=a(this).data("requestid");j(i.deny,c)})},l}); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/src/add_category.js b/admin/tool/dataprivacy/amd/src/add_category.js new file mode 100644 index 00000000000..7ea22c89bb4 --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/add_category.js @@ -0,0 +1,172 @@ +// 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 . + +/** + * Module to add categories. + * + * @module tool_dataprivacy/add_category + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/fragment'], + function($, Str, Ajax, Notification, ModalFactory, ModalEvents, Fragment) { + + var SELECTORS = { + CATEGORY_LINK: '[data-add-element="category"]', + }; + + var AddCategory = function(contextId) { + this.contextId = contextId; + + var stringKeys = [ + { + key: 'addcategory', + component: 'tool_dataprivacy' + }, + { + key: 'save', + component: 'admin' + } + ]; + this.strings = Str.get_strings(stringKeys); + + this.registerEventListeners(); + }; + + /** + * @var {int} contextId + * @private + */ + AddCategory.prototype.contextId = 0; + + /** + * @var {Promise} + * @private + */ + AddCategory.prototype.strings = 0; + + AddCategory.prototype.registerEventListeners = function() { + + var trigger = $(SELECTORS.CATEGORY_LINK); + trigger.on('click', function() { + return this.strings.then(function(strings) { + ModalFactory.create({ + type: ModalFactory.types.SAVE_CANCEL, + title: strings[0], + body: '', + }, trigger).done(function(modal) { + this.setupFormModal(modal, strings[1]); + }.bind(this)); + }.bind(this)) + .fail(Notification.exception); + }.bind(this)); + + }; + + /** + * @method getBody + * @param {Object} formdata + * @private + * @return {Promise} + */ + AddCategory.prototype.getBody = function(formdata) { + + var params = null; + if (typeof formdata !== "undefined") { + params = {jsonformdata: JSON.stringify(formdata)}; + } + // Get the content of the modal. + return Fragment.loadFragment('tool_dataprivacy', 'addcategory_form', this.contextId, params); + }; + + AddCategory.prototype.setupFormModal = function(modal, saveText) { + modal.setLarge(); + + modal.setSaveButtonText(saveText); + + // We want to reset the form every time it is opened. + modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this)); + + modal.setBody(this.getBody()); + + // We catch the modal save event, and use it to submit the form inside the modal. + // Triggering a form submission will give JS validation scripts a chance to check for errors. + modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this)); + // We also catch the form submit event and use it to submit the form with ajax. + modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this)); + + this.modal = modal; + + modal.show(); + }; + + /** + * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed. + * + * @method submitForm + * @param {Event} e Form submission event. + * @private + */ + AddCategory.prototype.submitForm = function(e) { + e.preventDefault(); + this.modal.getRoot().find('form').submit(); + }; + + AddCategory.prototype.submitFormAjax = function(e) { + // We don't want to do a real form submission. + e.preventDefault(); + + // Convert all the form elements values to a serialised string. + var formData = this.modal.getRoot().find('form').serialize(); + + Ajax.call([{ + methodname: 'tool_dataprivacy_create_category_form', + args: {jsonformdata: JSON.stringify(formData)}, + done: function(data) { + if (data.validationerrors) { + this.modal.setBody(this.getBody(formData)); + } else { + this.close(); + } + }.bind(this), + fail: Notification.exception + }]); + }; + + AddCategory.prototype.close = function() { + this.destroy(); + document.location.reload(); + }; + + AddCategory.prototype.destroy = function() { + Y.use('moodle-core-formchangechecker', function() { + M.core_formchangechecker.reset_form_dirty_state(); + }); + this.modal.destroy(); + }; + + AddCategory.prototype.removeListeners = function() { + $(SELECTORS.CATEGORY_LINK).off('click'); + }; + + return /** @alias module:tool_dataprivacy/add_category */ { + getInstance: function(contextId) { + return new AddCategory(contextId); + } + }; + } +); + diff --git a/admin/tool/dataprivacy/amd/src/add_purpose.js b/admin/tool/dataprivacy/amd/src/add_purpose.js new file mode 100644 index 00000000000..f3c573cf86c --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/add_purpose.js @@ -0,0 +1,173 @@ +// 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 . + +/** + * Module to add purposes. + * + * @module tool_dataprivacy/add_purpose + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/fragment'], + function($, Str, Ajax, Notification, ModalFactory, ModalEvents, Fragment) { + + var SELECTORS = { + PURPOSE_LINK: '[data-add-element="purpose"]', + }; + + var AddPurpose = function(contextId) { + this.contextId = contextId; + + var stringKeys = [ + { + key: 'addpurpose', + component: 'tool_dataprivacy' + }, + { + key: 'save', + component: 'admin' + } + ]; + this.strings = Str.get_strings(stringKeys); + + this.registerEventListeners(); + }; + + /** + * @var {int} contextId + * @private + */ + AddPurpose.prototype.contextId = 0; + + /** + * @var {Promise} + * @private + */ + AddPurpose.prototype.strings = 0; + + AddPurpose.prototype.registerEventListeners = function() { + + var trigger = $(SELECTORS.PURPOSE_LINK); + trigger.on('click', function() { + return this.strings.then(function(strings) { + ModalFactory.create({ + type: ModalFactory.types.SAVE_CANCEL, + title: strings[0], + body: '', + }, trigger).done(function(modal) { + this.setupFormModal(modal, strings[1]); + }.bind(this)); + }.bind(this)) + .fail(Notification.exception); + }.bind(this)); + + }; + + /** + * @method getBody + * @param {Object} formdata + * @private + * @return {Promise} + */ + AddPurpose.prototype.getBody = function(formdata) { + + var params = null; + if (typeof formdata !== "undefined") { + params = {jsonformdata: JSON.stringify(formdata)}; + } + // Get the content of the modal. + return Fragment.loadFragment('tool_dataprivacy', 'addpurpose_form', this.contextId, params); + }; + + AddPurpose.prototype.setupFormModal = function(modal, saveText) { + modal.setLarge(); + + modal.setSaveButtonText(saveText); + + // We want to reset the form every time it is opened. + modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this)); + + modal.setBody(this.getBody()); + + // We catch the modal save event, and use it to submit the form inside the modal. + // Triggering a form submission will give JS validation scripts a chance to check for errors. + modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this)); + // We also catch the form submit event and use it to submit the form with ajax. + modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this)); + + this.modal = modal; + + modal.show(); + }; + + /** + * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed. + * + * @method submitForm + * @param {Event} e Form submission event. + * @private + */ + AddPurpose.prototype.submitForm = function(e) { + e.preventDefault(); + this.modal.getRoot().find('form').submit(); + }; + + AddPurpose.prototype.submitFormAjax = function(e) { + // We don't want to do a real form submission. + e.preventDefault(); + + // Convert all the form elements values to a serialised string. + var formData = this.modal.getRoot().find('form').serialize(); + + Ajax.call([{ + methodname: 'tool_dataprivacy_create_purpose_form', + args: {jsonformdata: JSON.stringify(formData)}, + done: function(data) { + if (data.validationerrors) { + this.modal.setBody(this.getBody(formData)); + } else { + this.close(); + } + }.bind(this), + + fail: Notification.exception + }]); + }; + + AddPurpose.prototype.close = function() { + this.destroy(); + document.location.reload(); + }; + + AddPurpose.prototype.destroy = function() { + Y.use('moodle-core-formchangechecker', function() { + M.core_formchangechecker.reset_form_dirty_state(); + }); + this.modal.destroy(); + }; + + AddPurpose.prototype.removeListeners = function() { + $(SELECTORS.PURPOSE_LINK).off('click'); + }; + + return /** @alias module:tool_dataprivacy/add_purpose */ { + getInstance: function(contextId) { + return new AddPurpose(contextId); + } + }; + } +); + diff --git a/admin/tool/dataprivacy/amd/src/categoriesactions.js b/admin/tool/dataprivacy/amd/src/categoriesactions.js new file mode 100644 index 00000000000..c40a1a75c85 --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/categoriesactions.js @@ -0,0 +1,129 @@ +// 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 . + +/** + * AMD module for categories actions. + * + * @module tool_dataprivacy/categoriesactions + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([ + 'jquery', + 'core/ajax', + 'core/notification', + 'core/str', + 'core/modal_factory', + 'core/modal_events'], +function($, Ajax, Notification, Str, ModalFactory, ModalEvents) { + + /** + * List of action selectors. + * + * @type {{DELETE: string}} + */ + var ACTIONS = { + DELETE: '[data-action="deletecategory"]', + }; + + /** + * CategoriesActions class. + */ + var CategoriesActions = function() { + this.registerEvents(); + }; + + /** + * Register event listeners. + */ + CategoriesActions.prototype.registerEvents = function() { + $(ACTIONS.DELETE).click(function(e) { + e.preventDefault(); + + var id = $(this).data('id'); + var categoryname = $(this).data('name'); + var stringkeys = [ + { + key: 'deletecategory', + component: 'tool_dataprivacy', + param: categoryname + }, + { + key: 'deletecategorytext', + component: 'tool_dataprivacy', + param: categoryname + } + ]; + + Str.get_strings(stringkeys).then(function(langStrings) { + var title = langStrings[0]; + var confirmMessage = langStrings[1]; + return ModalFactory.create({ + title: title, + body: confirmMessage, + type: ModalFactory.types.SAVE_CANCEL + }).then(function(modal) { + modal.setSaveButtonText(title); + + // Handle save event. + modal.getRoot().on(ModalEvents.save, function() { + + var request = { + methodname: 'tool_dataprivacy_delete_category', + args: {'id': id} + }; + + Ajax.call([request])[0].done(function(data) { + if (data.result) { + $('tr[data-categoryid="' + id + '"]').remove(); + } else { + Notification.addNotification({ + message: data.warnings[0].message, + type: 'error' + }); + } + }).fail(Notification.exception); + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + + return modal; + }); + }).done(function(modal) { + modal.show(); + + }).fail(Notification.exception); + }); + }; + + return /** @alias module:tool_dataprivacy/categoriesactions */ { + // Public variables and functions. + + /** + * Initialise the module. + * + * @method init + * @return {CategoriesActions} + */ + 'init': function() { + return new CategoriesActions(); + } + }; +}); diff --git a/admin/tool/dataprivacy/amd/src/data_deletion.js b/admin/tool/dataprivacy/amd/src/data_deletion.js new file mode 100644 index 00000000000..e36af218370 --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/data_deletion.js @@ -0,0 +1,156 @@ +// 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 . + +/** + * Request actions. + * + * @module tool_dataprivacy/data_deletion + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([ + 'jquery', + 'core/ajax', + 'core/notification', + 'core/str', + 'core/modal_factory', + 'core/modal_events'], +function($, Ajax, Notification, Str, ModalFactory, ModalEvents) { + + /** + * List of action selectors. + * + * @type {{MARK_FOR_DELETION: string}} + * @type {{SELECT_ALL: string}} + */ + var ACTIONS = { + MARK_FOR_DELETION: '[data-action="markfordeletion"]', + SELECT_ALL: '[data-action="selectall"]', + }; + + /** + * List of selectors. + * + * @type {{SELECTCONTEXT: string}} + */ + var SELECTORS = { + SELECTCONTEXT: '.selectcontext', + }; + + /** + * DataDeletionActions class. + */ + var DataDeletionActions = function() { + this.registerEvents(); + }; + + /** + * Register event listeners. + */ + DataDeletionActions.prototype.registerEvents = function() { + $(ACTIONS.MARK_FOR_DELETION).click(function(e) { + e.preventDefault(); + + var selectedIds = []; + $(SELECTORS.SELECTCONTEXT).each(function() { + var checkbox = $(this); + if (checkbox.is(':checked')) { + selectedIds.push(checkbox.val()); + } + }); + showConfirmation(selectedIds); + }); + + $(ACTIONS.SELECT_ALL).change(function(e) { + e.preventDefault(); + + var selectallnone = $(this); + if (selectallnone.is(':checked')) { + $(SELECTORS.SELECTCONTEXT).attr('checked', 'checked'); + } else { + $(SELECTORS.SELECTCONTEXT).removeAttr('checked'); + } + }); + }; + + /** + * Show the confirmation dialogue. + * + * @param {Array} ids The array of expired context record IDs. + */ + function showConfirmation(ids) { + var keys = [ + { + key: 'confirm', + component: 'moodle' + }, + { + key: 'confirmcontextdeletion', + component: 'tool_dataprivacy' + } + ]; + var wsfunction = 'tool_dataprivacy_confirm_contexts_for_deletion'; + + var modalTitle = ''; + Str.get_strings(keys).then(function(langStrings) { + modalTitle = langStrings[0]; + var confirmMessage = langStrings[1]; + return ModalFactory.create({ + title: modalTitle, + body: confirmMessage, + type: ModalFactory.types.SAVE_CANCEL + }); + }).then(function(modal) { + modal.setSaveButtonText(modalTitle); + + // Handle save event. + modal.getRoot().on(ModalEvents.save, function() { + // Confirm the request. + var params = { + 'ids': ids + }; + + var request = { + methodname: wsfunction, + args: params + }; + + Ajax.call([request])[0].done(function(data) { + if (data.result) { + window.location.reload(); + } else { + Notification.addNotification({ + message: data.warnings[0].message, + type: 'error' + }); + } + }).fail(Notification.exception); + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + + return modal; + }).done(function(modal) { + modal.show(); + }).fail(Notification.exception); + } + + return DataDeletionActions; +}); diff --git a/admin/tool/dataprivacy/amd/src/data_registry.js b/admin/tool/dataprivacy/amd/src/data_registry.js new file mode 100644 index 00000000000..b06a0b1212c --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/data_registry.js @@ -0,0 +1,317 @@ +// 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 . + +/** + * Request actions. + * + * @module tool_dataprivacy/data_registry + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/templates', 'core/modal_factory', + 'core/modal_events', 'core/fragment', 'tool_dataprivacy/add_purpose', 'tool_dataprivacy/add_category'], + function($, Str, Ajax, Notification, Templates, ModalFactory, ModalEvents, Fragment, AddPurpose, AddCategory) { + + var SELECTORS = { + TREE_NODES: '[data-context-tree-node=1]', + FORM_CONTAINER: '#context-form-container', + }; + + var DataRegistry = function(systemContextId, initContextLevel, initContextId) { + this.systemContextId = systemContextId; + this.currentContextLevel = initContextLevel; + this.currentContextId = initContextId; + this.init(); + }; + + /** + * @var {int} systemContextId + * @private + */ + DataRegistry.prototype.systemContextId = 0; + + /** + * @var {int} currentContextLevel + * @private + */ + DataRegistry.prototype.currentContextLevel = 0; + + /** + * @var {int} currentContextId + * @private + */ + DataRegistry.prototype.currentContextId = 0; + + /** + * @var {AddPurpose} addpurpose + * @private + */ + DataRegistry.prototype.addpurpose = null; + + /** + * @var {AddCategory} addcategory + * @private + */ + DataRegistry.prototype.addcategory = null; + + DataRegistry.prototype.init = function() { + // Add purpose and category modals always at system context. + this.addpurpose = AddPurpose.getInstance(this.systemContextId); + this.addcategory = AddCategory.getInstance(this.systemContextId); + + var stringKeys = [ + { + key: 'changessaved', + component: 'moodle' + }, { + key: 'contextpurposecategorysaved', + component: 'tool_dataprivacy' + }, { + key: 'noblockstoload', + component: 'tool_dataprivacy' + }, { + key: 'noactivitiestoload', + component: 'tool_dataprivacy' + }, { + key: 'nocoursestoload', + component: 'tool_dataprivacy' + } + ]; + this.strings = Str.get_strings(stringKeys); + + this.registerEventListeners(); + + // Load the default context level form. + if (this.currentContextId) { + this.loadForm('context_form', [this.currentContextId], this.submitContextFormAjax.bind(this)); + } else { + this.loadForm('contextlevel_form', [this.currentContextLevel], this.submitContextLevelFormAjax.bind(this)); + } + }; + + DataRegistry.prototype.registerEventListeners = function() { + $(SELECTORS.TREE_NODES).on('click', function(ev) { + ev.preventDefault(); + + var trigger = $(ev.currentTarget); + + // Active node. + $(SELECTORS.TREE_NODES).removeClass('active'); + trigger.addClass('active'); + + var contextLevel = trigger.data('contextlevel'); + var contextId = trigger.data('contextid'); + if (contextLevel) { + // Context level level. + + window.history.pushState({}, null, '?contextlevel=' + contextLevel); + + // Remove previous add purpose and category listeners to avoid memory leaks. + this.addpurpose.removeListeners(); + this.addcategory.removeListeners(); + + // Load the context level form. + this.currentContextLevel = contextLevel; + this.loadForm('contextlevel_form', [this.currentContextLevel], this.submitContextLevelFormAjax.bind(this)); + } else if (contextId) { + // Context instance level. + + window.history.pushState({}, null, '?contextid=' + contextId); + + // Remove previous add purpose and category listeners to avoid memory leaks. + this.addpurpose.removeListeners(); + this.addcategory.removeListeners(); + + // Load the context level form. + this.currentContextId = contextId; + this.loadForm('context_form', [this.currentContextId], this.submitContextFormAjax.bind(this)); + } else { + // Expandable nodes. + + var expandContextId = trigger.data('expandcontextid'); + var expandElement = trigger.data('expandelement'); + var expanded = trigger.data('expanded'); + + // Extra checking that there is an expandElement because we remove it after loading 0 branches. + if (expandElement) { + + if (!expanded) { + if (trigger.data('loaded') || !expandContextId || !expandElement) { + this.expand(trigger); + } else { + + trigger.find('> i').removeClass('fa-plus'); + trigger.find('> i').addClass('fa-circle-o-notch fa-spin'); + this.loadExtra(trigger, expandContextId, expandElement); + } + } else { + this.collapse(trigger); + } + } + } + + }.bind(this)); + }; + + DataRegistry.prototype.removeListeners = function() { + $(SELECTORS.TREE_NODES).off('click'); + }; + + DataRegistry.prototype.loadForm = function(fragmentName, fragmentArgs, formSubmitCallback) { + + this.clearForm(); + + var fragment = Fragment.loadFragment('tool_dataprivacy', fragmentName, this.systemContextId, fragmentArgs); + fragment.done(function(html, js) { + + $(SELECTORS.FORM_CONTAINER).html(html); + Templates.runTemplateJS(js); + + this.addpurpose.registerEventListeners(); + this.addcategory.registerEventListeners(); + + // We also catch the form submit event and use it to submit the form with ajax. + $(SELECTORS.FORM_CONTAINER).on('submit', 'form', formSubmitCallback); + + }.bind(this)).fail(Notification.exception); + }; + + DataRegistry.prototype.clearForm = function() { + // For the previously loaded form. + Y.use('moodle-core-formchangechecker', function() { + M.core_formchangechecker.reset_form_dirty_state(); + }); + + // Remove previous listeners. + $(SELECTORS.FORM_CONTAINER).off('submit', 'form'); + }; + + /** + * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed. + * + * @method submitForm + * @param {Event} e Form submission event. + * @private + */ + DataRegistry.prototype.submitForm = function(e) { + e.preventDefault(); + $(SELECTORS.FORM_CONTAINER).find('form').submit(); + }; + + DataRegistry.prototype.submitContextLevelFormAjax = function(e) { + this.submitFormAjax(e, 'tool_dataprivacy_set_contextlevel_form'); + }; + + DataRegistry.prototype.submitContextFormAjax = function(e) { + this.submitFormAjax(e, 'tool_dataprivacy_set_context_form'); + }; + + DataRegistry.prototype.submitFormAjax = function(e, saveMethodName) { + // We don't want to do a real form submission. + e.preventDefault(); + + // Convert all the form elements values to a serialised string. + var formData = $(SELECTORS.FORM_CONTAINER).find('form').serialize(); + return this.strings.then(function(strings) { + Ajax.call([{ + methodname: saveMethodName, + args: {jsonformdata: JSON.stringify(formData)}, + done: function() { + Notification.alert(strings[0], strings[1]); + }, + fail: Notification.exception + }]); + }).fail(Notification.exception); + + }; + + DataRegistry.prototype.loadExtra = function(parentNode, expandContextId, expandElement) { + + Ajax.call([{ + methodname: 'tool_dataprivacy_tree_extra_branches', + args: { + contextid: expandContextId, + element: expandElement, + }, + done: function(data) { + if (data.branches.length == 0) { + this.noElements(parentNode, expandElement); + return; + } + Templates.render('tool_dataprivacy/context_tree_branches', data) + .then(function(html) { + parentNode.after(html); + this.removeListeners(); + this.registerEventListeners(); + this.expand(parentNode); + parentNode.data('loaded', 1); + return; + }.bind(this)) + .fail(Notification.exception); + }.bind(this), + fail: Notification.exception + }]); + }; + + DataRegistry.prototype.noElements = function(node, expandElement) { + node.data('expandcontextid', ''); + node.data('expandelement', ''); + this.strings.then(function(strings) { + + // 2 = blocks, 3 = activities, 4 = courses (although courses is not likely really). + var key = 2; + if (expandElement == 'module') { + key = 3; + } else if (expandElement == 'course') { + key = 4; + } + node.text(strings[key]); + return; + }).fail(Notification.exception); + }; + + DataRegistry.prototype.collapse = function(node) { + node.data('expanded', 0); + node.siblings('nav').addClass('hidden'); + node.find('> i').removeClass('fa-minus'); + node.find('> i').addClass('fa-plus'); + }; + + DataRegistry.prototype.expand = function(node) { + node.data('expanded', 1); + node.siblings('nav').removeClass('hidden'); + node.find('> i').removeClass('fa-plus'); + // Also remove the spinning one if data was just loaded. + node.find('> i').removeClass('fa-circle-o-notch fa-spin'); + node.find('> i').addClass('fa-minus'); + }; + return /** @alias module:tool_dataprivacy/data_registry */ { + + /** + * Initialise the page. + * + * @param {Number} systemContextId + * @param {Number} initContextLevel + * @param {Number} initContextId + * @return {DataRegistry} + */ + init: function(systemContextId, initContextLevel, initContextId) { + return new DataRegistry(systemContextId, initContextLevel, initContextId); + } + }; + } +); + diff --git a/admin/tool/dataprivacy/amd/src/data_request_modal.js b/admin/tool/dataprivacy/amd/src/data_request_modal.js new file mode 100644 index 00000000000..abf717b526b --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/data_request_modal.js @@ -0,0 +1,93 @@ +// 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 . + +/** + * Request actions. + * + * @module tool_dataprivacy/data_request_modal + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry', + 'tool_dataprivacy/events'], + function($, Notification, CustomEvents, Modal, ModalRegistry, DataPrivacyEvents) { + + var registered = false; + var SELECTORS = { + APPROVE_BUTTON: '[data-action="approve"]', + DENY_BUTTON: '[data-action="deny"]', + }; + + /** + * Constructor for the Modal. + * + * @param {object} root The root jQuery element for the modal + */ + var ModalDataRequest = function(root) { + Modal.call(this, root); + + if (!this.getFooter().find(SELECTORS.APPROVE_BUTTON).length) { + Notification.exception({message: 'No approve button found'}); + } + + if (!this.getFooter().find(SELECTORS.DENY_BUTTON).length) { + Notification.exception({message: 'No deny button found'}); + } + }; + + ModalDataRequest.TYPE = 'tool_dataprivacy-data_request'; + ModalDataRequest.prototype = Object.create(Modal.prototype); + ModalDataRequest.prototype.constructor = ModalDataRequest; + + /** + * Set up all of the event handling for the modal. + * + * @method registerEventListeners + */ + ModalDataRequest.prototype.registerEventListeners = function() { + // Apply parent event listeners. + Modal.prototype.registerEventListeners.call(this); + + this.getModal().on(CustomEvents.events.activate, SELECTORS.APPROVE_BUTTON, function(e, data) { + var approveEvent = $.Event(DataPrivacyEvents.approve); + this.getRoot().trigger(approveEvent, this); + + if (!approveEvent.isDefaultPrevented()) { + this.hide(); + data.originalEvent.preventDefault(); + } + }.bind(this)); + + this.getModal().on(CustomEvents.events.activate, SELECTORS.DENY_BUTTON, function(e, data) { + var denyEvent = $.Event(DataPrivacyEvents.deny); + this.getRoot().trigger(denyEvent, this); + + if (!denyEvent.isDefaultPrevented()) { + this.hide(); + data.originalEvent.preventDefault(); + } + }.bind(this)); + }; + + // Automatically register with the modal registry the first time this module is imported so that you can create modals + // of this type using the modal factory. + if (!registered) { + ModalRegistry.register(ModalDataRequest.TYPE, ModalDataRequest, 'tool_dataprivacy/data_request_modal'); + registered = true; + } + + return ModalDataRequest; + }); \ No newline at end of file diff --git a/admin/tool/dataprivacy/amd/src/effective_retention_period.js b/admin/tool/dataprivacy/amd/src/effective_retention_period.js new file mode 100644 index 00000000000..c73fb05e884 --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/effective_retention_period.js @@ -0,0 +1,89 @@ +// 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 . + +/** + * Module to update the displayed retention period. + * + * @module tool_dataprivacy/effective_retention_period + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery'], + function($) { + + var SELECTORS = { + PURPOSE_SELECT: '#id_purposeid', + RETENTION_FIELD: '.form-control-static', + }; + + /** + * Constructor for the retention period display. + * + * @param {Array} purposeRetentionPeriods Associative array of purposeids with effective retention period at this context + */ + var EffectiveRetentionPeriod = function(purposeRetentionPeriods) { + this.purposeRetentionPeriods = purposeRetentionPeriods; + this.registerEventListeners(); + }; + + /** + * Removes the current 'change' listeners. + * + * Useful when a new form is loaded. + */ + var removeListeners = function() { + $(SELECTORS.PURPOSE_SELECT).off('change'); + }; + + /** + * @var {Array} purposeRetentionPeriods + * @private + */ + EffectiveRetentionPeriod.prototype.purposeRetentionPeriods = []; + + /** + * Add purpose change listeners. + * + * @method registerEventListeners + */ + EffectiveRetentionPeriod.prototype.registerEventListeners = function() { + + $(SELECTORS.PURPOSE_SELECT).on('change', function(ev) { + var selected = $(ev.currentTarget).val(); + var selectedPurpose = this.purposeRetentionPeriods[selected]; + + $(SELECTORS.RETENTION_FIELD).each(function() { + var node = $(this); + // Sucky way to select the text, but no id for static fields :(. + var retentionField = node.siblings('#id_error_retention_current'); + if (retentionField.length > 0) { + node.text(selectedPurpose); + } + }); + + }.bind(this)); + }; + + return /** @alias module:tool_dataprivacy/effective_retention_period */ { + init: function(purposeRetentionPeriods) { + // Remove previously attached listeners. + removeListeners(); + return new EffectiveRetentionPeriod(purposeRetentionPeriods); + } + }; + } +); + diff --git a/admin/tool/dataprivacy/amd/src/events.js b/admin/tool/dataprivacy/amd/src/events.js new file mode 100644 index 00000000000..9398dc45398 --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/events.js @@ -0,0 +1,30 @@ +// 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 . + +/** + * Contain the events the data privacy tool can fire. + * + * @module tool_dataprivacy/events + * @class events + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([], function() { + return { + approve: 'tool_dataprivacy-data_request:approve', + deny: 'tool_dataprivacy-data_request:deny', + }; +}); diff --git a/admin/tool/dataprivacy/amd/src/form-user-selector.js b/admin/tool/dataprivacy/amd/src/form-user-selector.js new file mode 100644 index 00000000000..18c297ad2aa --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/form-user-selector.js @@ -0,0 +1,76 @@ +// 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 . + +/** + * Potential user selector module. + * + * @module tool_dataprivacy/form-user-selector + * @class form-user-selector + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) { + + return /** @alias module:tool_dataprivacy/form-user-selector */ { + + processResults: function(selector, results) { + var users = []; + $.each(results, function(index, user) { + users.push({ + value: user.id, + label: user._label + }); + }); + return users; + }, + + transport: function(selector, query, success, failure) { + var promise; + + promise = Ajax.call([{ + methodname: 'tool_dataprivacy_get_users', + args: { + query: query + } + }]); + + promise[0].then(function(results) { + var promises = [], + i = 0; + + // Render the label. + $.each(results, function(index, user) { + promises.push(Templates.render('tool_dataprivacy/form-user-selector-suggestion', user)); + }); + + // Apply the label to the results. + return $.when.apply($.when, promises).then(function() { + var args = arguments; + $.each(results, function(index, user) { + user._label = args[i]; + i++; + }); + success(results); + return; + }); + + }).fail(failure); + } + + }; + +}); diff --git a/admin/tool/dataprivacy/amd/src/myrequestactions.js b/admin/tool/dataprivacy/amd/src/myrequestactions.js new file mode 100644 index 00000000000..5ec631ef1a9 --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/myrequestactions.js @@ -0,0 +1,221 @@ +// 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 . + +/** + * AMD module to enable users to manage their own data requests. + * + * @module tool_dataprivacy/myrequestactions + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([ + 'jquery', + 'core/ajax', + 'core/notification', + 'core/str', + 'core/modal_factory', + 'core/modal_events', + 'core/templates'], +function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) { + + /** + * List of action selectors. + * + * @type {{CANCEL_REQUEST: string}} + * @type {{CONTACT_DPO: string}} + */ + var ACTIONS = { + CANCEL_REQUEST: '[data-action="cancel"]', + CONTACT_DPO: '[data-action="contactdpo"]', + }; + + /** + * MyRequestActions class. + */ + var MyRequestActions = function() { + this.registerEvents(); + }; + + /** + * Register event listeners. + */ + MyRequestActions.prototype.registerEvents = function() { + $(ACTIONS.CANCEL_REQUEST).click(function(e) { + e.preventDefault(); + + var requestId = $(this).data('requestid'); + var stringkeys = [ + { + key: 'cancelrequest', + component: 'tool_dataprivacy' + }, + { + key: 'cancelrequestconfirmation', + component: 'tool_dataprivacy' + } + ]; + + Str.get_strings(stringkeys).then(function(langStrings) { + var title = langStrings[0]; + var confirmMessage = langStrings[1]; + return ModalFactory.create({ + title: title, + body: confirmMessage, + type: ModalFactory.types.SAVE_CANCEL + }).then(function(modal) { + modal.setSaveButtonText(title); + + // Handle save event. + modal.getRoot().on(ModalEvents.save, function() { + // Cancel the request. + var params = { + 'requestid': requestId + }; + + var request = { + methodname: 'tool_dataprivacy_cancel_data_request', + args: params + }; + + Ajax.call([request])[0].done(function(data) { + if (data.result) { + window.location.reload(); + } else { + Notification.addNotification({ + message: data.warnings[0].message, + type: 'error' + }); + } + }).fail(Notification.exception); + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + + return modal; + }); + }).done(function(modal) { + // Show the modal! + modal.show(); + + }).fail(Notification.exception); + }); + + $(ACTIONS.CONTACT_DPO).click(function(e) { + e.preventDefault(); + + var replyToEmail = $(this).data('replytoemail'); + + var keys = [ + { + key: 'contactdataprotectionofficer', + component: 'tool_dataprivacy' + }, + { + key: 'send', + component: 'tool_dataprivacy' + }, + ]; + + var sendButtonText = ''; + Str.get_strings(keys).then(function(langStrings) { + var modalTitle = langStrings[0]; + sendButtonText = langStrings[1]; + var context = { + 'replytoemail': replyToEmail + }; + return ModalFactory.create({ + title: modalTitle, + body: Templates.render('tool_dataprivacy/contact_dpo', context), + type: ModalFactory.types.SAVE_CANCEL, + large: true + }); + }).done(function(modal) { + modal.setSaveButtonText(sendButtonText); + + // Handle send event. + modal.getRoot().on(ModalEvents.save, function(e) { + var message = $('#message').val().trim(); + if (message.length === 0) { + e.preventDefault(); + // Show validation error when the message is empty. + $('[data-region="messageinput"]').addClass('has-danger notifyproblem'); + $('#id_error_message').removeAttr('hidden'); + } else { + // Send the message. + sendMessageToDPO(message); + } + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + + // Show the modal! + modal.show(); + }).fail(Notification.exception); + }); + }; + + /** + * Send message to the Data Protection Officer. + * + * @param {String} message The message to send. + */ + function sendMessageToDPO(message) { + var request = { + methodname: 'tool_dataprivacy_contact_dpo', + args: { + message: message + } + }; + + var requestType = 'success'; + Ajax.call([request])[0].then(function(data) { + if (data.result) { + return Str.get_string('requestsubmitted', 'tool_dataprivacy'); + } + requestType = 'error'; + return data.warnings.join('
'); + + }).done(function(message) { + Notification.addNotification({ + message: message, + type: requestType + }); + + }).fail(Notification.exception); + } + + return /** @alias module:tool_dataprivacy/myrequestactions */ { + // Public variables and functions. + + /** + * Initialise the unified user filter. + * + * @method init + * @return {MyRequestActions} + */ + 'init': function() { + return new MyRequestActions(); + } + }; +}); diff --git a/admin/tool/dataprivacy/amd/src/purposesactions.js b/admin/tool/dataprivacy/amd/src/purposesactions.js new file mode 100644 index 00000000000..fd92141b869 --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/purposesactions.js @@ -0,0 +1,129 @@ +// 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 . + +/** + * AMD module for purposes actions. + * + * @module tool_dataprivacy/purposesactions + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([ + 'jquery', + 'core/ajax', + 'core/notification', + 'core/str', + 'core/modal_factory', + 'core/modal_events'], +function($, Ajax, Notification, Str, ModalFactory, ModalEvents) { + + /** + * List of action selectors. + * + * @type {{DELETE: string}} + */ + var ACTIONS = { + DELETE: '[data-action="deletepurpose"]', + }; + + /** + * PurposesActions class. + */ + var PurposesActions = function() { + this.registerEvents(); + }; + + /** + * Register event listeners. + */ + PurposesActions.prototype.registerEvents = function() { + $(ACTIONS.DELETE).click(function(e) { + e.preventDefault(); + + var id = $(this).data('id'); + var purposename = $(this).data('name'); + var stringkeys = [ + { + key: 'deletepurpose', + component: 'tool_dataprivacy', + param: purposename + }, + { + key: 'deletepurposetext', + component: 'tool_dataprivacy', + param: purposename + } + ]; + + Str.get_strings(stringkeys).then(function(langStrings) { + var title = langStrings[0]; + var confirmMessage = langStrings[1]; + return ModalFactory.create({ + title: title, + body: confirmMessage, + type: ModalFactory.types.SAVE_CANCEL + }).then(function(modal) { + modal.setSaveButtonText(title); + + // Handle save event. + modal.getRoot().on(ModalEvents.save, function() { + + var request = { + methodname: 'tool_dataprivacy_delete_purpose', + args: {'id': id} + }; + + Ajax.call([request])[0].done(function(data) { + if (data.result) { + $('tr[data-purposeid="' + id + '"]').remove(); + } else { + Notification.addNotification({ + message: data.warnings[0].message, + type: 'error' + }); + } + }).fail(Notification.exception); + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + + return modal; + }); + }).done(function(modal) { + modal.show(); + + }).fail(Notification.exception); + }); + }; + + return /** @alias module:tool_dataprivacy/purposesactions */ { + // Public variables and functions. + + /** + * Initialise the module. + * + * @method init + * @return {PurposesActions} + */ + 'init': function() { + return new PurposesActions(); + } + }; +}); diff --git a/admin/tool/dataprivacy/amd/src/requestactions.js b/admin/tool/dataprivacy/amd/src/requestactions.js new file mode 100644 index 00000000000..c0941f8f93c --- /dev/null +++ b/admin/tool/dataprivacy/amd/src/requestactions.js @@ -0,0 +1,227 @@ +// 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 . + +/** + * Request actions. + * + * @module tool_dataprivacy/requestactions + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([ + 'jquery', + 'core/ajax', + 'core/notification', + 'core/str', + 'core/modal_factory', + 'core/modal_events', + 'core/templates', + 'tool_dataprivacy/data_request_modal', + 'tool_dataprivacy/events'], +function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, ModalDataRequest, DataPrivacyEvents) { + + /** + * List of action selectors. + * + * @type {{APPROVE_REQUEST: string}} + * @type {{DENY_REQUEST: string}} + * @type {{VIEW_REQUEST: string}} + */ + var ACTIONS = { + APPROVE_REQUEST: '[data-action="approve"]', + DENY_REQUEST: '[data-action="deny"]', + VIEW_REQUEST: '[data-action="view"]' + }; + + /** + * RequestActions class. + */ + var RequestActions = function() { + this.registerEvents(); + }; + + /** + * Register event listeners. + */ + RequestActions.prototype.registerEvents = function() { + $(ACTIONS.VIEW_REQUEST).click(function(e) { + e.preventDefault(); + + var requestId = $(this).data('requestid'); + + // Cancel the request. + var params = { + 'requestid': requestId + }; + + var request = { + methodname: 'tool_dataprivacy_get_data_request', + args: params + }; + + var promises = Ajax.call([request]); + var modalTitle = ''; + var modalType = ModalFactory.types.DEFAULT; + $.when(promises[0]).then(function(data) { + if (data.result) { + // Check if the status is awaiting approval. + if (data.result.status == 2) { + modalType = ModalDataRequest.TYPE; + } + modalTitle = data.result.typename; + return Templates.render('tool_dataprivacy/request_details', data.result); + } + // Fail. + Notification.addNotification({ + message: data.warnings[0].message, + type: 'error' + }); + return false; + + }).then(function(html) { + return ModalFactory.create({ + title: modalTitle, + body: html, + type: modalType, + large: true + }).then(function(modal) { + // Handle approve event. + modal.getRoot().on(DataPrivacyEvents.approve, function() { + showConfirmation(DataPrivacyEvents.approve, requestId); + }); + + // Handle deny event. + modal.getRoot().on(DataPrivacyEvents.deny, function() { + showConfirmation(DataPrivacyEvents.deny, requestId); + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + + return modal; + }); + }).done(function(modal) { + // Show the modal! + modal.show(); + }).fail(Notification.exception); + }); + + $(ACTIONS.APPROVE_REQUEST).click(function(e) { + e.preventDefault(); + + var requestId = $(this).data('requestid'); + showConfirmation(DataPrivacyEvents.approve, requestId); + }); + + $(ACTIONS.DENY_REQUEST).click(function(e) { + e.preventDefault(); + + var requestId = $(this).data('requestid'); + showConfirmation(DataPrivacyEvents.deny, requestId); + }); + }; + + /** + * Show the confirmation dialogue. + * + * @param {String} action The action name. + * @param {Number} requestId The request ID. + */ + function showConfirmation(action, requestId) { + var keys = []; + var wsfunction = ''; + switch (action) { + case DataPrivacyEvents.approve: + keys = [ + { + key: 'approverequest', + component: 'tool_dataprivacy' + }, + { + key: 'confirmapproval', + component: 'tool_dataprivacy' + } + ]; + wsfunction = 'tool_dataprivacy_approve_data_request'; + break; + case DataPrivacyEvents.deny: + keys = [ + { + key: 'denyrequest', + component: 'tool_dataprivacy' + }, + { + key: 'confirmdenial', + component: 'tool_dataprivacy' + } + ]; + wsfunction = 'tool_dataprivacy_deny_data_request'; + break; + } + + var modalTitle = ''; + Str.get_strings(keys).then(function(langStrings) { + modalTitle = langStrings[0]; + var confirmMessage = langStrings[1]; + return ModalFactory.create({ + title: modalTitle, + body: confirmMessage, + type: ModalFactory.types.SAVE_CANCEL + }); + }).then(function(modal) { + modal.setSaveButtonText(modalTitle); + + // Handle save event. + modal.getRoot().on(ModalEvents.save, function() { + // Confirm the request. + var params = { + 'requestid': requestId + }; + + var request = { + methodname: wsfunction, + args: params + }; + + Ajax.call([request])[0].done(function(data) { + if (data.result) { + window.location.reload(); + } else { + Notification.addNotification({ + message: data.warnings[0].message, + type: 'error' + }); + } + }).fail(Notification.exception); + }); + + // Handle hidden event. + modal.getRoot().on(ModalEvents.hidden, function() { + // Destroy when hidden. + modal.destroy(); + }); + + return modal; + }).done(function(modal) { + modal.show(); + }).fail(Notification.exception); + } + + return RequestActions; +}); diff --git a/admin/tool/dataprivacy/categories.php b/admin/tool/dataprivacy/categories.php new file mode 100644 index 00000000000..b160ff3fd61 --- /dev/null +++ b/admin/tool/dataprivacy/categories.php @@ -0,0 +1,39 @@ +. + +/** + * This page lets users manage categories. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +$url = new moodle_url("/admin/tool/dataprivacy/categories.php"); +$title = get_string('editcategories', 'tool_dataprivacy'); + +\tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); + +$output = $PAGE->get_renderer('tool_dataprivacy'); +echo $output->header(); + +$categories = \tool_dataprivacy\api::get_categories(); +$renderable = new \tool_dataprivacy\output\categories($categories); + +echo $output->render($renderable); +echo $output->footer(); diff --git a/admin/tool/dataprivacy/classes/api.php b/admin/tool/dataprivacy/classes/api.php new file mode 100644 index 00000000000..9fca0960243 --- /dev/null +++ b/admin/tool/dataprivacy/classes/api.php @@ -0,0 +1,756 @@ +. + +/** + * Class containing helper methods for processing data requests. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; + +use coding_exception; +use context_system; +use core\invalid_persistent_exception; +use core\message\message; +use core\task\manager; +use core_user; +use dml_exception; +use moodle_exception; +use moodle_url; +use required_capability_exception; +use stdClass; +use tool_dataprivacy\task\initiate_data_request_task; +use tool_dataprivacy\task\process_data_request_task; +use tool_dataprivacy\purpose; +use tool_dataprivacy\category; +use tool_dataprivacy\contextlevel; +use tool_dataprivacy\context_instance; +use tool_dataprivacy\data_registry; +use tool_dataprivacy\expired_context; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class containing helper methods for processing data requests. + * + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + + /** Data export request type. */ + const DATAREQUEST_TYPE_EXPORT = 1; + + /** Data deletion request type. */ + const DATAREQUEST_TYPE_DELETE = 2; + + /** Other request type. Usually of enquiries to the DPO. */ + const DATAREQUEST_TYPE_OTHERS = 3; + + /** Newly submitted and we haven't yet started finding out where they have data. */ + const DATAREQUEST_STATUS_PENDING = 0; + + /** Newly submitted and we have started to find the location of data. */ + const DATAREQUEST_STATUS_PREPROCESSING = 1; + + /** Metadata ready and awaiting review and approval by the Data Protection officer. */ + const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2; + + /** Request approved and will be processed soon. */ + const DATAREQUEST_STATUS_APPROVED = 3; + + /** The request is now being processed. */ + const DATAREQUEST_STATUS_PROCESSING = 4; + + /** Data request completed. */ + const DATAREQUEST_STATUS_COMPLETE = 5; + + /** Data request cancelled by the user. */ + const DATAREQUEST_STATUS_CANCELLED = 6; + + /** Data request rejected by the DPO. */ + const DATAREQUEST_STATUS_REJECTED = 7; + + /** + * Determines whether the user can contact the site's Data Protection Officer via Moodle. + * + * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled. + * @throws dml_exception + */ + public static function can_contact_dpo() { + return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1; + } + + /** + * Check's whether the current user has the capability to manage data requests. + * + * @param int $userid The user ID. + * @return bool + * @throws coding_exception + * @throws dml_exception + */ + public static function can_manage_data_requests($userid) { + $context = context_system::instance(); + + // A user can manage data requests if he/she has the site DPO role and has the capability to manage data requests. + return self::is_site_dpo($userid) && has_capability('tool/dataprivacy:managedatarequests', $context, $userid); + } + + /** + * Checks if the current user can manage the data registry at the provided id. + * + * @param int $contextid Fallback to system context id. + * @throws \required_capability_exception + * @return null + */ + public static function check_can_manage_data_registry($contextid = false) { + if ($contextid) { + $context = \context_helper::instance_by_id($contextid); + } else { + $context = \context_system::instance(); + } + + require_capability('tool/dataprivacy:managedataregistry', $context); + } + + /** + * Fetches the list of users with the Data Protection Officer role. + * + * @throws dml_exception + */ + public static function get_site_dpos() { + // Get role(s) that can manage data requests. + $dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles')); + + $dpos = []; + $context = context_system::instance(); + foreach ($dporoles as $roleid) { + if (empty($roleid)) { + continue; + } + // Fetch users that can manage data requests. + $dpos += get_role_users($roleid, $context, false, 'u.*'); + } + + // If the site has no data protection officer, defer to site admin(s). + if (empty($dpos)) { + $dpos = get_admins(); + } + return $dpos; + } + + /** + * Checks whether a given user is a site DPO. + * + * @param int $userid The user ID. + * @return bool + * @throws dml_exception + */ + public static function is_site_dpo($userid) { + $dpos = self::get_site_dpos(); + return array_key_exists($userid, $dpos); + } + + /** + * Lodges a data request and sends the request details to the site Data Protection Officer(s). + * + * @param int $foruser The user whom the request is being made for. + * @param int $type The request type. + * @param string $comments Request comments. + * @return data_request + * @throws invalid_persistent_exception + * @throws coding_exception + */ + public static function create_data_request($foruser, $type, $comments = '') { + global $USER; + + $datarequest = new data_request(); + // The user the request is being made for. + $datarequest->set('userid', $foruser); + // The user making the request. + $datarequest->set('requestedby', $USER->id); + // Set status. + $datarequest->set('status', self::DATAREQUEST_STATUS_PENDING); + // Set request type. + $datarequest->set('type', $type); + // Set request comments. + $datarequest->set('comments', $comments); + + // Store subject access request. + $datarequest->create(); + + // Fire an ad hoc task to initiate the data request process. + $task = new initiate_data_request_task(); + $task->set_custom_data(['requestid' => $datarequest->get('id')]); + manager::queue_adhoc_task($task, true); + + return $datarequest; + } + + /** + * Fetches the list of the data requests. + * + * If user ID is provided, it fetches the data requests for the user. + * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests. + * (e.g. Users with the Data Protection Officer roles) + * + * @param int $userid The User ID. + * @return data_request[] + * @throws dml_exception + */ + public static function get_data_requests($userid = 0) { + global $USER; + $results = []; + if ($userid) { + // Get the data requests for the user or data requests made by the user. + $select = "userid = :userid OR requestedby = :requestedby"; + $params = [ + 'userid' => $userid, + 'requestedby' => $userid + ]; + $results = data_request::get_records_select($select, $params, 'status DESC, timemodified DESC'); + } else { + // If the current user is one of the site's Data Protection Officers, then fetch all data requests. + if (self::is_site_dpo($USER->id)) { + $results = data_request::get_records(null, 'status DESC, timemodified DESC', ''); + } + } + + return $results; + } + + /** + * Checks whether there is already an existing pending/in-progress data request for a user for a given request type. + * + * @param int $userid The user ID. + * @param int $type The request type. + * @return bool + * @throws coding_exception + * @throws dml_exception + */ + public static function has_ongoing_request($userid, $type) { + global $DB; + + // Check if the user already has an incomplete data request of the same type. + $nonpendingstatuses = [ + self::DATAREQUEST_STATUS_COMPLETE, + self::DATAREQUEST_STATUS_CANCELLED, + self::DATAREQUEST_STATUS_REJECTED, + ]; + list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED); + $select = 'type = :type AND userid = :userid AND status NOT ' . $insql; + $params = array_merge([ + 'type' => $type, + 'userid' => $userid + ], $inparams); + + return data_request::record_exists_select($select, $params); + } + + /** + * Determines whether a request is active or not based on its status. + * + * @param int $status The request status. + * @return bool + */ + public static function is_active($status) { + // List of statuses which doesn't require any further processing. + $finalstatuses = [ + self::DATAREQUEST_STATUS_COMPLETE, + self::DATAREQUEST_STATUS_CANCELLED, + self::DATAREQUEST_STATUS_REJECTED, + ]; + + return !in_array($status, $finalstatuses); + } + + /** + * Cancels the data request for a given request ID. + * + * @param int $requestid The request identifier. + * @param int $status The request status. + * @param int $dpoid The user ID of the Data Protection Officer + * @return bool + * @throws invalid_persistent_exception + * @throws coding_exception + */ + public static function update_request_status($requestid, $status, $dpoid = 0) { + // Update the request. + $datarequest = new data_request($requestid); + $datarequest->set('status', $status); + if ($dpoid) { + $datarequest->set('dpo', $dpoid); + } + return $datarequest->update(); + } + + /** + * Fetches a request based on the request ID. + * + * @param int $requestid The request identifier + * @return data_request + */ + public static function get_request($requestid) { + return new data_request($requestid); + } + + /** + * Approves a data request based on the request ID. + * + * @param int $requestid The request identifier + * @return bool + * @throws coding_exception + * @throws dml_exception + * @throws invalid_persistent_exception + * @throws required_capability_exception + * @throws moodle_exception + */ + public static function approve_data_request($requestid) { + global $USER; + + // Check first whether the user can manage data requests. + if (!self::can_manage_data_requests($USER->id)) { + $context = context_system::instance(); + throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', ''); + } + + // Check if request is already awaiting for approval. + $request = new data_request($requestid); + if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) { + throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy'); + } + + // Update the status and the DPO. + $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id); + + // Fire an ad hoc task to initiate the data request process. + $task = new process_data_request_task(); + $task->set_custom_data(['requestid' => $requestid]); + if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) { + $task->set_userid($request->get('userid')); + } + manager::queue_adhoc_task($task, true); + + return $result; + } + + /** + * Rejects a data request based on the request ID. + * + * @param int $requestid The request identifier + * @return bool + * @throws coding_exception + * @throws dml_exception + * @throws invalid_persistent_exception + * @throws required_capability_exception + * @throws moodle_exception + */ + public static function deny_data_request($requestid) { + global $USER; + + if (!self::can_manage_data_requests($USER->id)) { + $context = context_system::instance(); + throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', ''); + } + + // Check if request is already awaiting for approval. + $request = new data_request($requestid); + if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) { + throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy'); + } + + // Update the status and the DPO. + return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id); + } + + /** + * Sends a message to the site's Data Protection Officer about a request. + * + * @param stdClass $dpo The DPO user record + * @param data_request $request The data request + * @return int|false + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + public static function notify_dpo($dpo, data_request $request) { + global $PAGE, $SITE; + + // Create message to send to the Data Protection Officer(s). + $typetext = null; + switch ($request->get('type')) { + case self::DATAREQUEST_TYPE_EXPORT: + $typetext = get_string('requesttypeexport', 'tool_dataprivacy'); + break; + case self::DATAREQUEST_TYPE_DELETE: + $typetext = get_string('requesttypedelete', 'tool_dataprivacy'); + break; + case self::DATAREQUEST_TYPE_OTHERS: + $typetext = get_string('requesttypeothers', 'tool_dataprivacy'); + break; + default: + throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy'); + } + $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext); + + $requestedby = core_user::get_user($request->get('requestedby')); + $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php'); + $message = new message(); + $message->courseid = $SITE->id; + $message->component = 'tool_dataprivacy'; + $message->name = 'contactdataprotectionofficer'; + $message->userfrom = $requestedby; + $message->replyto = $requestedby->email; + $message->replytoname = fullname($requestedby->email); + $message->subject = $subject; + $message->fullmessageformat = FORMAT_HTML; + $message->notification = 1; + $message->contexturl = $datarequestsurl; + $message->contexturlname = get_string('datarequests', 'tool_dataprivacy'); + + // Prepare the context data for the email message body. + $messagetextdata = [ + 'requestedby' => fullname($requestedby), + 'requesttype' => $typetext, + 'requestdate' => userdate($request->get('timecreated')), + 'requestcomments' => text_to_html($request->get('comments')), + 'datarequestsurl' => $datarequestsurl + ]; + $requestingfor = core_user::get_user($request->get('userid')); + if ($requestedby->id == $requestingfor->id) { + $messagetextdata['requestfor'] = $messagetextdata['requestedby']; + } else { + $messagetextdata['requestfor'] = fullname($requestingfor); + } + + $output = $PAGE->get_renderer('tool_dataprivacy'); + // Email the data request to the Data Protection Officer(s)/Admin(s). + $messagetextdata['dponame'] = fullname($dpo); + // Render message email body. + $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata); + $message->userto = $dpo; + $message->fullmessage = html_to_text($messagehtml); + $message->fullmessagehtml = $messagehtml; + + // Send message. + return message_send($message); + } + + /** + * Creates a new data purpose. + * + * @param stdClass $record + * @return \tool_dataprivacy\purpose. + */ + public static function create_purpose(stdClass $record) { + self::check_can_manage_data_registry(); + + $purpose = new purpose(0, $record); + $purpose->create(); + + return $purpose; + } + + /** + * Updates an existing data purpose. + * + * @param stdClass $record + * @return \tool_dataprivacy\purpose. + */ + public static function update_purpose(stdClass $record) { + self::check_can_manage_data_registry(); + + $purpose = new purpose($record->id); + $purpose->from_record($record); + + $result = $purpose->update(); + + return $purpose; + } + + /** + * Deletes a data purpose. + * + * @param int $id + * @return bool + */ + public static function delete_purpose($id) { + self::check_can_manage_data_registry(); + + $purpose = new purpose($id); + if ($purpose->is_used()) { + throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.'); + } + return $purpose->delete(); + } + + /** + * Get all system data purposes. + * + * @return \tool_dataprivacy\purpose[] + */ + public static function get_purposes() { + self::check_can_manage_data_registry(); + + return purpose::get_records([], 'name', 'ASC'); + } + + /** + * Creates a new data category. + * + * @param stdClass $record + * @return \tool_dataprivacy\category. + */ + public static function create_category(stdClass $record) { + self::check_can_manage_data_registry(); + + $category = new category(0, $record); + $category->create(); + + return $category; + } + + /** + * Updates an existing data category. + * + * @param stdClass $record + * @return \tool_dataprivacy\category. + */ + public static function update_category(stdClass $record) { + self::check_can_manage_data_registry(); + + $category = new category($record->id); + $category->from_record($record); + + $result = $category->update(); + + return $category; + } + + /** + * Deletes a data category. + * + * @param int $id + * @return bool + */ + public static function delete_category($id) { + self::check_can_manage_data_registry(); + + $category = new category($id); + if ($category->is_used()) { + throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.'); + } + return $category->delete(); + } + + /** + * Get all system data categories. + * + * @return \tool_dataprivacy\category[] + */ + public static function get_categories() { + self::check_can_manage_data_registry(); + + return category::get_records([], 'name', 'ASC'); + } + + /** + * Sets the context instance purpose and category. + * + * @param \stdClass $record + * @return \tool_dataprivacy\context_instance + */ + public static function set_context_instance($record) { + self::check_can_manage_data_registry($record->contextid); + + if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) { + // Update. + $instance->from_record($record); + + if (empty($record->purposeid) && empty($record->categoryid)) { + // We accept one of them to be null but we delete it if both are null. + self::unset_context_instance($instance); + return; + } + + } else { + // Add. + $instance = new context_instance(0, $record); + } + $instance->save(); + + return $instance; + } + + /** + * Unsets the context instance record. + * + * @param \tool_dataprivacy\context_instance $instance + * @return null + */ + public static function unset_context_instance(context_instance $instance) { + self::check_can_manage_data_registry($instance->get('contextid')); + $instance->delete(); + } + + /** + * Sets the context level purpose and category. + * + * @throws \coding_exception + * @param \stdClass $record + * @return contextlevel + */ + public static function set_contextlevel($record) { + global $DB; + + // Only manager at system level can set this. + self::check_can_manage_data_registry(); + + if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) { + throw new \coding_exception('Only context system and context user can set a contextlevel ' . + 'purpose and retention'); + } + + if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) { + // Update. + $contextlevel->from_record($record); + } else { + // Add. + $contextlevel = new contextlevel(0, $record); + } + $contextlevel->save(); + + // We sync with their defaults as we removed these options from the defaults page. + $classname = \context_helper::get_class_for_level($record->contextlevel); + list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname); + set_config($purposevar, $record->purposeid, 'tool_dataprivacy'); + set_config($categoryvar, $record->categoryid, 'tool_dataprivacy'); + + return $contextlevel; + } + + /** + * Returns the effective category given a context instance. + * + * @param \context $context + * @param int $forcedvalue Use this categoryid value as if this was this context instance category. + * @return category|false + */ + public static function get_effective_context_category(\context $context, $forcedvalue=false) { + self::check_can_manage_data_registry($context->id); + if (!data_registry::defaults_set()) { + return false; + } + + return data_registry::get_effective_context_value($context, 'category', $forcedvalue); + } + + /** + * Returns the effective purpose given a context instance. + * + * @param \context $context + * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose. + * @return purpose|false + */ + public static function get_effective_context_purpose(\context $context, $forcedvalue=false) { + self::check_can_manage_data_registry($context->id); + if (!data_registry::defaults_set()) { + return false; + } + + return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue); + } + + /** + * Returns the effective category given a context level. + * + * @param int $contextlevel + * @param int $forcedvalue Use this categoryid value as if this was this context level category. + * @return category|false + */ + public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) { + self::check_can_manage_data_registry(\context_system::instance()->id); + if (!data_registry::defaults_set()) { + return false; + } + + return data_registry::get_effective_contextlevel_value($contextlevel, 'category', $forcedvalue); + } + + /** + * Returns the effective purpose given a context level. + * + * @param int $contextlevel + * @param int $forcedvalue Use this purposeid value as if this was this context level purpose. + * @return purpose|false + */ + public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) { + self::check_can_manage_data_registry(\context_system::instance()->id); + if (!data_registry::defaults_set()) { + return false; + } + + return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue); + } + + /** + * Creates an expired context record for the provided context id. + * + * @param int $contextid + * @return \tool_dataprivacy\expired_context + */ + public static function create_expired_context($contextid) { + self::check_can_manage_data_registry(); + + $record = (object)[ + 'contextid' => $contextid, + 'status' => expired_context::STATUS_EXPIRED, + ]; + $expiredctx = new expired_context(0, $record); + $expiredctx->save(); + + return $expiredctx; + } + + /** + * Deletes an expired context record. + * + * @param int $id The tool_dataprivacy_ctxexpire id. + * @return bool True on success. + */ + public static function delete_expired_context($id) { + self::check_can_manage_data_registry(); + + $expiredcontext = new expired_context($id); + return $expiredcontext->delete(); + } + + /** + * Updates the status of an expired context. + * + * @param \tool_dataprivacy\expired_context $expiredctx + * @param int $status + * @return null + */ + public static function set_expired_context_status(expired_context $expiredctx, $status) { + self::check_can_manage_data_registry(); + + $expiredctx->set('status', $status); + $expiredctx->save(); + } +} diff --git a/admin/tool/dataprivacy/classes/category.php b/admin/tool/dataprivacy/classes/category.php new file mode 100644 index 00000000000..8f334d5a3a8 --- /dev/null +++ b/admin/tool/dataprivacy/classes/category.php @@ -0,0 +1,91 @@ +. + +/** + * Class for loading/storing data categories from the DB. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); + +/** + * Class for loading/storing data categories from the DB. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class category extends \core\persistent { + + /** + * Database table. + */ + const TABLE = 'tool_dataprivacy_category'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'name' => array( + 'type' => PARAM_TEXT, + 'description' => 'The category name.', + ), + 'description' => array( + 'type' => PARAM_RAW, + 'description' => 'The category description.', + 'null' => NULL_ALLOWED, + 'default' => '', + ), + 'descriptionformat' => array( + 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), + 'type' => PARAM_INT, + 'default' => FORMAT_HTML + ), + ); + } + + /** + * Is this category used?. + * + * @return null + */ + public function is_used() { + + if (\tool_dataprivacy\contextlevel::is_category_used($this->get('id')) || + \tool_dataprivacy\context_instance::is_category_used($this->get('id'))) { + return true; + } + + $pluginconfig = get_config('tool_dataprivacy'); + $levels = \context_helper::get_all_levels(); + foreach ($levels as $level => $classname) { + + list($unused, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname); + if (!empty($pluginconfig->{$categoryvar}) && $pluginconfig->{$categoryvar} == $this->get('id')) { + return true; + } + } + + return false; + } +} diff --git a/admin/tool/dataprivacy/classes/context_instance.php b/admin/tool/dataprivacy/classes/context_instance.php new file mode 100644 index 00000000000..cd7e4f90962 --- /dev/null +++ b/admin/tool/dataprivacy/classes/context_instance.php @@ -0,0 +1,116 @@ +. + +/** + * Class for loading/storing context instances data from the DB. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for loading/storing context instances data from the DB. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class context_instance extends \core\persistent { + + /** + * Database table. + */ + const TABLE = 'tool_dataprivacy_ctxinstance'; + + /** + * Not set value. + */ + const NOTSET = 0; + + /** + * Inherit value. + */ + const INHERIT = -1; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'contextid' => array( + 'type' => PARAM_INT, + 'description' => 'The context id.', + ), + 'purposeid' => array( + 'type' => PARAM_INT, + 'description' => 'The purpose id.', + 'null' => NULL_ALLOWED, + ), + 'categoryid' => array( + 'type' => PARAM_INT, + 'description' => 'The category id.', + 'null' => NULL_ALLOWED, + ), + ); + } + + /** + * Returns an instance by contextid. + * + * @param mixed $contextid + * @param mixed $exception + * @return null + */ + public static function get_record_by_contextid($contextid, $exception = true) { + global $DB; + + if (!$record = $DB->get_record(self::TABLE, array('contextid' => $contextid))) { + if (!$exception) { + return false; + } else { + throw new \dml_missing_record_exception(self::TABLE); + } + } + + return new static(0, $record); + } + + /** + * Is the provided purpose used by any context instance? + * + * @param int $purposeid + * @return bool + */ + public static function is_purpose_used($purposeid) { + global $DB; + return $DB->record_exists(self::TABLE, array('purposeid' => $purposeid)); + } + + /** + * Is the provided category used by any context instance? + * + * @param int $categoryid + * @return bool + */ + public static function is_category_used($categoryid) { + global $DB; + return $DB->record_exists(self::TABLE, array('categoryid' => $categoryid)); + } +} diff --git a/admin/tool/dataprivacy/classes/contextlevel.php b/admin/tool/dataprivacy/classes/contextlevel.php new file mode 100644 index 00000000000..27d29842cfa --- /dev/null +++ b/admin/tool/dataprivacy/classes/contextlevel.php @@ -0,0 +1,140 @@ +. + +/** + * Class for loading/storing context level data from the DB. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for loading/storing context level data from the DB. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contextlevel extends \core\persistent { + + /** + * Database table. + */ + const TABLE = 'tool_dataprivacy_ctxlevel'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'contextlevel' => array( + 'type' => PARAM_INT, + 'description' => 'The context level.', + ), + 'purposeid' => array( + 'type' => PARAM_INT, + 'description' => 'The purpose id.', + ), + 'categoryid' => array( + 'type' => PARAM_INT, + 'description' => 'The category id.', + ), + ); + } + + /** + * Returns an instance by contextlevel. + * + * @param mixed $contextlevel + * @param mixed $exception + * @return null + */ + public static function get_record_by_contextlevel($contextlevel, $exception = true) { + global $DB; + + $cache = \cache::make('tool_dataprivacy', 'contextlevel'); + if ($data = $cache->get($contextlevel)) { + return new static(0, $data); + } + + if (!$record = $DB->get_record(self::TABLE, array('contextlevel' => $contextlevel))) { + if (!$exception) { + return false; + } else { + throw new \dml_missing_record_exception(self::TABLE); + } + } + + return new static(0, $record); + } + + /** + * Is the provided purpose used by any contextlevel? + * + * @param int $purposeid + * @return bool + */ + public static function is_purpose_used($purposeid) { + global $DB; + return $DB->record_exists(self::TABLE, array('purposeid' => $purposeid)); + } + + /** + * Is the provided category used by any contextlevel? + * + * @param int $categoryid + * @return bool + */ + public static function is_category_used($categoryid) { + global $DB; + return $DB->record_exists(self::TABLE, array('categoryid' => $categoryid)); + } + + /** + * Adds the new record to the cache. + * + * @return null + */ + protected function after_create() { + $cache = \cache::make('tool_dataprivacy', 'contextlevel'); + $cache->set($this->get('contextlevel'), $this->to_record()); + } + + /** + * Updates the cache record. + * + * @param $result + * @return null + */ + protected function after_update($result) { + $cache = \cache::make('tool_dataprivacy', 'contextlevel'); + $cache->set($this->get('contextlevel'), $this->to_record()); + } + + /** + * Removes unnecessary stuff from db. + * + * @return null + */ + protected function before_delete() { + $cache = \cache::make('tool_dataprivacy', 'contextlevel'); + $cache->delete($this->get('contextlevel')); + } +} diff --git a/admin/tool/dataprivacy/classes/data_registry.php b/admin/tool/dataprivacy/classes/data_registry.php new file mode 100644 index 00000000000..28fad5248f3 --- /dev/null +++ b/admin/tool/dataprivacy/classes/data_registry.php @@ -0,0 +1,356 @@ +. + +/** + * Data registry business logic methods. Mostly internal stuff. + * + * All methods should be considered part of the internal tool_dataprivacy API + * unless something different is specified. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy; + +use coding_exception; +use tool_dataprivacy\purpose; +use tool_dataprivacy\category; +use tool_dataprivacy\contextlevel; +use tool_dataprivacy\context_instance; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/coursecatlib.php'); + +/** + * Data registry business logic methods. Mostly internal stuff. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_registry { + + /** + * @var array Inheritance between context levels. + */ + private static $contextlevelinheritance = [ + CONTEXT_USER => [CONTEXT_SYSTEM], + CONTEXT_COURSECAT => [CONTEXT_SYSTEM], + CONTEXT_COURSE => [CONTEXT_COURSECAT, CONTEXT_SYSTEM], + CONTEXT_MODULE => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM], + CONTEXT_BLOCK => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM], + ]; + + /** + * Returns purpose and category var names from a context class name + * + * @access private + * @param string $classname + * @return string[] + */ + public static function var_names_from_context($classname) { + return [ + $classname . '_purpose', + $classname . '_category', + ]; + } + + /** + * Returns the default purpose id and category id for the provided context level. + * + * The caller code is responsible of checking that $contextlevel is an integer. + * + * @access private + * @param int $contextlevel + * @return int|false[] + */ + public static function get_defaults($contextlevel) { + + $classname = \context_helper::get_class_for_level($contextlevel); + list($purposevar, $categoryvar) = self::var_names_from_context($classname); + + $purposeid = get_config('tool_dataprivacy', $purposevar); + $categoryid = get_config('tool_dataprivacy', $categoryvar); + + if (empty($purposeid)) { + $purposeid = false; + } + if (empty($categoryid)) { + $categoryid = false; + } + + return [$purposeid, $categoryid]; + } + + /** + * Are data registry defaults set? + * + * At least the system defaults need to be set. + * + * @access private + * @return bool + */ + public static function defaults_set() { + list($purposeid, $categoryid) = self::get_defaults(CONTEXT_SYSTEM); + if (empty($purposeid) || empty($categoryid)) { + return false; + } + return true; + } + + /** + * Returns all site categories that are visible to the current user. + * + * @access private + * @return \coursecat[] + */ + public static function get_site_categories() { + global $DB; + + if (method_exists('\coursecat', 'get_all')) { + $categories = \coursecat::get_all(['returnhidden' => true]); + } else { + // Fallback (to be removed once this gets integrated into master). + $ids = $DB->get_fieldset_select('course_categories', 'id', ''); + $categories = \coursecat::get_many($ids); + } + + foreach ($categories as $key => $category) { + if (!$category->is_uservisible()) { + unset($categories[$key]); + } + } + return $categories; + } + + /** + * Returns the roles assigned to the provided level. + * + * Important to note that it returns course-level assigned roles + * if the provided context level is below course. + * + * @access private + * @param \context $context + * @return array + */ + public static function get_subject_scope(\context $context) { + + if ($contextcourse = $context->get_course_context(false)) { + // Below course level we only look at course-assigned roles. + $roles = get_user_roles($contextcourse, 0, false); + } else { + $roles = get_user_roles($context, 0, false); + } + + return array_map(function($role) { + if ($role->name) { + return $role->name; + } else { + return $role->shortname; + } + }, $roles); + } + + /** + * Returns the effective value given a context instance + * + * @access private + * @param \context $context + * @param string $element 'category' or 'purpose' + * @param int|false $forcedvalue Use this value as if this was this context instance value. + * @return persistent|false It return a 'purpose' instance or a 'category' instance, depending on $element + */ + public static function get_effective_context_value(\context $context, $element, $forcedvalue=false) { + + if ($element !== 'purpose' && $element !== 'category') { + throw new coding_exception('Only \'purpose\' and \'category\' are supported.'); + } + $fieldname = $element . 'id'; + + if ($forcedvalue === false) { + $instance = context_instance::get_record_by_contextid($context->id, false); + + if (!$instance) { + // If the instance does not have a value defaults to not set, so we grab the context level default as its value. + $instancevalue = context_instance::NOTSET; + } else { + $instancevalue = $instance->get($fieldname); + } + } else { + $instancevalue = $forcedvalue; + } + + // Not set. + if ($instancevalue == context_instance::NOTSET) { + + // The effective value varies depending on the context level. + if ($context->contextlevel == CONTEXT_USER) { + // Use the context level value as we don't allow people to set specific instances values. + return self::get_effective_contextlevel_value($context->contextlevel, $element); + } else { + // Use the default context level value. + list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($context->contextlevel); + return self::get_element_instance($element, $$fieldname); + } + } + + // Specific value for this context instance. + if ($instancevalue != context_instance::INHERIT) { + return self::get_element_instance($element, $instancevalue); + } + + // This context is using inherited so let's return the parent effective value. + $parentcontext = $context->get_parent_context(); + if (!$parentcontext) { + return false; + } + + // The forced value should not be transmitted to parent contexts. + return self::get_effective_context_value($parentcontext, $element); + } + + /** + * Returns the effective value for a context level. + * + * Note that this is different from the effective default context level + * (see get_effective_default_contextlevel_purpose_and_category) as this is returning + * the value set in the data registry, not in the defaults page. + * + * @param int $contextlevel + * @param string $element 'category' or 'purpose' + * @param int $forcedvalue Use this value as if this was this context level purpose. + * @return \tool_dataprivacy\purpose|false + */ + public static function get_effective_contextlevel_value($contextlevel, $element, $forcedvalue = false) { + + if ($element !== 'purpose' && $element !== 'category') { + throw new coding_exception('Only \'purpose\' and \'category\' are supported.'); + } + $fieldname = $element . 'id'; + + if ($contextlevel != CONTEXT_SYSTEM && $contextlevel != CONTEXT_USER) { + throw new \coding_exception('Only context_system and context_user values can be retrieved, no other context levels ' . + 'have a purpose or a category.'); + } + + if ($forcedvalue === false) { + $instance = contextlevel::get_record_by_contextlevel($contextlevel, false); + if (!$instance) { + // If the context level does not have a value defaults to not set, so we grab the context level default as its value. + $instancevalue = context_instance::NOTSET; + } else { + $instancevalue = $instance->get($fieldname); + } + } else { + $instancevalue = $forcedvalue; + } + + // Not set -> Use the default context level value. + if ($instancevalue == context_instance::NOTSET) { + list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel); + return self::get_element_instance($element, $$fieldname); + } + + // Specific value for this context instance. + if ($instancevalue != context_instance::INHERIT) { + return self::get_element_instance($element, $instancevalue); + } + + if ($contextlevel == CONTEXT_SYSTEM) { + throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.'); + } + + // If we reach this point is that we are inheriting so get the parent context level and repeat. + $parentcontextlevel = reset(self::$contextlevelinheritance[$contextlevel]); + + // Forced value are intentionally not passed as the force value should only affect the immediate context level. + return self::get_effective_contextlevel_value($parentcontextlevel, $element); + } + + /** + * Returns the effective default purpose and category for a context level. + * + * @param int $contextlevel + * @param int $forcedpurposevalue Use this value as if this was this context level purpose. + * @param int $forcedcategoryvalue Use this value as if this was this context level category. + * @return int[] + */ + public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false, $forcedcategoryvalue = false) { + + list($purposeid, $categoryid) = self::get_defaults($contextlevel); + + // Honour forced values. + if ($forcedpurposevalue) { + $purposeid = $forcedpurposevalue; + } + if ($forcedcategoryvalue) { + $categoryid = $forcedcategoryvalue; + } + + // Not set == INHERIT for defaults. + if ($purposeid == context_instance::INHERIT || $purposeid == context_instance::NOTSET) { + $purposeid = false; + } + if ($categoryid == context_instance::INHERIT || $categoryid == context_instance::NOTSET) { + $categoryid = false; + } + + if ($contextlevel != CONTEXT_SYSTEM && ($purposeid === false || $categoryid === false)) { + foreach (self::$contextlevelinheritance[$contextlevel] as $parent) { + + list($parentpurposeid, $parentcategoryid) = self::get_defaults($parent); + // Not set == INHERIT for defaults. + if ($parentpurposeid == context_instance::INHERIT || $parentpurposeid == context_instance::NOTSET) { + $parentpurposeid = false; + } + if ($parentcategoryid == context_instance::INHERIT || $parentcategoryid == context_instance::NOTSET) { + $parentcategoryid = false; + } + + if ($purposeid === false && $parentpurposeid) { + $purposeid = $parentpurposeid; + } + + if ($categoryid === false && $parentcategoryid) { + $categoryid = $parentcategoryid; + } + } + } + + // They may still be false, but we return anyway. + return [$purposeid, $categoryid]; + } + + /** + * Returns an instance of the provided element. + * + * @throws \coding_exception + * @param string $element The element name 'purpose' or 'category' + * @param int $id The element id + * @return \core\persistent + */ + private static function get_element_instance($element, $id) { + + if ($element !== 'purpose' && $element !== 'category') { + throw new coding_exception('No other elements than purpose and category are allowed'); + } + + $classname = '\tool_dataprivacy\\' . $element; + return new $classname($id); + } +} diff --git a/admin/tool/dataprivacy/classes/data_request.php b/admin/tool/dataprivacy/classes/data_request.php new file mode 100644 index 00000000000..ffad4ffee33 --- /dev/null +++ b/admin/tool/dataprivacy/classes/data_request.php @@ -0,0 +1,93 @@ +. + +/** + * Class for loading/storing data requests from the DB. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing competencies from the DB. + * + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_request extends persistent { + + /** The table name this persistent object maps to. */ + const TABLE = 'tool_dataprivacy_request'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return [ + 'type' => [ + 'choices' => [ + api::DATAREQUEST_TYPE_EXPORT, + api::DATAREQUEST_TYPE_DELETE, + api::DATAREQUEST_TYPE_OTHERS, + ], + 'type' => PARAM_INT + ], + 'comments' => [ + 'type' => PARAM_TEXT, + 'default' => '' + ], + 'userid' => [ + 'default' => 0, + 'type' => PARAM_INT + ], + 'requestedby' => [ + 'default' => 0, + 'type' => PARAM_INT + ], + 'status' => [ + 'default' => api::DATAREQUEST_STATUS_PENDING, + 'choices' => [ + api::DATAREQUEST_STATUS_PENDING, + api::DATAREQUEST_STATUS_PREPROCESSING, + api::DATAREQUEST_STATUS_AWAITING_APPROVAL, + api::DATAREQUEST_STATUS_APPROVED, + api::DATAREQUEST_STATUS_PROCESSING, + api::DATAREQUEST_STATUS_COMPLETE, + api::DATAREQUEST_STATUS_CANCELLED, + api::DATAREQUEST_STATUS_REJECTED, + ], + 'type' => PARAM_INT + ], + 'dpo' => [ + 'default' => 0, + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED + ], + 'dpocomment' => [ + 'default' => '', + 'type' => PARAM_TEXT, + 'null' => NULL_ALLOWED + ], + ]; + } +} diff --git a/admin/tool/dataprivacy/classes/expired_context.php b/admin/tool/dataprivacy/classes/expired_context.php new file mode 100644 index 00000000000..30104388ba5 --- /dev/null +++ b/admin/tool/dataprivacy/classes/expired_context.php @@ -0,0 +1,162 @@ +. + +/** + * Class that represents an expired context. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; +use dml_exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class that represents an expired context. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class expired_context extends \core\persistent { + + /** + * Database table. + */ + const TABLE = 'tool_dataprivacy_ctxexpired'; + + /** + * Expired contexts with no delete action scheduled. + */ + const STATUS_EXPIRED = 0; + + /** + * Expired contexts approved for deletion. + */ + const STATUS_APPROVED = 1; + + /** + * Already processed expired contexts. + */ + const STATUS_CLEANED = 2; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'contextid' => array( + 'type' => PARAM_INT, + 'description' => 'The context id.', + ), + 'status' => array( + 'choices' => [ + self::STATUS_EXPIRED, + self::STATUS_APPROVED, + self::STATUS_CLEANED, + ], + 'type' => PARAM_INT, + 'description' => 'The deletion status of the context.', + ), + ); + } + + /** + * Returns expired_contexts instances that match the provided level and status. + * + * @param int $contextlevel The context level filter criterion. + * @param bool $status The expired context record's status. + * @param string $sort The sort column. Must match the column name in {tool_dataprivacy_ctxexpired} table + * @param int $offset The query offset. + * @param int $limit The query limit. + * @return expired_context[] + * @throws dml_exception + */ + public static function get_records_by_contextlevel($contextlevel = null, $status = false, $sort = 'timecreated', + $offset = 0, $limit = 0) { + global $DB; + + $sql = "SELECT expiredctx.* + FROM {" . self::TABLE . "} expiredctx + JOIN {context} ctx + ON ctx.id = expiredctx.contextid"; + $params = []; + $conditions = []; + + if (!empty($contextlevel)) { + $conditions[] = "ctx.contextlevel = :contextlevel"; + $params['contextlevel'] = intval($contextlevel); + } + + if ($status !== false) { + $conditions[] = "expiredctx.status = :status"; + $params['status'] = intval($status); + } + + if (!empty($conditions)) { + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } + $sql .= " ORDER BY expiredctx.{$sort}"; + + $records = $DB->get_records_sql($sql, $params, $offset, $limit); + + // We return class instances. + $instances = array(); + foreach ($records as $key => $record) { + $instances[$key] = new static(0, $record); + } + + return $instances; + } + + /** + * Returns the number of expired_contexts instances that match the provided level and status. + * + * @param int $contextlevel + * @param bool $status + * @return int + * @throws dml_exception + */ + public static function get_record_count_by_contextlevel($contextlevel = null, $status = false) { + global $DB; + + $sql = "SELECT COUNT(1) + FROM {" . self::TABLE . "} expiredctx + JOIN {context} ctx + ON ctx.id = expiredctx.contextid"; + + $conditions = []; + $params = []; + + if (!empty($contextlevel)) { + $conditions[] = "ctx.contextlevel = :contextlevel"; + $params['contextlevel'] = intval($contextlevel); + } + + if ($status !== false) { + $sql .= " AND expiredctx.status = :status"; + $params['status'] = intval($status); + } + if (!empty($conditions)) { + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } + + return $DB->count_records_sql($sql, $params); + } +} diff --git a/admin/tool/dataprivacy/classes/expired_contexts_manager.php b/admin/tool/dataprivacy/classes/expired_contexts_manager.php new file mode 100644 index 00000000000..3f7b879272e --- /dev/null +++ b/admin/tool/dataprivacy/classes/expired_contexts_manager.php @@ -0,0 +1,132 @@ +. + +/** + * Expired contexts manager. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; + +use tool_dataprivacy\api; +use tool_dataprivacy\purpose; +use tool_dataprivacy\context_instance; +use tool_dataprivacy\data_registry; +use tool_dataprivacy\expired_context; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Expired contexts manager. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class expired_contexts_manager { + + const DELETE_LIMIT = 200; + + /** + * Returns the list of expired context instances. + * + * @return \stdClass[] + */ + abstract protected function get_expired_contexts(); + + /** + * Flag expired contexts as expired. + * + * @return int The number of contexts flagged as expired. + */ + public function flag_expired() { + + if (!$this->check_requirements()) { + return 0; + } + + $contexts = $this->get_expired_contexts(); + foreach ($contexts as $context) { + api::create_expired_context($context->id); + } + + return count($contexts); + } + + /** + * Deletes the expired contexts. + * + * @return int The number of deleted contexts. + */ + public function delete() { + + $numprocessed = 0; + + if (!$this->check_requirements()) { + return $numprocessed; + } + + $privacymanager = new \core_privacy\manager(); + + $levels = [CONTEXT_USER, CONTEXT_MODULE, CONTEXT_BLOCK, CONTEXT_COURSE]; + foreach ($levels as $level) { + + $expiredcontexts = expired_context::get_records_by_contextlevel($level, expired_context::STATUS_APPROVED); + + foreach ($expiredcontexts as $expiredctx) { + + $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING); + if (!$context) { + api::delete_expired_context($expiredctx->get('contextid')); + continue; + } + + if (!PHPUNIT_TEST && !BEHAT_TEST) { + mtrace('Deleting context ' . $context->id . ' - ' . + shorten_text($context->get_context_name(true, true))); + } + + $privacymanager->delete_data_for_all_users_in_context($context); + + api::set_expired_context_status($expiredctx, expired_context::STATUS_CLEANED); + + $numprocessed += 1; + if ($numprocessed == self::DELETE_LIMIT) { + // Close the recordset. + $expiredcontexts->close(); + break 2; + } + } + } + + return $numprocessed; + } + + /** + * Check that the requirements to start deleting contexts are satisified. + * + * @return bool + */ + protected function check_requirements() { + api::check_can_manage_data_registry(\context_system::instance()->id); + + if (!data_registry::defaults_set()) { + return false; + } + return true; + } +} diff --git a/admin/tool/dataprivacy/classes/expired_course_related_contexts.php b/admin/tool/dataprivacy/classes/expired_course_related_contexts.php new file mode 100644 index 00000000000..24fc5b22c15 --- /dev/null +++ b/admin/tool/dataprivacy/classes/expired_course_related_contexts.php @@ -0,0 +1,126 @@ +. + +/** + * Expired contexts manager for CONTEXT_COURSE, CONTEXT_MODULE and CONTEXT_BLOCK. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; + +use tool_dataprivacy\purpose; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Expired contexts manager for CONTEXT_COURSE, CONTEXT_MODULE and CONTEXT_BLOCK. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class expired_course_related_contexts extends \tool_dataprivacy\expired_contexts_manager { + + /** + * Returns a recordset with user context instances that are possibly expired (to be confirmed by get_recordset_callback). + * + * @return \stdClass[] + */ + protected function get_expired_contexts() { + global $DB; + + // Including context info + course end date + purposeid (this last one only if defined). + $fields = 'ctx.id AS id, ctxcourse.enddate AS courseenddate, dpctx.purposeid AS purposeid, ' . + \context_helper::get_preload_record_columns_sql('ctx'); + + // We want all contexts at course-dependant levels. + $parentpath = $DB->sql_concat('ctxcourse.path', "'/%'"); + + // This SQL query returns all course-dependant contexts (including the course context) + // which course end date already passed. + $sql = "SELECT $fields + FROM {context} ctx + JOIN ( + SELECT c.enddate, subctx.path + FROM {context} subctx + JOIN {course} c + ON subctx.contextlevel = ? AND subctx.instanceid = c.id + WHERE c.enddate < ? AND c.enddate > 0 + ) ctxcourse + ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path + LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx + ON dpctx.contextid = ctx.id + LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx + ON ctx.id = expiredctx.contextid + WHERE expiredctx.id IS NULL + ORDER BY ctx.contextlevel DESC, ctx.path"; + $possiblyexpired = $DB->get_recordset_sql($sql, [CONTEXT_COURSE, time()]); + + $expiredcontexts = []; + $excludedcontextids = []; + foreach ($possiblyexpired as $record) { + + \context_helper::preload_from_record($record); + + // No strict checking as the context may already be deleted (e.g. we just deleted a course, + // module contexts below it will not exist). + $context = \context::instance_by_id($record->id, false); + if (!$context) { + continue; + } + + // We pass the value we just got from SQL so get_effective_context_purpose don't need to query + // the db again to retrieve it. If there is no tool_dataprovider_ctxinstance record + // $record->purposeid will be null which is ok as it would force get_effective_context_purpose + // to return the default purpose for the context context level (no db queries involved). + $purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET; + + // It should be cheap as system purposes and context level purposes will be retrieved from a cache most of the time. + $purpose = api::get_effective_context_purpose($context, $purposevalue); + + $dt = new \DateTime(); + $dt->setTimestamp($record->courseenddate); + $di = new \DateInterval($purpose->get('retentionperiod')); + $dt->add($di); + + if (time() < $dt->getTimestamp()) { + // Exclude this context ID as it has not reached the retention period yet. + $excludedcontextids[] = $context->id; + continue; + } + + // Check if this context has children that have not yet expired. + $hasunexpiredchildren = false; + $children = $context->get_child_contexts(); + foreach ($children as $child) { + if (in_array($child->id, $excludedcontextids)) { + $hasunexpiredchildren = true; + break; + } + } + if ($hasunexpiredchildren) { + // Exclude this context ID as it has children that have not yet expired. + $excludedcontextids[] = $context->id; + continue; + } + + $expiredcontexts[$context->id] = $context; + } + + return $expiredcontexts; + } +} diff --git a/admin/tool/dataprivacy/classes/expired_user_contexts.php b/admin/tool/dataprivacy/classes/expired_user_contexts.php new file mode 100644 index 00000000000..a29fc5dd7c6 --- /dev/null +++ b/admin/tool/dataprivacy/classes/expired_user_contexts.php @@ -0,0 +1,98 @@ +. + +/** + * Expired contexts manager for CONTEXT_USER. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; + +use tool_dataprivacy\purpose; +use tool_dataprivacy\context_instance; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Expired contexts manager for CONTEXT_USER. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class expired_user_contexts extends \tool_dataprivacy\expired_contexts_manager { + + /** + * Returns the user context instances that are expired. + * + * @return \stdClass[] + */ + protected function get_expired_contexts() { + global $DB; + + // Including context info + last login timestamp. + $fields = 'ctx.id AS id, ' . \context_helper::get_preload_record_columns_sql('ctx'); + + $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER); + + // Calculate what is considered expired according to the context level effective purpose (= now + retention period). + $expiredtime = new \DateTime(); + $retention = new \DateInterval($purpose->get('retentionperiod')); + $expiredtime->sub($retention); + + $sql = "SELECT $fields FROM {context} ctx + JOIN {user} u ON ctx.contextlevel = ? AND ctx.instanceid = u.id + LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid + WHERE u.lastaccess <= ? AND u.lastaccess > 0 AND expiredctx.id IS NULL + ORDER BY ctx.path, ctx.contextlevel ASC"; + $possiblyexpired = $DB->get_recordset_sql($sql, [CONTEXT_USER, $expiredtime->getTimestamp()]); + + $expiredcontexts = []; + foreach ($possiblyexpired as $record) { + + \context_helper::preload_from_record($record); + + // No strict checking as the context may already be deleted (e.g. we just deleted a course, + // module contexts below it will not exist). + $context = \context::instance_by_id($record->id, false); + if (!$context) { + continue; + } + + if (is_siteadmin($context->instanceid)) { + continue; + } + + $courses = enrol_get_users_courses($context->instanceid, false, ['enddate']); + foreach ($courses as $course) { + if (!$course->enddate) { + // We can not know it what is going on here, so we prefer to be conservative. + continue 2; + } + + if ($course->enddate >= time()) { + // Future or ongoing course. + continue 2; + } + } + + $expiredcontexts[$context->id] = $context; + } + + return $expiredcontexts; + } +} diff --git a/admin/tool/dataprivacy/classes/external.php b/admin/tool/dataprivacy/classes/external.php new file mode 100644 index 00000000000..4e5a50a4685 --- /dev/null +++ b/admin/tool/dataprivacy/classes/external.php @@ -0,0 +1,1037 @@ +. +/** + * Class containing the external API functions functions for the Data Privacy tool. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/externallib.php"); +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); + +use coding_exception; +use context_helper; +use context_system; +use context_user; +use core\invalid_persistent_exception; +use core_user; +use dml_exception; +use external_api; +use external_description; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use external_warnings; +use invalid_parameter_exception; +use moodle_exception; +use required_capability_exception; +use restricted_context_exception; +use tool_dataprivacy\external\category_exporter; +use tool_dataprivacy\external\data_request_exporter; +use tool_dataprivacy\external\purpose_exporter; +use tool_dataprivacy\output\data_registry_page; + +/** + * Class external. + * + * The external API for the Data Privacy tool. + * + * @copyright 2017 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external extends external_api { + + /** + * Parameter description for cancel_data_request(). + * + * @return external_function_parameters + */ + public static function cancel_data_request_parameters() { + return new external_function_parameters([ + 'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) + ]); + } + + /** + * Cancel a data request. + * + * @param int $requestid The request ID. + * @return array + * @throws invalid_persistent_exception + * @throws coding_exception + * @throws invalid_parameter_exception + * @throws restricted_context_exception + */ + public static function cancel_data_request($requestid) { + global $USER; + + $warnings = []; + $params = external_api::validate_parameters(self::cancel_data_request_parameters(), [ + 'requestid' => $requestid + ]); + $requestid = $params['requestid']; + + // Validate context. + $context = context_user::instance($USER->id); + self::validate_context($context); + + // Ensure the request exists. + $select = 'id = :id AND requestedby = :requestedby'; + $params = ['id' => $requestid, 'requestedby' => $USER->id]; + $requestexists = data_request::record_exists_select($select, $params); + + $result = false; + if ($requestexists) { + // TODO: Do we want a request to be non-cancellable past a certain point? E.g. When it's already approved/processing. + $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED); + } else { + $warnings[] = [ + 'item' => $requestid, + 'warningcode' => 'errorrequestnotfound', + 'message' => get_string('errorrequestnotfound', 'tool_dataprivacy') + ]; + } + + return [ + 'result' => $result, + 'warnings' => $warnings + ]; + } + + /** + * Parameter description for cancel_data_request(). + * + * @return external_description + */ + public static function cancel_data_request_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'The processing result'), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for contact_dpo(). + * + * @return external_function_parameters + */ + public static function contact_dpo_parameters() { + return new external_function_parameters([ + 'message' => new external_value(PARAM_TEXT, 'The user\'s message to the Data Protection Officer(s)', VALUE_REQUIRED) + ]); + } + + /** + * Deny a data request. + * + * @param string $message The message to be sent to the DPO. + * @return array + * @throws coding_exception + * @throws invalid_parameter_exception + * @throws invalid_persistent_exception + * @throws restricted_context_exception + * @throws dml_exception + * @throws moodle_exception + */ + public static function contact_dpo($message) { + global $USER; + + $warnings = []; + $params = external_api::validate_parameters(self::contact_dpo_parameters(), [ + 'message' => $message + ]); + $message = $params['message']; + + // Validate context. + $userid = $USER->id; + $context = context_user::instance($userid); + self::validate_context($context); + + // Lodge the request. + $datarequest = new data_request(); + // The user the request is being made for. + $datarequest->set('userid', $userid); + // The user making the request. + $datarequest->set('requestedby', $userid); + // Set status. + $datarequest->set('status', api::DATAREQUEST_STATUS_PENDING); + // Set request type. + $datarequest->set('type', api::DATAREQUEST_TYPE_OTHERS); + // Set request comments. + $datarequest->set('comments', $message); + + // Store subject access request. + $datarequest->create(); + + // Get the list of the site Data Protection Officers. + $dpos = api::get_site_dpos(); + + // Email the data request to the Data Protection Officer(s)/Admin(s). + $result = true; + foreach ($dpos as $dpo) { + $sendresult = api::notify_dpo($dpo, $datarequest); + if (!$sendresult) { + $result = false; + $warnings[] = [ + 'item' => $dpo->id, + 'warningcode' => 'errorsendingtodpo', + 'message' => get_string('errorsendingmessagetodpo', 'tool_dataprivacy') + ]; + } + } + + return [ + 'result' => $result, + 'warnings' => $warnings + ]; + } + + /** + * Parameter description for deny_data_request(). + * + * @return external_description + */ + public static function contact_dpo_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'The processing result'), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for get_data_request(). + * + * @return external_function_parameters + */ + public static function get_data_request_parameters() { + return new external_function_parameters([ + 'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) + ]); + } + + /** + * Fetch the details of a user's data request. + * + * @param int $requestid The request ID. + * @return array + * @throws coding_exception + * @throws dml_exception + * @throws invalid_parameter_exception + * @throws restricted_context_exception + * @throws moodle_exception + */ + public static function get_data_request($requestid) { + global $PAGE; + + $warnings = []; + $params = external_api::validate_parameters(self::get_data_request_parameters(), [ + 'requestid' => $requestid + ]); + $requestid = $params['requestid']; + + // Validate context. + $context = context_system::instance(); + self::validate_context($context); + require_capability('tool/dataprivacy:managedatarequests', $context); + + $requestpersistent = new data_request($requestid); + $exporter = new data_request_exporter($requestpersistent, ['context' => $context]); + $renderer = $PAGE->get_renderer('tool_dataprivacy'); + $result = $exporter->export($renderer); + + return [ + 'result' => $result, + 'warnings' => $warnings + ]; + } + + /** + * Parameter description for get_data_request(). + * + * @return external_description + */ + public static function get_data_request_returns() { + return new external_single_structure([ + 'result' => data_request_exporter::get_read_structure(), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for approve_data_request(). + * + * @return external_function_parameters + */ + public static function approve_data_request_parameters() { + return new external_function_parameters([ + 'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) + ]); + } + + /** + * Approve a data request. + * + * @param int $requestid The request ID. + * @return array + * @throws coding_exception + * @throws dml_exception + * @throws invalid_parameter_exception + * @throws restricted_context_exception + * @throws moodle_exception + */ + public static function approve_data_request($requestid) { + $warnings = []; + $params = external_api::validate_parameters(self::approve_data_request_parameters(), [ + 'requestid' => $requestid + ]); + $requestid = $params['requestid']; + + // Validate context. + $context = context_system::instance(); + self::validate_context($context); + require_capability('tool/dataprivacy:managedatarequests', $context); + + // Ensure the request exists. + $requestexists = data_request::record_exists($requestid); + + $result = false; + if ($requestexists) { + $result = api::approve_data_request($requestid); + } else { + $warnings[] = [ + 'item' => $requestid, + 'warningcode' => 'errorrequestnotfound', + 'message' => get_string('errorrequestnotfound', 'tool_dataprivacy') + ]; + } + + return [ + 'result' => $result, + 'warnings' => $warnings + ]; + } + + /** + * Parameter description for approve_data_request(). + * + * @return external_description + */ + public static function approve_data_request_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'The processing result'), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for deny_data_request(). + * + * @return external_function_parameters + */ + public static function deny_data_request_parameters() { + return new external_function_parameters([ + 'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) + ]); + } + + /** + * Deny a data request. + * + * @param int $requestid The request ID. + * @return array + * @throws coding_exception + * @throws dml_exception + * @throws invalid_parameter_exception + * @throws restricted_context_exception + * @throws moodle_exception + */ + public static function deny_data_request($requestid) { + $warnings = []; + $params = external_api::validate_parameters(self::deny_data_request_parameters(), [ + 'requestid' => $requestid + ]); + $requestid = $params['requestid']; + + // Validate context. + $context = context_system::instance(); + self::validate_context($context); + require_capability('tool/dataprivacy:managedatarequests', $context); + + // Ensure the request exists. + $requestexists = data_request::record_exists($requestid); + + $result = false; + if ($requestexists) { + $result = api::deny_data_request($requestid); + } else { + $warnings[] = [ + 'item' => $requestid, + 'warningcode' => 'errorrequestnotfound', + 'message' => get_string('errorrequestnotfound', 'tool_dataprivacy') + ]; + } + + return [ + 'result' => $result, + 'warnings' => $warnings + ]; + } + + /** + * Parameter description for deny_data_request(). + * + * @return external_description + */ + public static function deny_data_request_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'The processing result'), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for get_data_request(). + * + * @return external_function_parameters + */ + public static function get_users_parameters() { + return new external_function_parameters([ + 'query' => new external_value(PARAM_TEXT, 'The search query', VALUE_REQUIRED) + ]); + } + + /** + * Fetch the details of a user's data request. + * + * @param string $query The search request. + * @return array + * @throws required_capability_exception + * @throws dml_exception + * @throws invalid_parameter_exception + * @throws restricted_context_exception + */ + public static function get_users($query) { + $params = external_api::validate_parameters(self::get_users_parameters(), [ + 'query' => $query + ]); + $query = $params['query']; + + // Validate context. + $context = context_system::instance(); + self::validate_context($context); + require_capability('tool/dataprivacy:managedatarequests', $context); + + $allusernames = get_all_user_name_fields(true); + // Exclude admins and guest user. + $excludedusers = array_keys(get_admins()) + [guest_user()->id]; + $sort = 'lastname ASC, firstname ASC'; + $fields = 'id, email, ' . $allusernames; + $users = get_users(true, $query, true, $excludedusers, $sort, '', '', 0, 30, $fields); + $useroptions = []; + foreach ($users as $user) { + $useroptions[$user->id] = (object)[ + 'id' => $user->id, + 'fullname' => fullname($user), + 'email' => $user->email + ]; + } + + return $useroptions; + } + + /** + * Parameter description for get_users(). + * + * @return external_description + * @throws coding_exception + */ + public static function get_users_returns() { + return new external_multiple_structure(new external_single_structure( + [ + 'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'), + 'fullname' => new external_value(core_user::get_property_type('firstname'), 'The fullname of the user'), + 'email' => new external_value(core_user::get_property_type('email'), 'The user\'s email address', VALUE_OPTIONAL), + ] + )); + } + + /** + * Parameter description for create_purpose_form(). + * + * @return external_function_parameters + */ + public static function create_purpose_form_parameters() { + return new external_function_parameters([ + 'jsonformdata' => new external_value(PARAM_RAW, 'The data to create the purpose, encoded as a json array') + ]); + } + + /** + * Creates a data purpose from form data. + * + * @param string $jsonformdata + * @return array + */ + public static function create_purpose_form($jsonformdata) { + global $PAGE; + + $warnings = []; + + $params = external_api::validate_parameters(self::create_purpose_form_parameters(), [ + 'jsonformdata' => $jsonformdata + ]); + + self::validate_context(\context_system::instance()); + + $serialiseddata = json_decode($params['jsonformdata']); + $data = array(); + parse_str($serialiseddata, $data); + + $purpose = new \tool_dataprivacy\purpose(0); + $mform = new \tool_dataprivacy\form\purpose(null, ['persistent' => $purpose], 'post', '', null, true, $data); + + $validationerrors = true; + if ($validateddata = $mform->get_data()) { + $purpose = api::create_purpose($validateddata); + $validationerrors = false; + } else if ($errors = $mform->is_validated()) { + throw new moodle_exception('generalerror'); + } + + $exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]); + return [ + 'purpose' => $exporter->export($PAGE->get_renderer('core')), + 'validationerrors' => $validationerrors, + 'warnings' => $warnings + ]; + } + + /** + * Returns for create_purpose_form(). + * + * @return external_single_structure + */ + public static function create_purpose_form_returns() { + return new external_single_structure([ + 'purpose' => purpose_exporter::get_read_structure(), + 'validationerrors' => new external_value(PARAM_BOOL, 'Were there validation errors', VALUE_REQUIRED), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for delete_purpose(). + * + * @return external_function_parameters + */ + public static function delete_purpose_parameters() { + return new external_function_parameters([ + 'id' => new external_value(PARAM_INT, 'The purpose ID', VALUE_REQUIRED) + ]); + } + + /** + * Deletes a data purpose. + * + * @param int $id The ID. + * @return array + * @throws invalid_persistent_exception + * @throws coding_exception + * @throws invalid_parameter_exception + */ + public static function delete_purpose($id) { + global $USER; + + $params = external_api::validate_parameters(self::delete_purpose_parameters(), [ + 'id' => $id + ]); + + $result = api::delete_purpose($params['id']); + + return [ + 'result' => $result, + 'warnings' => [] + ]; + } + + /** + * Parameter description for delete_purpose(). + * + * @return external_single_structure + */ + public static function delete_purpose_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'The processing result'), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for create_category_form(). + * + * @return external_function_parameters + */ + public static function create_category_form_parameters() { + return new external_function_parameters([ + 'jsonformdata' => new external_value(PARAM_RAW, 'The data to create the category, encoded as a json array') + ]); + } + + /** + * Creates a data category from form data. + * + * @param string $jsonformdata + * @return array + */ + public static function create_category_form($jsonformdata) { + global $PAGE; + + $warnings = []; + + $params = external_api::validate_parameters(self::create_category_form_parameters(), [ + 'jsonformdata' => $jsonformdata + ]); + + self::validate_context(\context_system::instance()); + + $serialiseddata = json_decode($params['jsonformdata']); + $data = array(); + parse_str($serialiseddata, $data); + + $category = new \tool_dataprivacy\category(0); + $mform = new \tool_dataprivacy\form\category(null, ['persistent' => $category], 'post', '', null, true, $data); + + $validationerrors = true; + if ($validateddata = $mform->get_data()) { + $category = api::create_category($validateddata); + $validationerrors = false; + } else if ($errors = $mform->is_validated()) { + throw new moodle_exception('generalerror'); + } + + $exporter = new category_exporter($category, ['context' => \context_system::instance()]); + return [ + 'category' => $exporter->export($PAGE->get_renderer('core')), + 'validationerrors' => $validationerrors, + 'warnings' => $warnings + ]; + } + + /** + * Returns for create_category_form(). + * + * @return external_single_structure + */ + public static function create_category_form_returns() { + return new external_single_structure([ + 'category' => category_exporter::get_read_structure(), + 'validationerrors' => new external_value(PARAM_BOOL, 'Were there validation errors', VALUE_REQUIRED), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for delete_category(). + * + * @return external_function_parameters + */ + public static function delete_category_parameters() { + return new external_function_parameters([ + 'id' => new external_value(PARAM_INT, 'The category ID', VALUE_REQUIRED) + ]); + } + + /** + * Deletes a data category. + * + * @param int $id The ID. + * @return array + * @throws invalid_persistent_exception + * @throws coding_exception + * @throws invalid_parameter_exception + */ + public static function delete_category($id) { + global $USER; + + $params = external_api::validate_parameters(self::delete_category_parameters(), [ + 'id' => $id + ]); + + $result = api::delete_category($params['id']); + + return [ + 'result' => $result, + 'warnings' => [] + ]; + } + + /** + * Parameter description for delete_category(). + * + * @return external_single_structure + */ + public static function delete_category_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'The processing result'), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for set_contextlevel_form(). + * + * @return external_function_parameters + */ + public static function set_contextlevel_form_parameters() { + return new external_function_parameters([ + 'jsonformdata' => new external_value(PARAM_RAW, 'The context level data, encoded as a json array') + ]); + } + + /** + * Creates a data category from form data. + * + * @param string $jsonformdata + * @return array + */ + public static function set_contextlevel_form($jsonformdata) { + global $PAGE; + + $warnings = []; + + $params = external_api::validate_parameters(self::set_contextlevel_form_parameters(), [ + 'jsonformdata' => $jsonformdata + ]); + + // Extra permission checkings are delegated to api::set_contextlevel. + self::validate_context(\context_system::instance()); + + $serialiseddata = json_decode($params['jsonformdata']); + $data = array(); + parse_str($serialiseddata, $data); + + $contextlevel = $data['contextlevel']; + + $customdata = \tool_dataprivacy\form\contextlevel::get_contextlevel_customdata($contextlevel); + $mform = new \tool_dataprivacy\form\contextlevel(null, $customdata, 'post', '', null, true, $data); + if ($validateddata = $mform->get_data()) { + $contextlevel = api::set_contextlevel($validateddata); + } else if ($errors = $mform->is_validated()) { + $warnings[] = json_encode($errors); + throw new moodle_exception('generalerror'); + } + + if ($contextlevel) { + $result = true; + } else { + $result = false; + } + return [ + 'result' => $result, + 'warnings' => $warnings + ]; + } + + /** + * Returns for set_contextlevel_form(). + * + * @return external_single_structure + */ + public static function set_contextlevel_form_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'Whether the data was properly set or not'), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for set_context_form(). + * + * @return external_function_parameters + */ + public static function set_context_form_parameters() { + return new external_function_parameters([ + 'jsonformdata' => new external_value(PARAM_RAW, 'The context level data, encoded as a json array') + ]); + } + + /** + * Creates a data category from form data. + * + * @param string $jsonformdata + * @return array + */ + public static function set_context_form($jsonformdata) { + global $PAGE; + + $warnings = []; + + $params = external_api::validate_parameters(self::set_context_form_parameters(), [ + 'jsonformdata' => $jsonformdata + ]); + + // Extra permission checkings are delegated to api::set_context_instance. + self::validate_context(\context_system::instance()); + + $serialiseddata = json_decode($params['jsonformdata']); + $data = array(); + parse_str($serialiseddata, $data); + + $context = context_helper::instance_by_id($data['contextid']); + $customdata = \tool_dataprivacy\form\context_instance::get_context_instance_customdata($context); + $mform = new \tool_dataprivacy\form\context_instance(null, $customdata, 'post', '', null, true, $data); + if ($validateddata = $mform->get_data()) { + $context = api::set_context_instance($validateddata); + } else if ($errors = $mform->is_validated()) { + $warnings[] = json_encode($errors); + throw new moodle_exception('generalerror'); + } + + if ($context) { + $result = true; + } else { + $result = false; + } + return [ + 'result' => $result, + 'warnings' => $warnings + ]; + } + + /** + * Returns for set_context_form(). + * + * @return external_single_structure + */ + public static function set_context_form_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'Whether the data was properly set or not'), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameter description for tree_extra_branches(). + * + * @return external_function_parameters + */ + public static function tree_extra_branches_parameters() { + return new external_function_parameters([ + 'contextid' => new external_value(PARAM_INT, 'The context id to expand'), + 'element' => new external_value(PARAM_ALPHA, 'The element we are interested on') + ]); + } + + /** + * Returns tree extra branches. + * + * @param int $contextid + * @param string $element + * @return array + */ + public static function tree_extra_branches($contextid, $element) { + + $params = external_api::validate_parameters(self::tree_extra_branches_parameters(), [ + 'contextid' => $contextid, + 'element' => $element, + ]); + + $context = context_helper::instance_by_id($params['contextid']); + + self::validate_context($context); + api::check_can_manage_data_registry($context->id); + + switch ($params['element']) { + case 'course': + $branches = data_registry_page::get_courses_branch($context); + break; + case 'module': + $branches = data_registry_page::get_modules_branch($context); + break; + case 'block': + $branches = data_registry_page::get_blocks_branch($context); + break; + default: + throw new \moodle_exception('Unsupported element provided.'); + } + + return [ + 'branches' => $branches, + 'warnings' => [], + ]; + } + + /** + * Returns for tree_extra_branches(). + * + * @return external_single_structure + */ + public static function tree_extra_branches_returns() { + return new external_single_structure([ + 'branches' => new external_multiple_structure(self::get_tree_node_structure(true)), + 'warnings' => new external_warnings() + ]); + } + + /** + * Parameters for confirm_contexts_for_deletion(). + * + * @return external_function_parameters + */ + public static function confirm_contexts_for_deletion_parameters() { + return new external_function_parameters([ + 'ids' => new external_multiple_structure( + new external_value(PARAM_INT, 'Expired context record ID', VALUE_REQUIRED), + 'Array of expired context record IDs', VALUE_DEFAULT, [] + ), + ]); + } + + /** + * Confirm a given array of expired context record IDs + * + * @param int[] $ids Array of record IDs from the expired contexts table. + * @return array + * @throws coding_exception + * @throws dml_exception + * @throws invalid_parameter_exception + * @throws restricted_context_exception + */ + public static function confirm_contexts_for_deletion($ids) { + $warnings = []; + $params = external_api::validate_parameters(self::confirm_contexts_for_deletion_parameters(), [ + 'ids' => $ids + ]); + $ids = $params['ids']; + + // Validate context. + $context = context_system::instance(); + self::validate_context($context); + + $result = true; + if (!empty($ids)) { + $expiredcontextstoapprove = []; + // Loop through the deletion of expired contexts and their children if necessary. + foreach ($ids as $id) { + $expiredcontext = new expired_context($id); + $targetcontext = context_helper::instance_by_id($expiredcontext->get('contextid')); + + // Fetch this context's child contexts. Make sure that all of the child contexts are flagged for deletion. + $childcontexts = $targetcontext->get_child_contexts(); + foreach ($childcontexts as $child) { + if ($expiredchildcontext = expired_context::get_record(['contextid' => $child->id])) { + // Add this child context to the list for approval. + $expiredcontextstoapprove[] = $expiredchildcontext; + } else { + // This context has not yet been flagged for deletion. + $result = false; + $message = get_string('errorcontexthasunexpiredchildren', 'tool_dataprivacy', + $targetcontext->get_context_name(false)); + $warnings[] = [ + 'item' => 'tool_dataprivacy_ctxexpired', + 'warningcode' => 'errorcontexthasunexpiredchildren', + 'message' => $message + ]; + // Exit the process. + break 2; + } + } + + $expiredcontextstoapprove[] = $expiredcontext; + } + + // Proceed with the approval if everything's in order. + if ($result) { + // Mark expired contexts as approved for deletion. + foreach ($expiredcontextstoapprove as $expired) { + // Only mark expired contexts that are pending approval. + if ($expired->get('status') == expired_context::STATUS_EXPIRED) { + api::set_expired_context_status($expired, expired_context::STATUS_APPROVED); + } + } + } + + } else { + // We don't have anything to process. + $result = false; + $warnings[] = [ + 'item' => 'tool_dataprivacy_ctxexpired', + 'warningcode' => 'errornoexpiredcontexts', + 'message' => get_string('errornoexpiredcontexts', 'tool_dataprivacy') + ]; + } + + return [ + 'result' => $result, + 'warnings' => $warnings + ]; + } + + /** + * Returns for confirm_contexts_for_deletion(). + * + * @return external_single_structure + */ + public static function confirm_contexts_for_deletion_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'Whether the record was properly marked for deletion or not'), + 'warnings' => new external_warnings() + ]); + } + + /** + * Gets the structure of a tree node (link + child branches). + * + * @param bool $allowchildbranches + * @return array + */ + private static function get_tree_node_structure($allowchildbranches = true) { + $fields = [ + 'text' => new external_value(PARAM_TEXT, 'The node text', VALUE_REQUIRED), + 'expandcontextid' => new external_value(PARAM_INT, 'The contextid this node expands', VALUE_REQUIRED), + 'expandelement' => new external_value(PARAM_ALPHA, 'What element is this node expanded to', VALUE_REQUIRED), + 'contextid' => new external_value(PARAM_INT, 'The node contextid', VALUE_REQUIRED), + 'contextlevel' => new external_value(PARAM_INT, 'The node contextlevel', VALUE_REQUIRED), + 'expanded' => new external_value(PARAM_INT, 'Is it expanded', VALUE_REQUIRED), + ]; + + if ($allowchildbranches) { + // Passing false as we will not have more than 1 sub-level. + $fields['branches'] = new external_multiple_structure( + self::get_tree_node_structure(false), + 'Children node structure', + VALUE_OPTIONAL + ); + } else { + // We will only have 1 sub-level and we don't want an infinite get_tree_node_structure, this is a hacky + // way to prevent this infinite loop when calling get_tree_node_structure recursively. + $fields['branches'] = new external_multiple_structure( + new external_value( + PARAM_TEXT, + 'Nothing really, it will always be an empty array', + VALUE_OPTIONAL + ) + ); + } + + return new external_single_structure($fields, 'Node structure', VALUE_OPTIONAL); + } +} diff --git a/admin/tool/dataprivacy/classes/external/category_exporter.php b/admin/tool/dataprivacy/classes/external/category_exporter.php new file mode 100644 index 00000000000..c5c350c17e9 --- /dev/null +++ b/admin/tool/dataprivacy/classes/external/category_exporter.php @@ -0,0 +1,56 @@ +. + +/** + * Class for exporting data category. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\external; +defined('MOODLE_INTERNAL') || die(); + +use core\external\persistent_exporter; + +/** + * Class for exporting field data. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class category_exporter extends persistent_exporter { + + /** + * Defines the persistent class. + * + * @return string + */ + protected static function define_class() { + return \tool_dataprivacy\category::class; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context', + ); + } +} diff --git a/admin/tool/dataprivacy/classes/external/context_instance_exporter.php b/admin/tool/dataprivacy/classes/external/context_instance_exporter.php new file mode 100644 index 00000000000..b90c13160ea --- /dev/null +++ b/admin/tool/dataprivacy/classes/external/context_instance_exporter.php @@ -0,0 +1,45 @@ +. + +/** + * Class for exporting context instance. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\external; +defined('MOODLE_INTERNAL') || die(); + +use core\external\persistent_exporter; + +/** + * Class for exporting context instance. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class context_instance_exporter extends persistent_exporter { + + /** + * Defines the persistent class. + * + * @return string + */ + protected static function define_class() { + return \tool_dataprivacy\context_instance::class; + } +} diff --git a/admin/tool/dataprivacy/classes/external/contextlevel_exporter.php b/admin/tool/dataprivacy/classes/external/contextlevel_exporter.php new file mode 100644 index 00000000000..132d8e70917 --- /dev/null +++ b/admin/tool/dataprivacy/classes/external/contextlevel_exporter.php @@ -0,0 +1,45 @@ +. + +/** + * Class for exporting context. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\external; +defined('MOODLE_INTERNAL') || die(); + +use core\external\persistent_exporter; + +/** + * Class for exporting context. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class category_exporter extends persistent_exporter { + + /** + * Defines the persistent class. + * + * @return string + */ + protected static function define_class() { + return \tool_dataprivacy\context::class; + } +} diff --git a/admin/tool/dataprivacy/classes/external/data_request_exporter.php b/admin/tool/dataprivacy/classes/external/data_request_exporter.php new file mode 100644 index 00000000000..d51c8b64637 --- /dev/null +++ b/admin/tool/dataprivacy/classes/external/data_request_exporter.php @@ -0,0 +1,189 @@ +. + +/** + * Class for exporting user evidence with all competencies. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\external; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use core\external\persistent_exporter; +use core_user; +use core_user\external\user_summary_exporter; +use dml_exception; +use renderer_base; +use tool_dataprivacy\api; +use tool_dataprivacy\data_request; + +/** + * Class for exporting user evidence with all competencies. + * + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_request_exporter extends persistent_exporter { + + /** + * Class definition. + * + * @return string + */ + protected static function define_class() { + return data_request::class; + } + + /** + * Related objects definition. + * + * @return array + */ + protected static function define_related() { + return [ + 'context' => 'context', + ]; + } + + /** + * Other properties definition. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'foruser' => [ + 'type' => user_summary_exporter::read_properties_definition(), + ], + 'requestedbyuser' => [ + 'type' => user_summary_exporter::read_properties_definition(), + 'optional' => true + ], + 'dpouser' => [ + 'type' => user_summary_exporter::read_properties_definition(), + 'optional' => true + ], + 'messagehtml' => [ + 'type' => PARAM_RAW, + 'optional' => true + ], + 'typename' => [ + 'type' => PARAM_TEXT, + ], + 'typenameshort' => [ + 'type' => PARAM_TEXT, + ], + 'statuslabel' => [ + 'type' => PARAM_TEXT, + ], + 'statuslabelclass' => [ + 'type' => PARAM_TEXT, + ], + 'canreview' => [ + 'type' => PARAM_BOOL, + 'optional' => true, + 'default' => false + ], + ]; + } + + /** + * Assign values to the defined other properties. + * + * @param renderer_base $output The output renderer object. + * @return array + * @throws coding_exception + * @throws dml_exception + */ + protected function get_other_values(renderer_base $output) { + $values = []; + + $foruserid = $this->persistent->get('userid'); + $user = core_user::get_user($foruserid, '*', MUST_EXIST); + $userexporter = new user_summary_exporter($user); + $values['foruser'] = $userexporter->export($output); + + $requestedbyid = $this->persistent->get('requestedby'); + if ($requestedbyid != $foruserid) { + $user = core_user::get_user($requestedbyid, '*', MUST_EXIST); + $userexporter = new user_summary_exporter($user); + $values['requestedbyuser'] = $userexporter->export($output); + } + + if (!empty($this->persistent->get('dpo'))) { + $dpoid = $this->persistent->get('dpo'); + $user = core_user::get_user($dpoid, '*', MUST_EXIST); + $userexporter = new user_summary_exporter($user); + $values['dpouser'] = $userexporter->export($output); + } + + $values['messagehtml'] = text_to_html($this->persistent->get('comments')); + + if ($this->persistent->get('type') == api::DATAREQUEST_TYPE_EXPORT) { + $values['typename'] = get_string('requesttypeexport', 'tool_dataprivacy'); + $values['typenameshort'] = get_string('requesttypeexportshort', 'tool_dataprivacy'); + } else if ($this->persistent->get('type') == api::DATAREQUEST_TYPE_DELETE) { + $values['typename'] = get_string('requesttypedelete', 'tool_dataprivacy'); + $values['typenameshort'] = get_string('requesttypedeleteshort', 'tool_dataprivacy'); + } else { + $values['typename'] = get_string('requesttypeothers', 'tool_dataprivacy'); + $values['typenameshort'] = get_string('requesttypeothersshort', 'tool_dataprivacy'); + } + + $values['canreview'] = false; + switch ($this->persistent->get('status')) { + case api::DATAREQUEST_STATUS_PENDING: + $values['statuslabelclass'] = 'label-default'; + $values['statuslabel'] = get_string('statuspending', 'tool_dataprivacy'); + break; + case api::DATAREQUEST_STATUS_PREPROCESSING: + $values['statuslabelclass'] = 'label-default'; + $values['statuslabel'] = get_string('statuspreprocessing', 'tool_dataprivacy'); + break; + case api::DATAREQUEST_STATUS_AWAITING_APPROVAL: + $values['statuslabelclass'] = 'label-info'; + $values['statuslabel'] = get_string('statusawaitingapproval', 'tool_dataprivacy'); + // DPO can review the request once it's ready. + $values['canreview'] = true; + break; + case api::DATAREQUEST_STATUS_APPROVED: + $values['statuslabelclass'] = 'label-info'; + $values['statuslabel'] = get_string('statusapproved', 'tool_dataprivacy'); + break; + case api::DATAREQUEST_STATUS_PROCESSING: + $values['statuslabelclass'] = 'label-info'; + $values['statuslabel'] = get_string('statusprocessing', 'tool_dataprivacy'); + break; + case api::DATAREQUEST_STATUS_COMPLETE: + $values['statuslabelclass'] = 'label-success'; + $values['statuslabel'] = get_string('statuscomplete', 'tool_dataprivacy'); + break; + case api::DATAREQUEST_STATUS_CANCELLED: + $values['statuslabelclass'] = 'label-warning'; + $values['statuslabel'] = get_string('statuscancelled', 'tool_dataprivacy'); + break; + case api::DATAREQUEST_STATUS_REJECTED: + $values['statuslabelclass'] = 'label-important'; + $values['statuslabel'] = get_string('statusrejected', 'tool_dataprivacy'); + break; + } + + return $values; + } +} diff --git a/admin/tool/dataprivacy/classes/external/purpose_exporter.php b/admin/tool/dataprivacy/classes/external/purpose_exporter.php new file mode 100644 index 00000000000..cb7f9f72ca2 --- /dev/null +++ b/admin/tool/dataprivacy/classes/external/purpose_exporter.php @@ -0,0 +1,98 @@ +. + +/** + * Class for exporting data purpose. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\external; +defined('MOODLE_INTERNAL') || die(); + +use core\external\persistent_exporter; + +/** + * Class for exporting field data. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class purpose_exporter extends persistent_exporter { + + /** + * Defines the persistent class. + * + * @return string + */ + protected static function define_class() { + return \tool_dataprivacy\purpose::class; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return array( + 'context' => 'context', + ); + } + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + return array( + 'formattedretentionperiod' => array( + 'type' => PARAM_TEXT + ), + ); + } + + /** + * Return other properties. + * + * @param \renderer_base $output + * @throws coding_exception + * @throws dml_exception + * @return array + */ + protected function get_other_values(\renderer_base $output) { + $retentionperiod = $this->persistent->get('retentionperiod'); + if ($retentionperiod) { + $interval = new \DateInterval($retentionperiod); + + // It is one or another. + if ($interval->y) { + $formattedtime = get_string('numyears', 'moodle', $interval->format('%y')); + } else if ($interval->m) { + $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m')); + } else if ($interval->d) { + $formattedtime = get_string('numdays', 'moodle', $interval->format('%d')); + } else { + $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy'); + } + } else { + $formattedtime = get_string('retentionperiodnotdefined', 'tool_dataprivacy'); + } + return ['formattedretentionperiod' => $formattedtime]; + } +} diff --git a/admin/tool/dataprivacy/classes/form/category.php b/admin/tool/dataprivacy/classes/form/category.php new file mode 100644 index 00000000000..cf213622908 --- /dev/null +++ b/admin/tool/dataprivacy/classes/form/category.php @@ -0,0 +1,67 @@ +. + +/** + * This file contains the form add/update a data category. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\form; +defined('MOODLE_INTERNAL') || die(); + +use core\form\persistent; + +/** + * Data category form. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class category extends persistent { + + /** + * @var The persistent class. + */ + protected static $persistentclass = 'tool_dataprivacy\\category'; + + /** + * Define the form - called by parent constructor + */ + public function definition() { + $mform = $this->_form; + + $mform->addElement('text', 'name', get_string('name'), 'maxlength="100"'); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', get_string('required'), 'required', null, 'server'); + $mform->addRule('name', get_string('maximumchars', '', 100), 'maxlength', 100, 'server'); + + $mform->addElement('editor', 'description', get_string('description'), null, ['autosave' => false]); + $mform->setType('description', PARAM_CLEANHTML); + + if (!empty($this->_customdata['showbuttons'])) { + if (!$this->get_persistent()->get('id')) { + $savetext = get_string('add'); + } else { + $savetext = get_string('savechanges'); + } + $this->add_action_buttons(true, $savetext); + } + } +} diff --git a/admin/tool/dataprivacy/classes/form/context_instance.php b/admin/tool/dataprivacy/classes/form/context_instance.php new file mode 100644 index 00000000000..57f53341115 --- /dev/null +++ b/admin/tool/dataprivacy/classes/form/context_instance.php @@ -0,0 +1,215 @@ +. + +/** + * This file contains the form add/update context instance data. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\form; +defined('MOODLE_INTERNAL') || die(); + +use tool_dataprivacy\api; +use tool_dataprivacy\data_registry; + +/** + * Context instance data form. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class context_instance extends \core\form\persistent { + + /** + * @var The persistent class. + */ + protected static $persistentclass = 'tool_dataprivacy\\context_instance'; + + /** + * Define the form - called by parent constructor + */ + public function definition() { + $this->_form->setDisableShortforms(); + + $this->_form->addElement('header', 'contextname', $this->_customdata['contextname']); + + $subjectscope = implode(', ', $this->_customdata['subjectscope']); + if (empty($subjectscope)) { + $subjectscope = get_string('noassignedroles', 'tool_dataprivacy'); + } + $this->_form->addElement('static', 'subjectscope', get_string('subjectscope', 'tool_dataprivacy'), $subjectscope); + + $this->add_purpose_category($this->_customdata['context']->contextlevel); + + $this->_form->addElement('hidden', 'contextid'); + $this->_form->setType('contextid', PARAM_INT); + + parent::add_action_buttons(false, get_string('savechanges')); + } + + /** + * Adds purpose and category selectors. + * + * @param int $contextlevel Apply this context level defaults. False for no defaults. + * @return null + */ + protected function add_purpose_category($contextlevel = false) { + + $mform = $this->_form; + + $addcategorytext = $this->get_add_element_content(get_string('addcategory', 'tool_dataprivacy')); + $categoryselect = $mform->createElement('select', 'categoryid', null, $this->_customdata['categories']); + $addcategory = $mform->createElement('button', 'addcategory', $addcategorytext, ['data-add-element' => 'category']); + $mform->addElement('group', 'categorygroup', get_string('category', 'tool_dataprivacy'), + [$categoryselect, $addcategory], null, false); + $mform->setType('categoryid', PARAM_INT); + $mform->setDefault('categoryid', 0); + + $addpurposetext = $this->get_add_element_content(get_string('addpurpose', 'tool_dataprivacy')); + $purposeselect = $mform->createElement('select', 'purposeid', null, $this->_customdata['purposes']); + $addpurpose = $mform->createElement('button', 'addpurpose', $addpurposetext, ['data-add-element' => 'purpose']); + $mform->addElement('group', 'purposegroup', get_string('purpose', 'tool_dataprivacy'), + [$purposeselect, $addpurpose], null, false); + $mform->setType('purposeid', PARAM_INT); + $mform->setDefault('purposeid', 0); + + if (!empty($this->_customdata['currentretentionperiod'])) { + $mform->addElement('static', 'retention_current', get_string('retentionperiod', 'tool_dataprivacy'), + $this->_customdata['currentretentionperiod']); + } + } + + /** + * Returns the 'add' label. + * + * It depends on the theme in use. + * + * @param string $label + * @return \renderable|string + */ + private function get_add_element_content($label) { + global $PAGE, $OUTPUT; + + $bs4 = false; + + $theme = $PAGE->theme; + if ($theme->name === 'boost') { + $bs4 = true; + } else { + foreach ($theme->parents as $basetheme) { + if ($basetheme === 'boost') { + $bs4 = true; + } + } + } + + if (!$bs4) { + return $label; + } + return $OUTPUT->pix_icon('e/insert', $label); + } + + /** + * Returns the customdata array for the provided context instance. + * + * @param \context $context + * @return array + */ + public static function get_context_instance_customdata(\context $context) { + + $persistent = \tool_dataprivacy\context_instance::get_record_by_contextid($context->id, false); + if (!$persistent) { + $persistent = new \tool_dataprivacy\context_instance(); + $persistent->set('contextid', $context->id); + } + + $purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options( + api::get_purposes() + ); + $categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options( + api::get_categories() + ); + + $customdata = [ + 'context' => $context, + 'subjectscope' => data_registry::get_subject_scope($context), + 'contextname' => $context->get_context_name(), + 'persistent' => $persistent, + 'purposes' => $purposeoptions, + 'categories' => $categoryoptions, + ]; + + $effectivepurpose = api::get_effective_context_purpose($context); + if ($effectivepurpose) { + + $customdata['currentretentionperiod'] = self::get_retention_display_text($effectivepurpose, $context->contextlevel, $context); + + $customdata['purposeretentionperiods'] = []; + foreach ($purposeoptions as $optionvalue => $unused) { + // Get the effective purpose if $optionvalue would be the selected value. + $purpose = api::get_effective_context_purpose($context, $optionvalue); + + $retentionperiod = self::get_retention_display_text( + $purpose, + $context->contextlevel, + $context + ); + $customdata['purposeretentionperiods'][$optionvalue] = $retentionperiod; + } + } + + return $customdata; + } + + /** + * Returns the purpose display text. + * + * @param \tool_dataprivacy\purpose $effectivepurpose + * @param int $retentioncontextlevel + * @param \context $context The context, just for displaying (filters) purposes. + * @return string + */ + protected static function get_retention_display_text(\tool_dataprivacy\purpose $effectivepurpose, $retentioncontextlevel, \context $context) { + global $PAGE; + + $renderer = $PAGE->get_renderer('tool_dataprivacy'); + + $exporter = new \tool_dataprivacy\external\purpose_exporter($effectivepurpose, ['context' => $context]); + $exportedpurpose = $exporter->export($renderer); + + switch ($retentioncontextlevel) { + case CONTEXT_COURSE: + case CONTEXT_MODULE: + case CONTEXT_BLOCK: + $str = get_string('effectiveretentionperiodcourse', 'tool_dataprivacy', + $exportedpurpose->formattedretentionperiod); + break; + case CONTEXT_USER: + $str = get_string('effectiveretentionperioduser', 'tool_dataprivacy', + $exportedpurpose->formattedretentionperiod); + break; + default: + $str = $exportedpurpose->formattedretentionperiod; + } + + return $str; + } + +} diff --git a/admin/tool/dataprivacy/classes/form/contextlevel.php b/admin/tool/dataprivacy/classes/form/contextlevel.php new file mode 100644 index 00000000000..d2f9fc61c6f --- /dev/null +++ b/admin/tool/dataprivacy/classes/form/contextlevel.php @@ -0,0 +1,124 @@ +. + +/** + * This file contains the form add/update context level data. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\form; +defined('MOODLE_INTERNAL') || die(); + +use core\form\persistent; +use tool_dataprivacy\api; +use tool_dataprivacy\data_registry; + +/** + * Context level data form. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contextlevel extends context_instance { + + /** + * @var The persistent class. + */ + protected static $persistentclass = 'tool_dataprivacy\\contextlevel'; + + /** + * Define the form - called by parent constructor + */ + public function definition() { + $this->_form->setDisableShortforms(); + + $this->_form->addElement('header', 'contextlevelname', $this->_customdata['contextlevelname']); + + $this->add_purpose_category(); + + $this->_form->addElement('hidden', 'contextlevel'); + $this->_form->setType('contextlevel', PARAM_INT); + + parent::add_action_buttons(false, get_string('savechanges')); + } + + /** + * Returns the customdata array for the provided context level. + * + * @param int $contextlevel + * @return array + */ + public static function get_contextlevel_customdata($contextlevel) { + + $persistent = \tool_dataprivacy\contextlevel::get_record_by_contextlevel($contextlevel, false); + if (!$persistent) { + $persistent = new \tool_dataprivacy\contextlevel(); + $persistent->set('contextlevel', $contextlevel); + } + + $includeinherit = true; + if ($contextlevel == CONTEXT_SYSTEM) { + // Nothing to inherit from Site level. + $includeinherit = false; + } + $includenotset = true; + if ($contextlevel == CONTEXT_SYSTEM || $contextlevel == CONTEXT_USER) { + // No 'not set' value for system and user because we do not have defaults for them. + $includenotset = false; + } + $purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options( + api::get_purposes(), $includenotset, $includeinherit); + $categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options( + api::get_categories(), $includenotset, $includeinherit); + + $customdata = [ + 'contextlevel' => $contextlevel, + 'contextlevelname' => get_string('contextlevelname' . $contextlevel, 'tool_dataprivacy'), + 'persistent' => $persistent, + 'purposes' => $purposeoptions, + 'categories' => $categoryoptions, + ]; + + $effectivepurpose = api::get_effective_contextlevel_purpose($contextlevel); + if ($effectivepurpose) { + + $customdata['currentretentionperiod'] = self::get_retention_display_text($effectivepurpose, $contextlevel, + \context_system::instance()); + + $customdata['purposeretentionperiods'] = []; + foreach ($purposeoptions as $optionvalue => $unused) { + + // Get the effective purpose if $optionvalue would be the selected value. + list($purposeid, $unused) = data_registry::get_effective_default_contextlevel_purpose_and_category($contextlevel, + $optionvalue); + $purpose = new \tool_dataprivacy\purpose($purposeid); + + $retentionperiod = self::get_retention_display_text( + $purpose, + $contextlevel, + \context_system::instance() + ); + $customdata['purposeretentionperiods'][$optionvalue] = $retentionperiod; + } + } + + return $customdata; + } +} diff --git a/admin/tool/dataprivacy/classes/form/defaults.php b/admin/tool/dataprivacy/classes/form/defaults.php new file mode 100644 index 00000000000..b3c9ddc70bb --- /dev/null +++ b/admin/tool/dataprivacy/classes/form/defaults.php @@ -0,0 +1,78 @@ +. + +/** + * This file contains the defaults form. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\form; +defined('MOODLE_INTERNAL') || die(); + +use \tool_dataprivacy\output\data_registry_page; + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); + +/** + * Context levels defaults form. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class defaults extends \moodleform { + + /** + * Define the form. + */ + public function definition() { + global $OUTPUT; + + $mform = $this->_form; + $mform->setDisableShortforms(); + + $notification = $OUTPUT->notification(get_string('defaultsinfo', 'tool_dataprivacy'), \core\output\notification::NOTIFY_INFO); + $mform->addElement('html', $notification); + + foreach ($this->_customdata['levels'] as $level => $classname) { + + $mform->addElement('header', $classname . '-header', + get_string('contextlevelname' . $level, 'tool_dataprivacy')); + + list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname); + + $includeinherit = true; + if ($level == CONTEXT_SYSTEM) { + $includeinherit = false; + } + + $categoryoptions = data_registry_page::category_options($this->_customdata['categories'], false, $includeinherit); + $purposeoptions = data_registry_page::category_options($this->_customdata['purposes'], false, $includeinherit); + + $mform->addElement('select', $categoryvar, get_string('category', 'tool_dataprivacy'), $categoryoptions); + $mform->setType($categoryvar, PARAM_INT); + + $mform->addElement('select', $purposevar, get_string('purpose', 'tool_dataprivacy'), $purposeoptions); + $mform->setType($purposevar, PARAM_INT); + } + + $this->add_action_buttons(true, get_string('savechanges')); + } +} diff --git a/admin/tool/dataprivacy/classes/form/purpose.php b/admin/tool/dataprivacy/classes/form/purpose.php new file mode 100644 index 00000000000..52c1d1f9d83 --- /dev/null +++ b/admin/tool/dataprivacy/classes/form/purpose.php @@ -0,0 +1,113 @@ +. + +/** + * This file contains the form add/update a data purpose. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\form; +defined('MOODLE_INTERNAL') || die(); + +use core\form\persistent; + +/** + * Data purpose form. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class purpose extends persistent { + + /** + * @var The persistent class. + */ + protected static $persistentclass = 'tool_dataprivacy\\purpose'; + + /** + * Define the form - called by parent constructor + */ + public function definition() { + $mform = $this->_form; + + $mform->addElement('text', 'name', get_string('name'), 'maxlength="100"'); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', get_string('required'), 'required', null, 'server'); + $mform->addRule('name', get_string('maximumchars', '', 100), 'maxlength', 100, 'server'); + + $mform->addElement('editor', 'description', get_string('description'), null, ['autosave' => false]); + $mform->setType('description', PARAM_CLEANHTML); + + $number = $mform->createElement('text', 'retentionperiodnumber', null, ['size' => 8]); + $unitoptions = [ + 'Y' => get_string('years'), + 'M' => strtolower(get_string('months')), + 'D' => strtolower(get_string('days')) + ]; + $unit = $mform->createElement('select', 'retentionperiodunit', '', $unitoptions); + $mform->addGroup(['number' => $number, 'unit' => $unit], 'retentionperiod', get_string('retentionperiod', 'tool_dataprivacy'), null, false); + $mform->setType('retentionperiodnumber', PARAM_INT); + + $this->_form->addElement('advcheckbox', 'protected', get_string('protected', 'tool_dataprivacy'), + get_string('protectedlabel', 'tool_dataprivacy')); + + if (!empty($this->_customdata['showbuttons'])) { + if (!$this->get_persistent()->get('id')) { + $savetext = get_string('add'); + } else { + $savetext = get_string('savechanges'); + } + $this->add_action_buttons(true, $savetext); + } + } + + /** + * Converts fields. + * + * @param \stdClass $data + * @return \stdClass + */ + protected static function convert_fields(\stdClass $data) { + $data = parent::convert_fields($data); + + // A single value. + $data->retentionperiod = 'P' . $data->retentionperiodnumber . $data->retentionperiodunit; + unset($data->retentionperiodnumber); + unset($data->retentionperiodunit); + return $data; + } + + /** + * Get the default data. + * + * @return \stdClass + */ + protected function get_default_data() { + $data = parent::get_default_data(); + + // Convert the single properties into number and unit. + $strlen = strlen($data->retentionperiod); + $data->retentionperiodnumber = substr($data->retentionperiod, 1, $strlen - 2); + $data->retentionperiodunit = substr($data->retentionperiod, $strlen - 1); + unset($data->retentionperiod); + + return $data; + } +} diff --git a/admin/tool/dataprivacy/classes/output/categories.php b/admin/tool/dataprivacy/classes/output/categories.php new file mode 100644 index 00000000000..57bfd7b5595 --- /dev/null +++ b/admin/tool/dataprivacy/classes/output/categories.php @@ -0,0 +1,88 @@ +. + +/** + * Categories renderable. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use stdClass; +use templatable; +use tool_dataprivacy\external\category_exporter; + +/** + * Class containing the categories page renderable. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class categories extends crud_element implements renderable, templatable { + + /** @var array $categories All system categories. */ + protected $categories = []; + + /** + * Construct this renderable. + * + * @param \tool_dataprivacy\category[] $categories + */ + public function __construct($categories) { + $this->categories = $categories; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $PAGE; + + $context = \context_system::instance(); + + $PAGE->requires->js_call_amd('tool_dataprivacy/categoriesactions', 'init'); + $PAGE->requires->js_call_amd('tool_dataprivacy/add_category', 'getInstance', [$context->id]); + + $data = new stdClass(); + + // Navigation links. + $data->navigation = []; + $navigationlinks = $this->get_navigation(); + foreach ($navigationlinks as $navlink) { + $data->navigation[] = $navlink->export_for_template($output); + } + + $data->categories = []; + foreach ($this->categories as $category) { + $exporter = new category_exporter($category, ['context' => \context_system::instance()]); + $exportedcategory = $exporter->export($output); + + $actionmenu = $this->action_menu('category', $exportedcategory, $category); + $exportedcategory->actions = $actionmenu->export_for_template($output); + $data->categories[] = $exportedcategory; + } + + return $data; + } +} diff --git a/admin/tool/dataprivacy/classes/output/crud_element.php b/admin/tool/dataprivacy/classes/output/crud_element.php new file mode 100644 index 00000000000..87d8ce9bd11 --- /dev/null +++ b/admin/tool/dataprivacy/classes/output/crud_element.php @@ -0,0 +1,92 @@ +. + +/** + * Abstract renderer for independent renderable elements. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use stdClass; +use templatable; +use tool_dataprivacy\external\purpose_exporter; +use tool_dataprivacy\external\category_exporter; + +/** + * Abstract renderer for independent renderable elements. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class crud_element { + + /** + * Returns the top navigation buttons. + * + * @return \action_link[] + */ + protected final function get_navigation() { + $back = new \action_link( + new \moodle_url('/admin/tool/dataprivacy/dataregistry.php'), + get_string('back'), + null, + ['class' => 'btn btn-default'] + ); + return [$back]; + } + + /** + * Adds an action menu for the provided element + * + * @param string $elementname 'purpose' or 'category'. + * @param \stdClass $exported + * @param \core\persistent $persistent + * @return \action_menu + */ + protected final function action_menu($elementname, $exported, $persistent) { + + // Just in case, we are doing funny stuff below. + $elementname = clean_param($elementname, PARAM_ALPHA); + + // Actions. + $actionmenu = new \action_menu(); + $actionmenu->set_menu_trigger(get_string('actions')); + $actionmenu->set_owner_selector($elementname . '-' . $exported->id . '-actions'); + $actionmenu->set_alignment(\action_menu::TL, \action_menu::BL); + + $url = new \moodle_url('/admin/tool/dataprivacy/edit' . $elementname . '.php', + ['id' => $exported->id]); + $link = new \action_menu_link_secondary($url, new \pix_icon('t/edit', + get_string('edit')), get_string('edit')); + $actionmenu->add($link); + + if (!$persistent->is_used()) { + $url = new \moodle_url('#'); + $attrs = ['data-id' => $exported->id, 'data-action' => 'delete' . $elementname, 'data-name' => $exported->name]; + $link = new \action_menu_link_secondary($url, new \pix_icon('t/delete', + get_string('delete')), get_string('delete'), $attrs); + $actionmenu->add($link); + } + + return $actionmenu; + } +} diff --git a/admin/tool/dataprivacy/classes/output/data_deletion_page.php b/admin/tool/dataprivacy/classes/output/data_deletion_page.php new file mode 100644 index 00000000000..b2a5d58fad7 --- /dev/null +++ b/admin/tool/dataprivacy/classes/output/data_deletion_page.php @@ -0,0 +1,97 @@ +. + +/** + * Class containing data for a user's data requests. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\output; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use dml_exception; +use moodle_exception; +use moodle_url; +use renderable; +use renderer_base; +use single_select; +use stdClass; +use templatable; +use tool_dataprivacy\data_request; + +/** + * Class containing data for a user's data requests. + * + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_deletion_page implements renderable, templatable { + + /** The default number of results to be shown per page. */ + const DEFAULT_PAGE_SIZE = 20; + + /** @var data_request[] $requests List of data requests. */ + protected $filter = null; + + /** @var data_request[] $requests List of data requests. */ + protected $expiredcontextstable = []; + + /** + * Construct this renderable. + * + * @param expired_contexts_table $expiredcontextstable + */ + public function __construct($filter, $expiredcontextstable) { + $this->filter = $filter; + $this->expiredcontextstable = $expiredcontextstable; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + + $url = new moodle_url('/admin/tool/dataprivacy/datadeletion.php'); + $options = [ + CONTEXT_USER => get_string('user'), + CONTEXT_COURSE => get_string('course'), + CONTEXT_MODULE => get_string('activitiesandresources', 'tool_dataprivacy'), + CONTEXT_BLOCK => get_string('blocks'), + ]; + $filterselector = new single_select($url, 'filter', $options, $this->filter, null); + $data->filter = $filterselector->export_for_template($output); + + ob_start(); + $this->expiredcontextstable->out(self::DEFAULT_PAGE_SIZE, true); + $expiredcontexts = ob_get_contents(); + ob_end_clean(); + $data->expiredcontexts = $expiredcontexts; + + $data->existingcontexts = $this->expiredcontextstable->rawdata ? true : false; + + return $data; + } +} diff --git a/admin/tool/dataprivacy/classes/output/data_registry_page.php b/admin/tool/dataprivacy/classes/output/data_registry_page.php new file mode 100644 index 00000000000..b16bfacbea8 --- /dev/null +++ b/admin/tool/dataprivacy/classes/output/data_registry_page.php @@ -0,0 +1,483 @@ +. + +/** + * Data registry renderable. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use stdClass; +use templatable; +use tool_dataprivacy\data_registry; + +require_once($CFG->libdir . '/coursecatlib.php'); +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); +require_once($CFG->libdir . '/blocklib.php'); + +/** + * Class containing the data registry renderable + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_registry_page implements renderable, templatable { + + /** + * @var int + */ + private $defaultcontextlevel; + + /** + * @var int + */ + private $defaultcontextid; + + /** + * Constructor. + * + * @param int $defaultcontextlevel + * @param int $defaultcontextid + * @return null + */ + public function __construct($defaultcontextlevel = false, $defaultcontextid = false) { + $this->defaultcontextlevel = $defaultcontextlevel; + $this->defaultcontextid = $defaultcontextid; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $PAGE; + + $params = [\context_system::instance()->id, $this->defaultcontextlevel, $this->defaultcontextid]; + $PAGE->requires->js_call_amd('tool_dataprivacy/data_registry', 'init', $params); + + $data = new stdClass(); + $defaultsbutton = new \action_link( + new \moodle_url('/admin/tool/dataprivacy/defaults.php'), + get_string('setdefaults', 'tool_dataprivacy'), + null, + ['class' => 'btn btn-default'] + ); + $data->defaultsbutton = $defaultsbutton->export_for_template($output); + + $actionmenu = new \action_menu(); + $actionmenu->set_menu_trigger(get_string('edit'), 'btn btn-default'); + $actionmenu->set_owner_selector('dataregistry-actions'); + $actionmenu->set_alignment(\action_menu::TL, \action_menu::BL); + + $url = new \moodle_url('/admin/tool/dataprivacy/categories.php'); + $categories = new \action_menu_link_secondary($url, null, get_string('categories', 'tool_dataprivacy')); + $actionmenu->add($categories); + + $url = new \moodle_url('/admin/tool/dataprivacy/purposes.php'); + $purposes = new \action_menu_link_secondary($url, null, get_string('purposes', 'tool_dataprivacy')); + $actionmenu->add($purposes); + + $data->actions = $actionmenu->export_for_template($output); + + if (!data_registry::defaults_set()) { + $data->nosystemdefaults = (object)[ + 'message' => get_string('nosystemdefaults', 'tool_dataprivacy'), + 'announce' => 1 + ]; + } + + $data->tree = $this->get_default_tree_structure(); + + return $data; + } + + /** + * Returns the tree default structure. + * + * @return array + */ + private function get_default_tree_structure() { + + $frontpage = \context_course::instance(SITEID); + + $categorybranches = $this->get_all_category_branches(); + + $elements = [ + 'text' => get_string('contextlevelname' . CONTEXT_SYSTEM, 'tool_dataprivacy'), + 'contextlevel' => CONTEXT_SYSTEM, + 'branches' => [ + [ + 'text' => get_string('user'), + 'contextlevel' => CONTEXT_USER, + ], [ + 'text' => get_string('categories', 'tool_dataprivacy'), + 'branches' => $categorybranches, + 'expandelement' => 'category', + ], [ + 'text' => get_string('frontpagecourse', 'tool_dataprivacy'), + 'contextid' => $frontpage->id, + 'branches' => [ + [ + 'text' => get_string('activitiesandresources', 'tool_dataprivacy'), + 'expandcontextid' => $frontpage->id, + 'expandelement' => 'module', + 'expanded' => 0, + ], [ + 'text' => get_string('blocks'), + 'expandcontextid' => $frontpage->id, + 'expandelement' => 'block', + 'expanded' => 0, + ], + ] + ] + ] + ]; + + // Returned as an array to follow a common array format. + return [self::complete($elements, $this->defaultcontextlevel, $this->defaultcontextid)]; + } + + /** + * Returns the hierarchy of system course categories. + * + * @return array + */ + private function get_all_category_branches() { + + $categories = data_registry::get_site_categories(); + + $categoriesbranch = []; + while (count($categories) > 0) { + foreach ($categories as $key => $category) { + + $context = \context_coursecat::instance($category->id); + $newnode = [ + 'text' => shorten_text(format_string($category->name, true, ['context' => $context])), + 'categoryid' => $category->id, + 'contextid' => $context->id, + ]; + if ($category->coursecount > 0) { + $newnode['branches'] = [ + [ + 'text' => get_string('courses'), + 'expandcontextid' => $context->id, + 'expandelement' => 'course', + 'expanded' => 0, + ] + ]; + } + + $added = false; + if ($category->parent == 0) { + // New categories root-level node. + $categoriesbranch[] = $newnode; + $added = true; + + } else { + // Add the new node under the appropriate parent. + if ($this->add_to_parent_category_branch($category, $newnode, $categoriesbranch)) { + $added = true; + } + } + + if ($added) { + unset($categories[$key]); + } + } + } + + return $categoriesbranch; + } + + /** + * Gets the courses branch for the provided category. + * + * @param \context $catcontext + * @return array + */ + public static function get_courses_branch(\context $catcontext) { + + if ($catcontext->contextlevel !== CONTEXT_COURSECAT) { + throw new \coding_exception('A course category context should be provided'); + } + + $coursecat = \coursecat::get($catcontext->instanceid); + $courses = $coursecat->get_courses(); + + $branches = []; + + foreach ($courses as $course) { + + $coursecontext = \context_course::instance($course->id); + + if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { + continue; + } + + $coursenode = [ + 'text' => shorten_text(format_string($course->shortname, true, ['context' => $coursecontext])), + 'contextid' => $coursecontext->id, + 'branches' => [ + [ + 'text' => get_string('activitiesandresources', 'tool_dataprivacy'), + 'expandcontextid' => $coursecontext->id, + 'expandelement' => 'module', + 'expanded' => 0, + ], [ + 'text' => get_string('blocks'), + 'expandcontextid' => $coursecontext->id, + 'expandelement' => 'block', + 'expanded' => 0, + ], + ] + ]; + $branches[] = self::complete($coursenode); + } + + return $branches; + } + + /** + * Gets the modules branch for the provided course. + * + * @param \context $coursecontext + * @return array + */ + public static function get_modules_branch(\context $coursecontext) { + + if ($coursecontext->contextlevel !== CONTEXT_COURSE) { + throw new \coding_exception('A course context should be provided'); + } + + $branches = []; + + // Using the current user. + $modinfo = get_fast_modinfo($coursecontext->instanceid); + foreach ($modinfo->get_instances() as $moduletype => $instances) { + foreach ($instances as $cm) { + + if (!$cm->uservisible) { + continue; + } + + $a = (object)[ + 'instancename' => shorten_text($cm->get_formatted_name()), + 'modulename' => get_string('pluginname', 'mod_' . $moduletype), + ]; + + $text = get_string('moduleinstancename', 'tool_dataprivacy', $a); + $branches[] = self::complete([ + 'text' => $text, + 'contextid' => $cm->context->id, + ]); + } + } + + return $branches; + } + + /** + * Gets the blocks branch for the provided course. + * + * @param \context $coursecontext + * @return null + */ + public static function get_blocks_branch(\context $coursecontext) { + global $DB; + + if ($coursecontext->contextlevel !== CONTEXT_COURSE) { + throw new \coding_exception('A course context should be provided'); + } + + $branches = []; + + $blockinstances = \core_block_external::get_course_blocks($coursecontext->instanceid); + if (empty($blockinstances['blocks'])) { + return $branches; + } + + foreach ($blockinstances['blocks'] as $bi) { + if (function_exists('block_instance_by_id')) { + $blockinstance = block_instance_by_id($bi['instanceid']); + } else { + // TODO To be removed when MDL-61621 gets integrated. + $blockinstance = $DB->get_record('block_instances', ['id' => $bi['instanceid']]); + $blockinstance = block_instance($blockinstance->blockname, $blockinstance); + } + $blockcontext = \context_block::instance($bi['instanceid']); + $displayname = shorten_text(format_string($blockinstance->get_title(), true, ['context' => $blockcontext->id])); + $branches[] = self::complete([ + 'text' => $displayname, + 'contextid' => $blockcontext->id, + ]); + } + + return $branches; + + } + + /** + * Adds the provided category to the categories branch. + * + * @param stdClass $category + * @param array $newnode + * @param array $categoriesbranch + * @return bool + */ + private function add_to_parent_category_branch($category, $newnode, &$categoriesbranch) { + + foreach ($categoriesbranch as $key => $branch) { + if (!empty($branch['categoryid']) && $branch['categoryid'] == $category->parent) { + // It may be empty (if it does not contain courses and this is the first child cat). + if (!isset($categoriesbranch[$key]['branches'])) { + $categoriesbranch[$key]['branches'] = []; + } + $categoriesbranch[$key]['branches'][] = $newnode; + return true; + } + if (!empty($branch['branches'])) { + $parent = $this->add_to_parent_category_branch($category, $newnode, $categoriesbranch[$key]['branches']); + if ($parent) { + return true; + } + } + } + + return false; + } + + /** + * Completes tree nodes with default values. + * + * @param array $node + * @param int|false $currentcontextlevel + * @param int|false $currentcontextid + * @return array + */ + private static function complete($node, $currentcontextlevel = false, $currentcontextid = false) { + if (!isset($node['active'])) { + if ($currentcontextlevel && !empty($node['contextlevel']) && + $currentcontextlevel == $node['contextlevel'] && + empty($currentcontextid)) { + // This is the active context level, we also checked that there + // is no default contextid set. + $node['active'] = true; + } else if ($currentcontextid && !empty($node['contextid']) && + $currentcontextid == $node['contextid']) { + $node['active'] = true; + } else { + $node['active'] = null; + } + } + + if (!isset($node['branches'])) { + $node['branches'] = []; + } else { + foreach ($node['branches'] as $key => $childnode) { + $node['branches'][$key] = self::complete($childnode, $currentcontextlevel, $currentcontextid); + } + } + + if (!isset($node['expandelement'])) { + $node['expandelement'] = null; + } + + if (!isset($node['expandcontextid'])) { + $node['expandcontextid'] = null; + } + + if (!isset($node['contextid'])) { + $node['contextid'] = null; + } + + if (!isset($node['contextlevel'])) { + $node['contextlevel'] = null; + } + + if (!isset($node['expanded'])) { + if (!empty($node['branches'])) { + $node['expanded'] = 1; + } else { + $node['expanded'] = 0; + } + } + return $node; + } + + /** + * From a list of purpose persistents to a list of id => name purposes. + * + * @param \tool_dataprivacy\purpose $purposes + * @param bool $includenotset + * @param bool $includeinherit + * @return string[] + */ + public static function purpose_options($purposes, $includenotset = true, $includeinherit = true) { + $options = self::base_options($includenotset, $includeinherit); + foreach ($purposes as $purpose) { + $options[$purpose->get('id')] = $purpose->get('name'); + } + + return $options; + } + + /** + * From a list of category persistents to a list of id => name categories. + * + * @param \tool_dataprivacy\category $categories + * @param bool $includenotset + * @param bool $includeinherit + * @return string[] + */ + public static function category_options($categories, $includenotset = true, $includeinherit = true) { + $options = self::base_options($includenotset, $includeinherit); + foreach ($categories as $category) { + $options[$category->get('id')] = $category->get('name'); + } + + return $options; + } + + /** + * Base not set and inherit options. + * + * @param bool $includenotset + * @param bool $includeinherit + * @return array + */ + private static function base_options($includenotset = true, $includeinherit = true) { + + $options = []; + + if ($includenotset) { + $options[\tool_dataprivacy\context_instance::NOTSET] = get_string('notset', 'tool_dataprivacy'); + } + + if ($includeinherit) { + $options[\tool_dataprivacy\context_instance::INHERIT] = get_string('inherit', 'tool_dataprivacy'); + } + + return $options; + } +} diff --git a/admin/tool/dataprivacy/classes/output/data_requests_page.php b/admin/tool/dataprivacy/classes/output/data_requests_page.php new file mode 100644 index 00000000000..572617ca922 --- /dev/null +++ b/admin/tool/dataprivacy/classes/output/data_requests_page.php @@ -0,0 +1,115 @@ +. + +/** + * Class containing data for a user's data requests. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\output; +defined('MOODLE_INTERNAL') || die(); + +use action_menu; +use action_menu_link_secondary; +use coding_exception; +use context_system; +use dml_exception; +use moodle_exception; +use moodle_url; +use renderable; +use renderer_base; +use stdClass; +use templatable; +use tool_dataprivacy\api; +use tool_dataprivacy\data_request; +use tool_dataprivacy\external\data_request_exporter; + +/** + * Class containing data for a user's data requests. + * + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class data_requests_page implements renderable, templatable { + + /** @var data_request[] $requests List of data requests. */ + protected $requests = []; + + /** + * Construct this renderable. + * + * @param data_request[] $requests + */ + public function __construct($requests) { + $this->requests = $requests; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->newdatarequesturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php'); + $data->newdatarequesturl->param('manage', true); + + $requests = []; + foreach ($this->requests as $request) { + $requestid = $request->get('id'); + $status = $request->get('status'); + $requestexporter = new data_request_exporter($request, ['context' => context_system::instance()]); + $item = $requestexporter->export($output); + + // Prepare actions. + $actions = []; + + // View action. + $actionurl = new moodle_url('#'); + $actiondata = ['data-action' => 'view', 'data-requestid' => $requestid]; + $actiontext = get_string('viewrequest', 'tool_dataprivacy'); + $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); + + if ($status == api::DATAREQUEST_STATUS_AWAITING_APPROVAL) { + // Approve. + $actiondata['data-action'] = 'approve'; + $actiontext = get_string('approverequest', 'tool_dataprivacy'); + $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); + + // Deny. + $actiondata['data-action'] = 'deny'; + $actiontext = get_string('denyrequest', 'tool_dataprivacy'); + $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); + } + + $actionsmenu = new action_menu($actions); + $actionsmenu->set_menu_trigger(get_string('actions')); + $actionsmenu->set_owner_selector('request-actions-' . $requestid); + $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL); + $item->actions = $actionsmenu->export_for_template($output); + + $requests[] = $item; + } + $data->requests = $requests; + return $data; + } +} diff --git a/admin/tool/dataprivacy/classes/output/expired_contexts_table.php b/admin/tool/dataprivacy/classes/output/expired_contexts_table.php new file mode 100644 index 00000000000..47be80e39ad --- /dev/null +++ b/admin/tool/dataprivacy/classes/output/expired_contexts_table.php @@ -0,0 +1,284 @@ +. + +/** + * Contains the class used for the displaying the expired contexts table. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\output; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); + +use coding_exception; +use context_helper; +use dml_exception; +use Exception; +use html_writer; +use pix_icon; +use stdClass; +use table_sql; +use tool_dataprivacy\api; +use tool_dataprivacy\expired_context; +use tool_dataprivacy\external\purpose_exporter; +use tool_dataprivacy\purpose; + +defined('MOODLE_INTERNAL') || die; + +/** + * The class for displaying the expired contexts table. + * + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class expired_contexts_table extends table_sql { + + /** @var int The context level acting as a filter for this table. */ + protected $contextlevel = null; + + /** + * @var bool $selectall Has the user selected all users on the page? True by default. + */ + protected $selectall = true; + + /** @var purpose[] Array of purposes mapped to the contexts. */ + protected $purposes = []; + + /** + * expired_contexts_table constructor. + * + * @param int $filter The filter type. + * @throws coding_exception + */ + public function __construct($contextlevel = null) { + parent::__construct('expired-contexts-table'); + + $this->contextlevel = $contextlevel; + + $columnheaders = [ + 'name' => get_string('name'), + 'info' => get_string('info'), + 'purpose' => get_string('purpose', 'tool_dataprivacy'), + 'category' => get_string('category', 'tool_dataprivacy'), + 'retentionperiod' => get_string('retentionperiod', 'tool_dataprivacy'), + 'timecreated' => get_string('expiry', 'tool_dataprivacy'), + ]; + $checkboxattrs = [ + 'title' => get_string('selectall'), + 'data-action' => 'selectall' + ]; + $columnheaders['select'] = html_writer::checkbox('selectall', 1, true, null, $checkboxattrs); + + $this->define_columns(array_keys($columnheaders)); + $this->define_headers(array_values($columnheaders)); + $this->no_sorting('name'); + $this->no_sorting('select'); + $this->no_sorting('info'); + $this->no_sorting('purpose'); + $this->no_sorting('category'); + $this->no_sorting('retentionperiod'); + + // Make this table sorted by first name by default. + $this->sortable(true, 'timecreated'); + } + + /** + * The context name column. + * + * @param stdClass $data The row data. + * @return string + * @throws coding_exception + */ + public function col_name($data) { + global $OUTPUT; + $context = context_helper::instance_by_id($data->contextid); + $parent = $context->get_parent_context(); + $contextdata = (object)[ + 'name' => $context->get_context_name(false, true), + 'parent' => $parent->get_context_name(false, true), + ]; + $fullcontexts = $context->get_parent_contexts(true); + $contextsinpath = []; + foreach ($fullcontexts as $contextinpath) { + $contextsinpath[] = $contextinpath->get_context_name(false, true); + } + $infoicon = new pix_icon('i/info', implode(' / ', array_reverse($contextsinpath))); + $infoiconhtml = $OUTPUT->render($infoicon); + $name = html_writer::span(get_string('nameandparent', 'tool_dataprivacy', $contextdata), 'm-r-1'); + + return $name . $infoiconhtml; + } + + /** + * The context information column. + * + * @param stdClass $data The row data. + * @return string + * @throws coding_exception + */ + public function col_info($data) { + global $OUTPUT; + + $context = context_helper::instance_by_id($data->contextid); + + $children = $context->get_child_contexts(); + if (empty($children)) { + return get_string('none'); + } else { + $childnames = []; + foreach ($children as $child) { + $childnames[] = $child->get_context_name(false, true); + } + $infoicon = new pix_icon('i/info', implode(', ', $childnames)); + $infoiconhtml = $OUTPUT->render($infoicon); + $name = html_writer::span(get_string('nchildren', 'tool_dataprivacy', count($children)), 'm-r-1'); + + return $name . $infoiconhtml; + } + } + + /** + * The category name column. + * + * @param stdClass $data The row data. + * @return mixed + * @throws coding_exception + * @throws dml_exception + */ + public function col_category($data) { + $context = context_helper::instance_by_id($data->contextid); + $category = api::get_effective_context_category($context); + + return s($category->get('name')); + } + + /** + * The purpose column. + * + * @param stdClass $data The row data. + * @return string + * @throws coding_exception + */ + public function col_purpose($data) { + $purpose = $this->purposes[$data->contextid]; + + return s($purpose->get('name')); + } + + /** + * The retention period column. + * + * @param stdClass $data The row data. + * @return string + * @throws Exception + */ + public function col_retentionperiod($data) { + global $PAGE; + + $purpose = $this->purposes[$data->contextid]; + + $exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]); + $exportedpurpose = $exporter->export($PAGE->get_renderer('core')); + + return $exportedpurpose->formattedretentionperiod; + } + + /** + * The timecreated a.k.a. the context expiry date column. + * + * @param stdClass $data The row data. + * @return string + */ + public function col_timecreated($data) { + return userdate($data->timecreated); + } + + /** + * Generate the select column. + * + * @param stdClass $data The row data. + * @return string + */ + public function col_select($data) { + $id = $data->id; + return html_writer::checkbox('expiredcontext_' . $id, $id, $this->selectall, '', ['class' => 'selectcontext']); + } + + /** + * Query the database for results to display in the table. + * + * @param int $pagesize size of page for paginated displayed table. + * @param bool $useinitialsbar do you want to use the initials bar. + * @throws dml_exception + * @throws coding_exception + */ + public function query_db($pagesize, $useinitialsbar = true) { + // Only count expired contexts that are awaiting confirmation. + $total = expired_context::get_record_count_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED); + $this->pagesize($pagesize, $total); + + $sort = $this->get_sql_sort(); + if (empty($sort)) { + $sort = 'timecreated'; + } + + // Only load expired contexts that are awaiting confirmation. + $expiredcontexts = expired_context::get_records_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED, + $sort, $this->get_page_start(), $this->get_page_size()); + $this->rawdata = []; + foreach ($expiredcontexts as $persistent) { + $data = $persistent->to_record(); + + $context = context_helper::instance_by_id($data->contextid); + + $purpose = api::get_effective_context_purpose($context); + $this->purposes[$data->contextid] = $purpose; + $this->rawdata[] = $data; + } + + // Set initial bars. + if ($useinitialsbar) { + $this->initialbars($total > $pagesize); + } + } + + /** + * Override default implementation to display a more meaningful information to the user. + */ + public function print_nothing_to_display() { + global $OUTPUT; + echo $this->render_reset_button(); + $this->print_initials_bar(); + echo $OUTPUT->notification(get_string('noexpiredcontexts', 'tool_dataprivacy'), 'warning'); + } + + /** + * Override the table's show_hide_link method to prevent the show/hide link for the select column from rendering. + * + * @param string $column the column name, index into various names. + * @param int $index numerical index of the column. + * @return string HTML fragment. + */ + protected function show_hide_link($column, $index) { + if ($index < 6) { + return parent::show_hide_link($column, $index); + } + return ''; + } +} diff --git a/admin/tool/dataprivacy/classes/output/my_data_requests_page.php b/admin/tool/dataprivacy/classes/output/my_data_requests_page.php new file mode 100644 index 00000000000..0e0da54121f --- /dev/null +++ b/admin/tool/dataprivacy/classes/output/my_data_requests_page.php @@ -0,0 +1,124 @@ +. + +/** + * Class containing data for a user's data requests. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\output; +defined('MOODLE_INTERNAL') || die(); + +use action_menu; +use action_menu_link_secondary; +use coding_exception; +use context_user; +use moodle_exception; +use moodle_url; +use renderable; +use renderer_base; +use stdClass; +use templatable; +use tool_dataprivacy\api; +use tool_dataprivacy\data_request; +use tool_dataprivacy\external\data_request_exporter; + +/** + * Class containing data for a user's data requests. + * + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class my_data_requests_page implements renderable, templatable { + + /** @var array $requests List of data requests. */ + protected $requests = []; + + /** + * Construct this renderable. + * + * @param data_request[] $requests + */ + public function __construct($requests) { + $this->requests = $requests; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + * @throws coding_exception + * @throws moodle_exception + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->newdatarequesturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php'); + + $requests = []; + foreach ($this->requests as $request) { + $requestid = $request->get('id'); + $status = $request->get('status'); + $userid = $request->get('userid'); + $usercontext = context_user::instance($userid); + $requestexporter = new data_request_exporter($request, ['context' => $usercontext]); + $item = $requestexporter->export($output); + + $candownload = false; + $cancancel = true; + switch ($status) { + case api::DATAREQUEST_STATUS_COMPLETE: + $item->statuslabelclass = 'label-success'; + $item->statuslabel = get_string('statuscomplete', 'tool_dataprivacy'); + $cancancel = false; + $candownload = true; + break; + case api::DATAREQUEST_STATUS_CANCELLED: + case api::DATAREQUEST_STATUS_REJECTED: + $cancancel = false; + break; + } + + // Prepare actions. + $actions = []; + if ($cancancel) { + $cancelurl = new moodle_url('#'); + $canceldata = ['data-action' => 'cancel', 'data-requestid' => $requestid]; + $canceltext = get_string('cancelrequest', 'tool_dataprivacy'); + $actions[] = new action_menu_link_secondary($cancelurl, null, $canceltext, $canceldata); + } + if ($candownload) { + $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $requestid, '/', + 'export.zip', true); + $downloadtext = get_string('download', 'tool_dataprivacy'); + $actions[] = new action_menu_link_secondary($downloadurl, null, $downloadtext); + } + if (!empty($actions)) { + $actionsmenu = new action_menu($actions); + $actionsmenu->set_menu_trigger(get_string('actions')); + $actionsmenu->set_owner_selector('request-actions-' . $requestid); + $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL); + $item->actions = $actionsmenu->export_for_template($output); + } + + $requests[] = $item; + } + $data->requests = $requests; + return $data; + } +} diff --git a/admin/tool/dataprivacy/classes/output/purposes.php b/admin/tool/dataprivacy/classes/output/purposes.php new file mode 100644 index 00000000000..ea55b2a5915 --- /dev/null +++ b/admin/tool/dataprivacy/classes/output/purposes.php @@ -0,0 +1,88 @@ +. + +/** + * Purposes renderable. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use stdClass; +use templatable; +use tool_dataprivacy\external\purpose_exporter; + +/** + * Class containing the purposes page renderable. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class purposes extends crud_element implements renderable, templatable { + + /** @var array $purposes All system purposes. */ + protected $purposes = []; + + /** + * Construct this renderable. + * + * @param \tool_dataprivacy\purpose[] $purposes + */ + public function __construct($purposes) { + $this->purposes = $purposes; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $PAGE; + + $context = \context_system::instance(); + + $PAGE->requires->js_call_amd('tool_dataprivacy/purposesactions', 'init'); + $PAGE->requires->js_call_amd('tool_dataprivacy/add_purpose', 'getInstance', [$context->id]); + + $data = new stdClass(); + + // Navigation links. + $data->navigation = []; + $navigationlinks = $this->get_navigation(); + foreach ($navigationlinks as $navlink) { + $data->navigation[] = $navlink->export_for_template($output); + } + + $data->purposes = []; + foreach ($this->purposes as $purpose) { + $exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]); + $exportedpurpose = $exporter->export($output); + + $actionmenu = $this->action_menu('purpose', $exportedpurpose, $purpose); + $exportedpurpose->actions = $actionmenu->export_for_template($output); + $data->purposes[] = $exportedpurpose; + } + + return $data; + } +} diff --git a/admin/tool/dataprivacy/classes/output/renderer.php b/admin/tool/dataprivacy/classes/output/renderer.php new file mode 100644 index 00000000000..c08f07fbc7a --- /dev/null +++ b/admin/tool/dataprivacy/classes/output/renderer.php @@ -0,0 +1,129 @@ +. + +/** + * Renderer class for tool_dataprivacy + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\output; + +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use html_writer; +use moodle_exception; +use plugin_renderer_base; + +/** + * Renderer class for tool_dataprivacy. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Render the user's data requests page. + * + * @param my_data_requests_page $page + * @return string html for the page + * @throws moodle_exception + */ + public function render_my_data_requests_page(my_data_requests_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_dataprivacy/my_data_requests', $data); + } + + /** + * Render the contact DPO link. + * + * @param string $replytoemail The Reply-to email address + * @return string The HTML for the link. + * @throws coding_exception + */ + public function render_contact_dpo_link($replytoemail) { + $params = [ + 'data-action' => 'contactdpo', + 'data-replytoemail' => $replytoemail + ]; + return html_writer::link('#', get_string('contactdataprotectionofficer', 'tool_dataprivacy'), $params); + } + + /** + * Render the data requests page for the DPO. + * + * @param data_requests_page $page + * @return string html for the page + * @throws moodle_exception + */ + public function render_data_requests_page(data_requests_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_dataprivacy/data_requests', $data); + } + + /** + * Render the data registry. + * + * @param data_registry_page $page + * @return string html for the page + * @throws moodle_exception + */ + public function render_data_registry_page(data_registry_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_dataprivacy/data_registry', $data); + } + + /** + * Render the purposes management page. + * + * @param purposes $page + * @return string html for the page + * @throws moodle_exception + */ + public function render_purposes(purposes $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_dataprivacy/purposes', $data); + } + + /** + * Render the categories management page. + * + * @param categories $page + * @return string html for the page + * @throws moodle_exception + */ + public function render_categories(categories $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_dataprivacy/categories', $data); + } + + /** + * Render the review page for the deletion of expired contexts. + * + * @param data_deletion_page $page + * @return string html for the page + * @throws moodle_exception + */ + public function render_data_deletion_page(data_deletion_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_dataprivacy/data_deletion', $data); + } +} diff --git a/admin/tool/dataprivacy/classes/page_helper.php b/admin/tool/dataprivacy/classes/page_helper.php new file mode 100644 index 00000000000..0a8cf7317f3 --- /dev/null +++ b/admin/tool/dataprivacy/classes/page_helper.php @@ -0,0 +1,85 @@ +. + +/** + * Page helper. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy; +use context_system; +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Page helper. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class page_helper { + + /** + * Sets up $PAGE for data privacy admin pages. + * + * @param moodle_url $url The page URL. + * @param string $title The page's title. + * @param string $attachtoparentnode The parent navigation node where this page can be accessed from. + * @param string $requiredcapability The required capability to view this page. + */ + public static function setup(moodle_url $url, $title, $attachtoparentnode = '', + $requiredcapability = 'tool/dataprivacy:managedataregistry') { + global $PAGE; + + $context = context_system::instance(); + + require_login(); + if (isguestuser()) { + print_error('noguest'); + } + + // TODO Check that data privacy is enabled. + require_capability($requiredcapability, $context); + + $PAGE->navigation->override_active_url($url); + + $PAGE->set_url($url); + $PAGE->set_context($context); + $PAGE->set_pagelayout('admin'); + $PAGE->set_title($title); + $PAGE->set_heading($title); + + // If necessary, override the settings navigation to add this page into the breadcrumb navigation. + if ($attachtoparentnode) { + if ($siteadmin = $PAGE->settingsnav->find('root', \navigation_node::TYPE_SITE_ADMIN)) { + $PAGE->navbar->add($siteadmin->get_content(), $siteadmin->action()); + } + if ($dataprivacy = $PAGE->settingsnav->find('privacy', \navigation_node::TYPE_SETTING)) { + $PAGE->navbar->add($dataprivacy->get_content(), $dataprivacy->action()); + } + if ($dataregistry = $PAGE->settingsnav->find($attachtoparentnode, \navigation_node::TYPE_SETTING)) { + $PAGE->navbar->add($dataregistry->get_content(), $dataregistry->action()); + } + + $PAGE->navbar->add($title, $url); + } + } +} diff --git a/admin/tool/dataprivacy/classes/privacy/provider.php b/admin/tool/dataprivacy/classes/privacy/provider.php new file mode 100644 index 00000000000..89bec002401 --- /dev/null +++ b/admin/tool/dataprivacy/classes/privacy/provider.php @@ -0,0 +1,92 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package tool_dataprivacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\context; +use core_privacy\local\request\contextlist; + +class provider implements + // This tool stores user data. + \core_privacy\local\metadata\provider, + + // This tool may provide access to and deletion of user data. + \core_privacy\local\request\plugin\provider { + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection): collection { + $collection->add_database_table( + 'tool_dataprivacy_request', + [ + 'comments' => 'privacy:metadata:request:comments', + 'userid' => 'privacy:metadata:request:userid', + 'requestedby' => 'privacy:metadata:request:requestedby', + 'dpocomment' => 'privacy:metadata:request:dpocomment', + 'timecreated' => 'privacy:metadata:request:timecreated' + ], + 'privacy:metadata:request' + ); + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid): contextlist { + return new contextlist(); + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + } + + /** + * Delete all data for all users in the specified context. + * + * @param context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + } +} diff --git a/admin/tool/dataprivacy/classes/purpose.php b/admin/tool/dataprivacy/classes/purpose.php new file mode 100644 index 00000000000..1aa2fabdbed --- /dev/null +++ b/admin/tool/dataprivacy/classes/purpose.php @@ -0,0 +1,167 @@ +. + +/** + * Class for loading/storing data purposes from the DB. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_dataprivacy; + +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); + +/** + * Class for loading/storing data purposes from the DB. + * + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class purpose extends \core\persistent { + + /** + * Database table. + */ + const TABLE = 'tool_dataprivacy_purpose'; + + /** + * Extended constructor to fetch from the cache if available. + * + * @param int $id If set, this is the id of an existing record, used to load the data. + * @param stdClass $record If set will be passed to {@link self::from_record()}. + */ + public function __construct($id = 0, stdClass $record = null) { + global $CFG; + + if ($id) { + $cache = \cache::make('tool_dataprivacy', 'purpose'); + if ($data = $cache->get($id)) { + + // Replicate self::read. + $this->from_record($data); + + // Using validate() as self::$validated is private. + $this->validate(); + + // Now replicate the parent constructor. + if (!empty($record)) { + $this->from_record($record); + } + if ($CFG->debugdeveloper) { + $this->verify_protected_methods(); + } + return; + } + } + parent::__construct($id, $record); + } + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'name' => array( + 'type' => PARAM_TEXT, + 'description' => 'The purpose name.', + ), + 'description' => array( + 'type' => PARAM_RAW, + 'description' => 'The purpose description.', + 'null' => NULL_ALLOWED, + 'default' => '', + ), + 'descriptionformat' => array( + 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), + 'type' => PARAM_INT, + 'default' => FORMAT_HTML + ), + 'retentionperiod' => array( + 'type' => PARAM_ALPHANUM, + 'description' => 'Retention period. ISO_8601 durations format (as in DateInterval format).', + 'default' => '', + ), + 'protected' => array( + 'type' => PARAM_INT, + 'description' => 'Data retention with higher precedent over user\'s request to be forgotten.', + 'default' => '0', + ), + ); + } + + /** + * Adds the new record to the cache. + * + * @return null + */ + protected function after_create() { + $cache = \cache::make('tool_dataprivacy', 'purpose'); + $cache->set($this->get('id'), $this->to_record()); + } + + /** + * Updates the cache record. + * + * @param $result + * @return null + */ + protected function after_update($result) { + $cache = \cache::make('tool_dataprivacy', 'purpose'); + $cache->set($this->get('id'), $this->to_record()); + } + + /** + * Removes unnecessary stuff from db. + * + * @return null + */ + protected function before_delete() { + $cache = \cache::make('tool_dataprivacy', 'purpose'); + $cache->delete($this->get('id')); + } + + /** + * Is this purpose used?. + * + * @return null + */ + public function is_used() { + + if (\tool_dataprivacy\contextlevel::is_purpose_used($this->get('id')) || + \tool_dataprivacy\context_instance::is_purpose_used($this->get('id'))) { + return true; + } + + $pluginconfig = get_config('tool_dataprivacy'); + $levels = \context_helper::get_all_levels(); + foreach ($levels as $level => $classname) { + + list($purposevar, $unused) = \tool_dataprivacy\data_registry::var_names_from_context($classname); + if (!empty($pluginconfig->{$purposevar}) && $pluginconfig->{$purposevar} == $this->get('id')) { + return true; + } + } + + return false; + } +} diff --git a/admin/tool/dataprivacy/classes/task/delete_expired_contexts.php b/admin/tool/dataprivacy/classes/task/delete_expired_contexts.php new file mode 100644 index 00000000000..55deeecad1f --- /dev/null +++ b/admin/tool/dataprivacy/classes/task/delete_expired_contexts.php @@ -0,0 +1,69 @@ +. + +/** + * Scheduled task to delete expired context instances once they are approved for deletion. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\task; + +use coding_exception; +use core\task\scheduled_task; +use tool_dataprivacy\api; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); + +/** + * Scheduled task to delete expired context instances once they are approved for deletion. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delete_expired_contexts extends scheduled_task { + + /** + * Returns the task name. + * + * @return string + */ + public function get_name() { + return get_string('deleteexpiredcontextstask', 'tool_dataprivacy'); + } + + /** + * Run the task to delete context instances based on their retention periods. + * + */ + public function execute() { + $manager = new \tool_dataprivacy\expired_course_related_contexts(); + $deleted = $manager->delete(); + if ($deleted > 0) { + mtrace($deleted . ' course-related contexts have been deleted'); + } + $manager = new \tool_dataprivacy\expired_user_contexts(); + $deleted = $manager->delete(); + if ($deleted > 0) { + mtrace($deleted . ' user contexts have been deleted'); + } + } +} diff --git a/admin/tool/dataprivacy/classes/task/expired_retention_period.php b/admin/tool/dataprivacy/classes/task/expired_retention_period.php new file mode 100644 index 00000000000..11230676adb --- /dev/null +++ b/admin/tool/dataprivacy/classes/task/expired_retention_period.php @@ -0,0 +1,68 @@ +. + +/** + * Scheduled task to flag contexts as expired. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\task; + +use coding_exception; +use core\task\scheduled_task; +use tool_dataprivacy\api; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); + +/** + * Scheduled task to flag contexts as expired. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class expired_retention_period extends scheduled_task { + + /** + * Returns the task name. + * + * @return string + */ + public function get_name() { + return get_string('expiredretentionperiodtask', 'tool_dataprivacy'); + } + + /** + * Run the task to flag context instances as expired. + */ + public function execute() { + $manager = new \tool_dataprivacy\expired_course_related_contexts(); + $flagged = $manager->flag_expired(); + if ($flagged > 0) { + mtrace($flagged . ' course-related contexts have been flagged as expired'); + } + $manager = new \tool_dataprivacy\expired_user_contexts(); + $flagged = $manager->flag_expired(); + if ($flagged > 0) { + mtrace($flagged . ' user contexts have been flagged as expired'); + } + } +} diff --git a/admin/tool/dataprivacy/classes/task/initiate_data_request_task.php b/admin/tool/dataprivacy/classes/task/initiate_data_request_task.php new file mode 100644 index 00000000000..e6baa049951 --- /dev/null +++ b/admin/tool/dataprivacy/classes/task/initiate_data_request_task.php @@ -0,0 +1,95 @@ +. + +/** + * Adhoc task that processes a data request and prepares the user's metadata for review. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\task; + +use coding_exception; +use core\task\adhoc_task; +use moodle_exception; +use tool_dataprivacy\api; +use tool_dataprivacy\data_request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class that processes a data request and prepares the user's metadata for review. + * + * Custom data accepted: + * - requestid -> The ID of the data request to be processed. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class initiate_data_request_task extends adhoc_task { + + /** + * Run the task to initiate the data request process. + * + * @throws coding_exception + * @throws moodle_exception + */ + public function execute() { + global $CFG; + + require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php'); + + if (!isset($this->get_custom_data()->requestid)) { + throw new coding_exception('The custom data \'requestid\' is required.'); + } + $requestid = $this->get_custom_data()->requestid; + + $datarequest = new data_request($requestid); + + // Check if this request still needs to be processed. e.g. The user might have cancelled it before this task has run. + $status = $datarequest->get('status'); + if (!api::is_active($status)) { + mtrace('Request ' . $requestid . ' with status ' . $status . ' doesn\'t need to be processed. Skipping...'); + return; + } + + // Update the status of this request as pre-processing. + mtrace('Generating user metadata...'); + api::update_request_status($requestid, api::DATAREQUEST_STATUS_PREPROCESSING); + + // TODO: Add code here to process the request and prepare the metadata to for review. + + // When the preparation of the metadata finishes, update the request status to awaiting approval. + api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); + mtrace('User metadata generation complete...'); + + // Get the list of the site Data Protection Officers. + $dpos = api::get_site_dpos(); + + // Email the data request to the Data Protection Officer(s)/Admin(s). + foreach ($dpos as $dpo) { + $dponame = fullname($dpo); + if (api::notify_dpo($dpo, $datarequest)) { + mtrace('Message sent to DPO: ' . $dponame); + } else { + mtrace('A problem was encountered while sending the message to the DPO: ' . $dponame); + } + } + } +} diff --git a/admin/tool/dataprivacy/classes/task/process_data_request_task.php b/admin/tool/dataprivacy/classes/task/process_data_request_task.php new file mode 100644 index 00000000000..19435a293ad --- /dev/null +++ b/admin/tool/dataprivacy/classes/task/process_data_request_task.php @@ -0,0 +1,207 @@ +. + +/** + * Adhoc task that processes an approved data request and prepares/deletes the user's data. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_dataprivacy\task; + +use action_link; +use coding_exception; +use core\message\message; +use core\task\adhoc_task; +use core_user; +use moodle_exception; +use moodle_url; +use tool_dataprivacy\api; +use tool_dataprivacy\data_request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class that processes an approved data request and prepares/deletes the user's data. + * + * Custom data accepted: + * - requestid -> The ID of the data request to be processed. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class process_data_request_task extends adhoc_task { + + /** + * Run the task to initiate the data request process. + * + * @throws coding_exception + * @throws moodle_exception + */ + public function execute() { + global $CFG, $PAGE, $SITE; + + require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php'); + + if (!isset($this->get_custom_data()->requestid)) { + throw new coding_exception('The custom data \'requestid\' is required.'); + } + $requestid = $this->get_custom_data()->requestid; + + $requestpersistent = new data_request($requestid); + $request = $requestpersistent->to_record(); + + // Check if this request still needs to be processed. e.g. The user might have cancelled it before this task has run. + if ($request->status != api::DATAREQUEST_STATUS_APPROVED) { + mtrace("Request {$request->id} hasn\'t been approved yet or it has already been processed. Skipping..."); + return; + } + + // Get the user details now. We might not be able to retrieve it later if it's a deletion processing. + $foruser = core_user::get_user($request->userid); + $usercontext = \context_user::instance($foruser->id); + + // Update the status of this request as pre-processing. + mtrace('Processing request...'); + api::update_request_status($requestid, api::DATAREQUEST_STATUS_PROCESSING); + + if ($request->type == api::DATAREQUEST_TYPE_EXPORT) { + // TODO: Update this code to retrieve the approved_contextlist properly. + $manager = new \core_privacy\manager(); + $contextcollection = $manager->get_contexts_for_userid($foruser->id); + $approvedcollection = new \core_privacy\local\request\contextlist_collection($foruser->id); + foreach ($contextcollection as $contextlist) { + $approvedcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist($foruser, + $contextlist->get_component(), $contextlist->get_contextids())); + } + $exportedcontent = $manager->export_user_data($approvedcollection); + $fs = get_file_storage(); + $filerecord = new \stdClass; + $filerecord->component = 'tool_dataprivacy'; + $filerecord->contextid = $usercontext->id; + $filerecord->userid = $foruser->id; + $filerecord->filearea = 'export'; + $filerecord->filename = 'export.zip'; + $filerecord->filepath = '/'; + $filerecord->itemid = $requestid; + $filerecord->license = $CFG->sitedefaultlicense; + $filerecord->author = fullname($foruser); + // Save somewhere. + $thing = $fs->create_file_from_pathname($filerecord, $exportedcontent); + + } else if ($request->type == api::DATAREQUEST_TYPE_DELETE) { + // TODO: Update this code to retrieve the approved_contextlist properly. + $manager = new \core_privacy\manager(); + $contextcollection = $manager->get_contexts_for_userid($foruser->id); + $approvedcollection = new \core_privacy\local\request\contextlist_collection($foruser->id); + foreach ($contextcollection as $contextlist) { + $approvedcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist($foruser, + $contextlist->get_component(), $contextlist->get_contextids())); + } + $manager->delete_user_data($approvedcollection); + } + + // When the preparation of the metadata finishes, update the request status to awaiting approval. + api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE); + mtrace('The processing of the user data request has been completed...'); + + // Create message to notify the user regarding the processing results. + $dpo = core_user::get_user($request->dpo); + $message = new message(); + $message->courseid = $SITE->id; + $message->component = 'tool_dataprivacy'; + $message->name = 'datarequestprocessingresults'; + $message->userfrom = $dpo; + $message->replyto = $dpo->email; + $message->replytoname = fullname($dpo->email); + + $typetext = null; + // Prepare the context data for the email message body. + $messagetextdata = [ + 'username' => fullname($foruser) + ]; + + $output = $PAGE->get_renderer('tool_dataprivacy'); + $emailonly = false; + switch ($request->type) { + case api::DATAREQUEST_TYPE_EXPORT: + $typetext = get_string('requesttypeexport', 'tool_dataprivacy'); + // We want to notify the user in Moodle about the processing results. + $message->notification = 1; + $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php'); + $message->contexturl = $datarequestsurl; + $message->contexturlname = get_string('datarequests', 'tool_dataprivacy'); + // Message to the recipient. + $messagetextdata['message'] = get_string('resultdownloadready', 'tool_dataprivacy', $SITE->fullname); + // Prepare download link. + $downloadurl = new moodle_url('#'); // TODO: Replace with the proper download URL. + $downloadlink = new action_link($downloadurl, get_string('download', 'tool_dataprivacy')); + $messagetextdata['downloadlink'] = $downloadlink->export_for_template($output); + break; + case api::DATAREQUEST_TYPE_DELETE: + $typetext = get_string('requesttypedelete', 'tool_dataprivacy'); + // No point notifying a deleted user in Moodle. + $message->notification = 0; + // Message to the recipient. + $messagetextdata['message'] = get_string('resultdeleted', 'tool_dataprivacy', $SITE->fullname); + // Message will be sent to the deleted user via email only. + $emailonly = true; + break; + default: + throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy'); + } + + $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext); + $message->subject = $subject; + $message->fullmessageformat = FORMAT_HTML; + $message->userto = $foruser; + + // Render message email body. + $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_results_email', $messagetextdata); + $message->fullmessage = html_to_text($messagehtml); + $message->fullmessagehtml = $messagehtml; + + // Send message to the user involved. + if ($emailonly) { + email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml); + } else { + message_send($message); + } + mtrace('Message sent to user: ' . $messagetextdata['username']); + + // Send to the requester as well. requestedby is 0 if the request was made on behalf of the user by a DPO. + if (!empty($request->requestedby) && $foruser->id != $request->requestedby) { + $requestedby = core_user::get_user($request->requestedby); + $message->userto = $requestedby; + $messagetextdata['username'] = fullname($requestedby); + // Render message email body. + $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_results_email', $messagetextdata); + $message->fullmessage = html_to_text($messagehtml); + $message->fullmessagehtml = $messagehtml; + + // Send message. + if ($emailonly) { + email_to_user($requestedby, $dpo, $subject, $message->fullmessage, $messagehtml); + } else { + message_send($message); + } + mtrace('Message sent to requester: ' . $messagetextdata['username']); + } + } +} diff --git a/admin/tool/dataprivacy/createdatarequest.php b/admin/tool/dataprivacy/createdatarequest.php new file mode 100644 index 00000000000..b5121f14cca --- /dev/null +++ b/admin/tool/dataprivacy/createdatarequest.php @@ -0,0 +1,84 @@ +. + +/** + * Prints the contact form to the site's Data Protection Officer + * + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package tool_dataprivacy + */ + +require_once('../../../config.php'); +require_once('lib.php'); +require_once('classes/api.php'); +require_once('createdatarequest_form.php'); + +$manage = optional_param('manage', 0, PARAM_INT); + +$url = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', ['manage' => $manage]); + +$PAGE->set_url($url); + +require_login(); +if (isguestuser()) { + print_error('noguest'); +} + +// Return URL and context. +if ($manage) { + // For the case where DPO creates data requests on behalf of another user. + $returnurl = new moodle_url($CFG->wwwroot . '/admin/tool/dataprivacy/datarequests.php'); + $context = context_system::instance(); + // Make sure the user has the proper capability. + require_capability('tool/dataprivacy:managedatarequests', $context); +} else { + // For the case where a user makes request for themselves (or for their children if they are the parent). + $returnurl = new moodle_url($CFG->wwwroot . '/admin/tool/dataprivacy/mydatarequests.php'); + $context = context_user::instance($USER->id); +} +$PAGE->set_context($context); + +// If contactdataprotectionofficer is disabled, send the user back to the profile page, or the privacy policy page. +if (!\tool_dataprivacy\api::can_contact_dpo()) { + redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), \core\output\notification::NOTIFY_ERROR); +} + +$mform = new tool_dataprivacy_data_request_form($url->out(false)); + +// Data request cancelled. +if ($mform->is_cancelled()) { + redirect($returnurl); +} + +// Data request submitted. +if ($data = $mform->get_data()) { + \tool_dataprivacy\api::create_data_request($data->userid, $data->type, $data->comments); + + redirect($returnurl, get_string('requestsubmitted', 'tool_dataprivacy')); +} + +$title = get_string('contactdataprotectionofficer', 'tool_dataprivacy'); +$PAGE->set_heading($title); +$PAGE->set_title($title); +echo $OUTPUT->header(); +echo $OUTPUT->heading($title); + +echo $OUTPUT->box_start(); +$mform->display(); +echo $OUTPUT->box_end(); + +echo $OUTPUT->footer(); diff --git a/admin/tool/dataprivacy/createdatarequest_form.php b/admin/tool/dataprivacy/createdatarequest_form.php new file mode 100644 index 00000000000..b42aaf3de4a --- /dev/null +++ b/admin/tool/dataprivacy/createdatarequest_form.php @@ -0,0 +1,140 @@ +. + +/** + * The contact form to the site's Data Protection Officer + * + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package tool_dataprivacy + */ + +use tool_dataprivacy\api; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +/** + * The contact form to the site's Data Protection Officer + * + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package tool_dataprivacy + */ +class tool_dataprivacy_data_request_form extends moodleform { + + /** + * Form definition. + * + * @throws HTML_QuickForm_Error + * @throws coding_exception + * @throws dml_exception + */ + public function definition() { + global $DB, $USER; + $mform =& $this->_form; + + if (api::is_site_dpo($USER->id)) { + $options = [ + 'ajax' => 'tool_dataprivacy/form-user-selector', + 'multiple' => false + ]; + $mform->addElement('autocomplete', 'userid', get_string('requestfor', 'tool_dataprivacy'), [], $options); + $mform->addRule('userid', null, 'required', null, 'client'); + + } else { + // Get users whom you are being a guardian to if your role has the capability to make data requests for children. + $allusernames = get_all_user_name_fields(true, 'u'); + $sql = "SELECT u.id, $allusernames + FROM {role_assignments} ra, {context} c, {user} u + WHERE ra.userid = :userid + AND ra.contextid = c.id + AND c.instanceid = u.id + AND c.contextlevel = :contextlevel"; + $params = [ + 'userid' => $USER->id, + 'contextlevel' => CONTEXT_USER + ]; + $children = $DB->get_records_sql($sql, $params); + + if ($children) { + $useroptions = []; + $useroptions[$USER->id] = fullname($USER); + foreach ($children as $child) { + $childcontext = context_user::instance($child->id); + if (has_capability('tool/dataprivacy:makedatarequestsforchildren', $childcontext)) { + $useroptions[$child->id] = fullname($child); + } + } + $mform->addElement('autocomplete', 'userid', get_string('requestfor', 'tool_dataprivacy'), $useroptions); + $mform->addRule('userid', null, 'required', null, 'client'); + + } else { + // Requesting for self. + $mform->addElement('hidden', 'userid', $USER->id); + } + } + + $mform->setType('userid', PARAM_INT); + + // Subject access request type. + $options = [ + api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'), + api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy') + ]; + $mform->addElement('select', 'type', get_string('requesttype', 'tool_dataprivacy'), $options); + $mform->setType('type', PARAM_INT); + $mform->addHelpButton('type', 'requesttype', 'tool_dataprivacy'); + + // Request comments text area. + $textareaoptions = ['cols' => 60, 'rows' => 10]; + $mform->addElement('textarea', 'comments', get_string('requestcomments', 'tool_dataprivacy'), $textareaoptions); + $mform->setType('type', PARAM_ALPHANUM); + $mform->addHelpButton('comments', 'requestcomments', 'tool_dataprivacy'); + + // Action buttons. + $this->add_action_buttons(); + + } + + /** + * Form validation. + * + * @param array $data + * @param array $files + * @return array + * @throws coding_exception + * @throws dml_exception + */ + public function validation($data, $files) { + $errors = []; + + $validrequesttypes = [ + api::DATAREQUEST_TYPE_EXPORT, + api::DATAREQUEST_TYPE_DELETE + ]; + if (!in_array($data['type'], $validrequesttypes)) { + $errors['type'] = get_string('errorinvalidrequesttype', 'tool_dataprivacy'); + } + + if (api::has_ongoing_request($data['userid'], $data['type'])) { + $errors['type'] = get_string('errorrequestalreadyexists', 'tool_dataprivacy'); + } + + return $errors; + } +} diff --git a/admin/tool/dataprivacy/datadeletion.php b/admin/tool/dataprivacy/datadeletion.php new file mode 100644 index 00000000000..eaa5056abc2 --- /dev/null +++ b/admin/tool/dataprivacy/datadeletion.php @@ -0,0 +1,47 @@ +. + +/** + * Prints the data deletion main page. + * + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package tool_dataprivacy + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); + +$filter = optional_param('filter', CONTEXT_COURSE, PARAM_INT); + +$url = new moodle_url('/admin/tool/dataprivacy/datadeletion.php'); + +$title = get_string('datadeletion', 'tool_dataprivacy'); + +\tool_dataprivacy\page_helper::setup($url, $title); + +echo $OUTPUT->header(); + +$table = new \tool_dataprivacy\output\expired_contexts_table($filter); +$table->baseurl = $url; +$table->baseurl->param('filter', $filter); + +$datadeletionpage = new \tool_dataprivacy\output\data_deletion_page($filter, $table); + +$output = $PAGE->get_renderer('tool_dataprivacy'); +echo $output->render($datadeletionpage); + +echo $OUTPUT->footer(); diff --git a/admin/tool/dataprivacy/dataregistry.php b/admin/tool/dataprivacy/dataregistry.php new file mode 100644 index 00000000000..58da78550b7 --- /dev/null +++ b/admin/tool/dataprivacy/dataregistry.php @@ -0,0 +1,42 @@ +. + +/** + * Prints the data registry main page. + * + * @copyright 2018 onwards David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package tool_dataprivacy + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); + +$contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT); +$contextid = optional_param('contextid', 0, PARAM_INT); + +$url = new moodle_url('/admin/tool/dataprivacy/dataregistry.php'); +$title = get_string('dataregistry', 'tool_dataprivacy'); + +\tool_dataprivacy\page_helper::setup($url, $title); + +$output = $PAGE->get_renderer('tool_dataprivacy'); +echo $output->header(); + +$dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid); + +echo $output->render($dataregistry); +echo $OUTPUT->footer(); diff --git a/admin/tool/dataprivacy/datarequests.php b/admin/tool/dataprivacy/datarequests.php new file mode 100644 index 00000000000..d2d9123e082 --- /dev/null +++ b/admin/tool/dataprivacy/datarequests.php @@ -0,0 +1,42 @@ +. + +/** + * Prints the contact form to the site's Data Protection Officer + * + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package tool_dataprivacy + */ + +require_once("../../../config.php"); +require_once('lib.php'); + +$url = new moodle_url('/admin/tool/dataprivacy/datarequests.php'); + +$title = get_string('datarequests', 'tool_dataprivacy'); + +\tool_dataprivacy\page_helper::setup($url, $title, '', 'tool/dataprivacy:managedatarequests'); + +echo $OUTPUT->header(); +echo $OUTPUT->heading($title); + +$requests = tool_dataprivacy\api::get_data_requests(); +$requestlist = new tool_dataprivacy\output\data_requests_page($requests); +$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy'); +echo $requestlistoutput->render($requestlist); + +echo $OUTPUT->footer(); diff --git a/admin/tool/dataprivacy/db/access.php b/admin/tool/dataprivacy/db/access.php new file mode 100644 index 00000000000..ecc2ec3b899 --- /dev/null +++ b/admin/tool/dataprivacy/db/access.php @@ -0,0 +1,52 @@ +. + +/** + * Capability definitions for this module. + * + * @package tool_dataprivacy + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + + // Capability for managing data requests. Usually given to the site's Data Protection Officer. + 'tool/dataprivacy:managedatarequests' => [ + 'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_DATALOSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [] + ], + + // Capability for managing the data registry. Usually given to the site's Data Protection Officer. + 'tool/dataprivacy:managedataregistry' => [ + 'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_DATALOSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [] + ], + + // Capability for parents/guardians to make data requests on behalf of their children. + 'tool/dataprivacy:makedatarequestsforchildren' => [ + 'riskbitmask' => RISK_SPAM | RISK_PERSONAL, + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => [] + ], +]; diff --git a/admin/tool/dataprivacy/db/caches.php b/admin/tool/dataprivacy/db/caches.php new file mode 100644 index 00000000000..00124bb34c0 --- /dev/null +++ b/admin/tool/dataprivacy/db/caches.php @@ -0,0 +1,43 @@ +. + +/** + * tool_dataprivacy cache definitions. + * + * @package tool_dataprivacy + * @category cache + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$definitions = array( + 'purpose' => array( + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => true, + 'staticacceleration' => true, + 'staticaccelerationsize' => 30, + ), + 'contextlevel' => array( + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => true, + 'staticacceleration' => true, + 'staticaccelerationsize' => 10, + ), +); diff --git a/admin/tool/dataprivacy/db/install.xml b/admin/tool/dataprivacy/db/install.xml new file mode 100644 index 00000000000..be5e889df40 --- /dev/null +++ b/admin/tool/dataprivacy/db/install.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+
+
diff --git a/admin/tool/dataprivacy/db/messages.php b/admin/tool/dataprivacy/db/messages.php new file mode 100644 index 00000000000..685d9b29ef0 --- /dev/null +++ b/admin/tool/dataprivacy/db/messages.php @@ -0,0 +1,44 @@ +. + +/** + * Defines message providers (types of messages being sent) + * + * @package tool_dataprivacy + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$messageproviders = [ + // Notify Data Protection Officer about incoming data requests. + 'contactdataprotectionofficer' => [ + 'defaults' => [ + 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, + 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, + ], + 'capability' => 'tool/dataprivacy:managedatarequests' + ], + + // Notify user about the processing results of their data request. + 'datarequestprocessingresults' => [ + 'defaults' => [ + 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, + 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, + ] + ], +]; diff --git a/admin/tool/dataprivacy/db/services.php b/admin/tool/dataprivacy/db/services.php new file mode 100644 index 00000000000..b72b2010dc4 --- /dev/null +++ b/admin/tool/dataprivacy/db/services.php @@ -0,0 +1,166 @@ +. +/** + * Chat external functions and service definitions. + * + * @package tool_dataprivacy + * @category external + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die; +$functions = [ + 'tool_dataprivacy_cancel_data_request' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'cancel_data_request', + 'classpath' => '', + 'description' => 'Cancel the data request made by the user', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_contact_dpo' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'contact_dpo', + 'classpath' => '', + 'description' => 'Contact the site Data Protection Officer(s)', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_get_data_request' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'get_data_request', + 'classpath' => '', + 'description' => 'Fetch the details of a user\'s data request', + 'type' => 'read', + 'capabilities' => 'tool/dataprivacy:managedatarequests', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_approve_data_request' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'approve_data_request', + 'classpath' => '', + 'description' => 'Approve a data request', + 'type' => 'write', + 'capabilities' => 'tool/dataprivacy:managedatarequests', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_deny_data_request' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'deny_data_request', + 'classpath' => '', + 'description' => 'Deny a data request', + 'type' => 'write', + 'capabilities' => 'tool/dataprivacy:managedatarequests', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_get_users' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'get_users', + 'classpath' => '', + 'description' => 'Fetches a list of users', + 'type' => 'read', + 'capabilities' => 'tool/dataprivacy:managedatarequests', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_create_purpose_form' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'create_purpose_form', + 'classpath' => '', + 'description' => 'Adds a data purpose', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_create_category_form' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'create_category_form', + 'classpath' => '', + 'description' => 'Adds a data category', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_delete_purpose' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'delete_purpose', + 'classpath' => '', + 'description' => 'Deletes an existing data purpose', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_delete_category' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'delete_category', + 'classpath' => '', + 'description' => 'Deletes an existing data category', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_set_contextlevel_form' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'set_contextlevel_form', + 'classpath' => '', + 'description' => 'Sets purpose and category across a context level', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_set_context_form' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'set_context_form', + 'classpath' => '', + 'description' => 'Sets purpose and category for a specific context', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_tree_extra_branches' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'tree_extra_branches', + 'classpath' => '', + 'description' => 'Return branches for the context tree', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], + 'tool_dataprivacy_confirm_contexts_for_deletion' => [ + 'classname' => 'tool_dataprivacy\external', + 'methodname' => 'confirm_contexts_for_deletion', + 'classpath' => '', + 'description' => 'Mark the selected expired contexts as confirmed for deletion', + 'type' => 'write', + 'capabilities' => '', + 'ajax' => true, + 'loginrequired' => true, + ], +]; diff --git a/admin/tool/dataprivacy/db/tasks.php b/admin/tool/dataprivacy/db/tasks.php new file mode 100644 index 00000000000..3a4915b5419 --- /dev/null +++ b/admin/tool/dataprivacy/db/tasks.php @@ -0,0 +1,46 @@ +. + +/** + * This file defines tasks performed by the tool. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// List of tasks. +$tasks = array( + array( + 'classname' => 'tool_dataprivacy\task\expired_retention_period', + 'blocking' => 0, + 'minute' => '0', + 'hour' => 'R', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), array( + 'classname' => 'tool_dataprivacy\task\delete_expired_contexts', + 'blocking' => 0, + 'minute' => '0', + 'hour' => 'R', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), +); diff --git a/admin/tool/dataprivacy/db/upgrade.php b/admin/tool/dataprivacy/db/upgrade.php new file mode 100644 index 00000000000..c7788230cc0 --- /dev/null +++ b/admin/tool/dataprivacy/db/upgrade.php @@ -0,0 +1,44 @@ +. + +/** + * tool_dataprivacy plugin upgrade code + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Function to upgrade auth_cas. + * @param int $oldversion the version we are upgrading from + * @return bool result + */ +function xmldb_tool_dataprivacy_upgrade($oldversion) { + global $CFG, $DB; + + $dbman = $DB->get_manager(); + + if ($oldversion < 2018040500) { + // Nothing to do here. Moodle Plugins site's just complaining about missing upgrade.php. + // Savepoint reached. + upgrade_plugin_savepoint(true, 2018040500, 'tool', 'dataprivacy'); + } + + return true; +} diff --git a/admin/tool/dataprivacy/defaults.php b/admin/tool/dataprivacy/defaults.php new file mode 100644 index 00000000000..06119084c6f --- /dev/null +++ b/admin/tool/dataprivacy/defaults.php @@ -0,0 +1,76 @@ +. + +/** + * This page lets users manage default purposes and categories. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); + +$url = new \moodle_url('/admin/tool/dataprivacy/defaults.php'); +$title = get_string('setdefaults', 'tool_dataprivacy'); + +\tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); + +$levels = \context_helper::get_all_levels(); +// They are set through the context level site and user. +unset($levels[CONTEXT_SYSTEM]); +unset($levels[CONTEXT_USER]); + +$customdata = [ + 'levels' => $levels, + 'purposes' => \tool_dataprivacy\api::get_purposes(), + 'categories' => \tool_dataprivacy\api::get_categories(), +]; +$form = new \tool_dataprivacy\form\defaults($PAGE->url->out(false), $customdata); + +$toform = new stdClass(); +foreach ($levels as $level => $classname) { + list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname); + $toform->{$purposevar} = get_config('tool_dataprivacy', $purposevar); + $toform->{$categoryvar} = get_config('tool_dataprivacy', $categoryvar); +} +$form->set_data($toform); + +$returnurl = new \moodle_url('/admin/tool/dataprivacy/dataregistry.php'); +if ($form->is_cancelled()) { + redirect($returnurl); +} else if ($data = $form->get_data()) { + + foreach ($levels as $level => $classname) { + + list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname); + + if (isset($data->{$purposevar})) { + set_config($purposevar, $data->{$purposevar}, 'tool_dataprivacy'); + } + if (isset($data->{$categoryvar})) { + set_config($categoryvar, $data->{$categoryvar}, 'tool_dataprivacy'); + } + } + redirect($returnurl, get_string('defaultssaved', 'tool_dataprivacy'), + 0, \core\output\notification::NOTIFY_SUCCESS); +} + +$output = $PAGE->get_renderer('tool_dataprivacy'); +echo $output->header(); +$form->display(); +echo $output->footer(); diff --git a/admin/tool/dataprivacy/editcategory.php b/admin/tool/dataprivacy/editcategory.php new file mode 100644 index 00000000000..a2f59fb4d72 --- /dev/null +++ b/admin/tool/dataprivacy/editcategory.php @@ -0,0 +1,58 @@ +. + +/** + * This page lets users manage categories. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +$id = optional_param('id', 0, PARAM_INT); + +$url = new \moodle_url('/admin/tool/dataprivacy/editcategory.php', array('id' => $id)); +if ($id) { + $title = get_string('editcategory', 'tool_dataprivacy'); +} else { + $title = get_string('addcategory', 'tool_dataprivacy'); +} +\tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); + +$category = new \tool_dataprivacy\category($id); +$form = new \tool_dataprivacy\form\category($PAGE->url->out(false), + array('persistent' => $category, 'showbuttons' => true)); + +$returnurl = new \moodle_url('/admin/tool/dataprivacy/categories.php'); +if ($form->is_cancelled()) { + redirect($returnurl); +} else if ($data = $form->get_data()) { + if (empty($data->id)) { + \tool_dataprivacy\api::create_category($data); + $messagesuccess = get_string('categorycreated', 'tool_dataprivacy'); + } else { + \tool_dataprivacy\api::update_category($data); + $messagesuccess = get_string('categoryupdated', 'tool_dataprivacy'); + } + redirect($returnurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS); +} + +$output = $PAGE->get_renderer('tool_dataprivacy'); +echo $output->header(); +$form->display(); +echo $output->footer(); diff --git a/admin/tool/dataprivacy/editpurpose.php b/admin/tool/dataprivacy/editpurpose.php new file mode 100644 index 00000000000..d7013dbd4a4 --- /dev/null +++ b/admin/tool/dataprivacy/editpurpose.php @@ -0,0 +1,59 @@ +. + +/** + * This page lets users manage purposes. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +$id = optional_param('id', 0, PARAM_INT); + +$url = new \moodle_url('/admin/tool/dataprivacy/editpurpose.php', array('id' => $id)); +if ($id) { + $title = get_string('editpurpose', 'tool_dataprivacy'); +} else { + $title = get_string('addpurpose', 'tool_dataprivacy'); +} + +\tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); + +$purpose = new \tool_dataprivacy\purpose($id); +$form = new \tool_dataprivacy\form\purpose($PAGE->url->out(false), + array('persistent' => $purpose, 'showbuttons' => true)); + +$returnurl = new \moodle_url('/admin/tool/dataprivacy/purposes.php'); +if ($form->is_cancelled()) { + redirect($returnurl); +} else if ($data = $form->get_data()) { + if (empty($data->id)) { + \tool_dataprivacy\api::create_purpose($data); + $messagesuccess = get_string('purposecreated', 'tool_dataprivacy'); + } else { + \tool_dataprivacy\api::update_purpose($data); + $messagesuccess = get_string('purposeupdated', 'tool_dataprivacy'); + } + redirect($returnurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS); +} + +$output = $PAGE->get_renderer('tool_dataprivacy'); +echo $output->header(); +$form->display(); +echo $output->footer(); diff --git a/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php b/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php new file mode 100644 index 00000000000..3df7aa49fde --- /dev/null +++ b/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php @@ -0,0 +1,171 @@ +. + +/** + * Strings for component 'tool_dataprivacy' + * + * @package tool_dataprivacy + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['pluginname'] = 'Data privacy'; +$string['pluginname_help'] = 'Data privacy plugin'; +$string['activitiesandresources'] = 'Activities and resources'; +$string['addcategory'] = 'Add category'; +$string['addpurpose'] = 'Add purpose'; +$string['approve'] = 'Approve'; +$string['approverequest'] = 'Approve request'; +$string['cancelrequest'] = 'Cancel request'; +$string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?'; +$string['categories'] = 'Categories'; +$string['category'] = 'Category'; +$string['categorycreated'] = 'Category created'; +$string['categorieslist'] = 'List of data categories'; +$string['categoryupdated'] = 'Category updated'; +$string['close'] = 'Close'; +$string['confirmapproval'] = 'Do you really want to approve this data request?'; +$string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.'; +$string['confirmdenial'] = 'Do you really want deny this data request?'; +$string['contactdataprotectionofficer'] = 'Contact Data Protection Officer'; +$string['contactdataprotectionofficer_desc'] = 'Enabling this feature will provide a link for users to contact the site\'s Data Protection Officer through this site. This link will be shown on their profile page, and on the site\'s privacy policy page, as well. The link leads to a form in which the user can make a data request to the Data Protection Officer.'; +$string['contextlevelname10'] = 'Site'; +$string['contextlevelname30'] = 'Users'; +$string['contextlevelname40'] = 'Course categories'; +$string['contextlevelname50'] = 'Courses'; +$string['contextlevelname70'] = 'Activity modules'; +$string['contextlevelname80'] = 'Blocks'; +$string['contextpurposecategorysaved'] = 'Purpose and category saved.'; +$string['contactdpoviaprivacypolicy'] = 'Please contact the site\'s Data Protection Officer as described in the Privacy Policy'; +$string['createcategory'] = 'Create data category'; +$string['createpurpose'] = 'Create data purpose'; +$string['datadeletion'] = 'Data deletion'; +$string['datadeletionpagehelp'] = 'This page lists the contexts that are already past their retention period and need to be confirmed for user data deletion. Once the selected contexts have been confirmed for deletion, the user data related to these contexts will be deleted on the next execution of the "Delete expired contexts" scheduled task.'; +$string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for children'; +$string['dataprivacy:managedatarequests'] = 'Manage data requests'; +$string['dataprivacy:managedataregistry'] = 'Manage data registry'; +$string['dataregistry'] = 'Data registry'; +$string['datarequestemailsubject'] = 'Data request: {$a}'; +$string['datarequests'] = 'Data requests'; +$string['daterequested'] = 'Date requested'; +$string['daterequesteddetail'] = 'Date requested:'; +$string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.'; +$string['deletecategory'] = 'Delete "{$a}" category'; +$string['deletecategorytext'] = 'Are you sure you want to delete "{$a}" category?'; +$string['deleteexpiredcontextstask'] = 'Delete expired contexts'; +$string['deletepurpose'] = 'Delete "{$a}" purpose'; +$string['deletepurposetext'] = 'Are you sure you want to delete "{$a}" purpose?'; +$string['defaultssaved'] = 'Defaults saved'; +$string['deny'] = 'Deny'; +$string['denyrequest'] = 'Deny request'; +$string['download'] = 'Download'; +$string['dporolemapping'] = 'Data Protection Officer role mapping'; +$string['dporolemapping_desc'] = 'Select one or more roles that map to the Data Protection Officer role. Users with these roles will be able to manage data requests. This requires the selected role(s) to have the capability \'tool/dataprivacy:managedatarequests\''; +$string['editcategories'] = 'Edit categories'; +$string['editcategory'] = 'Edit category'; +$string['editcategories'] = 'Edit categories'; +$string['editpurpose'] = 'Edit purpose'; +$string['editpurposes'] = 'Edit purposes'; +$string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)'; +$string['effectiveretentionperioduser'] = '{$a} (since the last time the user accessed the site)'; +$string['emailsalutation'] = 'Dear {$a},'; +$string['errorinvalidrequeststatus'] = 'Invalid request status!'; +$string['errorinvalidrequesttype'] = 'Invalid request type!'; +$string['errornoexpiredcontexts'] = 'There are no expired contexts to process'; +$string['errorcontexthasunexpiredchildren'] = 'The context "{$a}" still has sub-contexts that have not yet expired. No contexts have been flagged for deletion.'; +$string['errorrequestalreadyexists'] = 'You already have an ongoing request.'; +$string['errorrequestnotfound'] = 'Request not found'; +$string['errorrequestnotwaitingforapproval'] = 'The request is not awaiting approval. Either it is not yet ready or it has already been processed.'; +$string['errorsendingmessagetodpo'] = 'An error was encountered while trying to send a message to {$a}.'; +$string['expiredretentionperiodtask'] = 'Expired retention period'; +$string['expiry'] = 'Expiry'; +$string['frontpagecourse'] = 'Front page course'; +$string['inherit'] = 'Inherit'; +$string['messageprovider:contactdataprotectionofficer'] = 'Data requests'; +$string['messageprovider:datarequestprocessingresults'] = 'Data request processing results'; +$string['message'] = 'Message'; +$string['messagelabel'] = 'Message:'; +$string['moduleinstancename'] = '{$a->instancename} ({$a->modulename})'; +$string['mypersonaldatarequests'] = 'My personal data requests'; +$string['nameandparent'] = '{$a->parent} / {$a->name}'; +$string['nameemail'] = '{$a->name} ({$a->email})'; +$string['nchildren'] = '{$a} children'; +$string['newrequest'] = 'New request'; +$string['nodatarequests'] = 'There are no data requests'; +$string['noactivitiestoload'] = 'No activities'; +$string['noassignedroles'] = 'No assigned roles in this context'; +$string['noblockstoload'] = 'No blocks'; +$string['nocategories'] = 'There are no categories yet'; +$string['nocoursestoload'] = 'No activities'; +$string['noexpiredcontexts'] = 'There are no expired contexts in this level that need to be confirmed for deletion.'; +$string['nopersonaldatarequests'] = 'You don\'t have any personal data requests'; +$string['nopurposes'] = 'There are no purposes yet'; +$string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on'; +$string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.'; +$string['notset'] = 'Not set (use the default value)'; +$string['privacy'] = 'Privacy'; +$string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.'; +$string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.'; +$string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs'; +$string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.'; +$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s respective privacy officer regarding the request.'; +$string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.'; +$string['protected'] = 'Protected'; +$string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.'; +$string['purpose'] = 'Purpose'; +$string['purposecreated'] = 'Purpose created'; +$string['purposes'] = 'Purposes'; +$string['purposeslist'] = 'List of data purposes'; +$string['purposeupdated'] = 'Purpose updated'; +$string['replyto'] = 'Reply to'; +$string['requestactions'] = 'Actions'; +$string['requestby'] = 'Requested by'; +$string['requestcomments'] = 'Comments'; +$string['requestcomments_help'] = 'Please feel free to provide more details about your request'; +$string['requestemailintro'] = 'You have received a data request:'; +$string['requestfor'] = 'Requesting for'; +$string['requeststatus'] = 'Status'; +$string['requestsubmitted'] = 'Your request has been submitted to the site\'s Data Protection Officer'; +$string['requesttype'] = 'Type'; +$string['requesttype_help'] = 'Select the reason why you would like to contact the site\'s Data Protection Officer'; +$string['requesttypedelete'] = 'Delete all of my personal data'; +$string['requesttypedeleteshort'] = 'Delete'; +$string['requesttypeexport'] = 'Export all of my personal data'; +$string['requesttypeexportshort'] = 'Export'; +$string['requesttypeothers'] = 'General inquiry'; +$string['requesttypeothersshort'] = 'Others'; +$string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.'; +$string['resultdownloadready'] = 'Your copy of your personal data in {$a} that you recently requested is now available for download. Please click on the link below to go to the download page.'; +$string['reviewdata'] = 'Review data'; +$string['retentionperiod'] = 'Retention period'; +$string['retentionperiodnotdefined'] = 'No retention period was defined'; +$string['retentionperiodzero'] = 'No retention period'; +$string['send'] = 'Send'; +$string['setdefaults'] = 'Set defaults'; +$string['statusapproved'] = 'Approved'; +$string['statusawaitingapproval'] = 'Awaiting approval'; +$string['statuscancelled'] = 'Cancelled'; +$string['statuscomplete'] = 'Complete'; +$string['statusdetail'] = 'Status:'; +$string['statuspreprocessing'] = 'Pre-processing'; +$string['statusprocessing'] = 'Processing'; +$string['statuspending'] = 'Pending'; +$string['statusrejected'] = 'Rejected'; +$string['subjectscope'] = 'Subject scope'; +$string['user'] = 'User'; +$string['viewrequest'] = 'View the request'; diff --git a/admin/tool/dataprivacy/lib.php b/admin/tool/dataprivacy/lib.php new file mode 100644 index 00000000000..8e1e3722397 --- /dev/null +++ b/admin/tool/dataprivacy/lib.php @@ -0,0 +1,197 @@ +. + +/** + * Data privacy plugin library + * @package tool_dataprivacy + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_user\output\myprofile\tree; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Add nodes to myprofile page. + * + * @param tree $tree Tree object + * @param stdClass $user User object + * @param bool $iscurrentuser + * @param stdClass $course Course object + * @return bool + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ +function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser, $course) { + global $PAGE, $USER; + + // Get the Privacy and policies category. + if (!array_key_exists('privacyandpolicies', $tree->__get('categories'))) { + // Create the category. + $categoryname = get_string('privacyandpolicies', 'admin'); + $category = new core_user\output\myprofile\category('privacyandpolicies', $categoryname, 'contact'); + $tree->add_category($category); + } else { + // Get the existing category. + $category = $tree->__get('categories')['privacyandpolicies']; + } + + // Contact data protection officer link. + if (\tool_dataprivacy\api::can_contact_dpo() && $iscurrentuser) { + $renderer = $PAGE->get_renderer('tool_dataprivacy'); + $content = $renderer->render_contact_dpo_link($USER->email); + $node = new core_user\output\myprofile\node('privacyandpolicies', 'contactdpo', null, null, null, $content); + $category->add_node($node); + $PAGE->requires->js_call_amd('tool_dataprivacy/myrequestactions', 'init'); + + $url = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php'); + $node = new core_user\output\myprofile\node('privacyandpolicies', 'datarequests', + get_string('datarequests', 'tool_dataprivacy'), null, $url); + $category->add_node($node); + } + + // Add the Privacy category to the tree if it's not empty and it doesn't exist. + $nodes = $category->nodes; + if (!empty($nodes)) { + if (!array_key_exists('privacyandpolicies', $tree->__get('categories'))) { + $tree->add_category($category); + } + return true; + } + + return false; +} + +/** + * Fragment to add a new purpose. + * + * @param array $args The fragment arguments. + * @return string The rendered mform fragment. + */ +function tool_dataprivacy_output_fragment_addpurpose_form($args) { + + $formdata = []; + if (!empty($args['jsonformdata'])) { + $serialiseddata = json_decode($args['jsonformdata']); + parse_str($serialiseddata, $formdata); + } + + $persistent = new \tool_dataprivacy\purpose(); + $mform = new \tool_dataprivacy\form\purpose(null, ['persistent' => $persistent], + 'post', '', null, true, $formdata); + + if (!empty($args['jsonformdata'])) { + // Show errors if data was received. + $mform->is_validated(); + } + + return $mform->render(); +} + +/** + * Fragment to add a new category. + * + * @param array $args The fragment arguments. + * @return string The rendered mform fragment. + */ +function tool_dataprivacy_output_fragment_addcategory_form($args) { + + $formdata = []; + if (!empty($args['jsonformdata'])) { + $serialiseddata = json_decode($args['jsonformdata']); + parse_str($serialiseddata, $formdata); + } + + $persistent = new \tool_dataprivacy\category(); + $mform = new \tool_dataprivacy\form\category(null, ['persistent' => $persistent], + 'post', '', null, true, $formdata); + + if (!empty($args['jsonformdata'])) { + // Show errors if data was received. + $mform->is_validated(); + } + + return $mform->render(); +} + +/** + * Fragment to edit a context purpose and category. + * + * @param array $args The fragment arguments. + * @return string The rendered mform fragment. + */ +function tool_dataprivacy_output_fragment_context_form($args) { + global $PAGE; + + $contextid = $args[0]; + + $context = \context_helper::instance_by_id($contextid); + $customdata = \tool_dataprivacy\form\context_instance::get_context_instance_customdata($context); + + if (!empty($customdata['purposeretentionperiods'])) { + $PAGE->requires->js_call_amd('tool_dataprivacy/effective_retention_period', 'init', [$customdata['purposeretentionperiods']]); + } + $mform = new \tool_dataprivacy\form\context_instance(null, $customdata); + return $mform->render(); +} + +/** + * Fragment to edit a contextlevel purpose and category. + * + * @param array $args The fragment arguments. + * @return string The rendered mform fragment. + */ +function tool_dataprivacy_output_fragment_contextlevel_form($args) { + global $PAGE; + + $contextlevel = $args[0]; + $customdata = \tool_dataprivacy\form\contextlevel::get_contextlevel_customdata($contextlevel); + + if (!empty($customdata['purposeretentionperiods'])) { + $PAGE->requires->js_call_amd('tool_dataprivacy/effective_retention_period', 'init', [$customdata['purposeretentionperiods']]); + } + + $mform = new \tool_dataprivacy\form\contextlevel(null, $customdata); + return $mform->render(); +} + +/** + * Serves any files associated with the data privacy settings. + * + * @param stdClass $course Course object + * @param stdClass $cm Course module object + * @param context $context Context + * @param string $filearea File area for data privacy + * @param array $args Arguments + * @param bool $forcedownload If we are forcing the download + * @param array $options More options + * @return bool Returns false if we don't find a file. + */ +function tool_dataprivacy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { + if ($context->contextlevel == CONTEXT_USER) { + $fs = get_file_storage(); + $relativepath = implode('/', $args); + $fullpath = "/$context->id/tool_dataprivacy/$filearea/$relativepath"; + if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { + return false; + } + send_stored_file($file, 0, 0, $forcedownload, $options); + } else { + send_file_not_found(); + } +} diff --git a/admin/tool/dataprivacy/mydatarequests.php b/admin/tool/dataprivacy/mydatarequests.php new file mode 100644 index 00000000000..256809679e7 --- /dev/null +++ b/admin/tool/dataprivacy/mydatarequests.php @@ -0,0 +1,63 @@ +. + +/** + * Prints the contact form to the site's Data Protection Officer + * + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package tool_dataprivacy + */ + +require_once("../../../config.php"); +require_once('lib.php'); + +$courseid = optional_param('course', 0, PARAM_INT); + +$url = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php'); +if ($courseid) { + $url->param('course', $courseid); +} + +$PAGE->set_url($url); + +require_login(); +if (isguestuser()) { + print_error('noguest'); +} + +$usercontext = context_user::instance($USER->id); +$PAGE->set_context($usercontext); + +// Return URL. +$params = ['id' => $USER->id]; +if ($courseid) { + $params['course'] = $courseid; +} +$returnurl = new moodle_url('/user/profile.php', $params); + +$title = get_string('datarequests', 'tool_dataprivacy'); +$PAGE->set_heading($title); +$PAGE->set_title($title); +echo $OUTPUT->header(); +echo $OUTPUT->heading($title); + +$requests = tool_dataprivacy\api::get_data_requests($USER->id); +$requestlist = new tool_dataprivacy\output\my_data_requests_page($requests); +$requestlistoutput = $PAGE->get_renderer('tool_dataprivacy'); +echo $requestlistoutput->render($requestlist); + +echo $OUTPUT->footer(); diff --git a/admin/tool/dataprivacy/purposes.php b/admin/tool/dataprivacy/purposes.php new file mode 100644 index 00000000000..81d15173055 --- /dev/null +++ b/admin/tool/dataprivacy/purposes.php @@ -0,0 +1,39 @@ +. + +/** + * This page lets users manage purposes. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +$url = new moodle_url("/admin/tool/dataprivacy/purposes.php"); +$title = get_string('editpurposes', 'tool_dataprivacy'); + +\tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); + +$output = $PAGE->get_renderer('tool_dataprivacy'); +echo $output->header(); + +$purposes = \tool_dataprivacy\api::get_purposes(); +$renderable = new \tool_dataprivacy\output\purposes($purposes); + +echo $output->render($renderable); +echo $output->footer(); diff --git a/admin/tool/dataprivacy/settings.php b/admin/tool/dataprivacy/settings.php new file mode 100644 index 00000000000..e4b15384cca --- /dev/null +++ b/admin/tool/dataprivacy/settings.php @@ -0,0 +1,72 @@ +. + +/** + * Adds Data privacy-related settings. + * + * @package tool_dataprivacy + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($hassiteconfig) { + $privacysettings = $ADMIN->locate('privacysettings'); + + if ($ADMIN->fulltree) { + // Contact data protection officer. + $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/contactdataprotectionofficer', + new lang_string('contactdataprotectionofficer', 'tool_dataprivacy'), + new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 1) + ); + + // Fetch roles that are assignable. + $assignableroles = get_assignable_roles(context_system::instance()); + + // Fetch roles that have the capability to manage data requests. + $capableroles = get_roles_with_capability('tool/dataprivacy:managedatarequests'); + + // Role(s) that map to the Data Protection Officer role. These are assignable roles with the capability to manage data requests. + $roles = []; + foreach ($capableroles as $key => $role) { + if (array_key_exists($key, $assignableroles)) { + $roles[$key] = $assignableroles[$key]; + } + } + if (!empty($roles)) { + $privacysettings->add(new admin_setting_configmultiselect('tool_dataprivacy/dporoles', + new lang_string('dporolemapping', 'tool_dataprivacy'), + new lang_string('dporolemapping_desc', 'tool_dataprivacy'), null, $roles) + ); + } + } +} + +// Link that leads to the data requests management page. +$ADMIN->add('privacy', new admin_externalpage('datarequests', get_string('datarequests', 'tool_dataprivacy'), + new moodle_url('/admin/tool/dataprivacy/datarequests.php'), 'tool/dataprivacy:managedatarequests') +); + +// Link that leads to the data registry management page. +$ADMIN->add('privacy', new admin_externalpage('dataregistry', get_string('dataregistry', 'tool_dataprivacy'), + new moodle_url('/admin/tool/dataprivacy/dataregistry.php'), 'tool/dataprivacy:managedataregistry') +); + +// Link that leads to the review page of expired contexts that are up for deletion. +$ADMIN->add('privacy', new admin_externalpage('datadeletion', get_string('datadeletion', 'tool_dataprivacy'), + new moodle_url('/admin/tool/dataprivacy/datadeletion.php'), 'tool/dataprivacy:managedataregistry') +); diff --git a/admin/tool/dataprivacy/styles.css b/admin/tool/dataprivacy/styles.css new file mode 100644 index 00000000000..bff49a78ade --- /dev/null +++ b/admin/tool/dataprivacy/styles.css @@ -0,0 +1,14 @@ +.nav-pills .nav-pills { + margin-left: 1rem; +} + +/*Extra attribute selection to have preference over bs2's .moodle-actionmenu[data-enhance] */ +.data-registry > .top-nav > .singlebutton, +.data-registry > .top-nav > .moodle-actionmenu[data-owner='dataregistry-actions'] { + display: inline-block; +} + +.data-registry .context-tree { + height: 70vh; + overflow-y: scroll; +} diff --git a/admin/tool/dataprivacy/templates/categories.mustache b/admin/tool/dataprivacy/templates/categories.mustache new file mode 100644 index 00000000000..5bf63fb03f7 --- /dev/null +++ b/admin/tool/dataprivacy/templates/categories.mustache @@ -0,0 +1,88 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/categories + + Manage categories. + + Classes required for JS: + + Data attributes required for JS: + + Context variables required for this template: + * categories - array of objects + * actions - array of actions (already in HTML). + + Example context (json): + { + "categoriesexist": 1, + "categories": [ + { + "name" : "Category 1", + "description": "Description 1", + "actions": [ + ] + }, { + "name" : "Category 2", + "description": "Description 2", + "actions": [ + ] + } + ] + } +}} + +{{#navigation}} + {{> core/action_link}} +{{/navigation}} + +
+

{{#str}}categories, tool_dataprivacy{{/str}}

+
+ +
+ + + + + + + + + + + {{#categories}} + + + + + + {{/categories}} + +
{{#str}}categorieslist, tool_dataprivacy{{/str}}
{{#str}}name{{/str}}{{#str}}description{{/str}}{{#str}}actions{{/str}}
{{{name}}}{{{description}}} + {{#actions}} + {{> core/action_menu}} + {{/actions}} +
+ {{^categories}} +

+ {{#str}}nocategories, tool_dataprivacy{{/str}} +

+ {{/categories}} +
diff --git a/admin/tool/dataprivacy/templates/contact_dpo.mustache b/admin/tool/dataprivacy/templates/contact_dpo.mustache new file mode 100644 index 00000000000..886bf45b1db --- /dev/null +++ b/admin/tool/dataprivacy/templates/contact_dpo.mustache @@ -0,0 +1,58 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/contact_dpo + + The purpose of this template is to enable the user to contact the site's DPO via email. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * userid int The user's ID. + * email string The user's email address. + + Example context (json): + { + "userid": 1, + "replytoemail": "martha@example.com" + } +}} +
+
+ +
{{replytoemail}}
+
+
+ +
+ + +
+
+
+ diff --git a/admin/tool/dataprivacy/templates/context_tree_branches.mustache b/admin/tool/dataprivacy/templates/context_tree_branches.mustache new file mode 100644 index 00000000000..f30c470bd67 --- /dev/null +++ b/admin/tool/dataprivacy/templates/context_tree_branches.mustache @@ -0,0 +1,38 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/context_tree + + Context tree. + + Classes required for JS: + + Data attributes required for JS: + + Context variables required for this template: + + Example context (json): + { + } + +}} + +{{#branches}} + +{{/branches}} diff --git a/admin/tool/dataprivacy/templates/context_tree_node.mustache b/admin/tool/dataprivacy/templates/context_tree_node.mustache new file mode 100644 index 00000000000..7a02e06e9d6 --- /dev/null +++ b/admin/tool/dataprivacy/templates/context_tree_node.mustache @@ -0,0 +1,50 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/context_tree_branch + + A tree branch. + + Classes required for JS: + + Data attributes required for JS: + + Context variables required for this template: + + Example context (json): + { + } + +}} + + + + + + {{text}} + +{{> tool_dataprivacy/context_tree_branches}} diff --git a/admin/tool/dataprivacy/templates/data_deletion.mustache b/admin/tool/dataprivacy/templates/data_deletion.mustache new file mode 100644 index 00000000000..d87f2318b47 --- /dev/null +++ b/admin/tool/dataprivacy/templates/data_deletion.mustache @@ -0,0 +1,88 @@ +{{! + 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 comments. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/data_deletion + + Data deletion page. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * filter - The context data for single_select element that has the options for the table filter. + * expiredcontexts - The HTML for the table of expired contexts. + + Example context (json): + { + "filter": { + "name": "filter", + "method": "get", + "action": "#", + "classes": "singleselect", + "label": "", + "disabled": false, + "title": null, + "formid": "single_select_f5ac5e42bb035319", + "id": "single_select5ac5e42bb035320", + "params":[], + "options":[ + {"value": 50, "name": "Course", "selected": true, "optgroup": false}, + {"value": 70, "name": "Activities and resources", "selected":false, "optgroup": false}, + {"value": 80, "name": "Blocks", "selected": false, "optgroup": false} + ], + "labelattributes": [], + "helpicon": false + }, + "expiredcontexts": "
Name
Information
Purpose
Category
Retention period
ExpirySort by Expiry Ascending
Miscellaneous / TC 1 7 children Default purpose Default category 1 days Thursday, 5 April 2018, 10:29 AM
" + } +}} +
+
+
+ {{#str}}datadeletionpagehelp, tool_dataprivacy{{/str}} +
+
+ {{#filter}} + {{> core/single_select}} + {{/filter}} +
+ {{#existingcontexts}} +
+ +
+ {{/existingcontexts}} +
+
+ {{{expiredcontexts}}} +
+
+ {{#existingcontexts}} +
+ +
+ {{/existingcontexts}} +
+
+{{#js}} +// Initialise the JS. +require(['tool_dataprivacy/data_deletion'], function(DataDeletion) { + new DataDeletion(); +}); +{{/js}} diff --git a/admin/tool/dataprivacy/templates/data_registry.mustache b/admin/tool/dataprivacy/templates/data_registry.mustache new file mode 100644 index 00000000000..203510bbe8c --- /dev/null +++ b/admin/tool/dataprivacy/templates/data_registry.mustache @@ -0,0 +1,61 @@ +{{! + 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 comments. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/data_registry + + Data registry main page. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * none + + Example context (json): + { + } +}} +
+
+ {{#defaultsbutton}} + {{> core/action_link}} + {{/defaultsbutton}} + {{#actions}} + {{> core/action_menu}} + {{/actions}} +
+ {{#nosystemdefaults}} +
+ {{> core/notification_warning}} +
+ {{/nosystemdefaults}} + +
+
+ +
+
+
+
+
diff --git a/admin/tool/dataprivacy/templates/data_request_email.mustache b/admin/tool/dataprivacy/templates/data_request_email.mustache new file mode 100644 index 00000000000..8875b6cb1a3 --- /dev/null +++ b/admin/tool/dataprivacy/templates/data_request_email.mustache @@ -0,0 +1,105 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/data_request_email + + Email template for the data request. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * string dponame The name of the Data Protection Officer + * string requestfor The user the request is being made for. + * string requestedby The one making the request. + * string requesttype The request type. + * string requestdate The date the request was made. + * string requestcomments Additional details regarding the request. + * bool forself Whether the request has been made on behalf of another user or not. + * string datarequestsurl The URL to the data requests page. + + Example context (json): + { + "dponame": "Eva Ferrer", + "requestfor": "Oscar Olsen", + "requestedby": "Angus Zhang", + "requesttype": "Export user data", + "requestdate": "31 January 2018", + "requestcomments": "Dear admin,
I would like to request a copy of my son's user data. Thanks!", + "forself": true, + "datarequestsurl": "#" + } +}} + +
+

{{#str}}emailsalutation, tool_dataprivacy, {{dponame}}{{/str}}

+

{{#str}}requestemailintro, tool_dataprivacy{{/str}}

+ + + + + + + + + + {{^forself}} + + + + + {{/forself}} + + + + + + + + +
+ {{#str}}requesttype, tool_dataprivacy{{/str}} + + {{requesttype}} +
+ {{#str}}requestfor, tool_dataprivacy{{/str}} + + {{requestfor}} +
+ {{#str}}requestby, tool_dataprivacy{{/str}} + + {{requestedby}} +
+ {{#str}}requestcomments, tool_dataprivacy{{/str}} + + {{{requestcomments}}} +
+ {{#str}}daterequested, tool_dataprivacy{{/str}} + + {{requestdate}} +
+
+ {{#str}}viewrequest, tool_dataprivacy{{/str}} +
diff --git a/admin/tool/dataprivacy/templates/data_request_modal.mustache b/admin/tool/dataprivacy/templates/data_request_modal.mustache new file mode 100644 index 00000000000..b61bce1e99d --- /dev/null +++ b/admin/tool/dataprivacy/templates/data_request_modal.mustache @@ -0,0 +1,40 @@ +{{! + 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 comments. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/data_request_modal + + Data request details + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * none + + Example context (json): + { + } +}} +{{< core/modal }} + {{$footer}} + + + {{/footer}} +{{/ core/modal }} diff --git a/admin/tool/dataprivacy/templates/data_request_results_email.mustache b/admin/tool/dataprivacy/templates/data_request_results_email.mustache new file mode 100644 index 00000000000..9a5fc3b28ff --- /dev/null +++ b/admin/tool/dataprivacy/templates/data_request_results_email.mustache @@ -0,0 +1,54 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/data_request_results_email + + Email template for the data request processing results. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * string username The user's name. + * string message The message to the user. + * object downloadlink The action link context data for the download link (e.g. in case of user data export). + + Example context (json): + { + "username": "Eva Ferrer", + "message": "Your data is ready for download. Enjoy!", + "downloadlink": { + "url": "#", + "id": "test-id", + "attributes": [ + { + "name": "title", + "value": "Download here!" + } + ], + "text": "Download here!" + } + } +}} +
+

{{#str}}emailsalutation, tool_dataprivacy, {{username}}{{/str}}

+

{{message}}

+

{{#downloadlink}}{{> core/action_link}}{{/downloadlink}}

+
diff --git a/admin/tool/dataprivacy/templates/data_requests.mustache b/admin/tool/dataprivacy/templates/data_requests.mustache new file mode 100644 index 00000000000..db2f2ae7a5d --- /dev/null +++ b/admin/tool/dataprivacy/templates/data_requests.mustache @@ -0,0 +1,152 @@ +{{! + 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 comments. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/data_requests + + Data requests page. + + Classes required for JS: + * requestactions + + Data attributes required for JS: + * none + + Context variables required for this template: + * requests - Array of data requests. + + Example context (json): + { + "requests": [ + { + "id": 1, + "foruser" : { + "fullname": "Oscar Olsen", + "profileurl": "#" + }, + "typenameshort" : "Export", + "comments": "I would like to download all of my daughter's personal data", + "statuslabelclass": "label-default", + "statuslabel": "Pending", + "timecreated" : 1517902435 + }, + { + "id": 2, + "foruser" : { + "fullname": "Alexandre Denys", + "profileurl": "#" + }, + "typenameshort" : "Export", + "comments": "Please give me all of the information you have about me...", + "statuslabelclass": "label-warning", + "statuslabel": "Awaiting completion", + "timecreated" : 1517902435 + }, + { + "id": 3, + "foruser" : { + "fullname": "Hirondino Moura", + "profileurl": "#" + }, + "typenameshort" : "Delete", + "comments": "Please delete all of my son's personal data.", + "statuslabelclass": "label-success", + "statuslabel": "Complete", + "timecreated" : 1517902435 + }, + { + "id": 4, + "foruser" : { + "fullname": "Florian Krause", + "profileurl": "#" + }, + "typenameshort" : "Delete", + "comments": "I would like to request for my personal data to be deleted from your site. Thanks!", + "statuslabelclass": "label-danger", + "statuslabel": "Rejected", + "timecreated" : 1517902435 + }, + { + "id": 5, + "foruser" : { + "fullname": "Nicklas Sørensen", + "profileurl": "#" + }, + "typenameshort" : "Export", + "comments": "Please let me download my data", + "statuslabelclass": "label-info", + "statuslabel": "Processing", + "timecreated" : 1517902435 + } + ] + } +}} + +
+ + + + + + + + + + + + + {{#requests}} + + + + + + + + + {{/requests}} + {{^requests}} + + + + {{/requests}} + +
{{#str}}requesttype, tool_dataprivacy{{/str}}{{#str}}user, tool_dataprivacy{{/str}}{{#str}}daterequested, tool_dataprivacy{{/str}}{{#str}}requeststatus, tool_dataprivacy{{/str}}{{#str}}message, tool_dataprivacy{{/str}}
{{typenameshort}}{{foruser.fullname}}{{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}} + {{statuslabel}} + {{#shortentext}}60, {{comments}}{{/shortentext}} + {{#actions}} + {{> core/action_menu}} + {{/actions}} +
+ {{#str}}nodatarequests, tool_dataprivacy{{/str}} +
+
+ +{{#js}} +// Initialise the JS. +require(['tool_dataprivacy/requestactions'], function(ActionsMod) { + new ActionsMod(); +}); +{{/js}} diff --git a/admin/tool/dataprivacy/templates/form-user-selector-suggestion.mustache b/admin/tool/dataprivacy/templates/form-user-selector-suggestion.mustache new file mode 100644 index 00000000000..759650c4c9a --- /dev/null +++ b/admin/tool/dataprivacy/templates/form-user-selector-suggestion.mustache @@ -0,0 +1,41 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/form-user-selector-suggestion + + Moodle template for the list of valid options in an autocomplate form element. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * fullname string Users full name + * email string user email field + + Example context (json): + { + "fullname": "Admin User", + "email": "admin@example.com" + } +}} + + {{fullname}} + {{email}} + diff --git a/admin/tool/dataprivacy/templates/my_data_requests.mustache b/admin/tool/dataprivacy/templates/my_data_requests.mustache new file mode 100644 index 00000000000..c43ea9708f2 --- /dev/null +++ b/admin/tool/dataprivacy/templates/my_data_requests.mustache @@ -0,0 +1,130 @@ +{{! + 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 comments. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/my_data_requests + + A user's data requests page. + + Classes required for JS: + * requestactions + + Data attributes required for JS: + * none + + Context variables required for this template: + * requests - Array of data requests. + + Example context (json): + { + "requests": [ + { + "id": 1, + "typename" : "Data export", + "comments": "I would like to download all of my daughter's personal data", + "statuslabelclass": "label-default", + "statuslabel": "Pending", + "timecreated" : 1517902087 + }, + { + "id": 2, + "typename" : "Data export", + "comments": "Give me all of the information you have about me, or else...", + "statuslabelclass": "label-warning", + "statuslabel": "Awaiting completion", + "timecreated" : 1517902087 + }, + { + "id": 3, + "typename" : "Data deletion", + "comments": "Please delete all of my son's personal data.", + "statuslabelclass": "label-success", + "statuslabel": "Complete", + "timecreated" : 1517902087 + }, + { + "id": 4, + "typename" : "Data deletion", + "comments": "Delete my data or I'm coming for you...", + "statuslabelclass": "label-danger", + "statuslabel": "Rejected", + "timecreated" : 1517902087 + }, + { + "id": 5, + "typename" : "Data export", + "comments": "Please let me download my data", + "statuslabelclass": "label-info", + "statuslabel": "Processing", + "timecreated" : 1517902087 + } + ] + } +}} + +
+ + + + + + + + + + + + {{#requests}} + + + + + + + + {{/requests}} + {{^requests}} + + + + {{/requests}} + +
{{#str}}requesttype, tool_dataprivacy{{/str}}{{#str}}daterequested, tool_dataprivacy{{/str}}{{#str}}requeststatus, tool_dataprivacy{{/str}}{{#str}}message, tool_dataprivacy{{/str}}
{{typename}}{{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}} + {{statuslabel}} + {{comments}} + {{#actions}} + {{> core/action_menu}} + {{/actions}} +
+ {{#str}}nopersonaldatarequests, tool_dataprivacy{{/str}} +
+
+ +{{#js}} +// Initialise the JS. +require(['tool_dataprivacy/myrequestactions'], function(ActionsMod) { + ActionsMod.init(); +}); +{{/js}} diff --git a/admin/tool/dataprivacy/templates/purposes.mustache b/admin/tool/dataprivacy/templates/purposes.mustache new file mode 100644 index 00000000000..f8e675fa422 --- /dev/null +++ b/admin/tool/dataprivacy/templates/purposes.mustache @@ -0,0 +1,106 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/purposes + + Manage purposes. + + Classes required for JS: + + Data attributes required for JS: + + Context variables required for this template: + * purposes - array of objects + * actions - array of actions (already in HTML). + + Example context (json): + { + "purposesexist": 1, + "purposes": [ + { + "name" : "Purpose 1", + "description": "Purpose 1 description", + "retentionperiod": 86400, + "protected": 1, + "formattedretentionperiod": "1 day", + "actions": [ + ] + }, { + "name" : "Purpose 2", + "description": "Purpose 2 description", + "retentionperiod": 7200, + "protected": 0, + "formattedretentionperiod": "2 hours", + "actions": [ + ] + } + ] + } + +}} + +{{#navigation}} + {{> core/action_link}} +{{/navigation}} + +
+

{{#str}}purposes, tool_dataprivacy{{/str}}

+
+ +
+ + + + + + + + + + + + + {{#purposes}} + + + + + + + + {{/purposes}} + +
{{#str}}purposeslist, tool_dataprivacy{{/str}}
{{#str}}name{{/str}}{{#str}}description{{/str}}{{#str}}retentionperiod, tool_dataprivacy{{/str}}{{#str}}protected, tool_dataprivacy{{/str}}{{#str}}actions{{/str}}
{{{name}}}{{{description}}}{{formattedretentionperiod}} + {{#protected}} + {{#pix}}i/checked, core, {{#str}}yes{{/str}}{{/pix}} + {{/protected}} + {{^protected}} + {{#str}}no{{/str}} + {{/protected}} + + {{#actions}} + {{> core/action_menu}} + {{/actions}} +
+ {{^purposes}} +

+ {{#str}}nopurposes, tool_dataprivacy{{/str}} +

+ {{/purposes}} +
diff --git a/admin/tool/dataprivacy/templates/request_details.mustache b/admin/tool/dataprivacy/templates/request_details.mustache new file mode 100644 index 00000000000..6f00e12a950 --- /dev/null +++ b/admin/tool/dataprivacy/templates/request_details.mustache @@ -0,0 +1,85 @@ +{{! + 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 comments. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_dataprivacy/request_details + + Data request details + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * foruser Object The context data provided by core_user/external/user_summary_exporter. + * reviewurl String The URL for the Review request link. + * timecreated Number The timestamp when the request was created. + * statuslabel String The text equivalent of the status. + * statuslabelclass String The class to be used for rendering the status text. + * messagehtml String The HTML version of the request message. + + Example context (json): + { + "foruser" : { + "fullname": "Martha Smith", + "email": "martha@example.com", + "profileurl": "#", + "profileimageurl": "https://randomuser.me/api/portraits/women/60.jpg" + }, + "canreview": true, + "reviewurl": "#", + "timecreated": 1517561224, + "statuslabel": "Pending", + "statuslabelclass": "label-default", + "messagehtml": "

Hello,

I would like to download all of my personal data.

Thanks!

" + } +}} +
+
+
+ {{#str}}pictureof, moodle, {{foruser.fullname}}{{/str}} +
+
+

+ {{foruser.fullname}} +

+ {{foruser.email}} +
+ + {{#str}}daterequesteddetail, tool_dataprivacy{{/str}} {{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}} + + + {{#str}}statusdetail, tool_dataprivacy{{/str}} + {{statuslabel}} + +
+ {{#canreview}} + {{#str}}reviewdata, tool_dataprivacy{{/str}} + {{/canreview}} +
+
+
+
+ {{#str}}messagelabel, tool_dataprivacy{{/str}} +
+
+ {{{messagehtml}}} +
+
diff --git a/admin/tool/dataprivacy/tests/api_test.php b/admin/tool/dataprivacy/tests/api_test.php new file mode 100644 index 00000000000..8f228889133 --- /dev/null +++ b/admin/tool/dataprivacy/tests/api_test.php @@ -0,0 +1,1060 @@ +. + +/** + * API tests. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core\invalid_persistent_exception; +use core\task\manager; +use tool_dataprivacy\context_instance; +use tool_dataprivacy\api; +use tool_dataprivacy\data_registry; +use tool_dataprivacy\expired_context; +use tool_dataprivacy\data_request; +use tool_dataprivacy\task\initiate_data_request_task; +use tool_dataprivacy\task\process_data_request_task; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +/** + * API tests. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_dataprivacy_api_testcase extends advanced_testcase { + + /** + * setUp. + */ + public function setUp() { + $this->resetAfterTest(); + } + + /** + * Test for api::update_request_status(). + */ + public function test_update_request_status() { + $generator = new testing_data_generator(); + $s1 = $generator->create_user(); + + // Create the sample data request. + $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT); + + $requestid = $datarequest->get('id'); + + // Update with a valid status. + $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE); + $this->assertTrue($result); + + // Fetch the request record again. + $datarequest = new data_request($requestid); + $this->assertEquals(api::DATAREQUEST_STATUS_COMPLETE, $datarequest->get('status')); + + // Update with an invalid status. + $this->expectException(invalid_persistent_exception::class); + api::update_request_status($requestid, -1); + } + + /** + * Test for api::get_site_dpos() when there are no users with the DPO role. + */ + public function test_get_site_dpos_no_dpos() { + $admin = get_admin(); + + $dpos = api::get_site_dpos(); + $this->assertCount(1, $dpos); + $dpo = reset($dpos); + $this->assertEquals($admin->id, $dpo->id); + } + + /** + * Test for api::get_site_dpos() when there are no users with the DPO role. + */ + public function test_get_site_dpos() { + global $DB; + $generator = new testing_data_generator(); + $u1 = $generator->create_user(); + $u2 = $generator->create_user(); + + $context = context_system::instance(); + + // Give the manager role with the capability to manage data requests. + $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); + assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); + // Assign u1 as a manager. + role_assign($managerroleid, $u1->id, $context->id); + + // Give the editing teacher role with the capability to manage data requests. + $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); + assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true); + // Assign u1 as an editing teacher as well. + role_assign($editingteacherroleid, $u1->id, $context->id); + // Assign u2 as an editing teacher. + role_assign($editingteacherroleid, $u2->id, $context->id); + + // Only map the manager role to the DPO role. + set_config('dporoles', $managerroleid, 'tool_dataprivacy'); + + $dpos = api::get_site_dpos(); + $this->assertCount(1, $dpos); + $dpo = reset($dpos); + $this->assertEquals($u1->id, $dpo->id); + } + + /** + * Test for api::approve_data_request(). + */ + public function test_approve_data_request() { + global $DB; + + $generator = new testing_data_generator(); + $s1 = $generator->create_user(); + $u1 = $generator->create_user(); + + $context = context_system::instance(); + + // Manager role. + $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); + // Give the manager role with the capability to manage data requests. + assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); + // Assign u1 as a manager. + role_assign($managerroleid, $u1->id, $context->id); + + // Map the manager role to the DPO role. + set_config('dporoles', $managerroleid, 'tool_dataprivacy'); + + // Create the sample data request. + $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT); + $requestid = $datarequest->get('id'); + + // Make this ready for approval. + api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); + + $this->setUser($u1); + $result = api::approve_data_request($requestid); + $this->assertTrue($result); + $datarequest = new data_request($requestid); + $this->assertEquals($u1->id, $datarequest->get('dpo')); + $this->assertEquals(api::DATAREQUEST_STATUS_APPROVED, $datarequest->get('status')); + + // Test adhoc task creation. + $adhoctasks = manager::get_adhoc_tasks(process_data_request_task::class); + $this->assertCount(1, $adhoctasks); + } + + /** + * Test for api::approve_data_request() with the request not yet waiting for approval. + */ + public function test_approve_data_request_not_yet_ready() { + global $DB; + + $generator = new testing_data_generator(); + $s1 = $generator->create_user(); + $u1 = $generator->create_user(); + + $context = context_system::instance(); + + // Manager role. + $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); + // Give the manager role with the capability to manage data requests. + assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); + // Assign u1 as a manager. + role_assign($managerroleid, $u1->id, $context->id); + + // Map the manager role to the DPO role. + set_config('dporoles', $managerroleid, 'tool_dataprivacy'); + + // Create the sample data request. + $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT); + $requestid = $datarequest->get('id'); + + $this->setUser($u1); + $this->expectException(moodle_exception::class); + api::approve_data_request($requestid); + } + + /** + * Test for api::approve_data_request() when called by a user who doesn't have the DPO role. + */ + public function test_approve_data_request_non_dpo_user() { + $generator = new testing_data_generator(); + $student = $generator->create_user(); + $teacher = $generator->create_user(); + + // Create the sample data request. + $datarequest = api::create_data_request($student->id, api::DATAREQUEST_TYPE_EXPORT); + + $requestid = $datarequest->get('id'); + + // Login as a user without DPO role. + $this->setUser($teacher); + $this->expectException(required_capability_exception::class); + api::approve_data_request($requestid); + } + + /** + * Test for api::can_contact_dpo() + */ + public function test_can_contact_dpo() { + // Default ('contactdataprotectionofficer' is enabled by default). + $this->assertTrue(api::can_contact_dpo()); + + // Disable. + set_config('contactdataprotectionofficer', 0, 'tool_dataprivacy'); + $this->assertFalse(api::can_contact_dpo()); + + // Enable again. + set_config('contactdataprotectionofficer', 1, 'tool_dataprivacy'); + $this->assertTrue(api::can_contact_dpo()); + } + + /** + * Test for api::can_manage_data_requests() + */ + public function test_can_manage_data_requests() { + global $DB; + + // No configured site DPOs yet. + $admin = get_admin(); + $this->assertTrue(api::can_manage_data_requests($admin->id)); + + $generator = new testing_data_generator(); + $dpo = $generator->create_user(); + $nondpocapable = $generator->create_user(); + $nondpoincapable = $generator->create_user(); + + $context = context_system::instance(); + + // Manager role. + $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); + // Give the manager role with the capability to manage data requests. + assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); + // Assign u1 as a manager. + role_assign($managerroleid, $dpo->id, $context->id); + + // Editing teacher role. + $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); + // Give the editing teacher role with the capability to manage data requests. + assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); + // Assign u2 as an editing teacher. + role_assign($editingteacherroleid, $nondpocapable->id, $context->id); + + // Map only the manager role to the DPO role. + set_config('dporoles', $managerroleid, 'tool_dataprivacy'); + + // User with capability and has DPO role. + $this->assertTrue(api::can_manage_data_requests($dpo->id)); + // User with capability but has no DPO role. + $this->assertFalse(api::can_manage_data_requests($nondpocapable->id)); + // User without the capability and has no DPO role. + $this->assertFalse(api::can_manage_data_requests($nondpoincapable->id)); + } + + /** + * Test for api::create_data_request() + */ + public function test_create_data_request() { + $generator = new testing_data_generator(); + $user = $generator->create_user(); + $comment = 'sample comment'; + + // Login as user. + $this->setUser($user->id); + + // Test data request creation. + $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); + $this->assertEquals($user->id, $datarequest->get('userid')); + $this->assertEquals($user->id, $datarequest->get('requestedby')); + $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type')); + $this->assertEquals(api::DATAREQUEST_STATUS_PENDING, $datarequest->get('status')); + $this->assertEquals($comment, $datarequest->get('comments')); + + // Test adhoc task creation. + $adhoctasks = manager::get_adhoc_tasks(initiate_data_request_task::class); + $this->assertCount(1, $adhoctasks); + } + + /** + * Test for api::deny_data_request() + */ + public function test_deny_data_request() { + $generator = new testing_data_generator(); + $user = $generator->create_user(); + $comment = 'sample comment'; + + // Login as user. + $this->setUser($user->id); + + // Test data request creation. + $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); + + // Login as the admin (default DPO when no one is set). + $this->setAdminUser(); + + // Make this ready for approval. + api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL); + + // Deny the data request. + $result = api::deny_data_request($datarequest->get('id')); + $this->assertTrue($result); + } + + /** + * Test for api::deny_data_request() + */ + public function test_deny_data_request_without_permissions() { + $generator = new testing_data_generator(); + $user = $generator->create_user(); + $comment = 'sample comment'; + + // Login as user. + $this->setUser($user->id); + + // Test data request creation. + $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); + + // Login as a non-DPO user and try to call deny_data_request. + $user2 = $generator->create_user(); + $this->setUser($user2); + $this->expectException(required_capability_exception::class); + api::deny_data_request($datarequest->get('id')); + } + + /** + * Test for api::get_data_requests() + */ + public function test_get_data_requests() { + $generator = new testing_data_generator(); + $user1 = $generator->create_user(); + $user2 = $generator->create_user(); + $comment = 'sample comment'; + + // Make a data request as user 1. + $d1 = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT, $comment); + // Make a data request as user 2. + $d2 = api::create_data_request($user2->id, api::DATAREQUEST_TYPE_EXPORT, $comment); + + // Fetching data requests of specific users. + $requests = api::get_data_requests($user1->id); + $this->assertCount(1, $requests); + $datarequest = reset($requests); + $this->assertEquals($d1->to_record(), $datarequest->to_record()); + + $requests = api::get_data_requests($user2->id); + $this->assertCount(1, $requests); + $datarequest = reset($requests); + $this->assertEquals($d2->to_record(), $datarequest->to_record()); + + // Fetching data requests of all users. + // As guest. + $this->setGuestUser(); + $requests = api::get_data_requests(); + $this->assertEmpty($requests); + + // As DPO (admin in this case, which is default if no site DPOs are set). + $this->setAdminUser(); + $requests = api::get_data_requests(); + $this->assertCount(2, $requests); + } + + /** + * Data provider for test_has_ongoing_request. + */ + public function status_provider() { + return [ + [api::DATAREQUEST_STATUS_PENDING, true], + [api::DATAREQUEST_STATUS_PREPROCESSING, true], + [api::DATAREQUEST_STATUS_AWAITING_APPROVAL, true], + [api::DATAREQUEST_STATUS_APPROVED, true], + [api::DATAREQUEST_STATUS_PROCESSING, true], + [api::DATAREQUEST_STATUS_COMPLETE, false], + [api::DATAREQUEST_STATUS_CANCELLED, false], + [api::DATAREQUEST_STATUS_REJECTED, false], + ]; + } + + /** + * Test for api::has_ongoing_request() + * + * @dataProvider status_provider + * @param int $status The request status. + * @param bool $expected The expected result. + */ + public function test_has_ongoing_request($status, $expected) { + $generator = new testing_data_generator(); + $user1 = $generator->create_user(); + + // Make a data request as user 1. + $request = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT); + // Set the status. + api::update_request_status($request->get('id'), $status); + + // Check if this request is ongoing. + $result = api::has_ongoing_request($user1->id, api::DATAREQUEST_TYPE_EXPORT); + $this->assertEquals($expected, $result); + } + + /** + * Test for api::is_active() + * + * @dataProvider status_provider + * @param int $status The request status + * @param bool $expected The expected result + */ + public function test_is_active($status, $expected) { + // Check if this request is ongoing. + $result = api::is_active($status); + $this->assertEquals($expected, $result); + } + + /** + * Test for api::is_site_dpo() + */ + public function test_is_site_dpo() { + global $DB; + + // No configured site DPOs yet. + $admin = get_admin(); + $this->assertTrue(api::is_site_dpo($admin->id)); + + $generator = new testing_data_generator(); + $dpo = $generator->create_user(); + $nondpo = $generator->create_user(); + + $context = context_system::instance(); + + // Manager role. + $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); + // Give the manager role with the capability to manage data requests. + assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); + // Assign u1 as a manager. + role_assign($managerroleid, $dpo->id, $context->id); + + // Map only the manager role to the DPO role. + set_config('dporoles', $managerroleid, 'tool_dataprivacy'); + + // User is a DPO. + $this->assertTrue(api::is_site_dpo($dpo->id)); + // User is not a DPO. + $this->assertFalse(api::is_site_dpo($nondpo->id)); + } + + /** + * Data provider function for test_notify_dpo + * + * @return array + */ + public function notify_dpo_provider() { + return [ + [api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport'], + [api::DATAREQUEST_TYPE_DELETE, 'requesttypedelete'], + [api::DATAREQUEST_TYPE_OTHERS, 'requesttypeothers'], + ]; + } + + /** + * Test for api::notify_dpo() + * + * @dataProvider notify_dpo_provider + * @param int $type The request type + * @param string $typestringid The request lang string identifier + */ + public function test_notify_dpo($type, $typestringid) { + $generator = new testing_data_generator(); + $user1 = $generator->create_user(); + + // Make a data request as user 1. + $this->setUser($user1); + $request = api::create_data_request($user1->id, $type); + + $sink = $this->redirectMessages(); + // Let's just use admin as DPO (It's the default if not set). + $dpo = get_admin(); + $messageid = api::notify_dpo($dpo, $request); + $this->assertNotFalse($messageid); + $messages = $sink->get_messages(); + $this->assertCount(1, $messages); + $message = reset($messages); + + // Check some of the message properties. + $this->assertEquals($user1->id, $message->useridfrom); + $this->assertEquals($dpo->id, $message->useridto); + $typestring = get_string($typestringid, 'tool_dataprivacy'); + $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typestring); + $this->assertEquals($subject, $message->subject); + $this->assertEquals('tool_dataprivacy', $message->component); + $this->assertEquals('contactdataprotectionofficer', $message->eventtype); + $this->assertContains(fullname($dpo), $message->fullmessage); + $this->assertContains(fullname($user1), $message->fullmessage); + } + + /** + * Test of creating purpose as a user without privileges. + */ + public function test_create_purpose_non_dpo_user() { + $pleb = $this->getDataGenerator()->create_user(); + + $this->setUser($pleb); + $this->expectException(required_capability_exception::class); + api::create_purpose((object)[ + 'name' => 'aaa', + 'description' => 'yeah', + 'descriptionformat' => 1, + 'retentionperiod' => 'PT1M' + ]); + } + + /** + * Test fetching of purposes as a user without privileges. + */ + public function test_get_purposes_non_dpo_user() { + $pleb = $this->getDataGenerator()->create_user(); + $this->setAdminUser(); + api::create_purpose((object)[ + 'name' => 'bbb', + 'description' => 'yeah', + 'descriptionformat' => 1, + 'retentionperiod' => 'PT1M' + ]); + + $this->setUser($pleb); + $this->expectException(required_capability_exception::class); + api::get_purposes(); + } + + /** + * Test updating of purpose as a user without privileges. + */ + public function test_update_purposes_non_dpo_user() { + $pleb = $this->getDataGenerator()->create_user(); + $this->setAdminUser(); + $purpose = api::create_purpose((object)[ + 'name' => 'bbb', + 'description' => 'yeah', + 'descriptionformat' => 1, + 'retentionperiod' => 'PT1M' + ]); + + $this->setUser($pleb); + $this->expectException(required_capability_exception::class); + $purpose->set('retentionperiod', 'PT2M'); + api::update_purpose($purpose->to_record()); + } + + /** + * Test purpose deletion as a user without privileges. + */ + public function test_delete_purpose_non_dpo_user() { + $pleb = $this->getDataGenerator()->create_user(); + $this->setAdminUser(); + $purpose = api::create_purpose((object)[ + 'name' => 'bbb', + 'description' => 'yeah', + 'descriptionformat' => 1, + 'retentionperiod' => 'PT1M' + ]); + + $this->setUser($pleb); + $this->expectException(required_capability_exception::class); + api::delete_purpose($purpose->get('id')); + } + + /** + * Test data purposes CRUD actions. + * + * @return null + */ + public function test_purpose_crud() { + + $this->setAdminUser(); + + // Add. + $purpose = api::create_purpose((object)[ + 'name' => 'bbb', + 'description' => 'yeah', + 'descriptionformat' => 1, + 'retentionperiod' => 'PT1M' + ]); + $this->assertInstanceOf('\tool_dataprivacy\purpose', $purpose); + $this->assertEquals('bbb', $purpose->get('name')); + $this->assertEquals('PT1M', $purpose->get('retentionperiod')); + + // Update. + $purpose->set('retentionperiod', 'PT2M'); + $purpose = api::update_purpose($purpose->to_record()); + $this->assertEquals('PT2M', $purpose->get('retentionperiod')); + + // Retrieve. + $purpose = api::create_purpose((object)['name' => 'aaa', 'retentionperiod' => 'PT1M']); + $purposes = api::get_purposes(); + $this->assertCount(2, $purposes); + $this->assertEquals('aaa', $purposes[0]->get('name')); + $this->assertEquals('bbb', $purposes[1]->get('name')); + + // Delete. + api::delete_purpose($purposes[0]->get('id')); + $this->assertCount(1, api::get_purposes()); + api::delete_purpose($purposes[1]->get('id')); + $this->assertCount(0, api::get_purposes()); + } + + /** + * Test creation of data categories as a user without privileges. + */ + public function test_create_category_non_dpo_user() { + $pleb = $this->getDataGenerator()->create_user(); + + $this->setUser($pleb); + $this->expectException(required_capability_exception::class); + api::create_category((object)[ + 'name' => 'bbb', + 'description' => 'yeah', + 'descriptionformat' => 1 + ]); + } + + /** + * Test fetching of data categories as a user without privileges. + */ + public function test_get_categories_non_dpo_user() { + $pleb = $this->getDataGenerator()->create_user(); + + $this->setAdminUser(); + api::create_category((object)[ + 'name' => 'bbb', + 'description' => 'yeah', + 'descriptionformat' => 1 + ]); + + // Back to a regular user. + $this->setUser($pleb); + $this->expectException(required_capability_exception::class); + api::get_categories(); + } + + /** + * Test updating of data category as a user without privileges. + */ + public function test_update_category_non_dpo_user() { + $pleb = $this->getDataGenerator()->create_user(); + + $this->setAdminUser(); + $category = api::create_category((object)[ + 'name' => 'bbb', + 'description' => 'yeah', + 'descriptionformat' => 1 + ]); + + // Back to a regular user. + $this->setUser($pleb); + $this->expectException(required_capability_exception::class); + $category->set('name', 'yeah'); + api::update_category($category->to_record()); + } + + /** + * Test deletion of data category as a user without privileges. + */ + public function test_delete_category_non_dpo_user() { + $pleb = $this->getDataGenerator()->create_user(); + + $this->setAdminUser(); + $category = api::create_category((object)[ + 'name' => 'bbb', + 'description' => 'yeah', + 'descriptionformat' => 1 + ]); + + // Back to a regular user. + $this->setUser($pleb); + $this->expectException(required_capability_exception::class); + api::delete_category($category->get('id')); + $this->fail('Users shouldn\'t be allowed to manage categories by default'); + } + + /** + * Test data categories CRUD actions. + * + * @return null + */ + public function test_category_crud() { + + $this->setAdminUser(); + + // Add. + $category = api::create_category((object)[ + 'name' => 'bbb', + 'description' => 'yeah', + 'descriptionformat' => 1 + ]); + $this->assertInstanceOf('\tool_dataprivacy\category', $category); + $this->assertEquals('bbb', $category->get('name')); + + // Update. + $category->set('name', 'bcd'); + $category = api::update_category($category->to_record()); + $this->assertEquals('bcd', $category->get('name')); + + // Retrieve. + $category = api::create_category((object)['name' => 'aaa']); + $categories = api::get_categories(); + $this->assertCount(2, $categories); + $this->assertEquals('aaa', $categories[0]->get('name')); + $this->assertEquals('bcd', $categories[1]->get('name')); + + // Delete. + api::delete_category($categories[0]->get('id')); + $this->assertCount(1, api::get_categories()); + api::delete_category($categories[1]->get('id')); + $this->assertCount(0, api::get_categories()); + } + + /** + * Test context instances. + * + * @return null + */ + public function test_context_instances() { + global $DB; + + $this->setAdminUser(); + + list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); + + $coursecontext1 = \context_course::instance($courses[0]->id); + $coursecontext2 = \context_course::instance($courses[1]->id); + + $record1 = (object)['contextid' => $coursecontext1->id, 'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id')]; + $contextinstance1 = api::set_context_instance($record1); + + $record2 = (object)['contextid' => $coursecontext2->id, 'purposeid' => $purposes[1]->get('id'), 'categoryid' => $categories[1]->get('id')]; + $contextinstance2 = api::set_context_instance($record2); + + $this->assertCount(2, $DB->get_records('tool_dataprivacy_ctxinstance')); + + api::unset_context_instance($contextinstance1); + $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance')); + + $update = (object)['id' => $contextinstance2->get('id'), 'contextid' => $coursecontext2->id, + 'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id')]; + $contextinstance2 = api::set_context_instance($update); + $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance')); + } + + /** + * Test contextlevel. + * + * @return null + */ + public function test_contextlevel() { + global $DB; + + $this->setAdminUser(); + list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); + + $record = (object)[ + 'purposeid' => $purposes[0]->get('id'), + 'categoryid' => $categories[0]->get('id'), + 'contextlevel' => CONTEXT_SYSTEM, + ]; + $contextlevel = api::set_contextlevel($record); + $this->assertInstanceOf('\tool_dataprivacy\contextlevel', $contextlevel); + $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel')); + $this->assertEquals($record->purposeid, $contextlevel->get('purposeid')); + $this->assertEquals($record->categoryid, $contextlevel->get('categoryid')); + + // Now update it. + $record->purposeid = $purposes[1]->get('id'); + $contextlevel = api::set_contextlevel($record); + $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel')); + $this->assertEquals($record->purposeid, $contextlevel->get('purposeid')); + $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxlevel')); + + $record->contextlevel = CONTEXT_USER; + $contextlevel = api::set_contextlevel($record); + $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxlevel')); + } + + /** + * Test effective context levels purpose and category defaults. + * + * @return null + */ + public function test_effective_contextlevel_defaults() { + $this->setAdminUser(); + + list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); + + list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM); + $this->assertEquals(false, $purposeid); + $this->assertEquals(false, $categoryid); + + list($purposevar, $categoryvar) = data_registry::var_names_from_context( + \context_helper::get_class_for_level(CONTEXT_SYSTEM) + ); + set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy'); + + list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM); + $this->assertEquals($purposes[0]->get('id'), $purposeid); + $this->assertEquals(false, $categoryid); + + // Course inherits from system if not defined. + list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE); + $this->assertEquals($purposes[0]->get('id'), $purposeid); + $this->assertEquals(false, $categoryid); + + // Course defined values should have preference. + list($purposevar, $categoryvar) = data_registry::var_names_from_context( + \context_helper::get_class_for_level(CONTEXT_COURSE) + ); + set_config($purposevar, $purposes[1]->get('id'), 'tool_dataprivacy'); + set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy'); + + list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE); + $this->assertEquals($purposes[1]->get('id'), $purposeid); + $this->assertEquals($categories[0]->get('id'), $categoryid); + + // Context level defaults are also allowed to be set to 'inherit'. + set_config($purposevar, context_instance::INHERIT, 'tool_dataprivacy'); + + list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE); + $this->assertEquals($purposes[0]->get('id'), $purposeid); + $this->assertEquals($categories[0]->get('id'), $categoryid); + + list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_MODULE); + $this->assertEquals($purposes[0]->get('id'), $purposeid); + $this->assertEquals($categories[0]->get('id'), $categoryid); + } + + /** + * Test effective contextlevel return. + * + * @return null + */ + public function test_effective_contextlevel() { + $this->setAdminUser(); + + list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); + + // Set the system context level to purpose 1. + $record = (object)[ + 'contextlevel' => CONTEXT_SYSTEM, + 'purposeid' => $purposes[1]->get('id'), + 'categoryid' => $categories[1]->get('id'), + ]; + api::set_contextlevel($record); + + $purpose = api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM); + $this->assertEquals($purposes[1]->get('id'), $purpose->get('id')); + + // 'not set' will get the default value for the context level. For context level defaults + // both 'not set' and 'inherit' result in inherit, so the parent context (system) default + // will be retrieved. + $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER); + $this->assertEquals($purposes[1]->get('id'), $purpose->get('id')); + + // The behaviour forcing an inherit from context system should result in the same effective + // purpose. + $record->purposeid = context_instance::INHERIT; + $record->contextlevel = CONTEXT_USER; + api::set_contextlevel($record); + $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER); + $this->assertEquals($purposes[1]->get('id'), $purpose->get('id')); + + $record->purposeid = $purposes[2]->get('id'); + $record->contextlevel = CONTEXT_USER; + api::set_contextlevel($record); + + $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER); + $this->assertEquals($purposes[2]->get('id'), $purpose->get('id')); + + // Only system and user allowed. + $this->expectException(coding_exception::class); + $record->contextlevel = CONTEXT_COURSE; + $record->purposeid = $purposes[1]->get('id'); + api::set_contextlevel($record); + } + + /** + * Test effective context purposes and categories. + * + * @return null + */ + public function test_effective_context() { + $this->setAdminUser(); + + list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); + + // Define system defaults (all context levels below will inherit). + list($purposevar, $categoryvar) = data_registry::var_names_from_context( + \context_helper::get_class_for_level(CONTEXT_SYSTEM) + ); + set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy'); + set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy'); + + // Define course defaults. + list($purposevar, $categoryvar) = data_registry::var_names_from_context( + \context_helper::get_class_for_level(CONTEXT_COURSE) + ); + set_config($purposevar, $purposes[1]->get('id'), 'tool_dataprivacy'); + set_config($categoryvar, $categories[1]->get('id'), 'tool_dataprivacy'); + + $course0context = \context_course::instance($courses[0]->id); + $course1context = \context_course::instance($courses[1]->id); + $mod0context = \context_module::instance($modules[0]->cmid); + $mod1context = \context_module::instance($modules[1]->cmid); + + // Set course instance values. + $record = (object)[ + 'contextid' => $course0context->id, + 'purposeid' => $purposes[1]->get('id'), + 'categoryid' => $categories[2]->get('id'), + ]; + api::set_context_instance($record); + $category = api::get_effective_context_category($course0context); + $this->assertEquals($record->categoryid, $category->get('id')); + + // Module instances get the context level default if nothing specified. + $category = api::get_effective_context_category($mod0context); + $this->assertEquals($categories[1]->get('id'), $category->get('id')); + + // Module instances get the parent context category if they inherit. + $record->contextid = $mod0context->id; + $record->categoryid = context_instance::INHERIT; + api::set_context_instance($record); + $category = api::get_effective_context_category($mod0context); + $this->assertEquals($categories[2]->get('id'), $category->get('id')); + + // The $forcedvalue param allows us to override the actual value (method php-docs for more info). + $category = api::get_effective_context_category($mod0context, $categories[1]->get('id')); + $this->assertEquals($categories[1]->get('id'), $category->get('id')); + $category = api::get_effective_context_category($mod0context, $categories[0]->get('id')); + $this->assertEquals($categories[0]->get('id'), $category->get('id')); + + // Module instances get the parent context category if they inherit; in + // this case the parent context category is not set so it should use the + // context level default (see 'Define course defaults' above). + $record->contextid = $mod1context->id; + $record->categoryid = context_instance::INHERIT; + api::set_context_instance($record); + $category = api::get_effective_context_category($mod1context); + $this->assertEquals($categories[1]->get('id'), $category->get('id')); + + // User instances use the value set at user context level instead of the user default. + + // User defaults to cat 0 and user context level to 1. + list($purposevar, $categoryvar) = data_registry::var_names_from_context( + \context_helper::get_class_for_level(CONTEXT_USER) + ); + set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy'); + set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy'); + $usercontextlevel = (object)[ + 'contextlevel' => CONTEXT_USER, + 'purposeid' => $purposes[1]->get('id'), + 'categoryid' => $categories[1]->get('id'), + ]; + api::set_contextlevel($usercontextlevel); + + $newuser = $this->getDataGenerator()->create_user(); + $usercontext = \context_user::instance($newuser->id); + $category = api::get_effective_context_category($usercontext); + $this->assertEquals($categories[1]->get('id'), $category->get('id')); + } + + /** + * Tests the deletion of expired contexts. + * + * @return null + */ + public function test_expired_context_deletion() { + global $DB; + + $this->setAdminUser(); + + list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); + + $course0context = \context_course::instance($courses[0]->id); + $course1context = \context_course::instance($courses[1]->id); + + $expiredcontext0 = api::create_expired_context($course0context->id); + $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired')); + $expiredcontext1 = api::create_expired_context($course1context->id); + $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxexpired')); + + api::delete_expired_context($expiredcontext0->get('id')); + $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired')); + } + + /** + * Tests the status of expired contexts. + * + * @return null + */ + public function test_expired_context_status() { + global $DB; + + $this->setAdminUser(); + + list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); + + $course0context = \context_course::instance($courses[0]->id); + + $expiredcontext = api::create_expired_context($course0context->id); + + // Default status. + $this->assertEquals(expired_context::STATUS_EXPIRED, $expiredcontext->get('status')); + + api::set_expired_context_status($expiredcontext, expired_context::STATUS_APPROVED); + $this->assertEquals(expired_context::STATUS_APPROVED, $expiredcontext->get('status')); + } + + /** + * Creates test purposes and categories. + * + * @return null + */ + protected function add_purposes_and_categories() { + + $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H']); + $purpose2 = api::create_purpose((object)['name' => 'p2', 'retentionperiod' => 'PT2H']); + $purpose3 = api::create_purpose((object)['name' => 'p3', 'retentionperiod' => 'PT3H']); + + $cat1 = api::create_category((object)['name' => 'a']); + $cat2 = api::create_category((object)['name' => 'b']); + $cat3 = api::create_category((object)['name' => 'c']); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + + $module1 = $this->getDataGenerator()->create_module('resource', array('course' => $course1)); + $module2 = $this->getDataGenerator()->create_module('resource', array('course' => $course2)); + + return [ + [$purpose1, $purpose2, $purpose3], + [$cat1, $cat2, $cat3], + [$course1, $course2], + [$module1, $module2] + ]; + } +} diff --git a/admin/tool/dataprivacy/tests/expired_contexts_test.php b/admin/tool/dataprivacy/tests/expired_contexts_test.php new file mode 100644 index 00000000000..1b1f82f0bd9 --- /dev/null +++ b/admin/tool/dataprivacy/tests/expired_contexts_test.php @@ -0,0 +1,209 @@ +. + +/** + * Expired contexts tests. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use tool_dataprivacy\api; +use tool_dataprivacy\data_registry; +use tool_dataprivacy\expired_context; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +/** + * Expired contexts tests. + * + * @package tool_dataprivacy + * @copyright 2018 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase { + + /** + * setUp. + */ + public function setUp() { + $this->resetAfterTest(); + $this->setAdminUser(); + } + + /** + * Test expired users flagging and deletion. + * + * @return null + */ + public function test_expired_users() { + global $DB; + + $purpose = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H']); + $cat = api::create_category((object)['name' => 'a']); + + $record = (object)[ + 'purposeid' => $purpose->get('id'), + 'categoryid' => $cat->get('id'), + 'contextlevel' => CONTEXT_SYSTEM, + ]; + api::set_contextlevel($record); + $record->contextlevel = CONTEXT_USER; + api::set_contextlevel($record); + + $userdata = ['lastaccess' => '123']; + $user1 = $this->getDataGenerator()->create_user($userdata); + $user2 = $this->getDataGenerator()->create_user($userdata); + $user3 = $this->getDataGenerator()->create_user($userdata); + $user4 = $this->getDataGenerator()->create_user($userdata); + $user5 = $this->getDataGenerator()->create_user(); + + $course1 = $this->getDataGenerator()->create_course(); + // Old course. + $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => '2']); + // Ongoing course. + $course3 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => time()]); + + $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student'); + $this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student'); + $this->getDataGenerator()->enrol_user($user3->id, $course2->id, 'student'); + $this->getDataGenerator()->enrol_user($user4->id, $course3->id, 'student'); + + // Users without lastaccess are skipped as well as users enroled in courses with no end date. + $expired = new \tool_dataprivacy\expired_user_contexts(); + $numexpired = $expired->flag_expired(); + $this->assertEquals(2, $numexpired); + $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_EXPIRED])); + + // Approve user1 to be deleted. + $user2ctx = \context_user::instance($user2->id); + $expiredctx = expired_context::get_record(['contextid' => $user2ctx->id]); + api::set_expired_context_status($expiredctx, expired_context::STATUS_APPROVED); + $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_APPROVED])); + + // Delete expired contexts. + $deleted = $expired->delete(); + $this->assertEquals(1, $deleted); + $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_EXPIRED])); + $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_CLEANED])); + + // No new records are generated. + $numexpired = $expired->flag_expired(); + $this->assertEquals(0, $numexpired); + $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxexpired')); + $deleted = $expired->delete(); + $this->assertEquals(0, $deleted); + } + + /** + * Test expired course and course stuff flagging and deletion. + * + * @return null + */ + public function test_expired_course_related_contexts() { + global $DB; + + $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H']); + $purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1000Y']); + $cat = api::create_category((object)['name' => 'a']); + + $record = (object)[ + 'purposeid' => $purpose1->get('id'), + 'categoryid' => $cat->get('id'), + 'contextlevel' => CONTEXT_SYSTEM, + ]; + api::set_contextlevel($record); + + list($purposevar, $categoryvar) = data_registry::var_names_from_context( + \context_helper::get_class_for_level(CONTEXT_COURSE) + ); + set_config($purposevar, $purpose1->get('id'), 'tool_dataprivacy'); + + // A lot more time for modules + list($purposevar, $categoryvar) = data_registry::var_names_from_context( + \context_helper::get_class_for_level(CONTEXT_MODULE) + ); + set_config($purposevar, $purpose2->get('id'), 'tool_dataprivacy'); + + $course1 = $this->getDataGenerator()->create_course(); + + // Old course. + $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => '2']); + $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id)); + $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id)); + + // We want to override this last module instance purpose so we can test that modules are also + // returned as expired. + $forum2ctx = \context_module::instance($forum2->cmid); + $record = (object)[ + 'purposeid' => $purpose1->get('id'), + 'categoryid' => $cat->get('id'), + 'contextid' => $forum2ctx->id, + ]; + api::set_context_instance($record); + + // Ongoing course. + $course3 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => time()]); + $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id)); + + $expired = new \tool_dataprivacy\expired_course_related_contexts(); + $numexpired = $expired->flag_expired(); + + // Only 1 module has expired. + $this->assertEquals(1, $numexpired); + $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_EXPIRED])); + + // Add a forum1 override to 1h retention period so both forum1 and course2 are also expired. + $forum1ctx = \context_module::instance($forum1->cmid); + $record->purposeid = $purpose1->get('id'); + $record->contextid = $forum1ctx->id; + api::set_context_instance($record); + $numexpired = $expired->flag_expired(); + $this->assertEquals(2, $numexpired); + $this->assertEquals(3, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_EXPIRED])); + + // Approve forum1 to be deleted. + $expiredctx = expired_context::get_record(['contextid' => $forum1ctx->id]); + api::set_expired_context_status($expiredctx, expired_context::STATUS_APPROVED); + + // Delete expired contexts. + $deleted = $expired->delete(); + $this->assertEquals(1, $deleted); + $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_CLEANED])); + + $expiredctx = expired_context::get_record(['contextid' => $forum2ctx->id]); + api::set_expired_context_status($expiredctx, expired_context::STATUS_APPROVED); + + $course2ctx = \context_course::instance($course2->id); + $expiredctx = expired_context::get_record(['contextid' => $course2ctx->id]); + api::set_expired_context_status($expiredctx, expired_context::STATUS_APPROVED); + + // Delete expired contexts. + $deleted = $expired->delete(); + $this->assertEquals(2, $deleted); + $this->assertEquals(3, $DB->count_records('tool_dataprivacy_ctxexpired', ['status' => expired_context::STATUS_CLEANED])); + + // No new records are generated. + $numexpired = $expired->flag_expired(); + $this->assertEquals(0, $numexpired); + $this->assertEquals(3, $DB->count_records('tool_dataprivacy_ctxexpired')); + $deleted = $expired->delete(); + $this->assertEquals(0, $deleted); + + } +} diff --git a/admin/tool/dataprivacy/tests/external_test.php b/admin/tool/dataprivacy/tests/external_test.php new file mode 100644 index 00000000000..71d0111605a --- /dev/null +++ b/admin/tool/dataprivacy/tests/external_test.php @@ -0,0 +1,285 @@ +. + +/** + * External tests. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use tool_dataprivacy\api; +use tool_dataprivacy\external; + +/** + * External testcase. + * + * @package tool_dataprivacy + * @copyright 2018 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_dataprivacy_external_testcase extends externallib_advanced_testcase { + + /** @var stdClass The user making the request. */ + protected $requester; + + /** @var int The data request ID. */ + protected $requestid; + + /** + * Setup function- we will create a course and add an assign instance to it. + */ + protected function setUp() { + $this->resetAfterTest(); + + $generator = new testing_data_generator(); + $requester = $generator->create_user(); + + $comment = 'sample comment'; + + // Login as user. + $this->setUser($requester->id); + + // Test data request creation. + $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); + $this->requestid = $datarequest->get('id'); + $this->requester = $requester; + + // Log out the user and set force login to true. + $this->setUser(); + } + + /** + * Test for external::approve_data_request() with the user not logged in. + */ + public function test_approve_data_request_not_logged_in() { + $this->expectException(require_login_exception::class); + external::approve_data_request($this->requestid); + } + + /** + * Test for external::approve_data_request() with the user not having a DPO role. + */ + public function test_approve_data_request_not_dpo() { + // Login as the requester. + $this->setUser($this->requester->id); + $this->expectException(required_capability_exception::class); + external::approve_data_request($this->requestid); + } + + /** + * Test for external::approve_data_request() for request that's not ready for approval + */ + public function test_approve_data_request_not_waiting_for_approval() { + // Admin as DPO. (The default when no one's assigned as a DPO in the site). + $this->setAdminUser(); + $this->expectException(moodle_exception::class); + external::approve_data_request($this->requestid); + } + + /** + * Test for external::approve_data_request() + */ + public function test_approve_data_request() { + // Admin as DPO. (The default when no one's assigned as a DPO in the site). + $this->setAdminUser(); + api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); + $result = external::approve_data_request($this->requestid); + $return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result); + $this->assertTrue($return->result); + $this->assertEmpty($return->warnings); + } + + /** + * Test for external::approve_data_request() for a non-existent request ID. + */ + public function test_approve_data_request_non_existent() { + // Admin as DPO. (The default when no one's assigned as a DPO in the site). + $this->setAdminUser(); + api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); + $result = external::approve_data_request($this->requestid + 1); + $return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result); + $this->assertFalse($return->result); + $this->assertCount(1, $return->warnings); + $warning = reset($return->warnings); + $this->assertEquals('errorrequestnotfound', $warning['warningcode']); + } + + /** + * Test for external::cancel_data_request() of another user. + */ + public function test_cancel_data_request_other_user() { + $generator = $this->getDataGenerator(); + $otheruser = $generator->create_user(); + + // Login as another user. + $this->setUser($otheruser); + + $result = external::cancel_data_request($this->requestid); + $return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result); + $this->assertFalse($return->result); + $this->assertCount(1, $return->warnings); + $warning = reset($return->warnings); + $this->assertEquals('errorrequestnotfound', $warning['warningcode']); + } + + /** + * Test for external::cancel_data_request() + */ + public function test_cancel_data_request() { + // Login as the requester. + $this->setUser($this->requester); + + $result = external::cancel_data_request($this->requestid); + $return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result); + $this->assertTrue($return->result); + $this->assertEmpty($return->warnings); + } + + /** + * Test contact DPO. + */ + public function test_contact_dpo() { + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $this->setUser($user); + + $message = 'Hello world!'; + $result = external::contact_dpo($message); + $return = (object) external_api::clean_returnvalue(external::contact_dpo_returns(), $result); + $this->assertTrue($return->result); + $this->assertEmpty($return->warnings); + } + + /** + * Test contact DPO with message containing invalid input. + */ + public function test_contact_dpo_with_nasty_input() { + $generator = $this->getDataGenerator(); + $user = $generator->create_user(); + $this->setUser($user); + + $this->expectException('invalid_parameter_exception'); + external::contact_dpo('de<>\\..scription'); + } + + /** + * Test for external::deny_data_request() with the user not logged in. + */ + public function test_deny_data_request_not_logged_in() { + $this->expectException(require_login_exception::class); + external::deny_data_request($this->requestid); + } + + /** + * Test for external::deny_data_request() with the user not having a DPO role. + */ + public function test_deny_data_request_not_dpo() { + // Login as the requester. + $this->setUser($this->requester->id); + $this->expectException(required_capability_exception::class); + external::deny_data_request($this->requestid); + } + + /** + * Test for external::deny_data_request() for request that's not ready for approval + */ + public function test_deny_data_request_not_waiting_for_approval() { + // Admin as DPO. (The default when no one's assigned as a DPO in the site). + $this->setAdminUser(); + $this->expectException(moodle_exception::class); + external::deny_data_request($this->requestid); + } + + /** + * Test for external::deny_data_request() + */ + public function test_deny_data_request() { + // Admin as DPO. (The default when no one's assigned as a DPO in the site). + $this->setAdminUser(); + api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); + $result = external::approve_data_request($this->requestid); + $return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result); + $this->assertTrue($return->result); + $this->assertEmpty($return->warnings); + } + + /** + * Test for external::deny_data_request() for a non-existent request ID. + */ + public function test_deny_data_request_non_existent() { + // Admin as DPO. (The default when no one's assigned as a DPO in the site). + $this->setAdminUser(); + api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); + $result = external::deny_data_request($this->requestid + 1); + $return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result); + $this->assertFalse($return->result); + $this->assertCount(1, $return->warnings); + $warning = reset($return->warnings); + $this->assertEquals('errorrequestnotfound', $warning['warningcode']); + } + + /** + * Test for external::get_data_request() with the user not logged in. + */ + public function test_get_data_request_not_logged_in() { + $this->expectException(require_login_exception::class); + external::get_data_request($this->requestid); + } + + /** + * Test for external::get_data_request() with the user not having a DPO role. + */ + public function test_get_data_request_not_dpo() { + $generator = $this->getDataGenerator(); + $otheruser = $generator->create_user(); + // Login as the requester. + $this->setUser($otheruser); + $this->expectException(required_capability_exception::class); + external::get_data_request($this->requestid); + } + + /** + * Test for external::get_data_request() + */ + public function test_get_data_request() { + // Admin as DPO. (The default when no one's assigned as a DPO in the site). + $this->setAdminUser(); + $result = external::get_data_request($this->requestid); + $return = (object) external_api::clean_returnvalue(external::get_data_request_returns(), $result); + $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $return->result['type']); + $this->assertEquals('sample comment', $return->result['comments']); + $this->assertEquals($this->requester->id, $return->result['userid']); + $this->assertEquals($this->requester->id, $return->result['requestedby']); + $this->assertEmpty($return->warnings); + } + + /** + * Test for external::get_data_request() for a non-existent request ID. + */ + public function test_get_data_request_non_existent() { + // Admin as DPO. (The default when no one's assigned as a DPO in the site). + $this->setAdminUser(); + $this->expectException(dml_missing_record_exception::class); + external::get_data_request($this->requestid + 1); + } +} diff --git a/admin/tool/dataprivacy/version.php b/admin/tool/dataprivacy/version.php new file mode 100644 index 00000000000..9d73fe16ad5 --- /dev/null +++ b/admin/tool/dataprivacy/version.php @@ -0,0 +1,29 @@ +. + +/** + * Data privacy plugin version information + * + * @package tool_dataprivacy + * @copyright 2018 onwards Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2018040500; +$plugin->requires = 2018040500; // Moodle 3.5dev (Build 2018031600) and upwards. +$plugin->component = 'tool_dataprivacy';