Fix #2 - Reject IP addresses that Moodle cannot match

The custom IP field and the predefined ranges form accepted invalid
entries such as 2001:db8::1garbage, 192.168.1.0/24garbage or
192.168.1.0/999. address_in_subnet() silently skips those entries, so a
"must not" restriction with a typo let blocked users in.

Validation now only accepts formats address_in_subnet() can match: a
single address, a subnet (mask 0-32 / 0-128) or a range in the last
group with end >= start. IPv6 forms cleanremoteaddr() cannot parse are
rejected as well. JS and PHP share the same rules; PHP also checks each
entry against address_in_subnet() as a safety net.

- Allow spaces after commas in the restriction field
- Support IPv6 ranges, e.g. 2001:db8::1-ff
- Add PHPUnit tests for helper::is_valid_ipaddress_list()
- Bump version to 2026101500 (5.3.2)
This commit is contained in:
Nihaal Shaikh committed 2026-10-15 09:04:58 +02:00
1 parent 150067f43d
commit 06a5f9c8c9
10 files changed
+653 -198

No files matched your search

@@ -15,11 +15,12 @@ YUI.add('moodle-availability_ipaddress-form', function (Y, NAME) {
*/
M.availability_ipaddress = M.availability_ipaddress || {};
// A single IPv4 octet (0-255) without leading zeros.
M.availability_ipaddress.octet = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)';
// MIT https://github.com/sindresorhus/ip-regex
// Advanced ip-address regex for validating.
M.availability_ipaddress.v4 = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]' +
'[0-9]|[1-9][0-9]|[0-9])){3}';
M.availability_ipaddress.v6 = "^((?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]" +
// IPv6 address, without anchors so it can be combined.
M.availability_ipaddress.v6 = "((?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]" +
"\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}" +
"(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4})" +
"{1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(:[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]" +
@@ -29,7 +30,7 @@ M.availability_ipaddress.v6 = "^((?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|
"\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(:[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]" +
"|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,6}|:)|(?::" +
"((?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}" +
"|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(%[0-9a-zA-Z]{1,})?";
"|(?::[a-fA-F\\d]{1,4}){1,7}|:)))";
/**
* @class M.availability_ipaddress.form
@@ -163,60 +164,141 @@ M.availability_ipaddress.form.getNode = function(json) {
};
/**
* Validate ipaddresses
* Check if a value is a valid IPv4 address.
*
* @param {string[]} ipaddresses
* @param {string} value
* @returns {boolean}
*/
M.availability_ipaddress.isIpv4 = function(value) {
'use strict';
var octet = M.availability_ipaddress.octet;
return new RegExp('^' + octet + '(?:\\.' + octet + '){3}$').test(value);
};
/**
* Check if a value is a valid IPv6 address that Moodle's cleanremoteaddr() can parse.
*
* cleanremoteaddr() doesn't handle embedded IPv4 notation reliably and only expands "::"
* when it replaces at least two groups, or three at the start or end of the address.
*
* @param {string} value
* @returns {boolean}
*/
M.availability_ipaddress.isIpv6 = function(value) {
'use strict';
var groups;
if (value.indexOf('.') !== -1 || !new RegExp('^' + M.availability_ipaddress.v6 + '$').test(value)) {
return false;
}
if (value.indexOf('::') === -1) {
return true;
}
groups = value.split(':').filter(function(group) {
return group !== '';
}).length;
if (value.indexOf('::') === 0 || value.slice(-2) === '::') {
return groups <= 5;
}
return groups <= 6;
};
/**
* Trim the same characters as PHP's trim(), which address_in_subnet() uses.
*
* @param {string} value
* @returns {string}
*/
M.availability_ipaddress.trim = function(value) {
'use strict';
var chars = ' \t\n\r\x00\x0B',
start = 0,
end = value.length;
while (start < end && chars.indexOf(value.charAt(start)) !== -1) {
start++;
}
while (end > start && chars.indexOf(value.charAt(end - 1)) !== -1) {
end--;
}
return value.substring(start, end);
};
/**
* Validate a single entry. Only formats that Moodle's address_in_subnet() understands are
* accepted, anything else would be silently ignored when the restriction is checked.
*
* - Address: 192.168.1.1 or 2001:db8::1
* - Subnet: 192.168.1.0/24 or 2001:db8::/32
* - Range in the last group: 192.168.1.1-255 or 2001:db8::1-ff
*
* @param {string} entry
* @returns {boolean}
*/
M.availability_ipaddress.validateEntry = function(entry) {
'use strict';
var parts, mask, lastgroup;
if (entry.indexOf('/') !== -1) {
parts = entry.split('/');
// Moodle's is_number() rejects a mask with leading zeros.
if (parts.length !== 2 || !/^(0|[1-9]\d{0,2})$/.test(parts[1])) {
return false;
}
mask = parseInt(parts[1], 10);
if (M.availability_ipaddress.isIpv4(parts[0])) {
return mask <= 32;
}
return M.availability_ipaddress.isIpv6(parts[0]) && mask <= 128;
}
if (entry.indexOf('-') !== -1) {
parts = entry.split('-');
if (parts.length !== 2) {
return false;
}
// The end of the range may not be lower than the start, it would never match.
if (M.availability_ipaddress.isIpv4(parts[0])) {
return new RegExp('^' + M.availability_ipaddress.octet + '$').test(parts[1]) &&
parseInt(parts[1], 10) >= parseInt(parts[0].split('.')[3], 10);
}
if (M.availability_ipaddress.isIpv6(parts[0])) {
lastgroup = parts[0].split(':').pop() || '0';
return /^[a-fA-F\d]{1,4}$/.test(parts[1]) && parseInt(parts[1], 16) >= parseInt(lastgroup, 16);
}
return false;
}
// An address ending in ":" is treated as an incomplete prefix by address_in_subnet(), which fails to parse it.
return M.availability_ipaddress.isIpv4(entry) || (M.availability_ipaddress.isIpv6(entry) && entry.slice(-1) !== ':');
};
/**
* Validate a comma separated list of ipaddresses.
*
* @param {string} ipaddresses
* @returns {boolean}
*/
M.availability_ipaddress.validateIpaddress = function(ipaddresses) {
'use strict';
Y.log(ipaddresses);
// Return true for empty string - it's valid to have no custom IPs
if (!ipaddresses || ipaddresses.trim() === '') {
var entries, i;
// Return true for empty string - it's valid to have no custom IPs.
if (!ipaddresses || M.availability_ipaddress.trim(ipaddresses) === '') {
return true;
}
ipaddresses = ipaddresses.split(',');
for (var i in ipaddresses) {
// Test normal ip format.
// Strict ipv4 check.
if (new RegExp(/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/gm)
.test(ipaddresses[i])) {
Y.log('Correct ipv4');
continue;
entries = ipaddresses.split(',');
for (i = 0; i < entries.length; i++) {
// Whitespace around an entry is allowed, address_in_subnet() trims it as well.
if (!M.availability_ipaddress.validateEntry(M.availability_ipaddress.trim(entries[i]))) {
Y.log('Incorrect ip-address: ' + entries[i]);
return false;
}
var ipv4Regex = new RegExp(
'^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)' +
'(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}-' +
'(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$',
'gm'
);
if (ipv4Regex.test(ipaddresses[i])) {
Y.log('Correct ipv4 range.');
continue;
}
if (new RegExp(M.availability_ipaddress.v6)
.test(ipaddresses[i])) {
Y.log('Correct ipv6');
continue;
}
// Test subnet with a regex.
if (new RegExp("^(?:".concat(M.availability_ipaddress.v4 + "\\/(3[0-2]|[12]?[0-9])|(1\\*)", ")|(?:")
.concat(M.availability_ipaddress.v6 + "\\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])", ")?\\/gm"))
.test(ipaddresses[i])) {
Y.log('Correct subnet');
continue;
}
Y.log('Incorrect ip');
return false;
}
Y.log('Valid ipaddresses');
return true;
};
@@ -259,9 +341,8 @@ M.availability_ipaddress.form.fillErrors = function(errors, node) {
var value = {};
this.fillValue(value, node);
// Basic ipaddresses checks - only validate if not empty.
if (value.ipaddresses && value.ipaddresses.trim() !== '' &&
M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) {
// Basic ipaddresses checks - an empty value is valid.
if (M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) {
errors.push('availability_ipaddress:error_ipaddress');
}
};
@@ -1 +1 @@
YUI.add("moodle-availability_ipaddress-form",function(r,d){M.availability_ipaddress=M.availability_ipaddress||{},M.availability_ipaddress.v4="(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}",M.availability_ipaddress.v6="^((?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(:[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(:[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(:[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(:[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,6}|:)|(?::((?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(%[0-9a-zA-Z]{1,})?",M.availability_ipaddress.form=r.Object(M.core_availability.plugin),M.availability_ipaddress.form.initInner=function(d){"use strict";this.predefinedRanges=d||[]},M.availability_ipaddress.form.getValue=function(d,a){"use strict";d=a.one("input[name="+d+"]").get("value");return M.availability_ipaddress.validateIpaddress(d),d},M.availability_ipaddress.form.getNode=function(d){"use strict";var a,e,i,s,t="ipaddresses"+M.availability_ipaddress.form.instId,l="predefined"+M.availability_ipaddress.form.instId;if(M.availability_ipaddress.form.instId+=1,a='<div class="availability-ipaddress-container">',this.predefinedRanges&&0<this.predefinedRanges.length){for(a=(a+='<div class="availability-group">')+('<label><span class="p-r-1">'+M.util.get_string("use_predefined","availability_ipaddress")+"</span></label>")+('<select name="predefined_ranges" id="'+l+'" multiple="multiple" class="form-control" style="min-height: 100px;">'),e=0;e<this.predefinedRanges.length;e++)a=(a+='<option value="'+(i=this.predefinedRanges[e]).id+'" data-ipaddresses="'+r.Escape.html(i.ipaddresses)+'">')+r.Escape.html(i.name)+"</option>";a=(a=(a+="</select>")+"</div>"+'<div class="availability-group" style="margin-top: 10px;">')+('<label for="'+t+'"><span class="p-r-1">'+M.util.get_string("custom_ipaddress","availability_ipaddress")+"</span></label>")}else a=(a+='<div class="availability-group">')+'<label for="'+t+'"><span class="p-r-1">'+M.util.get_string("title","availability_ipaddress")+"</span></label>";return l=r.Node.create("<div>"+(a=(a+='<input type="text" placeholder="192.168.178.1,231.54.211.0/20,231.3.56.211" name="ipaddresses" id="'+t+'" class="form-control">')+"</div>"+"</div>")+"</div>"),d.ipaddresses!==undefined&&l.one("input[name=ipaddresses]").set("value",d.ipaddresses),d.predefined_ranges!==undefined&&this.predefinedRanges&&0<this.predefinedRanges.length&&(s=l.one("select[name=predefined_ranges]"))&&d.predefined_ranges.forEach(function(d){d=s.one('option[value="'+d+'"]');d&&d.set("selected",!0)}),M.availability_ipaddress.form.addedEvents||(M.availability_ipaddress.form.addedEvents=!0,(t=r.one(".availability-field")).delegate("valuechange",function(){M.core_availability.form.update()},".availability_ipaddress input[name=ipaddresses]"),t.delegate("change",function(){M.core_availability.form.update()},".availability_ipaddress select[name=predefined_ranges]")),l},M.availability_ipaddress.validateIpaddress=function(d){"use strict";if(!d||""===d.trim())return!0;for(var a in d=d.split(","))if(!(new RegExp(/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/gm).test(d[a])||new RegExp("^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}-(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$","gm").test(d[a])||new RegExp(M.availability_ipaddress.v6).test(d[a])||new RegExp("^(?:".concat(M.availability_ipaddress.v4+"\\/(3[0-2]|[12]?[0-9])|(1\\*)",")|(?:").concat(M.availability_ipaddress.v6+"\\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])",")?\\/gm")).test(d[a])))return!1;return!0},M.availability_ipaddress.form.fillValue=function(d,a){var e;d.ipaddresses=this.getValue("ipaddresses",a),(a=a.one("select[name=predefined_ranges]"))&&(e=[],a.get("options").each(function(d){d.get("selected")&&e.push(parseInt(d.get("value")))}),0<e.length&&(d.predefined_ranges=e))},M.availability_ipaddress.form.fillErrors=function(d,a){"use strict";var e={};this.fillValue(e,a),e.ipaddresses&&""!==e.ipaddresses.trim()&&!1===M.availabiLine truncated
YUI.add("moodle-availability_ipaddress-form",function(r,i){M.availability_ipaddress=M.availability_ipaddress||{},M.availability_ipaddress.octet="(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)",M.availability_ipaddress.v6="((?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(:[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,3}|:)|(?:[a-fA-F\\d]{1,4}:){3}(?:(:[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,4}|:)|(?:[a-fA-F\\d]{1,4}:){2}(?:(:[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(:[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,6}|:)|(?::((?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)))",M.availability_ipaddress.form=r.Object(M.core_availability.plugin),M.availability_ipaddress.form.initInner=function(i){"use strict";this.predefinedRanges=i||[]},M.availability_ipaddress.form.getValue=function(i,a){"use strict";i=a.one("input[name="+i+"]").get("value");return M.availability_ipaddress.validateIpaddress(i),i},M.availability_ipaddress.form.getNode=function(i){"use strict";var a,e,d,s,t="ipaddresses"+M.availability_ipaddress.form.instId,l="predefined"+M.availability_ipaddress.form.instId;if(M.availability_ipaddress.form.instId+=1,a='<div class="availability-ipaddress-container">',this.predefinedRanges&&0<this.predefinedRanges.length){for(a=(a+='<div class="availability-group">')+('<label><span class="p-r-1">'+M.util.get_string("use_predefined","availability_ipaddress")+"</span></label>")+('<select name="predefined_ranges" id="'+l+'" multiple="multiple" class="form-control" style="min-height: 100px;">'),e=0;e<this.predefinedRanges.length;e++)a=(a+='<option value="'+(d=this.predefinedRanges[e]).id+'" data-ipaddresses="'+r.Escape.html(d.ipaddresses)+'">')+r.Escape.html(d.name)+"</option>";a=(a=(a+="</select>")+"</div>"+'<div class="availability-group" style="margin-top: 10px;">')+('<label for="'+t+'"><span class="p-r-1">'+M.util.get_string("custom_ipaddress","availability_ipaddress")+"</span></label>")}else a=(a+='<div class="availability-group">')+'<label for="'+t+'"><span class="p-r-1">'+M.util.get_string("title","availability_ipaddress")+"</span></label>";return l=r.Node.create("<div>"+(a=(a+='<input type="text" placeholder="192.168.178.1,231.54.211.0/20,231.3.56.211" name="ipaddresses" id="'+t+'" class="form-control">')+"</div>"+"</div>")+"</div>"),i.ipaddresses!==undefined&&l.one("input[name=ipaddresses]").set("value",i.ipaddresses),i.predefined_ranges!==undefined&&this.predefinedRanges&&0<this.predefinedRanges.length&&(s=l.one("select[name=predefined_ranges]"))&&i.predefined_ranges.forEach(function(i){i=s.one('option[value="'+i+'"]');i&&i.set("selected",!0)}),M.availability_ipaddress.form.addedEvents||(M.availability_ipaddress.form.addedEvents=!0,(t=r.one(".availability-field")).delegate("valuechange",function(){M.core_availability.form.update()},".availability_ipaddress input[name=ipaddresses]"),t.delegate("change",function(){M.core_availability.form.update()},".availability_ipaddress select[name=predefined_ranges]")),l},M.availability_ipaddress.isIpv4=function(i){"use strict";var a=M.availability_ipaddress.octet;return new RegExp("^"+a+"(?:\\."+a+"){3}$").test(i)},M.availability_ipaddress.isIpv6=function(i){"use strict";var a;return!(-1!==i.indexOf(".")||!new RegExp("^"+M.availability_ipaddress.v6+"$").test(i))&&(-1===i.indexOf("::")||(a=i.split(":").filter(function(i){return""!==i}).length,0===i.indexOf("::")||"::"===i.slice(-2)?a<=5:a<=6))},M.availability_ipaddress.trim=function(i){"use strict";for(var a=" \t\n\r\0\x0B",e=0,d=i.length;e<d&&-1!==a.indexOf(i.charAt(e));)e++;for(;e<d&&-1!==a.indexOf(i.charAt(d-1));)d--;return i.substring(e,d)},M.availability_ipaddress.validateEntry=function(i){"use strict";var a,e;return-1!==i.indexOf("/")?!(2!==(a=i.split("/")).length||!/^(0|[1-9]\d{0,2})$/.test(a[1]))&&(e=parseInt(a[1],10),M.availability_ipaddress.isIpv4(a[0])?e<=32:M.availability_ipaddress.isIpv6(a[0])&&e<=128):-1!==i.indexOf("-")?2===(a=i.split("-")).length&&(M.availability_ipaddress.isIpv4(a[0])?new RegExp("^"+M.availability_ipaddress.octet+"$").test(a[1])&&parseInt(a[1],10)>=parseInt(a[0].split(".")[3],10):!!M.availability_ipaddress.isIpv6(a[0])&&(e=a[0].split(":").pop()||"0",/^[a-fA-F\d]{1,4}$/Line truncated
@@ -15,11 +15,12 @@ YUI.add('moodle-availability_ipaddress-form', function (Y, NAME) {
*/
M.availability_ipaddress = M.availability_ipaddress || {};
// A single IPv4 octet (0-255) without leading zeros.
M.availability_ipaddress.octet = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)';
// MIT https://github.com/sindresorhus/ip-regex
// Advanced ip-address regex for validating.
M.availability_ipaddress.v4 = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]' +
'[0-9]|[1-9][0-9]|[0-9])){3}';
M.availability_ipaddress.v6 = "^((?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]" +
// IPv6 address, without anchors so it can be combined.
M.availability_ipaddress.v6 = "((?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]" +
"\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)|(?:[a-fA-F\\d]{1,4}:){5}" +
"(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4})" +
"{1,2}|:)|(?:[a-fA-F\\d]{1,4}:){4}(?:(:[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]" +
@@ -29,7 +30,7 @@ M.availability_ipaddress.v6 = "^((?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)|
"\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,5}|:)|(?:[a-fA-F\\d]{1,4}:){1}(?:(:[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]" +
"|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(:[a-fA-F\\d]{1,4}){1,6}|:)|(?::" +
"((?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}" +
"|(?::[a-fA-F\\d]{1,4}){1,7}|:)))(%[0-9a-zA-Z]{1,})?";
"|(?::[a-fA-F\\d]{1,4}){1,7}|:)))";
/**
* @class M.availability_ipaddress.form
@@ -160,51 +161,138 @@ M.availability_ipaddress.form.getNode = function(json) {
};
/**
* Validate ipaddresses
* Check if a value is a valid IPv4 address.
*
* @param {string[]} ipaddresses
* @param {string} value
* @returns {boolean}
*/
M.availability_ipaddress.isIpv4 = function(value) {
'use strict';
var octet = M.availability_ipaddress.octet;
return new RegExp('^' + octet + '(?:\\.' + octet + '){3}$').test(value);
};
/**
* Check if a value is a valid IPv6 address that Moodle's cleanremoteaddr() can parse.
*
* cleanremoteaddr() doesn't handle embedded IPv4 notation reliably and only expands "::"
* when it replaces at least two groups, or three at the start or end of the address.
*
* @param {string} value
* @returns {boolean}
*/
M.availability_ipaddress.isIpv6 = function(value) {
'use strict';
var groups;
if (value.indexOf('.') !== -1 || !new RegExp('^' + M.availability_ipaddress.v6 + '$').test(value)) {
return false;
}
if (value.indexOf('::') === -1) {
return true;
}
groups = value.split(':').filter(function(group) {
return group !== '';
}).length;
if (value.indexOf('::') === 0 || value.slice(-2) === '::') {
return groups <= 5;
}
return groups <= 6;
};
/**
* Trim the same characters as PHP's trim(), which address_in_subnet() uses.
*
* @param {string} value
* @returns {string}
*/
M.availability_ipaddress.trim = function(value) {
'use strict';
var chars = ' \t\n\r\x00\x0B',
start = 0,
end = value.length;
while (start < end && chars.indexOf(value.charAt(start)) !== -1) {
start++;
}
while (end > start && chars.indexOf(value.charAt(end - 1)) !== -1) {
end--;
}
return value.substring(start, end);
};
/**
* Validate a single entry. Only formats that Moodle's address_in_subnet() understands are
* accepted, anything else would be silently ignored when the restriction is checked.
*
* - Address: 192.168.1.1 or 2001:db8::1
* - Subnet: 192.168.1.0/24 or 2001:db8::/32
* - Range in the last group: 192.168.1.1-255 or 2001:db8::1-ff
*
* @param {string} entry
* @returns {boolean}
*/
M.availability_ipaddress.validateEntry = function(entry) {
'use strict';
var parts, mask, lastgroup;
if (entry.indexOf('/') !== -1) {
parts = entry.split('/');
// Moodle's is_number() rejects a mask with leading zeros.
if (parts.length !== 2 || !/^(0|[1-9]\d{0,2})$/.test(parts[1])) {
return false;
}
mask = parseInt(parts[1], 10);
if (M.availability_ipaddress.isIpv4(parts[0])) {
return mask <= 32;
}
return M.availability_ipaddress.isIpv6(parts[0]) && mask <= 128;
}
if (entry.indexOf('-') !== -1) {
parts = entry.split('-');
if (parts.length !== 2) {
return false;
}
// The end of the range may not be lower than the start, it would never match.
if (M.availability_ipaddress.isIpv4(parts[0])) {
return new RegExp('^' + M.availability_ipaddress.octet + '$').test(parts[1]) &&
parseInt(parts[1], 10) >= parseInt(parts[0].split('.')[3], 10);
}
if (M.availability_ipaddress.isIpv6(parts[0])) {
lastgroup = parts[0].split(':').pop() || '0';
return /^[a-fA-F\d]{1,4}$/.test(parts[1]) && parseInt(parts[1], 16) >= parseInt(lastgroup, 16);
}
return false;
}
// An address ending in ":" is treated as an incomplete prefix by address_in_subnet(), which fails to parse it.
return M.availability_ipaddress.isIpv4(entry) || (M.availability_ipaddress.isIpv6(entry) && entry.slice(-1) !== ':');
};
/**
* Validate a comma separated list of ipaddresses.
*
* @param {string} ipaddresses
* @returns {boolean}
*/
M.availability_ipaddress.validateIpaddress = function(ipaddresses) {
'use strict';
// Return true for empty string - it's valid to have no custom IPs
if (!ipaddresses || ipaddresses.trim() === '') {
var entries, i;
// Return true for empty string - it's valid to have no custom IPs.
if (!ipaddresses || M.availability_ipaddress.trim(ipaddresses) === '') {
return true;
}
ipaddresses = ipaddresses.split(',');
for (var i in ipaddresses) {
// Test normal ip format.
// Strict ipv4 check.
if (new RegExp(/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/gm)
.test(ipaddresses[i])) {
continue;
entries = ipaddresses.split(',');
for (i = 0; i < entries.length; i++) {
// Whitespace around an entry is allowed, address_in_subnet() trims it as well.
if (!M.availability_ipaddress.validateEntry(M.availability_ipaddress.trim(entries[i]))) {
return false;
}
var ipv4Regex = new RegExp(
'^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)' +
'(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}-' +
'(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$',
'gm'
);
if (ipv4Regex.test(ipaddresses[i])) {
continue;
}
if (new RegExp(M.availability_ipaddress.v6)
.test(ipaddresses[i])) {
continue;
}
// Test subnet with a regex.
if (new RegExp("^(?:".concat(M.availability_ipaddress.v4 + "\\/(3[0-2]|[12]?[0-9])|(1\\*)", ")|(?:")
.concat(M.availability_ipaddress.v6 + "\\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])", ")?\\/gm"))
.test(ipaddresses[i])) {
continue;
}
return false;
}
return true;
@@ -249,9 +337,8 @@ M.availability_ipaddress.form.fillErrors = function(errors, node) {
var value = {};
this.fillValue(value, node);
// Basic ipaddresses checks - only validate if not empty.
if (value.ipaddresses && value.ipaddresses.trim() !== '' &&
M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) {
// Basic ipaddresses checks - an empty value is valid.
if (M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) {
errors.push('availability_ipaddress:error_ipaddress');
}
};