mirror of
https://github.com/LdesignMedia/moodle-availability_ipaddress.git
synced 2026-10-22 11:08:07 +02:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a5f9c8c9 | ||
|
|
150067f43d | ||
|
|
b1f0bc2f09 | ||
|
|
9269cb7f76 | ||
|
|
2210e85ca0 | ||
|
|
6b24f4a2b9 | ||
|
|
00941bbc54 | ||
|
|
cf5409707f | ||
|
|
d730ed97fd | ||
|
|
474f91ccbc | ||
|
|
d65fd40621 | ||
|
|
4632008dfb | ||
|
|
c91646df81 | ||
|
|
d2c231e657 | ||
|
|
700820480d | ||
|
|
e91befdf10 | ||
|
|
3f5f4ee6cb | ||
|
|
cccebb6c15 | ||
|
|
af275d1699 | ||
|
|
5dce3ed488 |
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
github: [MFreakNL]
|
||||
github: [LdesignMedia]
|
||||
Whitespace-only changes.
@@ -0,0 +1,48 @@
|
||||
# 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
|
||||
|
||||
## 5.2.0 (2026031802)
|
||||
- Tested and verified on Moodle 5.2
|
||||
|
||||
## 5.1.1 (2026031800)
|
||||
* Fixed PHP 7.3 compatibility issue that broke Moodle 4.0.x servers ([#13](https://github.com/LdesignMedia/moodle-availability_ipaddress/issues/13))
|
||||
* Removed typed properties (PHP 7.4+) to support PHP 7.3+
|
||||
* Lowered environment.xml PHP requirement from 7.4 to 7.3
|
||||
|
||||
## 5.1.0 (2026030300)
|
||||
- Tested and verified on Moodle 5.1
|
||||
|
||||
## 5.0.2 (2025080401)
|
||||
- Added support for pre-configuring IP ranges by admins
|
||||
|
||||
## 5.0.1 (2025052200)
|
||||
- Tested on Moodle 5.0
|
||||
|
||||
## 4.5.0 (2025040400)
|
||||
- Tested on Moodle 4.5
|
||||
|
||||
## 4.4.0 (2024072000)
|
||||
- Tested on Moodle 4.4
|
||||
|
||||
## 4.0.3 (2022052801)
|
||||
- Test in Moodle 4.0 @[hamzatamyachte](https://github.com/hamzatamyachte)
|
||||
|
||||
## 4.0.2 (2022052800)
|
||||
- Fixed the [issue 6](https://github.com/ldesignmediaNL/moodle-availability_ipaddress/issues/6) @[hamzatamyachte](https://github.com/hamzatamyachte)
|
||||
|
||||
## 3.9.0 (2022021100)
|
||||
- Thanks for adding ip-range support @[juacas](https://github.com/juacas)
|
||||
@@ -11,7 +11,7 @@ activity, making it accessible only to users from specified IP addresses.
|
||||
* Author: Luuk Verhoeven, [ldesignmedia.nl](https://ldesignmedia.nl/)
|
||||
* Author: Vincent Cornelis, [ldesignmedia.nl](https://ldesignmedia.nl/)
|
||||
* Min. required: Moodle 4.0
|
||||
* Supports PHP: 7.4
|
||||
* Supports PHP: 7.3+
|
||||
|
||||

|
||||

|
||||
@@ -20,7 +20,11 @@ activity, making it accessible only to users from specified IP addresses.
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
@@ -47,9 +51,6 @@ activity, making it accessible only to users from specified IP addresses.
|
||||
1. The users with matching ip addresses can view the activity.
|
||||
5. Save the activity
|
||||
|
||||
## TODO
|
||||
- Behat tests ip validation
|
||||
|
||||
## Security
|
||||
|
||||
If you discover any security related issues, please email [[email protected]](mailto:[email protected]) instead of using the issue tracker.
|
||||
@@ -62,12 +63,6 @@ The GNU GENERAL PUBLIC LICENSE. Please see [License File](LICENSE) for more info
|
||||
|
||||
Contributions are welcome and will be fully credited. We accept contributions via Pull Requests on Github.
|
||||
|
||||
## Changelog
|
||||
### Changelog
|
||||
|
||||
- 2024080401 Added support for pre-configuring IP ranges by admins
|
||||
- 2025052200 Tested on Moodle 5.0
|
||||
- 2025040400 Tested on Moodle 4.5
|
||||
- 2024072000 Tested on Moodle 4.4
|
||||
- 2022021100 Thanks for adding ip-range support @[juacas](https://github.com/juacas)
|
||||
- 2022052800 Fixed the [issue 6](https://github.com/ldesignmediaNL/moodle-availability_ipaddress/issues/6) @[hamzatamyachte](https://github.com/hamzatamyachte)
|
||||
- 2022052801 Test in Moodle 4.0 @[hamzatamyachte](https://github.com/hamzatamyachte)
|
||||
See [CHANGELOG.md](CHANGELOG.md) for a full list of changes.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Security Policy
|
||||
|
||||
At `Ldesign Media`, we take the security of our software very seriously. This policy outlines the steps we take to ensure the security of our software and how you can help us keep it secure.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
To ensure the best security for our users, we only provide support for the latest version of our software.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in our software, we kindly ask that you report it to us as soon as possible by sending an email to `[email protected]`. Please do not use the issue tracker for reporting security vulnerabilities.
|
||||
We will make every effort to investigate and resolve the vulnerability in a timely manner.
|
||||
|
||||
When reporting a vulnerability, please include the following information (as much as you can provide) to help us better understand the nature and scope of the issue:
|
||||
|
||||
- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
- Full paths of source file(s) related to the manifestation of the issue
|
||||
- The location of the affected source code (tag/branch/commit or direct URL)
|
||||
- Any special configuration required to reproduce the issue
|
||||
- Step-by-step instructions to reproduce the issue
|
||||
- Proof-of-concept or exploit code (if possible)
|
||||
- Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
## Rewards
|
||||
|
||||
We do not currently have a bug bounty program, but we are grateful for any responsible disclosures of security vulnerabilities. Your efforts help make our software safer for everyone.
|
||||
|
||||
## Communication
|
||||
|
||||
When a security vulnerability has been discovered and addressed, we will provide updates to the community as soon as possible to keep everyone informed.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
We would like to extend our gratitude to the security researchers who have helped us improve the security of our software by responsibly reporting security vulnerabilities to us. Your contributions are invaluable to maintaining a safe and secure environment for our users.
|
||||
@@ -36,20 +36,19 @@ use core_availability\info;
|
||||
* @copyright 2019-05-14 Mfreak.nl | LdesignMedia.nl - Luuk Verhoeven
|
||||
*/
|
||||
class condition extends \core_availability\condition {
|
||||
|
||||
/**
|
||||
* Manual provided IP addresses.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $ipaddresses = '';
|
||||
protected $ipaddresses = '';
|
||||
|
||||
/**
|
||||
* Predefined IP address ranges.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $predefinedranges = [];
|
||||
protected $predefinedranges = [];
|
||||
|
||||
/**
|
||||
* condition constructor.
|
||||
@@ -213,5 +212,4 @@ class condition extends \core_availability\condition {
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
+10
-47
@@ -25,6 +25,8 @@
|
||||
|
||||
namespace availability_ipaddress\form;
|
||||
|
||||
use availability_ipaddress\helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
@@ -39,7 +41,6 @@ require_once($CFG->libdir . '/formslib.php');
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class range_form extends \moodleform {
|
||||
|
||||
/**
|
||||
* Define the form.
|
||||
*
|
||||
@@ -72,8 +73,12 @@ class range_form extends \moodleform {
|
||||
$mform->addHelpButton('ipaddresses', 'ipaddresses_help', 'availability_ipaddress');
|
||||
|
||||
// Description field.
|
||||
$mform->addElement('textarea', 'description', get_string('description'),
|
||||
['rows' => 3, 'cols' => 60]);
|
||||
$mform->addElement(
|
||||
'textarea',
|
||||
'description',
|
||||
get_string('description'),
|
||||
['rows' => 3, 'cols' => 60]
|
||||
);
|
||||
$mform->setType('description', PARAM_TEXT);
|
||||
|
||||
// Enabled field.
|
||||
@@ -99,53 +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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,7 +34,6 @@ namespace availability_ipaddress;
|
||||
* @copyright 2019-05-14 Mfreak.nl | LdesignMedia.nl - Luuk Verhoeven
|
||||
*/
|
||||
class frontend extends \core_availability\frontend {
|
||||
|
||||
/**
|
||||
* get_javascript_strings
|
||||
*
|
||||
@@ -81,5 +80,4 @@ class frontend extends \core_availability\frontend {
|
||||
|
||||
return [$rangedata];
|
||||
}
|
||||
|
||||
}
|
||||
+136
-2
@@ -34,7 +34,6 @@ namespace availability_ipaddress;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class helper {
|
||||
|
||||
/**
|
||||
* Check if a predefined range is in use.
|
||||
*
|
||||
@@ -296,7 +295,6 @@ class helper {
|
||||
|
||||
// Process nested conditions.
|
||||
return self::process_nested_conditions($availability, $rangeid) || $modified;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -362,4 +360,140 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ namespace availability_ipaddress\privacy;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\null_provider {
|
||||
|
||||
/**
|
||||
* Get the language string identifier with the component's language
|
||||
* file to explain why this plugin stores no data.
|
||||
@@ -43,5 +42,4 @@ class provider implements \core_privacy\local\metadata\null_provider {
|
||||
public static function get_reason(): string {
|
||||
return 'privacy:metadata';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,7 +45,6 @@ use confirm_action;
|
||||
* @author Vincent Cornelis
|
||||
*/
|
||||
class ipranges_table extends table_sql {
|
||||
|
||||
/**
|
||||
* @var moodle_url The base URL for the page.
|
||||
*/
|
||||
@@ -185,8 +184,10 @@ class ipranges_table extends table_sql {
|
||||
|
||||
// Edit action.
|
||||
$editurl = new moodle_url($this->baseurl, ['action' => 'edit', 'id' => $range->id]);
|
||||
$actions[] = $OUTPUT->action_icon($editurl,
|
||||
new pix_icon('t/edit', get_string('edit')));
|
||||
$actions[] = $OUTPUT->action_icon(
|
||||
$editurl,
|
||||
new pix_icon('t/edit', get_string('edit'))
|
||||
);
|
||||
|
||||
// Toggle action.
|
||||
$toggleurl = new moodle_url($this->baseurl, ['action' => 'toggle', 'id' => $range->id, 'sesskey' => sesskey()]);
|
||||
@@ -199,10 +200,16 @@ class ipranges_table extends table_sql {
|
||||
if ($usage['inuse']) {
|
||||
// Create confirmation message with usage details.
|
||||
$message = \availability_ipaddress\helper::get_range_usage_html($range->id);
|
||||
$message .= \html_writer::tag('p', get_string('confirm_disable_range', 'availability_ipaddress'),
|
||||
['class' => 'font-weight-bold']);
|
||||
$actions[] = $OUTPUT->action_icon($toggleurl, new pix_icon($toggleicon, $togglestring),
|
||||
new confirm_action($message));
|
||||
$message .= \html_writer::tag(
|
||||
'p',
|
||||
get_string('confirm_disable_range', 'availability_ipaddress'),
|
||||
['class' => 'font-weight-bold']
|
||||
);
|
||||
$actions[] = $OUTPUT->action_icon(
|
||||
$toggleurl,
|
||||
new pix_icon($toggleicon, $togglestring),
|
||||
new confirm_action($message)
|
||||
);
|
||||
} else {
|
||||
$actions[] = $OUTPUT->action_icon($toggleurl, new pix_icon($toggleicon, $togglestring));
|
||||
}
|
||||
@@ -222,17 +229,21 @@ class ipranges_table extends table_sql {
|
||||
$usage = \availability_ipaddress\helper::is_range_in_use($range->id);
|
||||
if ($usage['inuse']) {
|
||||
$message = \availability_ipaddress\helper::get_range_usage_html($range->id);
|
||||
$message .= \html_writer::tag('p', get_string('confirm_delete_range', 'availability_ipaddress'),
|
||||
['class' => 'font-weight-bold']);
|
||||
$message .= \html_writer::tag(
|
||||
'p',
|
||||
get_string('confirm_delete_range', 'availability_ipaddress'),
|
||||
['class' => 'font-weight-bold']
|
||||
);
|
||||
} else {
|
||||
$message = get_string('confirm_delete_range', 'availability_ipaddress');
|
||||
}
|
||||
|
||||
$actions[] = $OUTPUT->action_icon($deleteurl,
|
||||
$actions[] = $OUTPUT->action_icon(
|
||||
$deleteurl,
|
||||
new pix_icon('t/delete', get_string('delete')),
|
||||
new confirm_action($message));
|
||||
new confirm_action($message)
|
||||
);
|
||||
|
||||
return implode(' ', $actions);
|
||||
}
|
||||
|
||||
}
|
||||
+6
-6
@@ -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"/>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<COMPATIBILITY_MATRIX>
|
||||
<PLUGIN name="availability_ipaddress">
|
||||
<PHP version="7.4" level="required"/>
|
||||
<PHP version="7.3" level="required"/>
|
||||
</PLUGIN>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
@@ -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>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>';
|
||||
</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';
|
||||
|
||||
+30
-10
@@ -44,11 +44,19 @@ if ($action === 'delete' && confirm_sesskey()) {
|
||||
$DB->delete_records('availability_ipaddress_pre', ['id' => $id]);
|
||||
|
||||
if ($removed > 0) {
|
||||
redirect($PAGE->url, get_string('range_deleted_and_removed', 'availability_ipaddress', $removed),
|
||||
null, \core\output\notification::NOTIFY_SUCCESS);
|
||||
redirect(
|
||||
$PAGE->url,
|
||||
get_string('range_deleted_and_removed', 'availability_ipaddress', $removed),
|
||||
null,
|
||||
\core\output\notification::NOTIFY_SUCCESS
|
||||
);
|
||||
} else {
|
||||
redirect($PAGE->url, get_string('range_deleted', 'availability_ipaddress'), null,
|
||||
\core\output\notification::NOTIFY_SUCCESS);
|
||||
redirect(
|
||||
$PAGE->url,
|
||||
get_string('range_deleted', 'availability_ipaddress'),
|
||||
null,
|
||||
\core\output\notification::NOTIFY_SUCCESS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +71,12 @@ if ($action === 'toggle' && confirm_sesskey()) {
|
||||
if (!$record->enabled) {
|
||||
$removed = \availability_ipaddress\helper::remove_range_from_restrictions($id);
|
||||
if ($removed > 0) {
|
||||
redirect($PAGE->url, get_string('range_disabled_and_removed', 'availability_ipaddress', $removed),
|
||||
null, \core\output\notification::NOTIFY_SUCCESS);
|
||||
redirect(
|
||||
$PAGE->url,
|
||||
get_string('range_disabled_and_removed', 'availability_ipaddress', $removed),
|
||||
null,
|
||||
\core\output\notification::NOTIFY_SUCCESS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,15 +97,23 @@ if ($action === 'add' || $action === 'edit') {
|
||||
// Update existing.
|
||||
$data->timemodified = time();
|
||||
$DB->update_record('availability_ipaddress_pre', $data);
|
||||
redirect($PAGE->url, get_string('range_updated', 'availability_ipaddress'), null,
|
||||
\core\output\notification::NOTIFY_SUCCESS);
|
||||
redirect(
|
||||
$PAGE->url,
|
||||
get_string('range_updated', 'availability_ipaddress'),
|
||||
null,
|
||||
\core\output\notification::NOTIFY_SUCCESS
|
||||
);
|
||||
} else {
|
||||
// Create new.
|
||||
$data->timecreated = time();
|
||||
$data->timemodified = time();
|
||||
$DB->insert_record('availability_ipaddress_pre', $data);
|
||||
redirect($PAGE->url, get_string('range_created', 'availability_ipaddress'), null,
|
||||
\core\output\notification::NOTIFY_SUCCESS);
|
||||
redirect(
|
||||
$PAGE->url,
|
||||
get_string('range_created', 'availability_ipaddress'),
|
||||
null,
|
||||
\core\output\notification::NOTIFY_SUCCESS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -29,7 +29,6 @@ defined('MOODLE_INTERNAL') || die();
|
||||
global $ADMIN;
|
||||
|
||||
if ($hassiteconfig) {
|
||||
|
||||
// Add external page for managing IP ranges.
|
||||
$ADMIN->add(
|
||||
'availabilitysettings',
|
||||
@@ -38,7 +37,8 @@ if ($hassiteconfig) {
|
||||
get_string('setting:manage_predefined_ranges', 'availability_ipaddress'),
|
||||
new moodle_url('/availability/condition/ipaddress/manage_ranges.php'),
|
||||
'moodle/site:config'
|
||||
));
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Set the visible name of auto generated settings page to empty string,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -27,8 +27,8 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'availability_ipaddress';
|
||||
$plugin->version = 2025080401;
|
||||
$plugin->release = '5.0.2';
|
||||
$plugin->version = 2026101500;
|
||||
$plugin->release = '5.3.2';
|
||||
$plugin->requires = 2016120500;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
$plugin->supported = [400, 500];
|
||||
$plugin->supported = [400, 503];
|
||||
+132
-51
@@ -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
@@ -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
|
||||
+131
-44
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
Vendored
+132
-51
@@ -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');
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user