This commit is contained in:
PMKuipers 2024-05-10 15:22:52 +02:00
parent 97ce14fe20
commit c938b994a1
27 changed files with 276 additions and 90 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
define("local_treestudyplan/util/psidebar-vue",["exports","./css-calc"],(function(_exports,_cssCalc){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;var _default={install(Vue){Vue.component("p-sidebar",{props:{value:{type:Boolean,default:!0},right:{type:Boolean,default:!1},shadow:{type:Boolean,default:!1},target:{type:String,default:"body"},offsetRef:{type:String,default:""}},data:()=>({wrapper:null,contentwrapper:null,resizeobserver:null}),computed:{},methods:{initWrappers(target){this.wrapper=document.querySelector("#p-sidebar-wrapper"),this.wrapper||(this.wrapper=document.createElement("div"),this.wrapper.setAttribute("id","p-sidebar-wrapper")),this.contentwrapper=document.querySelector("#p-sidebar-contentwrapper"),this.contentwrapper||(this.contentwrapper=document.createElement("div"),this.contentwrapper.setAttribute("id","p-sidebar-contentwrapper"),this.wrapper.appendChild(this.contentwrapper));let targetEl=document.querySelector(target);for(console.info(`Targeting '${target}' to `,targetEl),targetEl&&"HTML"!=targetEl.nodeType||(targetEl=document.querySelector("body"));targetEl.childNodes.length>0;)this.contentwrapper.appendChild(targetEl.childNodes[0]);targetEl.appendChild(this.wrapper)},rePosition(right){const el=this.$refs.container;right?this.wrapper.insertBefore(el,this.contentwrapper.nextSibling):this.wrapper.insertBefore(el,this.contentwrapper)},setOffset(reference){const ref=reference?document.querySelector(reference):null;console.info(`Setting offset from '${reference}'`,ref);let offsetTop=ref?ref.offsetTop:0;offsetTop+=0!=offsetTop?"px":"";const el=this.$refs.container;el.style.height=`calc( 100vh - ${offsetTop})`,el.style.marginTop=offsetTop}},watch:{right(newVal){this.rePosition(newVal)},offsetRef(reference){this.setOffset(reference)}},mounted(){const el=this.$refs.container;this.initWrappers(this.target),this.setOffset(this.offsetRef),this.rePosition(this.right,this.besides),this.resizeObserver=new ResizeObserver((()=>{let wx=0-el.getBoundingClientRect().width;wx+=0!=wx?"px":"",el.style.setProperty("--p-sidebar-hideoffset",wx)})),this.resizeObserver.observe(el)},unmounted(){this.resizeObserver&&this.resizeObserver.disconnect()},template:"\n <div>\n <div ref='container' \n :class=\"'p-sidebar ' + (right?'p-sidebar-right ':'') + (shadow?'p-sidebar-shadow ':'') + (value?'shown ':'hidden ')\"\n ><slot></slot></div>\n </div>\n "})}};return _exports.default=_default,_exports.default}));
define("local_treestudyplan/util/psidebar-vue",["exports","./debugger"],(function(_exports,_debugger){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;let debug=new(_debugger=(obj=_debugger)&&obj.__esModule?obj:{default:obj}).default("p-sidebar");var _default={install(Vue){Vue.component("p-sidebar",{props:{value:{type:Boolean,default:!0},right:{type:Boolean,default:!1},shadow:{type:Boolean,default:!1},target:{type:String,default:"body"},offsetRef:{type:String,default:""}},data:()=>({wrapper:null,contentwrapper:null,resizeobserver:null}),computed:{},methods:{initWrappers(target){let initializeWrapperContent=!1;if(this.wrapper=document.querySelector("#p-sidebar-wrapper"),this.wrapper||(initializeWrapperContent=!0,this.wrapper=document.createElement("div"),this.wrapper.setAttribute("id","p-sidebar-wrapper")),this.contentwrapper=document.querySelector("#p-sidebar-contentwrapper"),this.contentwrapper||(initializeWrapperContent=!0,this.contentwrapper=document.createElement("div"),this.contentwrapper.setAttribute("id","p-sidebar-contentwrapper"),this.wrapper.appendChild(this.contentwrapper)),initializeWrapperContent){let targetEl=document.querySelector(target);for(console.info(`Targeting '${target}' to `,targetEl),targetEl&&"HTML"!=targetEl.nodeType||(targetEl=document.querySelector("body")),debug.warn("Initializing wrappers with content of target ",targetEl);targetEl.childNodes.length>0;)this.contentwrapper.appendChild(targetEl.childNodes[0]);targetEl.appendChild(this.wrapper)}},rePosition(right){const el=this.$refs.container;right?this.wrapper.insertBefore(el,this.contentwrapper.nextSibling):this.wrapper.insertBefore(el,this.contentwrapper)},setOffset(reference){const ref=reference?document.querySelector(reference):null;console.info(`Setting offset from '${reference}'`,ref);let offsetTop=ref?ref.offsetTop:0;offsetTop+=0!=offsetTop?"px":"";const el=this.$refs.container;el.style.height=`calc( 100vh - ${offsetTop})`,el.style.marginTop=offsetTop}},watch:{right(newVal){this.rePosition(newVal)},offsetRef(reference){this.setOffset(reference)}},mounted(){const el=this.$refs.container;this.initWrappers(this.target),this.setOffset(this.offsetRef),this.rePosition(this.right,this.besides),this.resizeObserver=new ResizeObserver((()=>{let wx=0-el.getBoundingClientRect().width;wx+=0!=wx?"px":"",el.style.setProperty("--p-sidebar-hideoffset",wx)})),this.resizeObserver.observe(el)},unmounted(){this.resizeObserver&&this.resizeObserver.disconnect()},template:"\n <div>\n <div ref='container' \n :class=\"'p-sidebar ' + (right?'p-sidebar-right ':'') + (shadow?'p-sidebar-shadow ':'') + (value?'shown ':'hidden ')\"\n ><slot></slot></div>\n </div>\n "})}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=psidebar-vue.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -152,6 +152,7 @@ export default {
required_goal: "required_goal",
student_not_tracked: "student_not_tracked",
not_enrolled: "not_enrolled",
noenddate: "noenddate",
},
teachercourse: {
select_conditions: "select_conditions",
@ -164,6 +165,7 @@ export default {
required_goal: "required_goal",
student_from_plan_enrolled: "student_from_plan_enrolled",
students_from_plan_enrolled: "students_from_plan_enrolled",
noenddate: "noenddate",
},
competency: {
competency_not_configured: "competency_not_configured",
@ -1485,7 +1487,7 @@ export default {
return format_date(this.value.course.startdate);
},
enddate(){
if(this.value.course.enddate){
if(this.value.course.enddate > 0){
return format_date(this.value.course.enddate);
}
else {
@ -2264,7 +2266,7 @@ export default {
return format_date(this.value.course.startdate);
},
enddate(){
if(this.value.course.enddate){
if(this.value.course.enddate > 0){
return format_date(this.value.course.enddate);
}
else {
@ -2449,6 +2451,17 @@ export default {
};
},
computed: {
startdate(){
return format_date(this.value.course.startdate);
},
enddate(){
if(this.value.course.enddate > 0){
return format_date(this.value.course.enddate);
}
else {
return this.text.noenddate;
}
},
wwwroot() {
return Config.wwwroot;
}
@ -2495,10 +2508,9 @@ export default {
<div class="r-course-detail-header-right">
<div :class="'r-timing-'+value.course.timing">
{{text['coursetiming_'+value.course.timing]}}<br>
{{ value.course.startdate }} - {{ value.course.enddate }}
{{ startdate }} - {{ enddate }}
</div>
</div>
<s-
</template>
<b-form-group
:label="text.select_grades"

View File

@ -223,6 +223,7 @@ export default {
selected: 'selected',
name: 'name',
context: 'context',
search: 'search',
},
item_text: {
select_conditions: "select_conditions",
@ -248,6 +249,7 @@ export default {
ok: "ok@core",
cancel: "cancel@core",
delete: "delete@core",
noenddate: "noenddate",
},
invalid: {
error: 'error',
@ -1111,7 +1113,7 @@ export default {
<b-form-input
type="text"
@input="searchUsers($event)"
placeholder="Search users"></b-form-input>
:placeholder="text.search + ' ' + text.users"></b-form-input>
</b-col>
</b-row>
<b-row>
@ -1144,7 +1146,7 @@ export default {
</b-row>
</b-container>
</b-tab>
<b-tab :title="text.coaches">
<b-tab :title="text.coaches" v-if="premiumenabled()">
<b-container>
<b-row class='mb-2 mt-2'>
<b-col>{{text.associated_coaches}}</b-col>
@ -1157,7 +1159,7 @@ export default {
<b-form-input
type="text"
@input="searchCoaches($event)"
placeholder="Search coaches"></b-form-input>
:placeholder="text.search + ' ' + text.coaches"></b-form-input>
</b-col>
</b-row>
<b-row>
@ -1455,7 +1457,7 @@ export default {
},
mounted() {
const self=this;
if(this.value.pages[0].studylines.length == 0){
if(this.value.pages[0].studylines.length == 0 && !this.coaching){
// start in editmode if studylines on first page are empty
this.edit.studyline.editmode = true;
}
@ -3402,7 +3404,7 @@ export default {
return format_date(this.value.course.startdate);
},
enddate(){
if(this.value.course.enddate){
if(this.value.course.enddate > 0){
return format_date(this.value.course.enddate);
}
else {

View File

@ -304,7 +304,8 @@ export default {
<slot></slot
><p v-if="value.startdate > 0"
class="s-studyline-header-period-datespan small">
<span class="date">{{ startdate }}</span> - <span class="date">{{ enddate }}</span>
<span class="date">{{ startdate }}</span>
- <span class="date" v-if="this.value.enddate > 0">{{ enddate }}</span><span class="date" v-else>&infin;</span>
</p>
<div v-if="current && mode == 'view'" class='s-studyline-period-highlight'></div>
</div>

View File

@ -4,7 +4,8 @@
/*eslint-disable no-console */
/*eslint-env es6*/
import {calc} from "./css-calc";
import Debugger from './debugger';
let debug = new Debugger("p-sidebar");
export default {
install(Vue/*,options*/){
@ -42,9 +43,12 @@ export default {
},
methods: {
initWrappers(target) {
// First check if the sidebar wrapper already exists.
let initializeWrapperContent = false;
// First check if the sidebar wrapper already exists.
// Creating the wrappers over and over again is a recipe for disaster.
this.wrapper = document.querySelector("#p-sidebar-wrapper");
if (! this.wrapper) {
if (!this.wrapper) {
initializeWrapperContent = true;
// Otherwise, create it.
this.wrapper = document.createElement("div");
this.wrapper.setAttribute("id","p-sidebar-wrapper");
@ -52,31 +56,33 @@ export default {
// First check if the contentwrapper already exists
this.contentwrapper = document.querySelector("#p-sidebar-contentwrapper");
if (!this.contentwrapper) {
initializeWrapperContent = true;
// Otherwise, create it.
this.contentwrapper = document.createElement("div");
this.contentwrapper.setAttribute("id","p-sidebar-contentwrapper");
this.wrapper.appendChild(this.contentwrapper);
}
// Find containing target (otherwise use body)
let targetEl = document.querySelector(target);
console.info(`Targeting '${target}' to `,targetEl);
if (!targetEl || targetEl.nodeType == "HTML") {
targetEl = document.querySelector("body");
if (initializeWrapperContent) {
// Find containing target (otherwise use body)
let targetEl = document.querySelector(target);
console.info(`Targeting '${target}' to `,targetEl);
if (!targetEl || targetEl.nodeType == "HTML") {
targetEl = document.querySelector("body");
}
debug.warn(`Initializing wrappers with content of target `,targetEl);
// Move all target content parts to content wrapper....
while (targetEl.childNodes.length >0) {
this.contentwrapper.appendChild(targetEl.childNodes[0]);
}
// Add sidebar wrapper to target Element
targetEl.appendChild(this.wrapper);
}
// Move all target content parts to content wrapper....
while (targetEl.childNodes.length >0) {
this.contentwrapper.appendChild(targetEl.childNodes[0]);
}
// Add sidebar wrapper to target Element
targetEl.appendChild(this.wrapper);
},
rePosition(right) {
// Place the container elsewhere in the DOM.
const el = this.$refs.container;
if(right) {
this.wrapper.insertBefore(el,this.contentwrapper.nextSibling);
} else {

View File

@ -365,7 +365,7 @@ class corecompletioninfo {
$details = [
"type" => get_string('manualcompletionby', 'completion'),
"criteria" => $criteria->get_title(),
"requirement" => get_string('markedcompleteby', 'completion', $criteria),
"requirement" => get_string('markedcompleteby', 'completion', $criteria->get_title()),
"status" => "",
];
} else if ($type == COMPLETION_CRITERIA_TYPE_COURSE) {

View File

@ -408,7 +408,7 @@ class courseinfo {
*/
public function is_enrolled_student($userid) : bool {
global $CFG;
foreach (explode(', ', $CFG->gradebookroles) as $roleid) {
foreach (explode(',', $CFG->gradebookroles) as $roleid) {
if (user_has_role_assignment($userid, $roleid, $this->coursecontext->id)) {
return true;
}

View File

@ -7,7 +7,7 @@ require_once($CFG->dirroot.'/repository/lib.php');
use local_treestudyplan\aggregator;
use local_treestudyplan\studyplan;
use local_treestudyplan\premium;
use local_treestudyplan\contextinfo;
use local_treestudyplan\debug;
use local_treestudyplan\studyplanservice;
use local_treestudyplan\courseservice;
use local_treestudyplan\form\text_integer;
@ -384,7 +384,9 @@ class studyplan_editform extends formbase {
Parse it through the clean_returnvalue function of exernal api (of which studyplanservice is a subclass)
so we return it in a consistent way
*/
return studyplanservice::clean_returnvalue(studyplan::simple_structure(),$plan->simple_model());
$response = studyplanservice::clean_returnvalue(studyplan::simple_structure(),$plan->simple_model());
debug::dump($response);
return $response;
}

View File

@ -38,7 +38,7 @@ class premium extends \external_api {
// Toggle the variable below to enable support for premium stuff.
// If set to false, all premium features will be enabled and no premium settings panel will be visible.
private static $premium_supported = true;
private static $premium_supported = false;
private static $premiumcrt = "-----BEGIN CERTIFICATE-----
MIIDSzCCAjMCFFlyhmKf1fN7U5lQL/dtlsyP24AQMA0GCSqGSIb3DQEBCwUAMGEx
@ -67,7 +67,6 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
return self::$premium_supported;
}
private static function decrypt($encrypted) {
// Get the public key.
$key = \openssl_get_publickey(self::$premiumcrt);
@ -161,6 +160,36 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
}
}
public static function debuginfo() {
$s = "<pre>";
try {
$activationkey = \get_config("local_treestudyplan","premium_key");
if (strlen($activationkey) > 0) {
$keydata = self::trim_headers($activationkey);
$json = self::decrypt($keydata);
$s .= "Decoded key data:\n----\n";
$s .= $json . "\n----\n";
$decoded = \json_decode($json,false);
$s .= "Read as object:\n----\n";
$s .= print_r($decoded,true) ."\n----\n";
$status = self::premiumStatus();
$s .= "Parsed premium status block:\n----\n";
$s .= print_r($status,true) ."\n----\n";
$s .= "Message: " . self::statusdescription() . "\n";
} else {
$s .= "Premium key empty";
}
} catch (\Throwable $x) {
$s .= "!!! " . get_class($x) . ": " . $x->getCode() . " | " . $x->getMessage(). "\n";
$stack = explode("\n",$x->getTraceAsString());
foreach ($stack as $l) {
$s .= " " . trim($l) ."\n";
}
}
$s.="</pre>";
return $s;
}
/**
* Determine, cache and retrieve premium status
* @return object
@ -177,13 +206,10 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
$o->expires = "";
$o->expired = false;
$o->issued = "";
$o->message = \get_string("premium:notregistered","local_treestudyplan");
$activationkey = \get_config("local_treestudyplan","premium_key");
if (strlen($activationkey) > 0) {
$activationkey;
try {
try {
$activationkey = \get_config("local_treestudyplan","premium_key");
if (strlen($activationkey) > 0) {
$keydata = self::trim_headers($activationkey);
$json = self::decrypt($keydata);
$decoded = \json_decode($json,false);
@ -215,9 +241,7 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
} catch (\Exception $x) {}
}
if ( \in_array('treestudyplan',$o->intents)
if ( \in_array('treestudyplan',$o->intents)
&& !empty($o->issued)
&& self::website_match($o->website)
) {
@ -237,10 +261,9 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
}
}
}
} catch (\ValueError $x) {
$o->status = \get_string("premium:invalidactivationcontent","local_treestudyplan");
}
} catch (\ValueError $x) {
$o->status = \get_string("premium:invalidactivationcontent","local_treestudyplan");
}
self::$cachedpremiumstatus = $o;
@ -254,7 +277,7 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
private static function website_match($key) {
global $CFG;
$site = $CFG->wwwroot;
// Add double slashes to key and site if no scheme is set.
// Add double slashes to key and site if no scheme is set.
// Basically: if no double slashes present before any dots,shashes or @s.
if(!\preg_match_all('#^[^./@]*?//#',$key )) {
$key = "//".$key;
@ -271,6 +294,11 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
return false;
}
if ($keyurl->host == "*") {
// * matches all
return true;
}
// First match the host part.
$keyparts = \array_reverse(\explode(".",$keyurl->host));
$siteparts = \array_reverse(\explode(".",$siteurl->host));
@ -386,7 +414,7 @@ Klc5I28bGbvxIV5pnL6ZSjHEDp2WreM8HB0XFJwU+Q==
if ($status->website != "*") {
$msg->sitestatus = \get_string("premium:onsite",'local_treestudyplan',$status);
} else {
$msg->sitestatus = \get_string("premium:anywhere",'local_treestudyplan');;
$msg->sitestatus = \get_string("premium:anywhere",'local_treestudyplan');
}
if($status->enabled) {

View File

@ -412,10 +412,9 @@ class studyplan {
}
}
$page = studyplanpage::add($pageinfo);
$page = studyplanpage::add($pageinfo,$bare);
$plan->page_cache = [$page];
}
// End temporary skräpp code.
return $plan;
}

View File

@ -266,8 +266,9 @@ class studyplanpage {
/**
* Add new study plan page
* @param mixed $fields Parameter for new study plan page
* @param bool $bare If true, do not create a first page with copy of studyplan names
*/
public static function add($fields) : self {
public static function add($fields, $bare = false) : self {
global $DB;
if (!isset($fields['studyplan_id'])) {
@ -293,7 +294,19 @@ class studyplanpage {
$id = $DB->insert_record(self::TABLE, $info);
// Refresh the page cache for the studyplan
$plan->pages(true);
return self::find_by_id($id); // Make sure the new page is immediately cached.
$page = self::find_by_id($id); // Make sure the new page is immediately cached.
if (!$bare) {
// Add an empty study line
$lineinfo = ['page_id' => $id,
'name' => get_string("default_line_name","local_treestudyplan"),
'shortname' => get_string("default_line_shortname","local_treestudyplan"),
"color" => "#DDDDDD",
];
studyline::add($lineinfo);
}
return $page;
}
/**

View File

@ -142,9 +142,7 @@ class utilityservice extends \external_api {
* Return value description for webservice function submit_cm_editform
*/
public static function submit_mform_returns() : \external_description {
return new \external_multiple_structure(new \external_single_structure([
"id" => new \external_value(PARAM_INT, 'id of studyline'),
]));
return success::structure(); // Success structure includes data component which encodes date in json format.
}
/**
@ -165,6 +163,7 @@ class utilityservice extends \external_api {
$mform = self::load_mform($formname, $params, $ajaxformdata);
$return = $mform->process_submission();
// Pass form processing result as success data component.
return success::success($return)->model();
}

View File

@ -82,24 +82,22 @@ print $OUTPUT->header();
v-if="displayedstudyplan.description"
></s-studyplan-details>
<div class="flex-grow-1"><!-- Spacer to align student selector right --></div>
<div v-if="displayedstudyplan.description">
<span><?php t('selectstudent_btn') ?></span>
<s-prevnext-selector
:options="associatedstudents"
title="firstname"
v-model="selectedstudent"
defaultselectable
grouped
optionsfield='users'
arrows
@change="showStudentView"
class="ml-2"
variant="primary"
>
<template v-slot="{value}">{{value.firstname}} {{value.lastname}}</template>
<template #defaultlabel><span class='text-primary'><?php t("coacheditmode"); ?></span></template>
</s-prevnext-selector>
</div>
<span><?php t('selectstudent_btn') ?></span>
<s-prevnext-selector
:options="associatedstudents"
title="firstname"
v-model="selectedstudent"
defaultselectable
grouped
optionsfield='users'
arrows
@change="showStudentView"
class="ml-2"
variant="primary"
>
<template v-slot="{value}">{{value.firstname}} {{value.lastname}}</template>
<template #defaultlabel><span class='text-primary'><?php t("coacheditmode"); ?></span></template>
</s-prevnext-selector>
</template>
</div>
<div class='t-studyplan-container'>

View File

@ -111,7 +111,7 @@
</tr>
<tr>
<td>local/treestudyplan:forcescales</td>
<td>Allow studyplan managers with this right to push scales for grading to all selected grable activities (see above)<br>
<td><b>(DEPRECATED)</b>Allow studyplan managers with this right to push scales for grading to all selected grable activities (see above)<br>
(Effective only when Manual aggregation method is selected in the study plan)</td>
<td>manager</td>
<td>System and/or category</td>
@ -125,13 +125,27 @@
</tr>
<tr>
<td>local/treestudyplan:configure</td>
<td>Users with this right can configure global interpretation of scales and point grades </td>
<td>Users with this right can configure global interpretation of scales and point grades <br>
This is used when a per-activity passed grade is not set.
(Effective only when Manual aggregation method is selected in the study plan)</td>
<td>manager</td>
<td>System</td>
</tr>
<tr>
<td>local/treestudyplan:viewuserreports</td>
<td></td>
<td>Users with this right can view the student's studyplans in all contexts in which this permission is granted</td>
<td>manager</td>
<td>System and/or category</td>
</tr>
<tr>
<td>local/treestudyplan:lineunenrol</td>
<td>(Premium feature related) Users with this right in addition to <i>viewuserreports</i> can unregister students from an registerable line</td>
<td>manager</td>
<td>System and/or category</td>
</tr>
<tr>
<td>local/treestudyplan:coach</td>
<td>(Premium feature related) Users with this right can be selected as coaches in the given context</td>
<td>manager</td>
<td>System and/or category</td>
</tr>

View File

@ -61,6 +61,17 @@ Manage Study plans|/local/treestudyplan/edit-plan.php||en
If you already have a role that you hand out to all teachers/faculty in specific categories, you may want to integrate this role with that one
</div>
</li>
<li><b>Full Name:</b> <i>Studyplan Coach</i><br>
<b>Short Name:</b> <i>studyplancoach</i><br>
<b>Context types:</b> <i>System</i>, <i>Category</i><br>
<b>Capabilities</b>
<ul>
<li><b>local/treestudyplan:coach</b> <i>Available as coach</i></li>
</ul>
<div class="generalbox alert alert-success"><b>TIP:</b><br>
If you already have a role that you hand out to coaches of a limited group of students, you may want to integrate this role with that one
</div>
</li>
</ul>
<p> Then assign the role <b>studyplanmanager</b> in a specific category context, or the system context to all users who should be able to create and edit studyplans
in that specific context</p>

View File

@ -200,6 +200,8 @@ $string["studyline_shortname_ph"] = '';
$string["studyline_color"] = 'Background color';
$string["studyline_enrollable"] = 'Registration';
$string["studyline_enrolroles"] = 'Allowed roles';
$string["default_line_name"] = 'Courses';
$string["default_line_shortname"] = 'Courses';
$string["studyitem_confirm_remove"] = 'Are you sure you want to remove module {$a}?';
$string["editmode_modules_hidden"] = 'Modules hidden in edit mode';
@ -291,6 +293,7 @@ $string["coaches"] = 'Coaches';
$string["selected"] = 'Select';
$string["name"] = 'Name';
$string["context"] = 'Category';
$string["search"] = "Search";
$string["error"] = "Error";
$string["ungraded"] = 'Needs grading';
@ -487,7 +490,9 @@ $string["settingdesc_premium_heading"] = 'To access premium features, you need a
</ul>';
$string["setting_premium_status"] = 'Current premium status';
$string["setting_premium_key"] = 'Activation key';
$string["settingdesc_premium_key"] = 'Paste the premium key you received in the box above.';
$string["settingdesc_premium_key"] = 'Paste the premium key you received in the box above.<br> <b>Attention!</b> The lines <i>----- BEGIN ACTIVATION KEY -----</i> and <i>----- END ACTIVATION KEY -----</i> are part of the key.';
$string["setting_premium_debug"] = 'Debug info for premium key';
$string["premiumfeature:morestudyplans"] = 'Creating more than 5 studyplans in a single category is a premium feature.';
$string["premiumfeature:morecategories"] = 'Creating studyplans in more than 20 categories is a premium feature.';

View File

@ -129,7 +129,7 @@ $string["settingdesc_infofields_heading"] = 'Kies tot 5 extra velden om in het c
$string["setting_hivizdropslots"] = 'Extra zichtbare sleepvelden';
$string["settingdesc_hivizdropslots"] = 'Maak de velden waar cursussen heen gesleept kunnen worden extra goed zichtbaar.';
$string["setting_toolboxleft"] = 'Toolcox standaard links';
$string["setting_toolboxleft"] = 'Toolbox standaard links';
$string["settingdesc_toolboxleft"] = 'Toon de toolbox met cursussen en componenten standaard links in plaats van rechts.';
$string["infofield_position_above"] = 'Boven cursusresultaten';
@ -201,6 +201,8 @@ $string["studyline_shortname_ph"] = '';
$string["studyline_color"] = 'Achtergrondkleur';
$string["studyline_enrollable"] = 'Inschrijven';
$string["studyline_enrolroles"] = 'Rollen mogen inschrijven';
$string["default_line_name"] = 'Cursussen';
$string["default_line_shortname"] = 'Cursussen';
$string["studyitem_confirm_remove"] = 'Weet je zeker dat je module {$a} wilt verwijderen?';
$string["editmode_modules_hidden"] = 'Modules verborgen tijdens bewerken';
@ -292,6 +294,7 @@ $string["coaches"] = 'Coaches';
$string["selected"] = 'Kies';
$string["name"] = 'Naam';
$string["context"] = 'Categorie';
$string["search"] = "Zoek";
$string["error"] = "Fout";
$string["ungraded"] = 'Nog beoordelen';
@ -476,7 +479,7 @@ $string["premium:notregistered"] = 'Premium toegang staat <b>uitgeschakeld</n>';
$string["premium:invalidactivationcontent"] = 'Premium activeringssleutel niet herkend';
$string["premium:siteinvalid"] = 'Premium toegang staat <b>uitgeschakeld</n>. Activeringssleutel is alleen voor gebruik op <b>{$a}</b>';
$string["premium:expired"] = 'Premium toegang is <b class="text-danger">verlopen</b> op <b>{$a->expires}</b><br>Was uitgegeven aan <b>{$a->name}</b> {$a->sitestatus}';
$string["settingspage_premium"] = 'Premium registration';
$string["settingspage_premium"] = 'Premium registratie';
$string["setting_premium_heading"] = 'Premium features';
$string["settingdesc_premium_heading"] = 'Voor premium toegang is een activeringssleutel vereist.
<br>Premium toegang bevat onder andere:
@ -487,8 +490,9 @@ $string["settingdesc_premium_heading"] = 'Voor premium toegang is een activering
<li>Leerlijnen die student kan ontsluiten</li>
</ul>';
$string["setting_premium_status"] = 'Premium status';
$string["setting_premium_key"] = 'Activation key';
$string["settingdesc_premium_key"] = 'Premium activation key';
$string["setting_premium_key"] = 'Activatiesleutel';
$string["settingdesc_premium_key"] = 'Plak de activatiesleutel in het tekstvak hierboven.<br> <b>Let op!</b> De regels <i>----- BEGIN ACTIVATION KEY -----</i> en <i>----- END ACTIVATION KEY -----</i> zijn onderdeel van de sleutel.';
$string["setting_premium_debug"] = 'Debug info voor premium key';
$string["premiumfeature:morestudyplans"] = 'Meer dan 5 studieplannen in één categorie aanmaken kan alleen met premium toegang.';
$string["premiumfeature:morecategories"] = 'In meer dan 20 categoriën een studieplan aanmaken kan alleen met premium toegang.';

View File

@ -373,7 +373,7 @@ if ($hassiteconfig) {
* Settings page: Premium registration
*
*************************************/
if (\local_treestudyplan\premium::supported()) {
if (\local_treestudyplan\premium::supported() || $CFG->debugdeveloper) {
$pagepremium = new admin_settingpage('local_treestudyplan_settings_premium',
get_string('settingspage_premium', 'local_treestudyplan', null, true));
@ -396,6 +396,15 @@ if ($hassiteconfig) {
PARAM_RAW
));
if ($CFG->debugdeveloper) {
// show decrypted key data
$pagepremium->add(new admin_setting_description('local_treestudyplan/premium_debug',
get_string('setting_premium_debug', 'local_treestudyplan'),
premium::debuginfo() . "<br>&nbsp;<br>" // Add empty row at end.
));
}
// Add settings page2 to the admin settings category.
$ADMIN->add('local_treestudyplan', $pagepremium);
}

83
test.php Normal file
View File

@ -0,0 +1,83 @@
<?php
function sitematch($key,$site){
// Add double slashes to key and site if no scheme is set.
// Basically: if no double slashes present before any dots,shashes or @s.
if(!\preg_match_all('#^[^./@]*?//#',$key )) {
$key = "//".$key;
}
if(!\preg_match_all('#^[^./@]*?//#',$site)) {
$site = "//".$site;
}
// Use parse_url() to split path and host.
$keyurl = (object)\parse_url($key);
$siteurl = (object)\parse_url($site);
// No match if host is empty on key or site
if (empty($keyurl->host) || empty($siteurl->host)) {
return false;
}
if ($keyurl->host == "*") {
// * matches all
return true;
}
// First match the host part.
$keyparts = \array_reverse(\explode(".",$keyurl->host));
$siteparts = \array_reverse(\explode(".",$siteurl->host));
// Trim starting www from both parts, since site.domain and www.site.domain should be treated as the same.
if (($x = \array_pop($keyparts)) != "www") {\array_push($keyparts,$x);}
if (($x = \array_pop($siteparts)) != "www") {\array_push($siteparts,$x);}
for ($i = 0; $i < count($keyparts); $i++) {
// No match if the site does not have a part, but the key does. Unless the key part is *
if (!isset($siteparts[$i]) ) {
if($keyparts[$i] != "*") {
return false;
} else {
$i++; //increment $i by one before break, to make sure the comparison following this loop holds.
break; // Stop comparison. Host part matches.
}
}
// Now do a proper case insensitive check for matching.
// Uses fnmatch to easily handle shell type wildcards.
if ( ! \fnmatch($keyparts[$i],$siteparts[$i],\FNM_CASEFOLD)) {
return false;
}
}
// Fail if the site has a deeper subdomain than the key, unless the deepest key subdomain is *
if ($keyparts[$i-1] != '*' && count($siteparts) > ($i)) {
return false;
}
// If we made it here then the host part matches. Now check the path.
// If path is /*, matches all subpaths including /
$keypath = empty($keyurl->path)?"/":$keyurl->path;
$sitepath = empty($siteurl->path)?"/":$siteurl->path;
// Trim trailing / from both paths before comparison
if (\strlen($sitepath) > 1) {
$sitepath = \rtrim($sitepath,"/");
}
if (\strlen($keypath) > 1) {
$keypath = \rtrim($keypath,"/");
}
// Do a case insensitive fnmatch on the site so wildcards are matched too.
return \fnmatch($keypath,$sitepath,\FNM_CASEFOLD);
}
$tests = [
["*", "https://www.miqra.nl"],
["*/*", "https://www.miqra.nl"],
["*", "https://clients.openedu.nl/fith"],
["clients.openedu.nl/fith", "https://clients.openedu.nl/fith/"],
["clients.openedu.nl/fith/", "https://clients.openedu.nl/fith"]
];
foreach($tests as $test) {
[$key, $site] = $test;
print("Checking key '{$key}' on site '{$site}': " . (sitematch($key,$site)?"MATCH":"FAIL") . "\n");
}

View File

@ -22,7 +22,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'local_treestudyplan'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494).
$plugin->version = 2024042100; // YYYYMMDDHH (year, month, day, iteration).
$plugin->version = 2024051000; // YYYYMMDDHH (year, month, day, iteration).
$plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11).
$plugin->release = "1.2.0";