Compare commits

...
6 Commits
Author SHA1 Message Date
Luuk Verhoeven 197e5c3b58 Merge pull request #22 from LdesignMedia/2-fix-ip-validation
Fix #2 - Reject IP addresses that Moodle cannot match
2026-10-16 13:18:37 +02:00
Nihaal Shaikh 06a5f9c8c9 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)
2026-10-15 09:04:58 +02:00
Luuk Verhoeven 150067f43d Update CHANGELOG.md 2026-10-14 10:51:13 +02:00
Luuk Verhoeven b1f0bc2f09 Update version.php 2026-10-14 10:49:32 +02:00
Nihaal Shaikh 9269cb7f76 Merge pull request #21 from minhduchoang195/#20_fix_failed_unittest
#20 Correct db/install.xml so that the unit test passes successfully.
2026-10-14 10:27:27 +02:00
Hoang, Minh Duc 2210e85ca0 #20 Add the required attribute sequence to each field 2026-10-12 12:56:11 +02:00
11 changed files with 661 additions and 203 deletions

No files matched your search

+12
View File
@@ -1,5 +1,17 @@
# Changelog
## 5.3.2 (2026101500)
- Fixed IP address validation being too permissive https://github.com/LdesignMedia/moodle-availability_ipaddress/issues/2
- Invalid entries like `2001:db8::1garbage` or `192.168.1.0/24garbage` are no longer accepted. Moodle silently ignored them, so a "must not" restriction could let blocked users in.
- Only formats Moodle can match are accepted. Rejected: IPv6 written with an IPv4 part (`::ffff:192.168.1.1`), an IPv6 prefix without mask (`2001:db8::`, use `2001:db8::/32`), masks with leading zeros (`/024`) and some compressed IPv6 forms Moodle can't parse (`1::2:3:4:5:6:7`).
- Predefined IP ranges are now validated with the same rules (subnet mask range, range end).
- Spaces after commas are now allowed in the restriction field, e.g. `192.168.1.1, 10.0.0.1`.
- Added support for IPv6 ranges in the last group, e.g. `2001:db8::1-ff`.
- Note: existing restrictions and predefined ranges are not changed. If one contains an invalid entry, it now shows a validation error the next time it is edited. Entries that were already invalid never matched.
## 5.3.1 (2026101400)
- Solved CI tests https://github.com/LdesignMedia/moodle-availability_ipaddress/issues/20 | Thanks @minhduchoang195
## 5.3.0 (2026092300)
- Tested and verified on Moodle 5.3
+4 -43
View File
@@ -25,6 +25,8 @@
namespace availability_ipaddress\form;
use availability_ipaddress\helper;
defined('MOODLE_INTERNAL') || die();
global $CFG;
@@ -102,52 +104,11 @@ class range_form extends \moodleform {
public function validation($data, $files) {
$errors = parent::validation($data, $files);
// Validate IP addresses.
if (!empty($data['ipaddresses'])) {
$ipaddresses = explode(',', $data['ipaddresses']);
foreach ($ipaddresses as $ip) {
$ip = trim($ip);
if (!$this->validate_ip_format($ip)) {
// Validate IP addresses, the field is required so an empty value is invalid as well.
if (!helper::is_valid_ipaddress_list($data['ipaddresses'] ?? '')) {
$errors['ipaddresses'] = get_string('error_ipaddress', 'availability_ipaddress');
break;
}
}
}
return $errors;
}
/**
* Validate IP address format.
*
* @param string $ip
*
* @return bool
*/
private function validate_ip_format(string $ip): bool {
// Use the same validation logic as the main plugin.
// This is a simplified version - you might want to use the same regex as in JS.
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return true;
}
// Check for CIDR notation.
if (strpos($ip, '/') !== false) {
[$addr, $mask] = explode('/', $ip);
if (filter_var($addr, FILTER_VALIDATE_IP) && is_numeric($mask)) {
return true;
}
}
// Check for IP range.
if (strpos($ip, '-') !== false) {
[$start, $end] = explode('-', $ip);
if (filter_var($start, FILTER_VALIDATE_IP)) {
return true;
}
}
return false;
}
}
+137
View File
@@ -359,4 +359,141 @@ class helper {
return $modified;
}
/**
* Validate a comma separated list of IP addresses.
*
* @param string $ipaddresses Comma separated list of IP addresses.
*
* @return bool True if every entry is valid.
*/
public static function is_valid_ipaddress_list(string $ipaddresses): bool {
foreach (explode(',', $ipaddresses) as $entry) {
if (!self::is_valid_ipaddress_entry(trim($entry))) {
return false;
}
}
return true;
}
/**
* Validate a single IP address entry.
*
* Only formats that 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 A single IP address entry.
*
* @return bool True if the entry is valid.
*/
public static function is_valid_ipaddress_entry(string $entry): bool {
if (strpos($entry, '/') !== false) {
$valid = self::is_valid_subnet($entry);
} else if (strpos($entry, '-') !== false) {
$valid = self::is_valid_range($entry);
} else {
// An address ending in ":" is treated as an incomplete prefix by address_in_subnet(), which fails to parse it.
$valid = self::is_ipv4($entry) || (self::is_ipv6($entry) && substr($entry, -1) !== ':');
}
// Safety net: the entry must match its own start address, so address_in_subnet() is able to parse it.
return $valid && address_in_subnet(preg_split('/[\/-]/', $entry)[0], $entry, true);
}
/**
* Check if a value is a valid IPv4 address.
*
* @param string $value The value to check.
*
* @return bool True if the value is a valid IPv4 address.
*/
private static function is_ipv4(string $value): bool {
return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
}
/**
* Check if a value is a valid IPv6 address that 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 The value to check.
*
* @return bool True if the value is a valid IPv6 address.
*/
private static function is_ipv6(string $value): bool {
if (strpos($value, '.') !== false || filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false) {
return false;
}
if (strpos($value, '::') === false) {
return true;
}
$groups = count(array_filter(explode(':', $value), 'strlen'));
if (strpos($value, '::') === 0 || substr($value, -2) === '::') {
return $groups <= 5;
}
return $groups <= 6;
}
/**
* Validate a subnet in CIDR notation, e.g. 192.168.1.0/24 or 2001:db8::/32.
*
* @param string $entry The subnet.
*
* @return bool True if the subnet is valid.
*/
private static function is_valid_subnet(string $entry): bool {
$parts = explode('/', $entry);
// Moodle's is_number() rejects a mask with leading zeros.
if (count($parts) !== 2 || !preg_match('/^(0|[1-9]\d{0,2})$/D', $parts[1])) {
return false;
}
[$address, $mask] = $parts;
if (self::is_ipv4($address)) {
return (int) $mask <= 32;
}
return self::is_ipv6($address) && (int) $mask <= 128;
}
/**
* Validate a range in the last group of the address, e.g. 192.168.1.1-255 or 2001:db8::1-ff.
*
* The end of the range may not be lower than the start, it would never match.
*
* @param string $entry The range.
*
* @return bool True if the range is valid.
*/
private static function is_valid_range(string $entry): bool {
$parts = explode('-', $entry);
if (count($parts) !== 2) {
return false;
}
[$start, $end] = $parts;
if (self::is_ipv4($start)) {
$startgroup = (int) substr(strrchr($start, '.'), 1);
return preg_match('/^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/D', $end) === 1 && (int) $end >= $startgroup;
}
if (self::is_ipv6($start)) {
$startgroup = hexdec(substr(strrchr($start, ':'), 1) ?: '0');
return preg_match('/^[0-9a-f]{1,4}$/iD', $end) === 1 && hexdec($end) >= $startgroup;
}
return false;
}
}
+6 -6
View File
@@ -7,12 +7,12 @@
<TABLE NAME="availability_ipaddress_pre" COMMENT="Stores predefined IP address ranges for availability_ipaddress">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" COMMENT="Display name for the IP range"/>
<FIELD NAME="ipaddresses" TYPE="text" NOTNULL="true" COMMENT="Comma-separated list of IP addresses/ranges"/>
<FIELD NAME="description" TYPE="text" NOTNULL="false" COMMENT="Optional description of this IP range"/>
<FIELD NAME="enabled" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" COMMENT="Whether this range is available for selection"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" COMMENT="Display name for the IP range" SEQUENCE="false"/>
<FIELD NAME="ipaddresses" TYPE="text" NOTNULL="true" COMMENT="Comma-separated list of IP addresses/ranges" SEQUENCE="false"/>
<FIELD NAME="description" TYPE="text" NOTNULL="false" COMMENT="Optional description of this IP range" SEQUENCE="false"/>
<FIELD NAME="enabled" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" COMMENT="Whether this range is available for selection" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+6 -5
View File
@@ -56,11 +56,12 @@ $string['ipaddresses_help'] = 'Enter IP addresses separated by commas. Supports
$string['ipaddresses_help_help'] = '<p>Enter one or more IP addresses or ranges, separated by commas.</p>
<p><strong>Examples:</strong></p>
<ul>
<li><strong>Single IP:</strong> <code>192.168.1.1</code></li>
<li><strong>IP range:</strong> <code>192.168.1.1-255</code></li>
<li><strong>Subnet:</strong> <code>192.168.1.0/24</code></li>
<li><strong>Multiple:</strong> <code>192.168.1.1,10.0.0.0/8,172.16.0.1-255</code></li>
</ul>';
<li><strong>Single IP:</strong> <code>192.168.1.1</code> or <code>2001:db8::1</code></li>
<li><strong>IP range:</strong> <code>192.168.1.1-255</code> or <code>2001:db8::1-ff</code></li>
<li><strong>Subnet:</strong> <code>192.168.1.0/24</code> or <code>2001:db8::/32</code></li>
<li><strong>Multiple:</strong> <code>192.168.1.1, 10.0.0.0/8, 172.16.0.1-255</code></li>
</ul>
<p>A range only changes the last part of the address. Use a subnet for an IPv6 network prefix, e.g. <code>2001:db8::/32</code> instead of <code>2001:db8::</code>.</p>';
$string['enabled'] = 'Enabled';
$string['sortorder'] = 'Sort order';
$string['existing_ranges'] = 'Existing IP ranges';
+98
View File
@@ -0,0 +1,98 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace availability_ipaddress;
/**
* Unit tests for the IP address validation in the helper class.
*
* @package availability_ipaddress
* @copyright 2026 LdesignMedia.nl - Luuk Verhoeven
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \availability_ipaddress\helper
*/
final class helper_test extends \basic_testcase {
/**
* Data provider for test_is_valid_ipaddress_list.
*
* @return array
*/
public static function ipaddress_list_provider(): array {
return [
'IPv4 address' => ['192.168.1.1', true],
'IPv4 subnet' => ['192.168.1.0/24', true],
'IPv4 subnet all addresses' => ['0.0.0.0/0', true],
'IPv4 range' => ['192.168.1.10-20', true],
'IPv4 range single address' => ['192.168.1.5-5', true],
'IPv6 address' => ['2001:db8::1', true],
'IPv6 loopback' => ['::1', true],
'IPv6 full address' => ['2001:db8:0:0:0:0:0:1', true],
'IPv6 compressed in the middle' => ['1:2:3::4:5:6', true],
'IPv6 compressed at the start' => ['::1:2:3:4:5', true],
'IPv6 subnet' => ['2001:db8::/32', true],
'IPv6 subnet all addresses' => ['::/0', true],
'IPv6 range' => ['2001:db8::1-ff', true],
'IPv6 range from compressed start' => ['2001:db8::-ffff', true],
'List' => ['192.168.1.1,10.0.0.0/8,2001:db8::/32', true],
'List with spaces' => [' 192.168.1.1 , 10.0.0.1 ', true],
'IPv6 with trailing text' => ['2001:db8::1garbage', false],
'IPv6 with zone id' => ['fe80::1%eth0', false],
'IPv6 subnet mask too large' => ['2001:db8::/129', false],
'IPv6 address ending in ::' => ['2001:db8::', false],
'IPv6 unspecified address' => ['::', false],
'IPv6 :: replacing one group' => ['1::2:3:4:5:6:7', false],
'IPv6 :: at the start replacing two groups' => ['::1:2:3:4:5:6', false],
'IPv6 subnet :: at the end replacing two groups' => ['2001:db8:1:2:3:4::/112', false],
'IPv6 with embedded IPv4' => ['::ffff:192.168.1.1', false],
'IPv6 subnet with embedded IPv4' => ['::ffff:192.168.1.0/120', false],
'IPv4 subnet with trailing text' => ['192.168.1.0/24garbage', false],
'IPv4 subnet mask too large' => ['192.168.1.0/33', false],
'IPv4 subnet mask with leading zero' => ['192.168.1.0/024', false],
'IPv4 subnet negative mask' => ['192.168.1.0/-5', false],
'IPv4 subnet exponent mask' => ['192.168.1.0/1e3', false],
'IPv4 subnet trailing newline' => ["192.168.1.0/24\n", true],
'IPv4 with non-breaking space' => ["192.168.1.1\u{a0}", false],
'IPv4 subnet double mask' => ['192.168.1.0/24/8', false],
'IPv4 range with text' => ['192.168.1.1-garbage', false],
'IPv4 range end too large' => ['192.168.1.1-256', false],
'IPv4 range end below start' => ['192.168.1.20-10', false],
'IPv4 range double dash' => ['192.168.1.1-2-3', false],
'IPv6 range end below start' => ['2001:db8::ff-1', false],
'Invalid octet' => ['999.1.1.1', false],
'Leading zero' => ['192.168.01.1', false],
'Text' => ['abc', false],
'Text with subnet' => ['hello/gm', false],
'Empty' => ['', false],
'Zero' => ['0', false],
'Trailing comma' => ['192.168.1.1,', false],
'List with one invalid entry' => ['192.168.1.1, 2001:db8::1garbage', false],
];
}
/**
* Test validation of a comma separated list of IP addresses.
*
* @dataProvider ipaddress_list_provider
*
* @param string $ipaddresses
* @param bool $expected
*
* @return void
*/
public function test_is_valid_ipaddress_list(string $ipaddresses, bool $expected): void {
$this->assertSame($expected, helper::is_valid_ipaddress_list($ipaddresses));
}
}
+2 -2
View File
@@ -27,8 +27,8 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'availability_ipaddress';
$plugin->version = 2026092300;
$plugin->release = '5.3.0';
$plugin->version = 2026101500;
$plugin->release = '5.3.2';
$plugin->requires = 2016120500;
$plugin->maturity = MATURITY_STABLE;
$plugin->supported = [400, 503];
@@ -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;
}
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');
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;
}
}
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,52 +161,139 @@ 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;
}
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;
}
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;
}
}
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');
}
};
+132 -51
View File
@@ -13,11 +13,12 @@
*/
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]" +
@@ -27,7 +28,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
@@ -161,60 +162,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;
}
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');
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;
}
}
Y.log('Valid ipaddresses');
return true;
};
@@ -257,9 +339,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');
}
};