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..f2964fe3284
--- /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}])})["catch"](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..48f7d3c9f82
--- /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_BOOST:"#id_error_retention_current",RETENTION_FIELD_CLEAN:"#fitem_id_retention_current [data-fieldtype=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],f=a(b.RETENTION_FIELD_CLEAN);if(f.length>0)f.text(e);else{var g=a(b.RETENTION_FIELD_BOOST),h=g.siblings();h.length>0&&h.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/expand_contract.min.js b/admin/tool/dataprivacy/amd/build/expand_contract.min.js
new file mode 100644
index 00000000000..3419d58da10
--- /dev/null
+++ b/admin/tool/dataprivacy/amd/build/expand_contract.min.js
@@ -0,0 +1 @@
+define(["jquery","core/url","core/str"],function(a,b,c){var d=a(''),e=a('');return{expandCollapse:function(a,b){a.hasClass("hide")?(a.removeClass("hide"),a.addClass("visible"),a.attr("aria-expanded",!0),b.find(":header i.fa").removeClass("fa-plus-square"),b.find(":header i.fa").addClass("fa-minus-square"),b.find(":header img.icon").attr("src",d.attr("src"))):(a.removeClass("visible"),a.addClass("hide"),a.attr("aria-expanded",!1),b.find(":header i.fa").removeClass("fa-minus-square"),b.find(":header i.fa").addClass("fa-plus-square"),b.find(":header img.icon").attr("src",e.attr("src")))},expandCollapseAll:function(b){var f="visible"==b?"hide":"visible",g="visible"==b,h="visible"==b?"fa-plus-square":"fa-minus-square",i="visible"==b?"fa-minus-square":"fa-plus-square",j="visible"==b?d.attr("src"):e.attr("src");a("."+f).each(function(){a(this).removeClass(f),a(this).addClass(b),a(this).attr("aria-expanded",g)}),a(".tool_dataprivacy-expand-all").data("visibilityState",f),c.get_string(f,"tool_dataprivacy").then(function(b){a(".tool_dataprivacy-expand-all").html(b)})["catch"](Notification.exception),a(":header i.fa").each(function(){a(this).removeClass(h),a(this).addClass(i)}),a(":header img.icon").each(function(){a(this).attr("src",j)})}}});
\ 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/request_filter.min.js b/admin/tool/dataprivacy/amd/build/request_filter.min.js
new file mode 100644
index 00000000000..61344eb0fe6
--- /dev/null
+++ b/admin/tool/dataprivacy/amd/build/request_filter.min.js
@@ -0,0 +1 @@
+define(["jquery","core/form-autocomplete","core/str","core/notification"],function(a,b,c,d){var e={REQUEST_FILTERS:"#request-filters"},f=function(){var f=[{key:"filter",component:"moodle"},{key:"nofiltersapplied",component:"moodle"}];c.get_strings(f).then(function(a){var c=a[0],d=a[1];return b.enhance(e.REQUEST_FILTERS,!1,"",c,!1,!0,d,!0)}).fail(d.exception);var g=a(e.REQUEST_FILTERS).val();a(e.REQUEST_FILTERS).on("change",function(){var b=a(this).val();g.join(",")!==b.join(",")&&(0===b.length&&a("#filters-cleared").val(1),a(this.form).submit())})};return{init:function(){f()}}});
\ 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..07eb18dbe82
--- /dev/null
+++ b/admin/tool/dataprivacy/amd/src/data_registry.js
@@ -0,0 +1,318 @@
+// 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
+ }]);
+ return;
+ }).catch(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..8d587ca2d23
--- /dev/null
+++ b/admin/tool/dataprivacy/amd/src/effective_retention_period.js
@@ -0,0 +1,92 @@
+// 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_BOOST: '#id_error_retention_current',
+ RETENTION_FIELD_CLEAN: '#fitem_id_retention_current [data-fieldtype=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];
+
+ var cleanSelector = $(SELECTORS.RETENTION_FIELD_CLEAN);
+ if (cleanSelector.length > 0) {
+ cleanSelector.text(selectedPurpose);
+ } else {
+ var boostSelector = $(SELECTORS.RETENTION_FIELD_BOOST);
+ var retentionField = boostSelector.siblings();
+ if (retentionField.length > 0) {
+ retentionField.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/expand_contract.js b/admin/tool/dataprivacy/amd/src/expand_contract.js
new file mode 100644
index 00000000000..cf509b5574a
--- /dev/null
+++ b/admin/tool/dataprivacy/amd/src/expand_contract.js
@@ -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 .
+
+/**
+ * Potential user selector module.
+ *
+ * @module tool_dataprivacy/expand_contract
+ * @class page-expand-contract
+ * @package tool_dataprivacy
+ * @copyright 2018 Adrian Greeve
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/url', 'core/str'], function($, url, str) {
+
+ var expandedImage = $('');
+ var collapsedImage = $('');
+
+ return /** @alias module:tool_dataprivacy/expand-collapse */ {
+ /**
+ * Expand or collapse a selected node.
+ *
+ * @param {object} targetnode The node that we want to expand / collapse
+ * @param {object} thisnode The node that was clicked.
+ */
+ expandCollapse: function(targetnode, thisnode) {
+ if (targetnode.hasClass('hide')) {
+ targetnode.removeClass('hide');
+ targetnode.addClass('visible');
+ targetnode.attr('aria-expanded', true);
+ thisnode.find(':header i.fa').removeClass('fa-plus-square');
+ thisnode.find(':header i.fa').addClass('fa-minus-square');
+ thisnode.find(':header img.icon').attr('src', expandedImage.attr('src'));
+ } else {
+ targetnode.removeClass('visible');
+ targetnode.addClass('hide');
+ targetnode.attr('aria-expanded', false);
+ thisnode.find(':header i.fa').removeClass('fa-minus-square');
+ thisnode.find(':header i.fa').addClass('fa-plus-square');
+ thisnode.find(':header img.icon').attr('src', collapsedImage.attr('src'));
+ }
+ },
+
+ /**
+ * Expand or collapse all nodes on this page.
+ *
+ * @param {string} nextstate The next state to change to.
+ */
+ expandCollapseAll: function(nextstate) {
+ var currentstate = (nextstate == 'visible') ? 'hide' : 'visible';
+ var ariaexpandedstate = (nextstate == 'visible') ? true : false;
+ var iconclassnow = (nextstate == 'visible') ? 'fa-plus-square' : 'fa-minus-square';
+ var iconclassnext = (nextstate == 'visible') ? 'fa-minus-square' : 'fa-plus-square';
+ var imagenow = (nextstate == 'visible') ? expandedImage.attr('src') : collapsedImage.attr('src');
+ $('.' + currentstate).each(function() {
+ $(this).removeClass(currentstate);
+ $(this).addClass(nextstate);
+ $(this).attr('aria-expanded', ariaexpandedstate);
+ });
+ $('.tool_dataprivacy-expand-all').data('visibilityState', currentstate);
+
+ str.get_string(currentstate, 'tool_dataprivacy').then(function(langString) {
+ $('.tool_dataprivacy-expand-all').html(langString);
+ return;
+ }).catch(Notification.exception);
+
+ $(':header i.fa').each(function() {
+ $(this).removeClass(iconclassnow);
+ $(this).addClass(iconclassnext);
+ });
+ $(':header img.icon').each(function() {
+ $(this).attr('src', imagenow);
+ });
+ }
+ };
+});
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/request_filter.js b/admin/tool/dataprivacy/amd/src/request_filter.js
new file mode 100644
index 00000000000..6b915c85259
--- /dev/null
+++ b/admin/tool/dataprivacy/amd/src/request_filter.js
@@ -0,0 +1,84 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * JS module for the data requests filter.
+ *
+ * @module tool_dataprivacy/request_filter
+ * @package tool_dataprivacy
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], function($, Autocomplete, Str, Notification) {
+
+ /**
+ * Selectors.
+ *
+ * @access private
+ * @type {{REQUEST_FILTERS: string}}
+ */
+ var SELECTORS = {
+ REQUEST_FILTERS: '#request-filters'
+ };
+
+ /**
+ * Init function.
+ *
+ * @method init
+ * @private
+ */
+ var init = function() {
+ var stringkeys = [
+ {
+ key: 'filter',
+ component: 'moodle'
+ },
+ {
+ key: 'nofiltersapplied',
+ component: 'moodle'
+ }
+ ];
+
+ Str.get_strings(stringkeys).then(function(langstrings) {
+ var placeholder = langstrings[0];
+ var noSelectionString = langstrings[1];
+ return Autocomplete.enhance(SELECTORS.REQUEST_FILTERS, false, '', placeholder, false, true, noSelectionString, true);
+ }).fail(Notification.exception);
+
+ var last = $(SELECTORS.REQUEST_FILTERS).val();
+ $(SELECTORS.REQUEST_FILTERS).on('change', function() {
+ var current = $(this).val();
+ // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.
+ if (last.join(',') !== current.join(',')) {
+ // If we're submitting without filters, set the hidden input 'filters-cleared' to 1.
+ if (current.length === 0) {
+ $('#filters-cleared').val(1);
+ }
+ $(this.form).submit();
+ }
+ });
+ };
+
+ return /** @alias module:core/form-autocomplete */ {
+ /**
+ * Initialise the unified user filter.
+ *
+ * @method init
+ */
+ init: function() {
+ init();
+ }
+ };
+});
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..f323278844b
--- /dev/null
+++ b/admin/tool/dataprivacy/categories.php
@@ -0,0 +1,41 @@
+.
+
+/**
+ * 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');
+
+require_login(null, false);
+
+$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..42acb4259e7
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/api.php
@@ -0,0 +1,1035 @@
+.
+
+/**
+ * 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_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist_collection;
+use core_user;
+use dml_exception;
+use moodle_exception;
+use moodle_url;
+use required_capability_exception;
+use stdClass;
+use tool_dataprivacy\external\data_request_exporter;
+use tool_dataprivacy\local\helper;
+use tool_dataprivacy\task\initiate_data_request_task;
+use tool_dataprivacy\task\process_data_request_task;
+
+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;
+ }
+ $allnames = get_all_user_name_fields(true, 'u');
+ $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
+ 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
+ 'u.country, u.picture, u.idnumber, u.department, u.institution, '.
+ 'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' .
+ 'r.name AS rolename, r.sortorder, '.
+ 'r.shortname AS roleshortname, rn.name AS rolecoursealias';
+ // Fetch users that can manage data requests.
+ $dpos += get_role_users($roleid, $context, false, $fields);
+ }
+
+ // 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);
+
+ $requestinguser = $USER->id;
+ // Check when the user is making a request on behalf of another.
+ if ($requestinguser != $foruser) {
+ if (self::is_site_dpo($requestinguser)) {
+ // The user making the request is a DPO. Should be fine.
+ $datarequest->set('dpo', $requestinguser);
+ } else {
+ // If not a DPO, only users with the capability to make data requests for the user should be allowed.
+ // (e.g. users with the Parent role, etc).
+ if (!self::can_create_data_request_for_user($foruser)) {
+ $forusercontext = \context_user::instance($foruser);
+ throw new required_capability_exception($forusercontext,
+ 'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
+ }
+ }
+ }
+ // The user making the request.
+ $datarequest->set('requestedby', $requestinguser);
+ // 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.
+ * @param int[] $statuses The status filters.
+ * @param int[] $types The request type filters.
+ * @param string $sort The order by clause.
+ * @param int $offset Amount of records to skip.
+ * @param int $limit Amount of records to fetch.
+ * @return data_request[]
+ * @throws coding_exception
+ * @throws dml_exception
+ */
+ public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
+ global $DB, $USER;
+ $results = [];
+ $sqlparams = [];
+ $sqlconditions = [];
+
+ // Set default sort.
+ if (empty($sort)) {
+ $sort = 'status ASC, timemodified ASC';
+ }
+
+ // Set status filters.
+ if (!empty($statuses)) {
+ list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
+ $sqlconditions[] = "status $statusinsql";
+ }
+
+ // Set request type filter.
+ if (!empty($types)) {
+ list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
+ $sqlconditions[] = "type $typeinsql";
+ $sqlparams = array_merge($sqlparams, $typeparams);
+ }
+
+ if ($userid) {
+ // Get the data requests for the user or data requests made by the user.
+ $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
+ $params = [
+ 'userid' => $userid,
+ 'requestedby' => $userid
+ ];
+
+ // Build a list of user IDs that the user is allowed to make data requests for.
+ // Of course, the user should be included in this list.
+ $alloweduserids = [$userid];
+ // Get any users that the user can make data requests for.
+ if ($children = helper::get_children_of_user($userid)) {
+ // Get the list of user IDs of the children and merge to the allowed user IDs.
+ $alloweduserids = array_merge($alloweduserids, array_keys($children));
+ }
+ list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
+ $sqlconditions[] .= "userid $insql";
+ $select = implode(' AND ', $sqlconditions);
+ $params = array_merge($params, $inparams, $sqlparams);
+
+ $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
+ } 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)) {
+ if (!empty($sqlconditions)) {
+ $select = implode(' AND ', $sqlconditions);
+ $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
+ } else {
+ $results = data_request::get_records(null, $sort, '', $offset, $limit);
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetches the count of data request records based on the given parameters.
+ *
+ * @param int $userid The User ID.
+ * @param int[] $statuses The status filters.
+ * @param int[] $types The request type filters.
+ * @return int
+ * @throws coding_exception
+ * @throws dml_exception
+ */
+ public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
+ global $DB, $USER;
+ $count = 0;
+ $sqlparams = [];
+ $sqlconditions = [];
+ if (!empty($statuses)) {
+ list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
+ $sqlconditions[] = "status $statusinsql";
+ }
+ if (!empty($types)) {
+ list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
+ $sqlconditions[] = "type $typeinsql";
+ $sqlparams = array_merge($sqlparams, $typeparams);
+ }
+ if ($userid) {
+ // Get the data requests for the user or data requests made by the user.
+ $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
+ $params = [
+ 'userid' => $userid,
+ 'requestedby' => $userid
+ ];
+
+ // Build a list of user IDs that the user is allowed to make data requests for.
+ // Of course, the user should be included in this list.
+ $alloweduserids = [$userid];
+ // Get any users that the user can make data requests for.
+ if ($children = helper::get_children_of_user($userid)) {
+ // Get the list of user IDs of the children and merge to the allowed user IDs.
+ $alloweduserids = array_merge($alloweduserids, array_keys($children));
+ }
+ list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
+ $sqlconditions[] .= "userid $insql";
+ $select = implode(' AND ', $sqlconditions);
+ $params = array_merge($params, $inparams, $sqlparams);
+
+ $count = data_request::count_records_select($select, $params);
+ } 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)) {
+ if (!empty($sqlconditions)) {
+ $select = implode(' AND ', $sqlconditions);
+ $count = data_request::count_records_select($select, $sqlparams);
+ } else {
+ $count = data_request::count_records();
+ }
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * 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
+ * @param string $comment The comment about the status update.
+ * @return bool
+ * @throws invalid_persistent_exception
+ * @throws coding_exception
+ */
+ public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') {
+ // Update the request.
+ $datarequest = new data_request($requestid);
+ $datarequest->set('status', $status);
+ if ($dpoid) {
+ $datarequest->set('dpo', $dpoid);
+ }
+ $datarequest->set('dpocomment', $comment);
+ 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);
+
+ // Approve all the contexts attached to the request.
+ // Currently, approving the request implicitly approves all associated contexts, but this may change in future, allowing
+ // users to selectively approve certain contexts only.
+ self::update_request_contexts_with_status($requestid, contextlist_context::STATUS_APPROVED);
+
+ // 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;
+
+ $output = $PAGE->get_renderer('tool_dataprivacy');
+
+ $usercontext = \context_user::instance($request->get('requestedby'));
+ $requestexporter = new data_request_exporter($request, ['context' => $usercontext]);
+ $requestdata = $requestexporter->export($output);
+
+ // Create message to send to the Data Protection Officer(s).
+ $typetext = null;
+ $typetext = $requestdata->typename;
+ $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
+
+ $requestedby = $requestdata->requestedbyuser;
+ $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->id;
+ $message->replyto = $requestedby->email;
+ $message->replytoname = $requestedby->fullname;
+ $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' => $requestedby->fullname,
+ 'requesttype' => $typetext,
+ 'requestdate' => userdate($requestdata->timecreated),
+ 'requestcomments' => $requestdata->messagehtml,
+ 'datarequestsurl' => $datarequestsurl
+ ];
+ $requestingfor = $requestdata->foruser;
+ if ($requestedby->id == $requestingfor->id) {
+ $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
+ } else {
+ $messagetextdata['requestfor'] = $requestingfor->fullname;
+ }
+
+ // 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);
+ }
+
+ /**
+ * Checks whether a non-DPO user can make a data request for another user.
+ *
+ * @param int $user The user ID of the target user.
+ * @param int $requester The user ID of the user making the request.
+ * @return bool
+ * @throws coding_exception
+ */
+ public static function can_create_data_request_for_user($user, $requester = null) {
+ $usercontext = \context_user::instance($user);
+ return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
+ }
+
+ /**
+ * 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();
+
+ if (!isset($record->sensitivedatareasons)) {
+ $record->sensitivedatareasons = '';
+ }
+
+ $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();
+ }
+
+ /**
+ * Adds the contexts from the contextlist_collection to the request with the status provided.
+ *
+ * @param contextlist_collection $clcollection a collection of contextlists for all components.
+ * @param int $requestid the id of the request.
+ * @param int $status the status to set the contexts to.
+ */
+ public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
+ $request = new data_request($requestid);
+ foreach ($clcollection as $contextlist) {
+ // Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
+ $clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
+ $clp->create();
+ $contextlistid = $clp->get('id');
+
+ // Store the associated contexts in the contextlist.
+ foreach ($contextlist->get_contextids() as $contextid) {
+ if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
+ $context = \context::instance_by_id($contextid);
+ if (($purpose = static::get_effective_context_purpose($context)) && !empty($purpose->get('protected'))) {
+ continue;
+ }
+ }
+ $context = new contextlist_context();
+ $context->set('contextid', $contextid)
+ ->set('contextlistid', $contextlistid)
+ ->set('status', $status)
+ ->create();
+ }
+
+ // Create the relation to the request.
+ $requestcontextlist = request_contextlist::create_relation($requestid, $contextlistid);
+ $requestcontextlist->create();
+ }
+ }
+
+ /**
+ * Sets the status of all contexts associated with the request.
+ *
+ * @param int $requestid the requestid to which the contexts belong.
+ * @param int $status the status to set to.
+ * @throws \dml_exception if the requestid is invalid.
+ * @throws \moodle_exception if the status is invalid.
+ */
+ public static function update_request_contexts_with_status(int $requestid, int $status) {
+ // Validate contextlist_context status using the persistent's attribute validation.
+ $contextlistcontext = new contextlist_context();
+ $contextlistcontext->set('status', $status);
+ if (array_key_exists('status', $contextlistcontext->get_errors())) {
+ throw new moodle_exception("Invalid contextlist_context status: $status");
+ }
+
+ // Validate requestid using the persistent's record validation.
+ // A dml_exception is thrown if the record is missing.
+ $datarequest = new data_request($requestid);
+
+ // Bulk update the status of the request contexts.
+ global $DB;
+
+ $select = "SELECT ctx.id as id
+ FROM {" . request_contextlist::TABLE . "} rcl
+ JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
+ JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
+ WHERE rcl.requestid = ?";
+
+ // Fetch records IDs to be updated and update by chunks, if applicable (limit of 1000 records per update).
+ $limit = 1000;
+ $idstoupdate = $DB->get_fieldset_sql($select, [$requestid]);
+ $count = count($idstoupdate);
+ $idchunks = $idstoupdate;
+ if ($count > $limit) {
+ $idchunks = array_chunk($idstoupdate, $limit);
+ }
+ $transaction = $DB->start_delegated_transaction();
+ $initialparams = [$status];
+ foreach ($idchunks as $chunk) {
+ list($insql, $inparams) = $DB->get_in_or_equal($chunk);
+ $update = "UPDATE {" . contextlist_context::TABLE . "}
+ SET status = ?
+ WHERE id $insql";
+ $params = array_merge($initialparams, $inparams);
+ $DB->execute($update, $params);
+ }
+ $transaction->allow_commit();
+ }
+
+ /**
+ * Finds all request contextlists having at least on approved context, and returns them as in a contextlist_collection.
+ *
+ * @param data_request $request the data request with which the contextlists are associated.
+ * @return contextlist_collection the collection of approved_contextlist objects.
+ */
+ public static function get_approved_contextlist_collection_for_request(data_request $request) : contextlist_collection {
+ $foruser = core_user::get_user($request->get('userid'));
+
+ // Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
+ global $DB;
+ $sql = "SELECT cl.component, ctx.contextid
+ FROM {" . request_contextlist::TABLE . "} rcl
+ JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
+ JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
+ WHERE rcl.requestid = ?
+ AND ctx.status = ?
+ ORDER BY cl.component, ctx.contextid";
+
+ // Create the approved contextlist collection object.
+ $lastcomponent = null;
+ $approvedcollection = new contextlist_collection($foruser->id);
+
+ $rs = $DB->get_recordset_sql($sql, [$request->get('id'), contextlist_context::STATUS_APPROVED]);
+ foreach ($rs as $record) {
+ // If we encounter a new component, and we've built up contexts for the last, then add the approved_contextlist for the
+ // last (the one we've just finished with) and reset the context array for the next one.
+ if ($lastcomponent != $record->component) {
+ if (!empty($contexts)) {
+ $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
+ }
+ $contexts = [];
+ }
+
+ $contexts[] = $record->contextid;
+ $lastcomponent = $record->component;
+ }
+ $rs->close();
+
+ // The data for the last component contextlist won't have been written yet, so write it now.
+ if (!empty($contexts)) {
+ $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
+ }
+
+ return $approvedcollection;
+ }
+}
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..97af9622079
--- /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 bool $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/contextlist.php b/admin/tool/dataprivacy/classes/contextlist.php
new file mode 100644
index 00000000000..ef25c11b9a3
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/contextlist.php
@@ -0,0 +1,64 @@
+.
+
+/**
+ * Contains the contextlist persistent.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jake Dallimore
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\persistent;
+
+/**
+ * The contextlist persistent.
+ *
+ * @copyright 2018 Jake Dallimore
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contextlist extends persistent {
+
+ /** The table name this persistent object maps to. */
+ const TABLE = 'tool_dataprivacy_contextlist';
+
+ /**
+ * Return the definition of the properties of this model.
+ *
+ * @return array
+ */
+ protected static function define_properties() {
+ return [
+ 'component' => [
+ 'type' => PARAM_TEXT
+ ]
+ ];
+ }
+
+ /**
+ * Create a new contextlist persistent from an instance of \core_privacy\local\request\contextlist.
+ *
+ * @param \core_privacy\local\request\contextlist $contextlist the core privacy contextlist.
+ * @return contextlist a contextlist persistent.
+ */
+ public static function from_contextlist(\core_privacy\local\request\contextlist $contextlist) : contextlist {
+ $contextlistpersistent = new contextlist();
+ return $contextlistpersistent->set('component', $contextlist->get_component());
+ }
+}
diff --git a/admin/tool/dataprivacy/classes/contextlist_context.php b/admin/tool/dataprivacy/classes/contextlist_context.php
new file mode 100644
index 00000000000..0f4ca9fcff7
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/contextlist_context.php
@@ -0,0 +1,74 @@
+.
+
+/**
+ * Contains the contextlist_context persistent.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jake Dallimore
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\persistent;
+
+/**
+ * The contextlist_context persistent.
+ *
+ * @copyright 2018 Jake Dallimore
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contextlist_context extends persistent {
+
+ /** The table name this persistent object maps to. */
+ const TABLE = 'tool_dataprivacy_ctxlst_ctx';
+
+ /** This context is pending approval. */
+ const STATUS_PENDING = 0;
+
+ /** This context has been approved. */
+ const STATUS_APPROVED = 1;
+
+ /** This context has been rejected. */
+ const STATUS_REJECTED = 2;
+
+ /**
+ * Return the definition of the properties of this model.
+ *
+ * @return array
+ */
+ protected static function define_properties() {
+ return [
+ 'contextid' => [
+ 'type' => PARAM_INT
+ ],
+ 'contextlistid' => [
+ 'type' => PARAM_INT
+ ],
+ 'status' => [
+ 'choices' => [
+ self::STATUS_PENDING,
+ self::STATUS_APPROVED,
+ self::STATUS_REJECTED,
+ ],
+ 'default' => self::STATUS_PENDING,
+ 'type' => PARAM_INT
+ ]
+ ];
+ }
+}
diff --git a/admin/tool/dataprivacy/classes/data_registry.php b/admin/tool/dataprivacy/classes/data_registry.php
new file mode 100644
index 00000000000..1435fcddb62
--- /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
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @param \context $context
+ * @return array
+ */
+ public static function get_subject_scope(\context $context) {
+
+ if ($contextcourse = $context->get_course_context(false)) {
+ // Below course level we look at module or block level roles + course-assigned roles.
+ $courseroles = get_roles_with_assignment_on_context($contextcourse);
+ $roles = $courseroles + get_roles_with_assignment_on_context($context);
+ } else {
+ // We list category + system for others (we don't work with user instances so no need to work about them).
+ $roles = get_roles_used_in_context($context);
+ }
+
+ return array_map(function($role) {
+ if ($role->name) {
+ return $role->name;
+ } else {
+ return $role->shortname;
+ }
+ }, $roles);
+ }
+
+ /**
+ * Returns the effective value given a context instance
+ *
+ * @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..d5ab2187636
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/data_request.php
@@ -0,0 +1,113 @@
+.
+
+/**
+ * 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' => ''
+ ],
+ 'commentsformat' => [
+ 'choices' => [
+ FORMAT_HTML,
+ FORMAT_MOODLE,
+ FORMAT_PLAIN,
+ FORMAT_MARKDOWN
+ ],
+ 'type' => PARAM_INT,
+ 'default' => FORMAT_PLAIN
+ ],
+ '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
+ ],
+ 'dpocommentformat' => [
+ 'choices' => [
+ FORMAT_HTML,
+ FORMAT_MOODLE,
+ FORMAT_PLAIN,
+ FORMAT_MARKDOWN
+ ],
+ 'type' => PARAM_INT,
+ 'default' => FORMAT_PLAIN
+ ],
+ ];
+ }
+}
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..539fc289a48
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/expired_contexts_manager.php
@@ -0,0 +1,154 @@
+.
+
+/**
+ * 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 core_privacy\manager;
+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 {
+
+ /**
+ * Number of deleted contexts for each scheduled task run.
+ */
+ const DELETE_LIMIT = 200;
+
+ /**
+ * Returns the list of expired context instances.
+ *
+ * @return \stdClass[]
+ */
+ abstract protected function get_expired_contexts();
+
+ /**
+ * Specify with context levels this expired contexts manager is deleting.
+ *
+ * @return int[]
+ */
+ abstract protected function get_context_levels();
+
+ /**
+ * 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 manager();
+ $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
+
+ foreach ($this->get_context_levels() as $level) {
+
+ $expiredcontexts = expired_context::get_records_by_contextlevel($level, expired_context::STATUS_APPROVED);
+
+ foreach ($expiredcontexts as $expiredctx) {
+
+ if (!$this->delete_expired_context($privacymanager, $expiredctx)) {
+ continue;
+ }
+
+ $numprocessed += 1;
+ if ($numprocessed == self::DELETE_LIMIT) {
+ // Close the recordset.
+ $expiredcontexts->close();
+ break 2;
+ }
+ }
+ }
+
+ return $numprocessed;
+ }
+
+ /**
+ * Deletes user data from the provided context.
+ *
+ * @param manager $privacymanager
+ * @param expired_context $expiredctx
+ * @return \context|false
+ */
+ protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
+
+ $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
+ if (!$context) {
+ api::delete_expired_context($expiredctx->get('contextid'));
+ return false;
+ }
+
+ if (!PHPUNIT_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);
+
+ return $context;
+ }
+
+ /**
+ * 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..f878edff093
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/expired_course_related_contexts.php
@@ -0,0 +1,135 @@
+.
+
+/**
+ * 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 {
+
+ /**
+ * Course-related context levels.
+ *
+ * @return int[]
+ */
+ protected function get_context_levels() {
+ return [CONTEXT_MODULE, CONTEXT_BLOCK, CONTEXT_COURSE];
+ }
+
+ /**
+ * 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..924d5650132
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/expired_user_contexts.php
@@ -0,0 +1,150 @@
+.
+
+/**
+ * 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 core_privacy\manager;
+
+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 {
+
+ /**
+ * Only user level.
+ *
+ * @return int[]
+ */
+ protected function get_context_levels() {
+ return [CONTEXT_USER];
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Deletes user data from the provided context.
+ *
+ * Overwritten to delete the user.
+ *
+ * @param manager $privacymanager
+ * @param expired_context $expiredctx
+ * @return \context|false
+ */
+ protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
+ $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
+ if (!$context) {
+ api::delete_expired_context($expiredctx->get('contextid'));
+ return false;
+ }
+
+ if (!PHPUNIT_TEST) {
+ mtrace('Deleting context ' . $context->id . ' - ' .
+ shorten_text($context->get_context_name(true, true)));
+ }
+
+ // To ensure that all user data is deleted, instead of deleting by context, we run through and collect any stray
+ // contexts for the user that may still exist and call delete_data_for_user().
+ $user = \core_user::get_user($context->instanceid, '*', MUST_EXIST);
+ $approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id);
+ $contextlistcollection = $privacymanager->get_contexts_for_userid($user->id);
+
+ foreach ($contextlistcollection as $contextlist) {
+ $approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist(
+ $user,
+ $contextlist->get_component(),
+ $contextlist->get_contextids()
+ ));
+ }
+
+ $privacymanager->delete_data_for_user($approvedlistcollection);
+ api::set_expired_context_status($expiredctx, expired_context::STATUS_CLEANED);
+
+ // Delete the user.
+ delete_user($user);
+
+ return $context;
+ }
+}
diff --git a/admin/tool/dataprivacy/classes/external.php b/admin/tool/dataprivacy/classes/external.php
new file mode 100644
index 00000000000..276b419d4c3
--- /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 (userid = :userid OR requestedby = :requestedby)';
+ $params = ['id' => $requestid, 'userid' => $USER->id, '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/data_request_exporter.php b/admin/tool/dataprivacy/classes/external/data_request_exporter.php
new file mode 100644
index 00000000000..99963493317
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/external/data_request_exporter.php
@@ -0,0 +1,179 @@
+.
+
+/**
+ * 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 moodle_exception;
+use renderer_base;
+use tool_dataprivacy\api;
+use tool_dataprivacy\data_request;
+use tool_dataprivacy\local\helper;
+
+/**
+ * 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
+ * @throws moodle_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);
+ } else {
+ $values['requestedbyuser'] = $values['foruser'];
+ }
+
+ 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'));
+
+ $values['typename'] = helper::get_request_type_string($this->persistent->get('type'));
+ $values['typenameshort'] = helper::get_shortened_request_type_string($this->persistent->get('type'));
+
+ $values['canreview'] = false;
+ $values['statuslabel'] = helper::get_request_status_string($this->persistent->get('status'));
+ switch ($this->persistent->get('status')) {
+ case api::DATAREQUEST_STATUS_PENDING:
+ $values['statuslabelclass'] = 'label-default';
+ break;
+ case api::DATAREQUEST_STATUS_PREPROCESSING:
+ $values['statuslabelclass'] = 'label-default';
+ break;
+ case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
+ $values['statuslabelclass'] = 'label-info';
+ // DPO can review the request once it's ready.
+ $values['canreview'] = true;
+ break;
+ case api::DATAREQUEST_STATUS_APPROVED:
+ $values['statuslabelclass'] = 'label-info';
+ break;
+ case api::DATAREQUEST_STATUS_PROCESSING:
+ $values['statuslabelclass'] = 'label-info';
+ break;
+ case api::DATAREQUEST_STATUS_COMPLETE:
+ $values['statuslabelclass'] = 'label-success';
+ break;
+ case api::DATAREQUEST_STATUS_CANCELLED:
+ $values['statuslabelclass'] = 'label-warning';
+ break;
+ case api::DATAREQUEST_STATUS_REJECTED:
+ $values['statuslabelclass'] = 'label-important';
+ break;
+ }
+
+ return $values;
+ }
+}
diff --git a/admin/tool/dataprivacy/classes/external/name_description_exporter.php b/admin/tool/dataprivacy/classes/external/name_description_exporter.php
new file mode 100644
index 00000000000..eeb39a23d80
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/external/name_description_exporter.php
@@ -0,0 +1,63 @@
+.
+
+/**
+ * Class for exporting an object containing a name and a description.
+ *
+ * @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 core\external\exporter;
+
+/**
+ * Class that exports an object containing a name and a description.
+ *
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class name_description_exporter extends exporter {
+
+ /**
+ * Returns a list of objects that are related.
+ *
+ * @return array
+ */
+ protected static function define_related() {
+ return array(
+ 'context' => 'context',
+ );
+ }
+
+ /**
+ * Return the list of properties.
+ *
+ * @return array
+ */
+ protected static function define_properties() {
+ return [
+ 'name' => [
+ 'type' => PARAM_TEXT,
+ ],
+ 'description' => [
+ 'type' => PARAM_TEXT,
+ ],
+ ];
+ }
+}
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..f91e2f28b8d
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/external/purpose_exporter.php
@@ -0,0 +1,146 @@
+.
+
+/**
+ * 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 coding_exception;
+use core\external\persistent_exporter;
+use DateInterval;
+use Exception;
+use renderer_base;
+use tool_dataprivacy\purpose;
+
+/**
+ * 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 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 [
+ 'formattedretentionperiod' => [
+ 'type' => PARAM_TEXT
+ ],
+ 'formattedlawfulbases' => [
+ 'type' => name_description_exporter::read_properties_definition(),
+ 'multiple' => true
+ ],
+ 'formattedsensitivedatareasons' => [
+ 'type' => name_description_exporter::read_properties_definition(),
+ 'multiple' => true,
+ 'optional' => true
+ ],
+ ];
+ }
+
+ /**
+ * Return other properties.
+ *
+ * @param renderer_base $output
+ * @return array
+ * @throws coding_exception
+ * @throws Exception
+ */
+ protected function get_other_values(renderer_base $output) {
+ $values = [];
+
+ $formattedbases = [];
+ $lawfulbases = explode(',', $this->persistent->get('lawfulbases'));
+ if (!empty($lawfulbases)) {
+ foreach ($lawfulbases as $basis) {
+ if (empty(trim($basis))) {
+ continue;
+ }
+ $formattedbases[] = (object)[
+ 'name' => get_string($basis . '_name', 'tool_dataprivacy'),
+ 'description' => get_string($basis . '_description', 'tool_dataprivacy')
+ ];
+ }
+ }
+ $values['formattedlawfulbases'] = $formattedbases;
+
+ $formattedsensitivereasons = [];
+ $sensitivereasons = explode(',', $this->persistent->get('sensitivedatareasons'));
+ if (!empty($sensitivereasons)) {
+ foreach ($sensitivereasons as $reason) {
+ if (empty(trim($reason))) {
+ continue;
+ }
+ $formattedsensitivereasons[] = (object)[
+ 'name' => get_string($reason . '_name', 'tool_dataprivacy'),
+ 'description' => get_string($reason . '_description', 'tool_dataprivacy')
+ ];
+ }
+ }
+ $values['formattedsensitivedatareasons'] = $formattedsensitivereasons;
+
+ $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');
+ }
+ $values['formattedretentionperiod'] = $formattedtime;
+
+ return $values;
+ }
+}
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..bd1204f08f3
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/form/context_instance.php
@@ -0,0 +1,221 @@
+.
+
+/**
+ * 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;
+use tool_dataprivacy\purpose;
+
+/**
+ * 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->_form->addHelpButton('subjectscope', 'subjectscope', 'tool_dataprivacy');
+
+ $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->addHelpButton('categorygroup', 'category', 'tool_dataprivacy');
+ $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->addHelpButton('purposegroup', 'purpose', 'tool_dataprivacy');
+ $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']);
+ $mform->addHelpButton('retention_current', 'retentionperiod', 'tool_dataprivacy');
+ }
+ }
+
+ /**
+ * 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 purpose $effectivepurpose
+ * @param int $retentioncontextlevel
+ * @param \context $context The context, just for displaying (filters) purposes.
+ * @return string
+ */
+ protected static function get_retention_display_text(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..4e599c3bef7
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/form/defaults.php
@@ -0,0 +1,81 @@
+.
+
+/**
+ * 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->addHelpButton($categoryvar, 'categorydefault', 'tool_dataprivacy');
+ $mform->setType($categoryvar, PARAM_INT);
+
+ $mform->addElement('select', $purposevar, get_string('purpose', 'tool_dataprivacy'), $purposeoptions);
+ $mform->addHelpButton($purposevar, 'purposedefault', 'tool_dataprivacy');
+ $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..dba39dfd339
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/form/purpose.php
@@ -0,0 +1,149 @@
+.
+
+/**
+ * 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 string 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);
+
+ // Field for selecting lawful bases (from GDPR Article 6.1).
+ $lawfulbases = [];
+ foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) {
+ $key = 'gdpr_art_6_1_' . $article;
+ $lawfulbases[$key] = get_string($key . '_name', 'tool_dataprivacy');
+ }
+ $options = array(
+ 'multiple' => true,
+ );
+ $mform->addElement('autocomplete', 'lawfulbases', get_string('lawfulbases', 'tool_dataprivacy'), $lawfulbases, $options);
+ $mform->addRule('lawfulbases', get_string('required'), 'required', null, 'server');
+ $mform->addHelpButton('lawfulbases', 'lawfulbases', 'tool_dataprivacy');
+
+ // Optional field for selecting reasons for collecting sensitive personal data (from GDPR Article 9.2).
+ $sensitivereasons = [];
+ foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) {
+ $key = 'gdpr_art_9_2_' . $article;
+ $sensitivereasons[$key] = get_string($key . '_name', 'tool_dataprivacy');
+ }
+ $mform->addElement('autocomplete', 'sensitivedatareasons', get_string('sensitivedatareasons', 'tool_dataprivacy'),
+ $sensitivereasons, $options);
+ $mform->addHelpButton('sensitivedatareasons', 'sensitivedatareasons', 'tool_dataprivacy');
+
+ $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);
+
+ if (is_array($data->lawfulbases)) {
+ $data->lawfulbases = implode(',', $data->lawfulbases);
+ }
+ if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) {
+ $data->sensitivedatareasons = implode(',', $data->sensitivedatareasons);
+ }
+
+ // 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();
+
+ $data->lawfulbases = explode(',', $data->lawfulbases);
+ if (!empty($data->sensitivedatareasons)) {
+ $data->sensitivedatareasons = explode(',', $data->sensitivedatareasons);
+ }
+
+ // 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/form/request_user_autocomplete.php b/admin/tool/dataprivacy/classes/form/request_user_autocomplete.php
new file mode 100644
index 00000000000..f04b01f07df
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/form/request_user_autocomplete.php
@@ -0,0 +1,103 @@
+.
+
+
+/**
+ * Filter selector field.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_dataprivacy\form;
+
+use MoodleQuickForm_autocomplete;
+
+global $CFG;
+require_once($CFG->libdir . '/form/autocomplete.php');
+
+/**
+ * Form field type for choosing a user on the data request creation form.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class request_user_autocomplete extends MoodleQuickForm_autocomplete {
+
+ /**
+ * Constructor.
+ *
+ * @param string $elementName Element name
+ * @param mixed $elementLabel Label(s) for an element
+ * @param array $options Options to control the element's display
+ * Valid options are:
+ * - multiple bool Whether or not the field accepts more than one values.
+ */
+ public function __construct($elementName = null, $elementLabel = null, $options = array()) {
+ $validattributes = array(
+ 'ajax' => 'tool_dataprivacy/form-user-selector',
+ );
+ if (!empty($options['multiple'])) {
+ $validattributes['multiple'] = 'multiple';
+ }
+
+ parent::__construct($elementName, $elementLabel, array(), $validattributes);
+ }
+
+ /**
+ * Set the value of this element.
+ *
+ * @param string|array $value The value to set.
+ * @return boolean
+ */
+ public function setValue($value) {
+ global $DB;
+ $values = (array) $value;
+ $ids = [];
+ foreach ($values as $onevalue) {
+ if (!empty($onevalue) && (!$this->optionExists($onevalue)) &&
+ ($onevalue !== '_qf__force_multiselect_submission')) {
+ array_push($ids, $onevalue);
+ }
+ }
+
+ if (empty($ids)) {
+ return $this->setSelected([]);
+ }
+
+ $toselect = [];
+ list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
+ $allusernames = get_all_user_name_fields(true);
+ // Exclude admins and guest user.
+ $excludedusers = array_keys(get_admins()) + [guest_user()->id];
+ $sort = 'firstname ASC';
+ $fields = 'id, email, ' . $allusernames;
+ // Fetch the selected users, exclude the admins and guest users.
+ $users = get_users(true, '', true, $excludedusers, $sort, '', '', 0, 30, $fields, "id $insql", $inparams);
+ foreach ($users as $user) {
+ $userdata = (object)[
+ 'name' => fullname($user),
+ 'email' => $user->email
+ ];
+ $this->addOption(get_string('nameemail', 'tool_dataprivacy', $userdata), $user->id);
+ array_push($toselect, $user->id);
+ }
+
+ return $this->setSelected($toselect);
+ }
+}
diff --git a/admin/tool/dataprivacy/classes/local/helper.php b/admin/tool/dataprivacy/classes/local/helper.php
new file mode 100644
index 00000000000..f98362da954
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/local/helper.php
@@ -0,0 +1,208 @@
+.
+
+/**
+ * Collection of helper 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\local;
+defined('MOODLE_INTERNAL') || die();
+
+use coding_exception;
+use moodle_exception;
+use tool_dataprivacy\api;
+
+/**
+ * Class containing helper functions for the data privacy tool.
+ *
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+ /** The default number of results to be shown per page. */
+ const DEFAULT_PAGE_SIZE = 20;
+
+ /** Filter constant associated with the request type filter. */
+ const FILTER_TYPE = 1;
+
+ /** Filter constant associated with the request status filter. */
+ const FILTER_STATUS = 2;
+
+ /** The request filters preference key. */
+ const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters';
+
+ /**
+ * Retrieves the human-readable text value of a data request type.
+ *
+ * @param int $requesttype The request type.
+ * @return string
+ * @throws coding_exception
+ * @throws moodle_exception
+ */
+ public static function get_request_type_string($requesttype) {
+ $types = self::get_request_types();
+ if (!isset($types[$requesttype])) {
+ throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+ }
+ return $types[$requesttype];
+ }
+
+ /**
+ * Retrieves the human-readable shortened text value of a data request type.
+ *
+ * @param int $requesttype The request type.
+ * @return string
+ * @throws coding_exception
+ * @throws moodle_exception
+ */
+ public static function get_shortened_request_type_string($requesttype) {
+ $types = self::get_request_types_short();
+ if (!isset($types[$requesttype])) {
+ throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+ }
+ return $types[$requesttype];
+ }
+
+ /**
+ * Returns the key value-pairs of request type code and their string value.
+ *
+ * @return array
+ */
+ public static function get_request_types() {
+ return [
+ api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'),
+ api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy'),
+ api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothers', 'tool_dataprivacy'),
+ ];
+ }
+
+ /**
+ * Returns the key value-pairs of request type code and their shortened string value.
+ *
+ * @return array
+ */
+ public static function get_request_types_short() {
+ return [
+ api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexportshort', 'tool_dataprivacy'),
+ api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedeleteshort', 'tool_dataprivacy'),
+ api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothersshort', 'tool_dataprivacy'),
+ ];
+ }
+
+ /**
+ * Retrieves the human-readable value of a data request status.
+ *
+ * @param int $status The request status.
+ * @return string
+ * @throws moodle_exception
+ */
+ public static function get_request_status_string($status) {
+ $statuses = self::get_request_statuses();
+ if (!isset($statuses[$status])) {
+ throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy');
+ }
+ return $statuses[$status];
+ }
+
+ /**
+ * Returns the key value-pairs of request status code and string value.
+ *
+ * @return array
+ */
+ public static function get_request_statuses() {
+ return [
+ api::DATAREQUEST_STATUS_PENDING => get_string('statuspending', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_PREPROCESSING => get_string('statuspreprocessing', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_AWAITING_APPROVAL => get_string('statusawaitingapproval', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'),
+ api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'),
+ ];
+ }
+
+ /**
+ * Get the users that a user can make data request for.
+ *
+ * E.g. User having a parent role and has the 'tool/dataprivacy:makedatarequestsforchildren' capability.
+ * @param int $userid The user's ID.
+ * @return array
+ */
+ public static function get_children_of_user($userid) {
+ global $DB;
+
+ // Get users that the user has role assignments to.
+ $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' => $userid,
+ 'contextlevel' => CONTEXT_USER
+ ];
+
+ // The final list of users that we will return.
+ $finalresults = [];
+
+ // Our prospective list of users.
+ if ($candidates = $DB->get_records_sql($sql, $params)) {
+ foreach ($candidates as $key => $child) {
+ $childcontext = \context_user::instance($child->id);
+ if (has_capability('tool/dataprivacy:makedatarequestsforchildren', $childcontext, $userid)) {
+ $finalresults[$key] = $child;
+ }
+ }
+ }
+ return $finalresults;
+ }
+
+ /**
+ * Get options for the data requests filter.
+ *
+ * @return array
+ * @throws coding_exception
+ */
+ public static function get_request_filter_options() {
+ $filters = [
+ self::FILTER_TYPE => (object)[
+ 'name' => get_string('requesttype', 'tool_dataprivacy'),
+ 'options' => self::get_request_types_short()
+ ],
+ self::FILTER_STATUS => (object)[
+ 'name' => get_string('requeststatus', 'tool_dataprivacy'),
+ 'options' => self::get_request_statuses()
+ ],
+ ];
+ $options = [];
+ foreach ($filters as $category => $filtercategory) {
+ foreach ($filtercategory->options as $key => $name) {
+ $option = (object)[
+ 'category' => $filtercategory->name,
+ 'name' => $name
+ ];
+ $options["{$category}:{$key}"] = get_string('filteroption', 'tool_dataprivacy', $option);
+ }
+ }
+ return $options;
+ }
+}
diff --git a/admin/tool/dataprivacy/classes/manager_observer.php b/admin/tool/dataprivacy/classes/manager_observer.php
new file mode 100644
index 00000000000..ca613a4450f
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/manager_observer.php
@@ -0,0 +1,76 @@
+.
+
+/**
+ * Class \tool_dataprivacy\manager_observer.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_dataprivacy;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A failure observer for the \core_privacy\manager.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager_observer implements \core_privacy\manager_observer {
+ /**
+ * Notifies all DPOs that an exception occurred.
+ *
+ * @param \Throwable $e
+ * @param string $component
+ * @param string $interface
+ * @param string $methodname
+ * @param array $params
+ */
+ public function handle_component_failure($e, $component, $interface, $methodname, array $params) {
+ // Get the list of the site Data Protection Officers.
+ $dpos = api::get_site_dpos();
+
+ $messagesubject = get_string('exceptionnotificationsubject', 'tool_dataprivacy');
+ $a = (object)[
+ 'fullmethodname' => \core_privacy\manager::get_provider_classname_for_component($component) . '::' . $methodname,
+ 'component' => $component,
+ 'message' => $e->getMessage(),
+ 'backtrace' => $e->getTraceAsString()
+ ];
+ $messagebody = get_string('exceptionnotificationbody', 'tool_dataprivacy', $a);
+
+ // Email the data request to the Data Protection Officer(s)/Admin(s).
+ foreach ($dpos as $dpo) {
+ $message = new \core\message\message();
+ $message->courseid = SITEID;
+ $message->component = 'tool_dataprivacy';
+ $message->name = 'notifyexceptions';
+ $message->userfrom = \core_user::get_noreply_user();
+ $message->subject = $messagesubject;
+ $message->fullmessageformat = FORMAT_HTML;
+ $message->notification = 1;
+ $message->userto = $dpo;
+ $message->fullmessagehtml = $messagebody;
+ $message->fullmessage = html_to_text($messagebody);
+
+ // Send message.
+ message_send($message);
+ }
+ }
+}
diff --git a/admin/tool/dataprivacy/classes/metadata_registry.php b/admin/tool/dataprivacy/classes/metadata_registry.php
new file mode 100644
index 00000000000..6282cc41291
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/metadata_registry.php
@@ -0,0 +1,155 @@
+.
+
+/**
+ * Class containing helper methods for processing data requests.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Adrian Greeve
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class containing helper methods for processing data requests.
+ *
+ * @copyright 2018 Adrian Greeve
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class metadata_registry {
+
+ /**
+ * Returns plugin types / plugins and the user data that it stores in a format that can be sent to a template.
+ *
+ * @return array An array with all of the plugin types / plugins and the user data they store.
+ */
+ public function get_registry_metadata() {
+ $manager = new \core_privacy\manager();
+ $manager->set_observer(new \tool_dataprivacy\manager_observer());
+
+ $pluginman = \core_plugin_manager::instance();
+ $contributedplugins = $this->get_contrib_list();
+ $metadata = $manager->get_metadata_for_components();
+ $fullyrichtree = $this->get_full_component_list();
+ foreach ($fullyrichtree as $branch => $leaves) {
+ $plugintype = $leaves['plugin_type'];
+ $plugins = array_map(function($component) use ($manager, $metadata, $contributedplugins, $plugintype, $pluginman) {
+ // Use the plugin name for the plugins, ignore for core subsystems.
+ $internaldata = ($plugintype == 'core') ? ['component' => $component] :
+ ['component' => $pluginman->plugin_name($component)];
+ $internaldata['raw_component'] = $component;
+ if ($manager->component_is_compliant($component)) {
+ $internaldata['compliant'] = true;
+ if (isset($metadata[$component])) {
+ $collection = $metadata[$component]->get_collection();
+ $internaldata = $this->format_metadata($collection, $component, $internaldata);
+ } else if ($manager->is_empty_subsystem($component)) {
+ // This is an unused subsystem.
+ // Use the generic string.
+ $internaldata['nullprovider'] = get_string('privacy:subsystem:empty', 'core_privacy');
+ } else {
+ // Call get_reason for null provider.
+ $internaldata['nullprovider'] = get_string($manager->get_null_provider_reason($component), $component);
+ }
+ } else {
+ $internaldata['compliant'] = false;
+ }
+ // Check to see if we are an external plugin.
+ $componentshortname = explode('_', $component);
+ $shortname = array_pop($componentshortname);
+ if (isset($contributedplugins[$plugintype][$shortname])) {
+ $internaldata['external'] = true;
+ }
+ return $internaldata;
+ }, $leaves['plugins']);
+ $fullyrichtree[$branch]['plugin_type_raw'] = $plugintype;
+ // We're done using the plugin type. Convert it to a readable string.
+ $fullyrichtree[$branch]['plugin_type'] = $pluginman->plugintype_name($plugintype);
+ $fullyrichtree[$branch]['plugins'] = $plugins;
+ }
+ return $fullyrichtree;
+ }
+
+ /**
+ * Formats the metadata for use with a template.
+ *
+ * @param array $collection The collection associated with the component that we want to expand and format.
+ * @param string $component The component that we are dealing in
+ * @param array $internaldata The array to add the formatted metadata to.
+ * @return array The internal data array with the formatted metadata.
+ */
+ protected function format_metadata($collection, $component, $internaldata) {
+ foreach ($collection as $collectioninfo) {
+ $privacyfields = $collectioninfo->get_privacy_fields();
+ $fields = '';
+ if (!empty($privacyfields)) {
+ $fields = array_map(function($key, $field) use ($component) {
+ return [
+ 'field_name' => $key,
+ 'field_summary' => get_string($field, $component)
+ ];
+ }, array_keys($privacyfields), $privacyfields);
+ }
+ // Can the metadata types be located somewhere else besides core?
+ $items = explode('\\', get_class($collectioninfo));
+ $type = array_pop($items);
+ $typedata = [
+ 'name' => $collectioninfo->get_name(),
+ 'type' => $type,
+ 'fields' => $fields,
+ 'summary' => get_string($collectioninfo->get_summary(), $component)
+ ];
+ if (strpos($type, 'subsystem_link') === 0 || strpos($type, 'plugintype_link') === 0) {
+ $typedata['link'] = true;
+ }
+ $internaldata['metadata'][] = $typedata;
+ }
+ return $internaldata;
+ }
+
+ /**
+ * Return the full list of components.
+ *
+ * @return array An array of plugin types which contain plugin data.
+ */
+ protected function get_full_component_list() {
+ global $CFG;
+
+ $list = \core_component::get_component_list();
+ $list['core']['core'] = "{$CFG->dirroot}/lib";
+ $formattedlist = [];
+ foreach ($list as $plugintype => $plugin) {
+ $formattedlist[] = ['plugin_type' => $plugintype, 'plugins' => array_keys($plugin)];
+ }
+
+ return $formattedlist;
+ }
+
+ /**
+ * Returns a list of contributed plugins installed on the system.
+ *
+ * @return array A list of contributed plugins installed.
+ */
+ protected function get_contrib_list() {
+ return array_map(function($plugins) {
+ return array_filter($plugins, function($plugindata) {
+ return !$plugindata->is_standard();
+ });
+ }, \core_plugin_manager::instance()->get_plugins());
+ }
+}
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..444569e493d
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/output/data_deletion_page.php
@@ -0,0 +1,94 @@
+.
+
+/**
+ * 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 moodle_exception;
+use moodle_url;
+use renderable;
+use renderer_base;
+use single_select;
+use stdClass;
+use templatable;
+use tool_dataprivacy\data_request;
+use tool_dataprivacy\local\helper;
+
+/**
+ * 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 {
+
+ /** @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 \tool_dataprivacy\data_request[] $filter
+ * @param expired_contexts_table $expiredcontextstable
+ */
+ public function __construct($filter, expired_contexts_table $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 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(helper::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_compliance_page.php b/admin/tool/dataprivacy/classes/output/data_registry_compliance_page.php
new file mode 100644
index 00000000000..58665811cfa
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/output/data_registry_compliance_page.php
@@ -0,0 +1,60 @@
+.
+
+/**
+ * Contains the data registry compliance renderable.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Adrian Greeve
+ * @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 templatable;
+
+/**
+ * Class containing the data registry compliance renderable
+ *
+ * @copyright 2018 Adrian Greeve
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class data_registry_compliance_page implements renderable, templatable {
+
+ /** @var array meta-data to be displayed about the system. */
+ protected $metadata;
+
+ /**
+ * Constructor.
+ *
+ * @param array $metadata
+ */
+ public function __construct($metadata) {
+ $this->metadata = $metadata;
+ }
+
+ /**
+ * 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) {
+ return ['types' => $this->metadata];
+ }
+}
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..cfdad43d72a
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/output/data_registry_page.php
@@ -0,0 +1,481 @@
+.
+
+/**
+ * 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->info = (object)[
+ 'message' => get_string('dataregistryinfo', 'tool_dataprivacy'),
+ 'announce' => 1
+ ];
+ $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'),
+ '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 = [];
+
+ $children = $coursecontext->get_child_contexts();
+ foreach ($children as $childcontext) {
+
+ if ($childcontext->contextlevel !== CONTEXT_BLOCK) {
+ continue;
+ }
+
+ $blockinstance = block_instance_by_id($childcontext->instanceid);
+ $displayname = shorten_text(format_string($blockinstance->get_title(), true, ['context' => $childcontext]));
+ $branches[] = self::complete([
+ 'text' => $displayname,
+ 'contextid' => $childcontext->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..7ea4bf87ffb
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/output/data_requests_page.php
@@ -0,0 +1,96 @@
+.
+
+/**
+ * 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\api;
+use tool_dataprivacy\local\helper;
+
+/**
+ * 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_requests_table $table The data requests table. */
+ protected $table;
+
+ /** @var int[] $filters The applied filters. */
+ protected $filters = [];
+
+ /**
+ * Construct this renderable.
+ *
+ * @param data_requests_table $table The data requests table.
+ * @param int[] $filters The applied filters.
+ */
+ public function __construct($table, $filters) {
+ $this->table = $table;
+ $this->filters = $filters;
+ }
+
+ /**
+ * 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);
+
+ if (!is_https()) {
+ $httpwarningmessage = get_string('httpwarning', 'tool_dataprivacy');
+ $data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1);
+ }
+
+ $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
+ $filteroptions = helper::get_request_filter_options();
+ $filter = new request_filter($filteroptions, $this->filters, $url);
+ $data->filter = $filter->export_for_template($output);
+
+ ob_start();
+ $this->table->out(helper::DEFAULT_PAGE_SIZE, true);
+ $requests = ob_get_contents();
+ ob_end_clean();
+
+ $data->datarequests = $requests;
+ return $data;
+ }
+}
diff --git a/admin/tool/dataprivacy/classes/output/data_requests_table.php b/admin/tool/dataprivacy/classes/output/data_requests_table.php
new file mode 100644
index 00000000000..97918d9b86a
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/output/data_requests_table.php
@@ -0,0 +1,262 @@
+.
+
+/**
+ * Contains the class used for the displaying the data requests 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 action_menu;
+use action_menu_link_secondary;
+use coding_exception;
+use dml_exception;
+use html_writer;
+use moodle_url;
+use stdClass;
+use table_sql;
+use tool_dataprivacy\api;
+use tool_dataprivacy\external\data_request_exporter;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * The class for displaying the data requests table.
+ *
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class data_requests_table extends table_sql {
+
+ /** @var int The user ID. */
+ protected $userid = 0;
+
+ /** @var int[] The status filters. */
+ protected $statuses = [];
+
+ /** @var int[] The request type filters. */
+ protected $types = [];
+
+ /** @var bool Whether this table is being rendered for managing data requests. */
+ protected $manage = false;
+
+ /** @var stdClass[] Array of data request persistents. */
+ protected $datarequests = [];
+
+ /**
+ * data_requests_table constructor.
+ *
+ * @param int $userid The user ID
+ * @param int[] $statuses
+ * @param int[] $types
+ * @param bool $manage
+ * @throws coding_exception
+ */
+ public function __construct($userid = 0, $statuses = [], $types = [], $manage = false) {
+ parent::__construct('data-requests-table');
+
+ $this->userid = $userid;
+ $this->statuses = $statuses;
+ $this->types = $types;
+ $this->manage = $manage;
+
+ $columnheaders = [
+ 'type' => get_string('requesttype', 'tool_dataprivacy'),
+ 'userid' => get_string('user', 'tool_dataprivacy'),
+ 'timecreated' => get_string('daterequested', 'tool_dataprivacy'),
+ 'requestedby' => get_string('requestby', 'tool_dataprivacy'),
+ 'status' => get_string('requeststatus', 'tool_dataprivacy'),
+ 'comments' => get_string('message', 'tool_dataprivacy'),
+ 'actions' => '',
+ ];
+
+ $this->define_columns(array_keys($columnheaders));
+ $this->define_headers(array_values($columnheaders));
+ $this->no_sorting('actions');
+ }
+
+ /**
+ * The type column.
+ *
+ * @param stdClass $data The row data.
+ * @return string
+ */
+ public function col_type($data) {
+ if ($this->manage) {
+ return $data->typenameshort;
+ }
+ return $data->typename;
+ }
+
+ /**
+ * The user column.
+ *
+ * @param stdClass $data The row data.
+ * @return mixed
+ */
+ public function col_userid($data) {
+ $user = $data->foruser;
+ return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
+ }
+
+ /**
+ * The context information column.
+ *
+ * @param stdClass $data The row data.
+ * @return string
+ */
+ public function col_timecreated($data) {
+ return userdate($data->timecreated);
+ }
+
+ /**
+ * The requesting user's column.
+ *
+ * @param stdClass $data The row data.
+ * @return mixed
+ */
+ public function col_requestedby($data) {
+ $user = $data->requestedbyuser;
+ return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]);
+ }
+
+ /**
+ * The status column.
+ *
+ * @param stdClass $data The row data.
+ * @return mixed
+ */
+ public function col_status($data) {
+ return html_writer::span($data->statuslabel, 'label ' . $data->statuslabelclass);
+ }
+
+ /**
+ * The comments column.
+ *
+ * @param stdClass $data The row data.
+ * @return string
+ */
+ public function col_comments($data) {
+ return shorten_text($data->comments, 60);
+ }
+
+ /**
+ * The actions column.
+ *
+ * @param stdClass $data The row data.
+ * @return string
+ */
+ public function col_actions($data) {
+ global $OUTPUT;
+
+ $requestid = $data->id;
+ $status = $data->status;
+
+ // 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);
+
+ return $OUTPUT->render($actionsmenu);
+ }
+
+ /**
+ * 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) {
+ global $PAGE;
+
+ // Count data requests from the given conditions.
+ $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types);
+ $this->pagesize($pagesize, $total);
+
+ $sort = $this->get_sql_sort();
+
+ // Get data requests from the given conditions.
+ $datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $sort,
+ $this->get_page_start(), $this->get_page_size());
+ $this->rawdata = [];
+ $context = \context_system::instance();
+ $renderer = $PAGE->get_renderer('tool_dataprivacy');
+ foreach ($datarequests as $persistent) {
+ $exporter = new data_request_exporter($persistent, ['context' => $context]);
+ $this->rawdata[] = $exporter->export($renderer);
+ }
+
+ // 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();
+ if (!empty($this->statuses) || !empty($this->types)) {
+ $message = get_string('nodatarequestsmatchingfilter', 'tool_dataprivacy');
+ } else {
+ $message = get_string('nodatarequests', 'tool_dataprivacy');
+ }
+ echo $OUTPUT->notification($message, 'warning');
+ }
+
+ /**
+ * Override the table's show_hide_link method to prevent the show/hide links 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) {
+ return '';
+ }
+}
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..c6f0e65c7be
--- /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|null $contextlevel
+ * @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..25229f008ac
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/output/my_data_requests_page.php
@@ -0,0 +1,145 @@
+.
+
+/**
+ * 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) {
+ global $USER;
+
+ $data = new stdClass();
+ $data->newdatarequesturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php');
+
+ if (!is_https()) {
+ $httpwarningmessage = get_string('httpwarning', 'tool_dataprivacy');
+ $data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1);
+ }
+
+ $requests = [];
+ foreach ($this->requests as $request) {
+ $requestid = $request->get('id');
+ $status = $request->get('status');
+ $userid = $request->get('userid');
+
+ $usercontext = context_user::instance($userid, IGNORE_MISSING);
+ if (!$usercontext) {
+ // Use the context system.
+ $outputcontext = \context_system::instance();
+ } else {
+ $outputcontext = $usercontext;
+ }
+
+ $requestexporter = new data_request_exporter($request, ['context' => $outputcontext]);
+ $item = $requestexporter->export($output);
+
+ if ($request->get('userid') != $USER->id) {
+ // Append user name if it differs from $USER.
+ $a = (object)['typename' => $item->typename, 'user' => $item->foruser->fullname];
+ $item->typename = get_string('requesttypeuser', 'tool_dataprivacy', $a);
+ }
+
+ $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 && $usercontext) {
+ $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..09bcb1c45a1
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/output/renderer.php
@@ -0,0 +1,142 @@
+.
+
+/**
+ * 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,
+ 'class' => 'contactdpo'
+ ];
+ 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 data compliance registry.
+ *
+ * @param data_registry_compliance_page $page
+ * @return string html for the page
+ * @throws moodle_exception
+ */
+ public function render_data_registry_compliance_page(data_registry_compliance_page $page) {
+ $data = $page->export_for_template($this);
+ return parent::render_from_template('tool_dataprivacy/data_registry_compliance', $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/output/request_filter.php b/admin/tool/dataprivacy/classes/output/request_filter.php
new file mode 100644
index 00000000000..ff3108d77f7
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/output/request_filter.php
@@ -0,0 +1,98 @@
+.
+
+/**
+ * Class containing the filter options data for rendering the autocomplete element for the data requests page.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class containing the filter options data for rendering the autocomplete element for the data requests page.
+ *
+ * @copyright 2018 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class request_filter implements renderable, templatable {
+
+ /** @var array $filteroptions The filter options. */
+ protected $filteroptions;
+
+ /** @var array $selectedoptions The list of selected filter option values. */
+ protected $selectedoptions;
+
+ /** @var moodle_url|string $baseurl The url with params needed to call up this page. */
+ protected $baseurl;
+
+ /**
+ * request_filter constructor.
+ *
+ * @param array $filteroptions The filter options.
+ * @param array $selectedoptions The list of selected filter option values.
+ * @param string|moodle_url $baseurl The url with params needed to call up this page.
+ */
+ public function __construct($filteroptions, $selectedoptions, $baseurl = null) {
+ $this->filteroptions = $filteroptions;
+ $this->selectedoptions = $selectedoptions;
+ if (!empty($baseurl)) {
+ $this->baseurl = new moodle_url($baseurl);
+ }
+ }
+
+ /**
+ * Function to export the renderer data in a format that is suitable for a mustache template.
+ *
+ * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
+ * @return stdClass|array
+ */
+ public function export_for_template(renderer_base $output) {
+ global $PAGE;
+ $data = new stdClass();
+ if (empty($this->baseurl)) {
+ $this->baseurl = $PAGE->url;
+ }
+ $data->action = $this->baseurl->out(false);
+
+ foreach ($this->selectedoptions as $option) {
+ if (!isset($this->filteroptions[$option])) {
+ $this->filteroptions[$option] = $option;
+ }
+ }
+
+ $data->filteroptions = [];
+ foreach ($this->filteroptions as $value => $label) {
+ $selected = in_array($value, $this->selectedoptions);
+ $filteroption = (object)[
+ 'value' => $value,
+ 'label' => $label
+ ];
+ $filteroption->selected = $selected;
+ $data->filteroptions[] = $filteroption;
+ }
+ return $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..a185e1338d6
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/privacy/provider.php
@@ -0,0 +1,204 @@
+.
+
+/**
+ * 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;
+defined('MOODLE_INTERNAL') || die();
+
+use coding_exception;
+use context;
+use context_user;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\helper;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use dml_exception;
+use stdClass;
+use tool_dataprivacy\api;
+use tool_dataprivacy\local\helper as tool_helper;
+
+/**
+ * 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
+ */
+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,
+
+ // This plugin has some sitewide user preferences to export.
+ \core_privacy\local\request\user_preference_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'
+ );
+
+ $collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS,
+ 'privacy:metadata:preference:tool_dataprivacy_request-filters');
+
+ 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 {
+ $sql = "SELECT id
+ FROM {context}
+ WHERE instanceid = :userid
+ AND contextlevel = :contextlevel";
+
+ $contextlist = new contextlist();
+ $contextlist->set_component('tool_dataprivacy');
+ $contextlist->add_from_sql($sql, ['userid' => $userid, 'contextlevel' => CONTEXT_USER]);
+ return $contextlist;
+ }
+
+ /**
+ * Export all user data for the specified user, in the specified contexts.
+ *
+ * @param approved_contextlist $contextlist The approved contexts to export information for.
+ * @throws coding_exception
+ * @throws dml_exception
+ * @throws \moodle_exception
+ */
+ public static function export_user_data(approved_contextlist $contextlist) {
+ if (empty($contextlist->count())) {
+ return;
+ }
+
+ $user = $contextlist->get_user();
+ $datarequests = api::get_data_requests($user->id);
+ $context = context_user::instance($user->id);
+ $contextdatatowrite = [];
+ foreach ($datarequests as $request) {
+ $record = $request->to_record();
+ $data = new stdClass();
+
+ // The user ID that made the request/the request is made for.
+ if ($record->requestedby != $record->userid) {
+ if ($user->id != $record->requestedby) {
+ // This request is done by this user for another user.
+ $data->userid = fullname($user);
+ } else if ($user->id != $record->userid) {
+ // This request was done by another user on behalf of this user.
+ $data->requestedby = fullname($user);
+ }
+ }
+
+ // Request type.
+ $data->type = tool_helper::get_shortened_request_type_string($record->type);
+ // Status.
+ $data->status = tool_helper::get_request_status_string($record->status);
+ // Comments.
+ $data->comments = $record->comments;
+ // The DPO's comment about this request.
+ $data->dpocomment = $record->dpocomment;
+ // The date and time this request was lodged.
+ $data->timecreated = transform::datetime($record->timecreated);
+ $contextdatatowrite[] = $data;
+ }
+
+ // {User context} / Privacy and policies / Data requests.
+ $subcontext = [
+ get_string('privacyandpolicies', 'admin'),
+ get_string('datarequests', 'tool_dataprivacy'),
+ ];
+ writer::with_context($context)->export_data($subcontext, (object)$contextdatatowrite);
+
+ // Write generic module intro files.
+ helper::export_context_files($context, $user);
+ }
+
+ /**
+ * 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) {
+ }
+
+ /**
+ * Export all user preferences for the plugin.
+ *
+ * @param int $userid The userid of the user whose data is to be exported.
+ */
+ public static function export_user_preferences(int $userid) {
+ $preffilter = get_user_preferences(tool_helper::PREF_REQUEST_FILTERS, null, $userid);
+ if ($preffilter !== null) {
+ $filters = json_decode($preffilter);
+ $descriptions = [];
+ foreach ($filters as $filter) {
+ list($category, $value) = explode(':', $filter);
+ $option = new stdClass();
+ switch($category) {
+ case tool_helper::FILTER_TYPE:
+ $option->category = get_string('requesttype', 'tool_dataprivacy');
+ $option->name = tool_helper::get_shortened_request_type_string($value);
+ break;
+ case tool_helper::FILTER_STATUS:
+ $option->category = get_string('requeststatus', 'tool_dataprivacy');
+ $option->name = tool_helper::get_request_status_string($value);
+ break;
+ }
+ $descriptions[] = get_string('filteroption', 'tool_dataprivacy', $option);
+ }
+ // Export the filter preference as comma-separated values and text descriptions.
+ $values = implode(', ', $filters);
+ $descriptionstext = implode(', ', $descriptions);
+ writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext);
+ }
+ }
+}
diff --git a/admin/tool/dataprivacy/classes/purpose.php b/admin/tool/dataprivacy/classes/purpose.php
new file mode 100644
index 00000000000..ca4fb170515
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/purpose.php
@@ -0,0 +1,183 @@
+.
+
+/**
+ * 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';
+
+ /** Items under GDPR Article 6.1. */
+ const GDPR_ART_6_1_ITEMS = ['a', 'b', 'c', 'd', 'e', 'f'];
+
+ /** Items under GDPR Article 9.2. */
+ const GDPR_ART_9_2_ITEMS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
+
+ /**
+ * 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);
+
+ // Validate the purpose record.
+ $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
+ ),
+ 'lawfulbases' => array(
+ 'type' => PARAM_TEXT,
+ 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis.',
+ ),
+ 'sensitivedatareasons' => array(
+ 'type' => PARAM_TEXT,
+ 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_sensitive',
+ 'null' => NULL_ALLOWED,
+ 'default' => ''
+ ),
+ '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 bool $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/request_contextlist.php b/admin/tool/dataprivacy/classes/request_contextlist.php
new file mode 100644
index 00000000000..5764b7e11f8
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/request_contextlist.php
@@ -0,0 +1,70 @@
+.
+
+/**
+ * Contains the request_contextlist persistent.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Jake Dallimore
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\persistent;
+
+/**
+ * The request_contextlist persistent.
+ *
+ * @copyright 2018 Jake Dallimore
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class request_contextlist extends persistent {
+
+ /** The table name this persistent object maps to. */
+ const TABLE = 'tool_dataprivacy_rqst_ctxlst';
+
+ /**
+ * Return the definition of the properties of this model.
+ *
+ * @return array
+ */
+ protected static function define_properties() {
+ return [
+ 'requestid' => [
+ 'type' => PARAM_INT
+ ],
+ 'contextlistid' => [
+ 'type' => PARAM_INT
+ ]
+ ];
+ }
+
+ /**
+ * Creates a new relation, but does not persist it.
+ *
+ * @param int $requestid
+ * @param int $contextlistid
+ * @return $this
+ * @throws \coding_exception
+ */
+ public static function create_relation($requestid, $contextlistid) {
+ $requestcontextlist = new request_contextlist();
+ return $requestcontextlist->set('requestid', $requestid)
+ ->set('contextlistid', $contextlistid);
+ }
+}
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..70402f48f4b
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/task/initiate_data_request_task.php
@@ -0,0 +1,122 @@
+.
+
+/**
+ * Adhoc task that processes a data request and prepares the user's relevant contexts 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\contextlist_context;
+use tool_dataprivacy\data_request;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class that processes a data request and prepares the user's relevant contexts 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 . "/{$CFG->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;
+ }
+
+ $requestedby = $datarequest->get('requestedby');
+ $valid = true;
+ $comment = '';
+ $foruser = $datarequest->get('userid');
+ if ($foruser != $requestedby) {
+ if (!$valid = api::can_create_data_request_for_user($foruser, $requestedby)) {
+ $params = (object)[
+ 'requestedby' => $requestedby,
+ 'userid' => $foruser
+ ];
+ $comment = get_string('errornocapabilitytorequestforothers', 'tool_dataprivacy', $params);
+ mtrace($comment);
+ }
+ }
+ // Reject the request outright if it's invalid.
+ if (!$valid) {
+ $dpo = $datarequest->get('dpo');
+ api::update_request_status($requestid, api::DATAREQUEST_STATUS_REJECTED, $dpo, $comment);
+ return;
+ }
+
+ // Update the status of this request as pre-processing.
+ mtrace('Generating the contexts containing personal data for the user...');
+ api::update_request_status($requestid, api::DATAREQUEST_STATUS_PREPROCESSING);
+
+ // Add the list of relevant contexts to the request, and mark all as pending approval.
+ $privacymanager = new \core_privacy\manager();
+ $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
+
+ $contextlistcollection = $privacymanager->get_contexts_for_userid($datarequest->get('userid'));
+ api::add_request_contexts_with_status($contextlistcollection, $requestid, contextlist_context::STATUS_PENDING);
+
+ // When the preparation of the contexts finishes, update the request status to awaiting approval.
+ api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
+ mtrace('Context 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..c58f5749ceb
--- /dev/null
+++ b/admin/tool/dataprivacy/classes/task/process_data_request_task.php
@@ -0,0 +1,217 @@
+.
+
+/**
+ * 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 . "/{$CFG->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.
+ $status = $requestpersistent->get('status');
+ if (!api::is_active($status)) {
+ mtrace("Request {$requestid} with status {$status} doesn't need to be 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) {
+ // Get the collection of approved_contextlist objects needed for core_privacy data export.
+ $approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent);
+
+ // Export the data.
+ $manager = new \core_privacy\manager();
+ $manager->set_observer(new \tool_dataprivacy\manager_observer());
+
+ $exportedcontent = $manager->export_user_data($approvedclcollection);
+
+ $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) {
+ // Get the collection of approved_contextlist objects needed for core_privacy data deletion.
+ $approvedclcollection = api::get_approved_contextlist_collection_for_request($requestpersistent);
+
+ // Delete the data.
+ $manager = new \core_privacy\manager();
+ $manager->set_observer(new \tool_dataprivacy\manager_observer());
+
+ $manager->delete_data_for_user($approvedclcollection);
+ }
+
+ // 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 = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $thing->get_itemid(),
+ $thing->get_filepath(), $thing->get_filename(), true);
+ $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 requester as well if this request was made on behalf of another user who's not a DPO,
+ // and has the capability to make data requests for the user (e.g. Parent).
+ if (!api::is_site_dpo($request->requestedby) && $foruser->id != $request->requestedby) {
+ // Ensure the requester has the capability to make data requests for this user.
+ if (api::can_create_data_request_for_user($request->userid, $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']);
+ }
+ }
+
+ if ($request->type == api::DATAREQUEST_TYPE_DELETE) {
+ // Delete the user.
+ delete_user($foruser);
+ }
+ }
+}
diff --git a/admin/tool/dataprivacy/createdatarequest.php b/admin/tool/dataprivacy/createdatarequest.php
new file mode 100755
index 00000000000..83a5c901406
--- /dev/null
+++ b/admin/tool/dataprivacy/createdatarequest.php
@@ -0,0 +1,91 @@
+.
+
+/**
+ * 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.
+// That is, unless you have sufficient capabilities to perform this on behalf of a user.
+if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) {
+ redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), 0, \core\output\notification::NOTIFY_ERROR);
+}
+
+$mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage)]);
+
+// 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);
+
+ if ($manage) {
+ $foruser = core_user::get_user($data->userid);
+ $redirectmessage = get_string('datarequestcreatedforuser', 'tool_dataprivacy', fullname($foruser));
+ } else {
+ $redirectmessage = get_string('requestsubmitted', 'tool_dataprivacy');
+ }
+ redirect($returnurl, $redirectmessage);
+}
+
+$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 100755
index 00000000000..6053f0b5d02
--- /dev/null
+++ b/admin/tool/dataprivacy/createdatarequest_form.php
@@ -0,0 +1,129 @@
+.
+
+/**
+ * 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;
+use tool_dataprivacy\local\helper;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+\MoodleQuickForm::registerElementType('request_user_autocomplete',
+ $CFG->dirroot . '/admin/tool/dataprivacy/classes/form/request_user_autocomplete.php',
+ '\\tool_dataprivacy\\form\\request_user_autocomplete');
+/**
+ * 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 {
+
+ /** @var bool Flag to indicate whether this form is being rendered for managing data requests or for regular requests. */
+ protected $manage = false;
+
+ /**
+ * Form definition.
+ *
+ * @throws coding_exception
+ */
+ public function definition() {
+ global $USER;
+ $mform =& $this->_form;
+
+ $this->manage = $this->_customdata['manage'];
+ if ($this->manage) {
+ $options = [
+ 'ajax' => 'tool_dataprivacy/form-user-selector',
+ ];
+ $mform->addElement('request_user_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.
+ if ($children = helper::get_children_of_user($USER->id)) {
+ $useroptions = [
+ $USER->id => fullname($USER)
+ ];
+ foreach ($children as $key => $child) {
+ $useroptions[$key] = 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..f622e5b4ebb
--- /dev/null
+++ b/admin/tool/dataprivacy/datadeletion.php
@@ -0,0 +1,49 @@
+.
+
+/**
+ * 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');
+
+require_login(null, false);
+
+$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..52eff0a4cc4
--- /dev/null
+++ b/admin/tool/dataprivacy/dataregistry.php
@@ -0,0 +1,44 @@
+.
+
+/**
+ * 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');
+
+require_login(null, false);
+
+$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 100755
index 00000000000..0887134ca65
--- /dev/null
+++ b/admin/tool/dataprivacy/datarequests.php
@@ -0,0 +1,74 @@
+.
+
+/**
+ * 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_login(null, false);
+
+$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);
+
+$filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
+$filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
+if ($filtersapplied === [-1]) {
+ // If there are no filters submitted, check if there is a saved filters from the user preferences.
+ $filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null);
+ if ($filterprefs && empty($filterscleared)) {
+ $filtersapplied = json_decode($filterprefs);
+ } else {
+ $filtersapplied = [];
+ }
+}
+// Save the current applied filters to the user preferences.
+set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied));
+
+$types = [];
+$statuses = [];
+foreach ($filtersapplied as $filter) {
+ list($category, $value) = explode(':', $filter);
+ switch($category) {
+ case \tool_dataprivacy\local\helper::FILTER_TYPE:
+ $types[] = $value;
+ break;
+ case \tool_dataprivacy\local\helper::FILTER_STATUS:
+ $statuses[] = $value;
+ break;
+ }
+}
+
+$table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, true);
+$table->baseurl = $url;
+
+$requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied);
+$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..65d8926a90e
--- /dev/null
+++ b/admin/tool/dataprivacy/db/install.xml
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/tool/dataprivacy/db/messages.php b/admin/tool/dataprivacy/db/messages.php
new file mode 100644
index 00000000000..2c1d239d4a0
--- /dev/null
+++ b/admin/tool/dataprivacy/db/messages.php
@@ -0,0 +1,52 @@
+.
+
+/**
+ * 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,
+ ]
+ ],
+
+ // Notify Data Protection Officer about exceptions.
+ 'notifyexceptions' => [
+ 'defaults' => [
+ 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
+ ],
+ 'capability' => 'tool/dataprivacy:managedatarequests'
+ ],
+];
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..d0f44fba3af
--- /dev/null
+++ b/admin/tool/dataprivacy/db/upgrade.php
@@ -0,0 +1,166 @@
+.
+
+/**
+ * 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 < 2017111300) {
+ // Nothing to do here. Moodle Plugins site's just complaining about missing upgrade.php.
+ // Savepoint reached.
+ upgrade_plugin_savepoint(true, 2017111300, 'error', 'dataprivacy');
+ }
+
+ if ($oldversion < 2017111303) {
+
+ // Define table tool_dataprivacy_ctxexpired to be created.
+ $table = new xmldb_table('tool_dataprivacy_ctxexpired');
+
+ // Adding fields to table tool_dataprivacy_ctxexpired.
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+ // Adding keys to table tool_dataprivacy_ctxexpired.
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $table->add_key('contextid', XMLDB_KEY_FOREIGN_UNIQUE, array('contextid'), 'context', array('id'));
+
+ // Conditionally launch create table for tool_dataprivacy_ctxexpired.
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ // Dataprivacy savepoint reached.
+ upgrade_plugin_savepoint(true, 2017111303, 'tool', 'dataprivacy');
+ }
+
+ if ($oldversion < 2017111304) {
+
+ // Define table tool_dataprivacy_contextlist to be created.
+ $table = new xmldb_table('tool_dataprivacy_contextlist');
+
+ // Adding fields to table tool_dataprivacy_contextlist.
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('component', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+
+ // Adding keys to table tool_dataprivacy_contextlist.
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+ // Conditionally launch create table for tool_dataprivacy_contextlist.
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ // Define table tool_dataprivacy_ctxlst_ctx to be created.
+ $table = new xmldb_table('tool_dataprivacy_ctxlst_ctx');
+
+ // Adding fields to table tool_dataprivacy_ctxlst_ctx.
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('contextlistid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+
+ // Adding keys to table tool_dataprivacy_ctxlst_ctx.
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $table->add_key('contextlistid', XMLDB_KEY_FOREIGN, array('contextlistid'), 'tool_dataprivacy_contextlist', array('id'));
+
+ // Conditionally launch create table for tool_dataprivacy_ctxlst_ctx.
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ // Define table tool_dataprivacy_rqst_ctxlst to be created.
+ $table = new xmldb_table('tool_dataprivacy_rqst_ctxlst');
+
+ // Adding fields to table tool_dataprivacy_rqst_ctxlst.
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('requestid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+ $table->add_field('contextlistid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+ // Adding keys to table tool_dataprivacy_rqst_ctxlst.
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $table->add_key('requestid', XMLDB_KEY_FOREIGN, array('requestid'), 'tool_dataprivacy_request', array('id'));
+ $table->add_key('contextlistid', XMLDB_KEY_FOREIGN, array('contextlistid'), 'tool_dataprivacy_contextlist', array('id'));
+ $table->add_key('request_contextlist', XMLDB_KEY_UNIQUE, array('requestid', 'contextlistid'));
+
+ // Conditionally launch create table for tool_dataprivacy_rqst_ctxlst.
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ // Dataprivacy savepoint reached.
+ upgrade_plugin_savepoint(true, 2017111304, 'tool', 'dataprivacy');
+ }
+
+ if ($oldversion < 2017111305) {
+ // Define field lawfulbases to be added to tool_dataprivacy_purpose.
+ $table = new xmldb_table('tool_dataprivacy_purpose');
+
+ // It is a required field. We initially define and add it as null and later update it to XMLDB_NOTNULL.
+ $field = new xmldb_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, null, null, null, 'descriptionformat');
+
+ // Conditionally launch add field lawfulbases.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+
+ // Set a kind-of-random value to lawfulbasis field.
+ $DB->set_field('tool_dataprivacy_purpose', 'lawfulbases', 'gdpr_art_6_1_a');
+
+ // We redefine it now as not null.
+ $field = new xmldb_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null, 'descriptionformat');
+
+ // Launch change of nullability for field lawfulbases.
+ $dbman->change_field_notnull($table, $field);
+ }
+
+ // Define field sensitivedatareasons to be added to tool_dataprivacy_purpose.
+ $table = new xmldb_table('tool_dataprivacy_purpose');
+ $field = new xmldb_field('sensitivedatareasons', XMLDB_TYPE_TEXT, null, null, null, null, null, 'lawfulbases');
+
+ // Conditionally launch add field sensitivedatareasons.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Dataprivacy savepoint reached.
+ upgrade_plugin_savepoint(true, 2017111305, 'tool', 'dataprivacy');
+ }
+
+ return true;
+}
diff --git a/admin/tool/dataprivacy/defaults.php b/admin/tool/dataprivacy/defaults.php
new file mode 100644
index 00000000000..d936ba221d6
--- /dev/null
+++ b/admin/tool/dataprivacy/defaults.php
@@ -0,0 +1,78 @@
+.
+
+/**
+ * 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');
+
+require_login(null, false);
+
+$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..0ee630f0b83
--- /dev/null
+++ b/admin/tool/dataprivacy/editcategory.php
@@ -0,0 +1,60 @@
+.
+
+/**
+ * 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');
+
+require_login(null, false);
+
+$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..20ee631f9c0
--- /dev/null
+++ b/admin/tool/dataprivacy/editpurpose.php
@@ -0,0 +1,61 @@
+.
+
+/**
+ * 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');
+
+require_login(null, false);
+
+$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 100755
index 00000000000..aa640478bae
--- /dev/null
+++ b/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
@@ -0,0 +1,243 @@
+.
+
+/**
+ * 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['cachedef_purpose'] = 'Data purposes';
+$string['cachedef_contextlevel'] = 'Context levels purpose and category';
+$string['cancelrequest'] = 'Cancel request';
+$string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?';
+$string['categories'] = 'Categories';
+$string['category'] = 'Category';
+$string['category_help'] = 'A category in the data registry describes a type of data. A new category may be added, or if Inherit is selected, the data category from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > Site.';
+$string['categorycreated'] = 'Category created';
+$string['categorydefault'] = 'Default category';
+$string['categorydefault_help'] = 'The default category is the data category applied to any new instances. If Inherit is selected, the data category from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > User > Site.';
+$string['categorieslist'] = 'List of data categories';
+$string['categoryupdated'] = 'Category updated';
+$string['close'] = 'Close';
+$string['compliant'] = 'Compliant';
+$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 the privacy officer';
+$string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.';
+$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 privacy officer as described in the privacy policy.';
+$string['createcategory'] = 'Create data category';
+$string['createpurpose'] = 'Create data purpose';
+$string['datadeletion'] = 'Data deletion';
+$string['datadeletionpagehelp'] = 'Data for which the retention period has expired are listed here. Please review and confirm data deletion, which will then be executed by the "Delete expired contexts" scheduled task.';
+$string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for minors';
+$string['dataprivacy:managedatarequests'] = 'Manage data requests';
+$string['dataprivacy:managedataregistry'] = 'Manage data registry';
+$string['dataregistry'] = 'Data registry';
+$string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
+$string['datarequestcreatedforuser'] = 'Data request created for {$a}';
+$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'] = 'Privacy officer role mapping';
+$string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
+$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['errornocapabilitytorequestforothers'] = 'User {$a->requestedby} doesn\'t have the capability to make a data request on behalf of user {$a->userid}';
+$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['exceptionnotificationsubject'] = 'Exception occurred while processing privacy data';
+$string['exceptionnotificationbody'] = '
Exception occurred while calling {$a->fullmethodname}. This means that plugin {$a->component} did not complete the processing of data. The following exception information may be passed on to the plugin developer:
{$a->message}
+
+{$a->backtrace}
';
+$string['expiredretentionperiodtask'] = 'Expired retention period';
+$string['expiry'] = 'Expiry';
+$string['expandplugin'] = 'Expand and collapse plugin.';
+$string['expandplugintype'] = 'Expand and collapse plugin type.';
+$string['explanationtitle'] = 'Icons used on this page and what they mean.';
+$string['external'] = 'Additional';
+$string['externalexplanation'] = 'An additional plugin installed on this site.';
+$string['filteroption'] = '{$a->category}: {$a->name}';
+$string['frontpagecourse'] = 'Front page course';
+$string['gdpr_art_6_1_a_description'] = 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes';
+$string['gdpr_art_6_1_a_name'] = 'Consent (GDPR Art. 6.1(a))';
+$string['gdpr_art_6_1_b_description'] = 'Processing is necessary for the performance of a contract to which the data subject is party or in order to take steps at the request of the data subject prior to entering into a contract';
+$string['gdpr_art_6_1_b_name'] = 'Contract (GDPR Art. 6.1(b))';
+$string['gdpr_art_6_1_c_description'] = 'Processing is necessary for compliance with a legal obligation to which the controller is subject';
+$string['gdpr_art_6_1_c_name'] = 'Legal obligation (GDPR Art 6.1(c))';
+$string['gdpr_art_6_1_d_description'] = 'Processing is necessary in order to protect the vital interests of the data subject or of another natural person';
+$string['gdpr_art_6_1_d_name'] = 'Vital interests (GDPR Art. 6.1(d))';
+$string['gdpr_art_6_1_e_description'] = 'Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller';
+$string['gdpr_art_6_1_e_name'] = 'Public task (GDPR Art. 6.1(e))';
+$string['gdpr_art_6_1_f_description'] = 'Processing is necessary for the purposes of the legitimate interests pursued by the controller or by a third party, except where such interests are overridden by the interests or fundamental rights and freedoms of the data subject which require protection of personal data, in particular where the data subject is a child';
+$string['gdpr_art_6_1_f_name'] = 'Legitimate interests (GDPR Art. 6.1(f))';
+$string['gdpr_art_9_2_a_description'] = 'The data subject has given explicit consent to the processing of those personal data for one or more specified purposes, except where Union or Member State law provide that the prohibition referred to in paragraph 1 of GDPR Article 9 may not be lifted by the data subject';
+$string['gdpr_art_9_2_a_name'] = 'Explicit consent (GDPR Art. 9.2(a))';
+$string['gdpr_art_9_2_b_description'] = 'Processing is necessary for the purposes of carrying out the obligations and exercising specific rights of the controller or of the data subject in the field of employment and social security and social protection law in so far as it is authorised by Union or Member State law or a collective agreement pursuant to Member State law providing for appropriate safeguards for the fundamental rights and the interests of the data subject';
+$string['gdpr_art_9_2_b_name'] = 'Employment and social security/protection law (GDPR Art. 9.2(b))';
+$string['gdpr_art_9_2_c_description'] = 'Processing is necessary to protect the vital interests of the data subject or of another natural person where the data subject is physically or legally incapable of giving consent';
+$string['gdpr_art_9_2_c_name'] = 'Protection of vital interests (GDPR Art. 9.2(c))';
+$string['gdpr_art_9_2_d_description'] = 'Processing is carried out in the course of its legitimate activities with appropriate safeguards by a foundation, association or any other not-for-profit body with a political, philosophical, religious or trade-union aim and on condition that the processing relates solely to the members or to former members of the body or to persons who have regular contact with it in connection with its purposes and that the personal data are not disclosed outside that body without the consent of the data subjects';
+$string['gdpr_art_9_2_d_name'] = 'Legitimate activities regarding the members/close contacts of a foundation, association or other not-for-profit body (GDPR Art. 9.2(d))';
+$string['gdpr_art_9_2_e_description'] = 'Processing relates to personal data which are manifestly made public by the data subject';
+$string['gdpr_art_9_2_e_name'] = 'Data made public by the data subject (GDPR Art. 9.2(e))';
+$string['gdpr_art_9_2_f_description'] = 'Processing is necessary for the establishment, exercise or defence of legal claims or whenever courts are acting in their judicial capacity';
+$string['gdpr_art_9_2_f_name'] = 'Legal claims and court actions (GDPR Art. 9.2(f))';
+$string['gdpr_art_9_2_g_description'] = 'Processing is necessary for reasons of substantial public interest, on the basis of Union or Member State law which shall be proportionate to the aim pursued, respect the essence of the right to data protection and provide for suitable and specific measures to safeguard the fundamental rights and the interests of the data subject';
+$string['gdpr_art_9_2_g_name'] = 'Substantial public interest (GDPR Art. 9.2(g))';
+$string['gdpr_art_9_2_h_description'] = 'Processing is necessary for the purposes of preventive or occupational medicine, for the assessment of the working capacity of the employee, medical diagnosis, the provision of health or social care or treatment or the management of health or social care systems and services on the basis of Union or Member State law or pursuant to contract with a health professional and subject to the conditions and safeguards referred to in paragraph 3 of GDPR Article 9';
+$string['gdpr_art_9_2_h_name'] = 'Medical purposes (GDPR Art. 9.2(h))';
+$string['gdpr_art_9_2_i_description'] = 'Processing is necessary for reasons of public interest in the area of public health, such as protecting against serious cross-border threats to health or ensuring high standards of quality and safety of health care and of medicinal products or medical devices, on the basis of Union or Member State law which provides for suitable and specific measures to safeguard the rights and freedoms of the data subject, in particular professional secrecy';
+$string['gdpr_art_9_2_i_name'] = 'Public health (GDPR Art. 9.2(i))';
+$string['gdpr_art_9_2_j_description'] = 'Processing is necessary for archiving purposes in the public interest, scientific or historical research purposes or statistical purposes in accordance with Article 89(1) based on Union or Member State law which shall be proportionate to the aim pursued, respect the essence of the right to data protection and provide for suitable and specific measures to safeguard the fundamental rights and the interests of the data subject';
+$string['gdpr_art_9_2_j_name'] = 'Public interest, or scientific/historical/statistical research (GDPR Art. 9.2(j))';
+$string['hide'] = 'Collapse all';
+$string['httpwarning'] = 'Any data downloaded from this site may not be encrypted. Please contact your system administrator and request that they install SSL on this site.';
+$string['inherit'] = 'Inherit';
+$string['lawfulbases'] = 'Lawful bases';
+$string['lawfulbases_help'] = 'Select at least one option that will serve as the lawful basis for processing personal data. For details on these lawful bases, please see GDPR Art. 6.1';
+$string['messageprovider:contactdataprotectionofficer'] = 'Data requests';
+$string['messageprovider:datarequestprocessingresults'] = 'Data request processing results';
+$string['messageprovider:notifyexceptions'] = 'Data requests exceptions notifications';
+$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['nodatarequestsmatchingfilter'] = 'There are no data requests matching the given filter';
+$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'] = 'This context level has no data for which the retention period has expired.';
+$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['pluginregistry'] = 'Plugin privacy registry';
+$string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
+$string['privacy'] = 'Privacy';
+$string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.';
+$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 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['purpose_help'] = 'The purpose describes the reason for processing the data. A new purpose may be added, or if Inherit is selected, the purpose from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > User > Site.';
+$string['purposecreated'] = 'Purpose created';
+$string['purposedefault'] = 'Default purpose';
+$string['purposedefault_help'] = 'The default purpose is the purpose which is applied to any new instances. If Inherit is selected, the purpose from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > User > Site.';
+$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['requestbydetail'] = 'Requested by:';
+$string['requestcomments'] = 'Comments';
+$string['requestcomments_help'] = 'This box enables you to enter any further details about your data 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 privacy officer';
+$string['requesttype'] = 'Type';
+$string['requesttypeuser'] = '{$a->typename} ({$a->user})';
+$string['requesttype_help'] = 'Select the reason why you would like to contact the privacy 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'] = 'Message';
+$string['requiresattention'] = 'Requires attention.';
+$string['requiresattentionexplanation'] = 'This plugin does not implement the Moodle privacy API. If this plugin stores any personal data it will not be able to be exported or deleted through Moodle\'s privacy system.';
+$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['retentionperiod_help'] = 'The retention period specifies the length of time that data should be kept for. When the retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
+$string['retentionperiodnotdefined'] = 'No retention period was defined';
+$string['retentionperiodzero'] = 'No retention period';
+$string['send'] = 'Send';
+$string['sensitivedatareasons'] = 'Sensitive personal data processing reasons';
+$string['sensitivedatareasons_help'] = 'Select one or more applicable reasons that exempts the prohibition of processing sensitive personal data tied to this purpose. For more information, please see GDPR Art. 9.2';
+$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['subjectscope_help'] = 'The subject scope lists the roles which may be assigned in this context.';
+$string['user'] = 'User';
+$string['viewrequest'] = 'View the request';
+$string['visible'] = 'Expand all';
diff --git a/admin/tool/dataprivacy/lib.php b/admin/tool/dataprivacy/lib.php
new file mode 100755
index 00000000000..3c66485ef3c
--- /dev/null
+++ b/admin/tool/dataprivacy/lib.php
@@ -0,0 +1,221 @@
+.
+
+/**
+ * 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()) {
+ global $USER;
+
+ if ($context->contextlevel == CONTEXT_USER) {
+ // Make sure the user is logged in.
+ require_login(null, false);
+
+ // Validate the user downloading this archive.
+ $usercontext = context_user::instance($USER->id);
+ // The user downloading this is not the user the archive has been prepared for. Check if it's the requester (e.g. parent).
+ if ($usercontext->instanceid !== $context->instanceid) {
+ // Get the data request ID. This should be the first element of the $args array.
+ $itemid = $args[0];
+ // Fetch the data request object. An invalid ID will throw an exception.
+ $datarequest = new \tool_dataprivacy\data_request($itemid);
+
+ // Check if the user is the requester and has the capability to make data requests for the target user.
+ $candownloadforuser = has_capability('tool/dataprivacy:makedatarequestsforchildren', $context);
+ if ($USER->id != $datarequest->get('requestedby') || !$candownloadforuser) {
+ return false;
+ }
+ }
+
+ // All good. Serve the exported data.
+ $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 100755
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/pluginregistry.php b/admin/tool/dataprivacy/pluginregistry.php
new file mode 100644
index 00000000000..3b3f1c47780
--- /dev/null
+++ b/admin/tool/dataprivacy/pluginregistry.php
@@ -0,0 +1,48 @@
+.
+
+/**
+ * Prints the compliance data registry main page.
+ *
+ * @copyright 2018 onwards Adrian Greeve
+ * @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');
+
+require_login(null, false);
+
+$contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT);
+$contextid = optional_param('contextid', 0, PARAM_INT);
+
+$url = new moodle_url('/' . $CFG->admin . '/tool/dataprivacy/pluginregistry.php');
+$title = get_string('pluginregistry', 'tool_dataprivacy');
+
+\tool_dataprivacy\page_helper::setup($url, $title);
+
+$output = $PAGE->get_renderer('tool_dataprivacy');
+echo $output->header();
+
+// Get data!
+$metadatatool = new \tool_dataprivacy\metadata_registry();
+$metadata = $metadatatool->get_registry_metadata();
+
+$dataregistry = new tool_dataprivacy\output\data_registry_compliance_page($metadata);
+
+echo $output->render($dataregistry);
+echo $OUTPUT->footer();
diff --git a/admin/tool/dataprivacy/purposes.php b/admin/tool/dataprivacy/purposes.php
new file mode 100644
index 00000000000..5fad9222cdc
--- /dev/null
+++ b/admin/tool/dataprivacy/purposes.php
@@ -0,0 +1,41 @@
+.
+
+/**
+ * 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');
+
+require_login(null, false);
+
+$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 100755
index 00000000000..f04fc10b078
--- /dev/null
+++ b/admin/tool/dataprivacy/settings.php
@@ -0,0 +1,78 @@
+.
+
+/**
+ * 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. Disabled by default.
+ $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/contactdataprotectionofficer',
+ new lang_string('contactdataprotectionofficer', 'tool_dataprivacy'),
+ new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 0)
+ );
+
+ // 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_configmulticheckbox('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')
+);
+
+// Link that leads to the other data registry management page.
+$ADMIN->add('privacy', new admin_externalpage('pluginregistry', get_string('pluginregistry', 'tool_dataprivacy'),
+ new moodle_url('/admin/tool/dataprivacy/pluginregistry.php'), 'tool/dataprivacy:managedataregistry')
+);
diff --git a/admin/tool/dataprivacy/styles.css b/admin/tool/dataprivacy/styles.css
new file mode 100644
index 00000000000..a3510528a4e
--- /dev/null
+++ b/admin/tool/dataprivacy/styles.css
@@ -0,0 +1,26 @@
+.nav-pills .nav-pills {
+ margin-left: 1rem;
+}
+.data-registry > .top-nav > * {
+ margin-right: 0.5rem;
+}
+/*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;
+}
+
+dd a.contactdpo {
+ /* Reverting dd's left margin */
+ margin-left: -10px;
+}
+
+.card dd a.contactdpo {
+ /* Reverting dd's left margin */
+ margin-left: inherit;
+}
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}}
+
+
diff --git a/admin/tool/dataprivacy/templates/component_status.mustache b/admin/tool/dataprivacy/templates/component_status.mustache
new file mode 100644
index 00000000000..c62dc0b65d9
--- /dev/null
+++ b/admin/tool/dataprivacy/templates/component_status.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 comments.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template tool_dataprivacy/component_status
+
+ 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):
+ {
+ "compliant" : "True",
+ "raw_component" : "core_comment",
+ "component" : "Core comment",
+ "external" : "True",
+ "metadata" : {
+ "name" : "comments",
+ "type" : "database_table",
+ "summary" : "Stores comments of users",
+ "fields" : {
+ "field_name" : "content",
+ "field_summary" : "Stores the text of the content."
+ }
+ }
+ }
+}}
+
+
\ No newline at end of file
diff --git a/admin/tool/dataprivacy/templates/contact_dpo.mustache b/admin/tool/dataprivacy/templates/contact_dpo.mustache
new file mode 100644
index 00000000000..fa84f1494ef
--- /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}}
+
+
+
+
+
+
+ {{#str}}required, moodle{{/str}}
+
+
+
+
+
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..cbf5ed34115
--- /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": "
This is the table that will contain the list of expired contexts
+{{#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..fb86ae90167
--- /dev/null
+++ b/admin/tool/dataprivacy/templates/data_registry.mustache
@@ -0,0 +1,66 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more 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):
+ {
+ }
+}}
+
diff --git a/admin/tool/dataprivacy/templates/data_registry_compliance.mustache b/admin/tool/dataprivacy/templates/data_registry_compliance.mustache
new file mode 100644
index 00000000000..caadac15f81
--- /dev/null
+++ b/admin/tool/dataprivacy/templates/data_registry_compliance.mustache
@@ -0,0 +1,94 @@
+{{!
+ 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_compliance
+
+ 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):
+ {
+ "types" : {
+ "plugin_type_raw" : "mod",
+ "plugin_type" : "Activities and Modules"
+ }
+ }
+}}
+
+{{#js}}
+require(['jquery', 'tool_dataprivacy/expand_contract'], function($, ec) {
+
+ $('.type-expand').click(function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ var thisnode = $(this);
+ var plugin = thisnode.data('plugin');
+ var metadata = $('[data-plugintarget=\'' + plugin + '\']');
+ ec.expandCollapse(metadata, thisnode);
+ });
+
+ $('.component-expand').click(function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ var thisnode = $(this);
+ var plugin = thisnode.data('component');
+ var metadata = $('[data-section=\'' + plugin + '\']');
+ ec.expandCollapse(metadata, thisnode);
+ });
+
+ $('.tool_dataprivacy-expand-all').click(function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ var nextstate = $(this).data('visibilityState');
+ ec.expandCollapseAll(nextstate);
+ });
+});
+{{/js}}
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..1c993b29c2e
--- /dev/null
+++ b/admin/tool/dataprivacy/templates/data_request_email.mustache
@@ -0,0 +1,114 @@
+{{!
+ 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}}datarequestemailsubject, tool_dataprivacy, {{requesttype}}{{/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..cae5022f947
--- /dev/null
+++ b/admin/tool/dataprivacy/templates/data_request_modal.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 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):
+ {
+ "title": "Data request modal title"
+ }
+}}
+{{< 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!"
+ }
+ }
+}}
+
diff --git a/admin/tool/dataprivacy/templates/data_requests.mustache b/admin/tool/dataprivacy/templates/data_requests.mustache
new file mode 100644
index 00000000000..594aaab7b19
--- /dev/null
+++ b/admin/tool/dataprivacy/templates/data_requests.mustache
@@ -0,0 +1,84 @@
+{{!
+ 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:
+ * newdatarequesturl string The URL pointing to the data request creation page.
+ * datarequests string The HTML of the data requests table.
+
+ Example context (json):
+ {
+ "newdatarequesturl": "#",
+ "datarequests": "
This is the table where the list of data requests will be rendered
+
+{{#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..2b654b29467
--- /dev/null
+++ b/admin/tool/dataprivacy/templates/my_data_requests.mustache
@@ -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 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,
+ "requestedbyuser" : {
+ "fullname": "Martha Smith",
+ "profileurl": "#"
+ }
+ },
+ {
+ "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,
+ "requestedbyuser" : {
+ "fullname": "Martha Smith",
+ "profileurl": "#"
+ }
+ },
+ {
+ "id": 3,
+ "typename" : "Data deletion",
+ "comments": "Please delete all of my son's personal data.",
+ "statuslabelclass": "label-success",
+ "statuslabel": "Complete",
+ "timecreated" : 1517902087,
+ "requestedbyuser" : {
+ "fullname": "Martha Smith",
+ "profileurl": "#"
+ }
+ },
+ {
+ "id": 4,
+ "typename" : "Data deletion",
+ "comments": "Delete my data or I'm coming for you...",
+ "statuslabelclass": "label-danger",
+ "statuslabel": "Rejected",
+ "timecreated" : 1517902087,
+ "requestedbyuser" : {
+ "fullname": "Martha Smith",
+ "profileurl": "#"
+ }
+ },
+ {
+ "id": 5,
+ "typename" : "Data export",
+ "comments": "Please let me download my data",
+ "statuslabelclass": "label-info",
+ "statuslabel": "Processing",
+ "timecreated" : 1517902087,
+ "requestedbyuser" : {
+ "fullname": "Martha Smith",
+ "profileurl": "#"
+ }
+ }
+ ]
+ }
+}}
+
+{{#httpsite}}
+ {{> core/notification_warning}}
+{{/httpsite}}
+
+
+
+{{#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..ebdc10f0f06
--- /dev/null
+++ b/admin/tool/dataprivacy/templates/purposes.mustache
@@ -0,0 +1,126 @@
+{{!
+ 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}}
+
+
diff --git a/admin/tool/dataprivacy/templates/request_details.mustache b/admin/tool/dataprivacy/templates/request_details.mustache
new file mode 100644
index 00000000000..c8e9dc19d95
--- /dev/null
+++ b/admin/tool/dataprivacy/templates/request_details.mustache
@@ -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 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,
+ "requestedbyuser" : {
+ "fullname": "Martha Smith",
+ "profileurl": "#"
+ },
+ "statuslabel": "Pending",
+ "statuslabelclass": "label-default",
+ "messagehtml": "