diff --git a/README.md b/README.md index 2ae8f33..0c7784d 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,36 @@ ## Moodle - availability ip address plugin + +[![CI Status](https://github.com/LdesignMedia/moodle-availability_ipaddress/workflows/ci/badge.svg)](https://github.com/LdesignMedia/moodle-availability_ipaddress/actions/workflows/ci.yml) + Enhance activity security by restricting access based on IP address. This plugin allows you to control the availability of any chosen activity, making it accessible only to users from specified IP addresses. ## Author -![MFreak.nl](https://MFreak.nl/logo_small.png) +ldesignmedia * 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 -![Moodle400](https://img.shields.io/badge/moodle-4.0-brightgreen.svg?logo=moodle) -![Moodle401](https://img.shields.io/badge/moodle-4.1-brightgreen.svg?logo=moodle) -![Moodle402](https://img.shields.io/badge/moodle-4.2-brightgreen.svg?logo=moodle) -![Moodle403](https://img.shields.io/badge/moodle-4.3-brightgreen.svg?logo=moodle) -![Moodle404](https://img.shields.io/badge/moodle-4.4-brightgreen.svg?logo=moodle) -![Moodle405](https://img.shields.io/badge/moodle-4.5-brightgreen.svg?logo=moodle) -![Moodle500](https://img.shields.io/badge/moodle-5.0-brightgreen.svg?logo=moodle) +![Moodle400](https://img.shields.io/badge/moodle-4.0-F98012.svg?logo=moodle) +![Moodle401](https://img.shields.io/badge/moodle-4.1-F98012.svg?logo=moodle) +![Moodle402](https://img.shields.io/badge/moodle-4.2-F98012.svg?logo=moodle) +![Moodle403](https://img.shields.io/badge/moodle-4.3-F98012.svg?logo=moodle) +![Moodle404](https://img.shields.io/badge/moodle-4.4-F98012.svg?logo=moodle) +![Moodle405](https://img.shields.io/badge/moodle-4.5-F98012.svg?logo=moodle) +![Moodle500](https://img.shields.io/badge/moodle-5.0-F98012.svg?logo=moodle) -![PHP7.4](https://img.shields.io/badge/PHP-7.4-brightgreen.svg?logo=php) -![PHP8.0](https://img.shields.io/badge/PHP-8.0-brightgreen.svg?logo=php) -![PHP8.1](https://img.shields.io/badge/PHP-8.1-brightgreen.svg?logo=php) -![PHP8.2](https://img.shields.io/badge/PHP-8.2-brightgreen.svg?logo=php) +![PHP7.4](https://img.shields.io/badge/PHP-7.4-777BB4.svg?logo=php) +![PHP8.0](https://img.shields.io/badge/PHP-8.0-777BB4.svg?logo=php) +![PHP8.1](https://img.shields.io/badge/PHP-8.1-777BB4.svg?logo=php) +![PHP8.2](https://img.shields.io/badge/PHP-8.2-777BB4.svg?logo=php) ## List of features - Supports comma separate list of ip-addresses - Subnet support, eg 192.168.1.0/24 - Inline ip-address validation -- Turning on/off with eye icon, without lossing the input value. +- Turning on/off with eye icon, without losing the input value. ## Installation 1. Copy this plugin to the `availability\condition\ipaddress` folder on the server @@ -60,6 +64,8 @@ Contributions are welcome and will be fully credited. We accept contributions vi ## 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) diff --git a/classes/condition.php b/classes/condition.php index 855a48b..87f7104 100644 --- a/classes/condition.php +++ b/classes/condition.php @@ -38,25 +38,41 @@ use core_availability\info; class condition extends \core_availability\condition { /** + * Manual provided IP addresses. + * * @var string */ protected string $ipaddresses = ''; + /** + * Predefined IP address ranges. + * + * @var array + */ + protected array $predefinedranges = []; + /** * condition constructor. * * @param \stdClass $structure */ - public function __construct($structure) { + public function __construct(\stdClass $structure) { if (isset($structure->ipaddresses)) { $this->ipaddresses = $structure->ipaddresses; } + if (isset($structure->predefined_ranges)) { + $this->predefinedranges = $structure->predefined_ranges; + } } /** * Determines whether a particular item is currently available * according to this availability condition. * + * Note: Cannot add type declarations for $not, $grabthelot, and $userid parameters + * as the parent core_availability\condition::is_available() method doesn't have them, + * and PHP requires compatibility with parent method signatures when overriding. + * * If implementations require a course or modinfo, they should use * the get methods in $info. * @@ -78,14 +94,43 @@ class condition extends \core_availability\condition { * @return bool True if available */ public function is_available($not, info $info, $grabthelot, $userid): bool { + global $DB; - if (empty($this->ipaddresses)) { + // Collect all IP addresses to check. + $allipaddresses = []; + + // Add custom IP addresses. + if (!empty($this->ipaddresses)) { + $allipaddresses[] = trim($this->ipaddresses); + } + + // Add predefined ranges. + if (!empty($this->predefinedranges)) { + $ranges = $DB->get_records_list( + 'availability_ipaddress_pre', + 'id', + $this->predefinedranges, + '', + 'ipaddresses' + ); + foreach ($ranges as $range) { + if (!empty($range->ipaddresses)) { + $allipaddresses[] = trim($range->ipaddresses); + } + } + } + + // If no IP addresses are configured, the condition passes. + if (empty($allipaddresses)) { return !$not; } - // Check if ip-address matches. - if (address_in_subnet(getremoteaddr(), trim($this->ipaddresses))) { - return !$not; + // Check if user's IP matches any of the allowed addresses. + $userip = getremoteaddr(); + foreach ($allipaddresses as $iplist) { + if (address_in_subnet($userip, $iplist)) { + return !$not; + } } return $not; @@ -97,6 +142,9 @@ class condition extends \core_availability\condition { * students if the activity is not available to them, and for staff to see * what conditions are. * + * Note: Cannot add type declarations for $full and $not parameters as the parent + * core_availability\condition::get_description() method doesn't have them. + * * The $full parameter can be used to distinguish between 'staff' cases * (when displaying all information about the activity) and 'student' cases * (when displaying only conditions they don't meet). @@ -151,10 +199,16 @@ class condition extends \core_availability\condition { * @return \stdClass Structure object (ready to be made into JSON format) */ public function save(): \stdClass { - return (object) [ + $result = (object) [ 'type' => 'ipaddress', 'ipaddresses' => $this->ipaddresses, ]; + + if (!empty($this->predefinedranges)) { + $result->predefined_ranges = $this->predefinedranges; + } + + return $result; } } diff --git a/classes/form/range_form.php b/classes/form/range_form.php new file mode 100644 index 0000000..5d4ff2a --- /dev/null +++ b/classes/form/range_form.php @@ -0,0 +1,151 @@ +. + +/** + * Form for managing predefined IP ranges. + * + * @package availability_ipaddress + * @copyright 04/08/2025 LdesignMedia.nl - Luuk Verhoeven + * @author Vincent Cornelis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace availability_ipaddress\form; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/formslib.php'); + +/** + * Form for managing predefined IP ranges. + * + * @package availability_ipaddress + * @copyright 04/08/2025 LdesignMedia.nl - Luuk Verhoeven + * @author Vincent Cornelis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class range_form extends \moodleform { + + /** + * Define the form. + * + * @return void + */ + protected function definition(): void { + $mform = $this->_form; + $id = $this->_customdata['id'] ?? 0; + + // Hidden id field. + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + $mform->setDefault('id', $id); + + // Name field. + $mform->addElement('text', 'name', get_string('range_name', 'availability_ipaddress')); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addHelpButton('name', 'range_name', 'availability_ipaddress'); + + // IP addresses field. + $mform->addElement( + 'text', + 'ipaddresses', + get_string('ipaddresses', 'availability_ipaddress'), + ['size' => 100] + ); + $mform->setType('ipaddresses', PARAM_TEXT); + $mform->addRule('ipaddresses', null, 'required', null, 'client'); + $mform->addHelpButton('ipaddresses', 'ipaddresses_help', 'availability_ipaddress'); + + // Description field. + $mform->addElement('textarea', 'description', get_string('description'), + ['rows' => 3, 'cols' => 60]); + $mform->setType('description', PARAM_TEXT); + + // Enabled field. + $mform->addElement('advcheckbox', 'enabled', get_string('enabled', 'availability_ipaddress')); + $mform->setDefault('enabled', 1); + + // Action buttons. + $this->add_action_buttons(); + } + + /** + * Validate the form data. + * + * Note: Parameter type declarations cannot be added here as the parent + * moodleform::validation() method doesn't have them, and PHP requires + * compatibility with parent method signatures when overriding. + * + * @param array $data + * @param array $files + * + * @return array + */ + 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)) { + $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; + } + +} diff --git a/classes/frontend.php b/classes/frontend.php index 73bf21a..4d826b1 100644 --- a/classes/frontend.php +++ b/classes/frontend.php @@ -44,7 +44,42 @@ class frontend extends \core_availability\frontend { return [ 'js:ipaddress', 'error_ipaddress', + 'predefined_ranges', + 'custom_ipaddress', + 'use_predefined', ]; } + /** + * Get additional parameters for the JavaScript module. + * + * Note: Cannot add type declaration for $course parameter as the parent + * core_availability\frontend::get_javascript_init_params() method doesn't + * have it, and PHP requires compatibility with parent method signatures. + * + * @param \stdClass $course Course object + * @param \cm_info|null $cm Course module + * @param \section_info|null $section Section + * + * @return array + */ + protected function get_javascript_init_params($course, ?\cm_info $cm = null, ?\section_info $section = null): array { + global $DB; + + // Get enabled predefined IP ranges. + $ranges = $DB->get_records('availability_ipaddress_pre', ['enabled' => 1], 'name', 'id, name, ipaddresses'); + + // Format for JavaScript. + $rangedata = []; + foreach ($ranges as $range) { + $rangedata[] = [ + 'id' => $range->id, + 'name' => format_string($range->name), + 'ipaddresses' => $range->ipaddresses, + ]; + } + + return [$rangedata]; + } + } diff --git a/classes/helper.php b/classes/helper.php new file mode 100644 index 0000000..804dc33 --- /dev/null +++ b/classes/helper.php @@ -0,0 +1,365 @@ +. + +/** + * Helper functions for availability_ipaddress. + * + * @package availability_ipaddress + * @copyright 04/08/2025 LdesignMedia.nl - Luuk Verhoeven + * @author Vincent Cornelis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace availability_ipaddress; + +/** + * Helper class for availability_ipaddress. + * + * @package availability_ipaddress + * @copyright 04/08/2025 LdesignMedia.nl - Luuk Verhoeven + * @author Vincent Cornelis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + + /** + * Check if a predefined range is in use. + * + * @param int $rangeid The ID of the range to check. + * + * @return array Array with 'inuse' boolean and 'count' of uses. + */ + public static function is_range_in_use(int $rangeid): array { + $activities = []; + + // Check course modules. + $moduleactivities = self::check_range_in_modules($rangeid); + $activities = array_merge($activities, $moduleactivities); + + // Check sections. + $sectionactivities = self::check_range_in_sections($rangeid); + $activities = array_merge($activities, $sectionactivities); + + $count = count($activities); + + return [ + 'inuse' => ($count > 0), + 'count' => $count, + 'activities' => $activities, + ]; + } + + /** + * Check if a range is used in course modules. + * + * @param int $rangeid The ID of the range to check. + * + * @return array Array of activities using the range. + */ + private static function check_range_in_modules(int $rangeid): array { + global $DB; + + $activities = []; + + $sql = "SELECT cm.id, cm.course, cm.availability, cm.module, cm.instance, + c.fullname as coursename, m.name as modname + FROM {course_modules} cm + JOIN {course} c ON c.id = cm.course + JOIN {modules} m ON m.id = cm.module + WHERE cm.availability IS NOT NULL AND cm.availability != ''"; + + $modules = $DB->get_records_sql($sql); + + foreach ($modules as $module) { + $availability = json_decode($module->availability); + if (!$availability || !self::check_availability_tree($availability, $rangeid)) { + continue; + } + + $activityname = self::get_module_name($module); + $activities[] = [ + 'coursename' => $module->coursename, + 'cmid' => $module->id, + 'name' => $activityname, + ]; + } + + return $activities; + } + + /** + * Get the name of a module. + * + * @param \stdClass $module The module record. + * + * @return string The module name. + */ + private static function get_module_name(\stdClass $module): string { + global $DB; + + try { + if ($DB->get_manager()->table_exists($module->modname)) { + $activity = $DB->get_record($module->modname, ['id' => $module->instance], 'name', IGNORE_MISSING); + if ($activity && !empty($activity->name)) { + return $activity->name; + } + } + } catch (\Exception $e) { + // Table doesn't exist or other database error - fall through to default. + debugging('Error getting module name: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + + return get_string('modulename', $module->modname); + } + + /** + * Check if a range is used in course sections. + * + * @param int $rangeid The ID of the range to check. + * + * @return array Array of sections using the range. + */ + private static function check_range_in_sections(int $rangeid): array { + global $DB; + + $activities = []; + + $sql = "SELECT cs.id, cs.course, cs.availability, cs.name, cs.section, c.fullname as coursename + FROM {course_sections} cs + JOIN {course} c ON c.id = cs.course + WHERE cs.availability IS NOT NULL AND cs.availability != ''"; + + $sections = $DB->get_records_sql($sql); + + foreach ($sections as $section) { + $availability = json_decode($section->availability); + if (!$availability || !self::check_availability_tree($availability, $rangeid)) { + continue; + } + + $sectionname = $section->name ?: get_string('section') . ' ' . $section->section; + $activities[] = [ + 'coursename' => $section->coursename, + 'cmid' => 0, + 'name' => $sectionname, + ]; + } + + return $activities; + } + + /** + * Recursively check availability tree for range usage. + * + * @param \stdClass $availability The availability tree. + * @param int $rangeid The range ID to look for. + * + * @return bool True if range is found in tree. + */ + private static function check_availability_tree(\stdClass $availability, int $rangeid): bool { + + // Check if this is an IP address condition. + if (isset($availability->type) && $availability->type === 'ipaddress') { + if (isset($availability->predefined_ranges) && is_array($availability->predefined_ranges)) { + return in_array($rangeid, $availability->predefined_ranges); + } + } + + // Check nested conditions (for groups). + if (isset($availability->c) && is_array($availability->c)) { + foreach ($availability->c as $condition) { + if (self::check_availability_tree($condition, $rangeid)) { + return true; + } + } + } + + return false; + } + + /** + * Get usage details for a range as HTML. + * + * @param int $rangeid The range ID. + * + * @return string HTML string with usage details. + */ + public static function get_range_usage_html(int $rangeid): string { + $usage = self::is_range_in_use($rangeid); + + if (!$usage['inuse']) { + return ''; + } + + $html = \html_writer::tag('p', get_string('range_in_use_count', 'availability_ipaddress', $usage['count'])); + + if (!empty($usage['activities'])) { + $items = []; + foreach (array_slice($usage['activities'], 0, 5) as $activity) { + $items[] = $activity['coursename'] . ': ' . $activity['name']; + } + $html .= \html_writer::alist($items); + + if (count($usage['activities']) > 5) { + $more = count($usage['activities']) - 5; + $html .= \html_writer::tag('p', get_string('and_x_more', 'availability_ipaddress', $more)); + } + } + + return $html; + } + + /** + * Remove a predefined range from all availability restrictions. + * + * @param int $rangeid The ID of the range to remove. + * + * @return int Number of restrictions updated. + */ + public static function remove_range_from_restrictions(int $rangeid): int { + global $DB; + + $updatecount = 0; + + // Update course modules. + $sql = "SELECT cm.id, cm.availability + FROM {course_modules} cm + WHERE cm.availability IS NOT NULL AND cm.availability != ''"; + + $modules = $DB->get_records_sql($sql); + + foreach ($modules as $module) { + $availability = json_decode($module->availability); + if ($availability && self::remove_range_from_tree($availability, $rangeid)) { + $module->availability = json_encode($availability); + $DB->update_record('course_modules', $module); + $updatecount++; + + // Rebuild course cache. + $course = $DB->get_record('course', ['id' => $DB->get_field('course_modules', 'course', ['id' => $module->id])]); + if ($course) { + rebuild_course_cache($course->id, true); + } + } + } + + // Update course sections. + $sql = "SELECT cs.id, cs.course, cs.availability + FROM {course_sections} cs + WHERE cs.availability IS NOT NULL AND cs.availability != ''"; + + $sections = $DB->get_records_sql($sql); + + foreach ($sections as $section) { + $availability = json_decode($section->availability); + if ($availability && self::remove_range_from_tree($availability, $rangeid)) { + $section->availability = json_encode($availability); + $DB->update_record('course_sections', $section); + $updatecount++; + + // Rebuild course cache. + rebuild_course_cache($section->course, true); + } + } + + return $updatecount; + } + + /** + * Recursively remove range from availability tree. + * + * @param \stdClass $availability The availability tree. + * @param int $rangeid The range ID to remove. + * + * @return bool True if tree was modified. + */ + private static function remove_range_from_tree(\stdClass $availability, int $rangeid): bool { + $modified = false; + + // Process IP address conditions. + if (self::is_ipaddress_condition($availability)) { + $modified = self::remove_range_from_condition($availability, $rangeid); + } + + // Process nested conditions. + return self::process_nested_conditions($availability, $rangeid) || $modified; + + } + + /** + * Check if the availability item is an IP address condition. + * + * @param \stdClass $availability The availability item. + * + * @return bool True if it's an IP address condition. + */ + private static function is_ipaddress_condition(\stdClass $availability): bool { + return isset($availability->type) && $availability->type === 'ipaddress'; + } + + /** + * Remove range from an IP address condition. + * + * @param \stdClass $availability The availability condition. + * @param int $rangeid The range ID to remove. + * + * @return bool True if the condition was modified. + */ + private static function remove_range_from_condition(\stdClass $availability, int $rangeid): bool { + if (!isset($availability->predefined_ranges) || !is_array($availability->predefined_ranges)) { + return false; + } + + $key = array_search($rangeid, $availability->predefined_ranges); + if ($key === false) { + return false; + } + + // Remove the range from the array. + array_splice($availability->predefined_ranges, $key, 1); + + // If no ranges left, remove the predefined_ranges property. + if (empty($availability->predefined_ranges)) { + unset($availability->predefined_ranges); + } + + return true; + } + + /** + * Process nested conditions in availability tree. + * + * @param \stdClass $availability The availability item. + * @param int $rangeid The range ID to remove. + * + * @return bool True if any nested condition was modified. + */ + private static function process_nested_conditions(\stdClass $availability, int $rangeid): bool { + if (!isset($availability->c) || !is_array($availability->c)) { + return false; + } + + $modified = false; + foreach ($availability->c as $condition) { + if (self::remove_range_from_tree($condition, $rangeid)) { + $modified = true; + } + } + + return $modified; + } + +} diff --git a/classes/table/ipranges_table.php b/classes/table/ipranges_table.php new file mode 100644 index 0000000..b7f232e --- /dev/null +++ b/classes/table/ipranges_table.php @@ -0,0 +1,238 @@ +. + +/** + * Table class for displaying IP address ranges. + * + * @package availability_ipaddress + * @copyright 04/08/2025 LdesignMedia.nl - Luuk Verhoeven + * @author Vincent Cornelis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace availability_ipaddress\table; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/tablelib.php'); + +use table_sql; +use html_writer; +use moodle_url; +use pix_icon; +use confirm_action; + +/** + * Table class for IP address ranges. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package availability_ipaddress + * @copyright 04/08/2025 LdesignMedia.nl - Luuk Verhoeven + * @author Vincent Cornelis + */ +class ipranges_table extends table_sql { + + /** + * @var moodle_url The base URL for the page. + */ + public $baseurl; + + /** + * Constructor. + * + * @param string $uniqueid Unique ID for the table. + * @param moodle_url $baseurl The base URL for the page. + */ + public function __construct(string $uniqueid, moodle_url $baseurl) { + parent::__construct($uniqueid); + $this->baseurl = $baseurl; + + // Define columns and headers. + $columns = ['name', 'description', 'ipaddresses', 'enabled', 'actions']; + $headers = [ + get_string('name'), + get_string('description'), + get_string('ipaddresses', 'availability_ipaddress'), + get_string('enabled', 'availability_ipaddress'), + get_string('actions'), + ]; + + $this->define_columns($columns); + $this->define_headers($headers); + + // Set attributes. + $this->set_attribute('class', 'generaltable'); + $this->sortable(true, 'name', SORT_ASC); + $this->no_sorting('description'); + $this->no_sorting('actions'); + $this->collapsible(false); + $this->pageable(true); + $this->is_downloadable(false); + + $this->define_baseurl($baseurl); + } + + /** + * Set up the SQL query. + * + * @param int $pagesize Number of records per page. + * @param bool $useinitialsbar Whether to use the initials bar. + * + * @return void + */ + public function set_sql_data(int $pagesize = 30, bool $useinitialsbar = false): void { + $fields = 'id, name, description, ipaddresses, enabled, timecreated, timemodified'; + $from = '{availability_ipaddress_pre}'; + $where = '1=1'; + $params = []; + + $this->set_sql($fields, $from, $where, $params); + $this->set_count_sql("SELECT COUNT(*) FROM {availability_ipaddress_pre}"); + } + + /** + * Column for name. + * + * @param \stdClass $range The range record. + * + * @return string + */ + public function col_name(\stdClass $range): string { + return format_string($range->name); + } + + /** + * Column for description. + * + * @param \stdClass $range The range record. + * + * @return string + */ + public function col_description(\stdClass $range): string { + if (empty($range->description)) { + return '-'; + } + $description = format_string($range->description); + // Truncate if too long. + if (strlen($description) > 100) { + $truncated = substr($description, 0, 97) . '...'; + + return html_writer::tag('span', $truncated, ['title' => $description]); + } + + return $description; + } + + /** + * Column for IP addresses. + * + * @param \stdClass $range The range record. + * + * @return string + */ + public function col_ipaddresses(\stdClass $range): string { + $ips = s($range->ipaddresses); + // Truncate if too long and add tooltip. + if (strlen($ips) > 50) { + $truncated = substr($ips, 0, 47) . '...'; + + return html_writer::tag( + 'span', + html_writer::tag('code', $truncated), + ['title' => $ips] + ); + } + + return html_writer::tag('code', $ips); + } + + /** + * Column for enabled status. + * + * @param \stdClass $range The range record. + * + * @return string + */ + public function col_enabled(\stdClass $range): string { + return $range->enabled ? get_string('yes') : get_string('no'); + } + + /** + * Column for actions. + * + * @param \stdClass $range The range record. + * + * @return string + */ + public function col_actions(\stdClass $range): string { + global $OUTPUT; + + $actions = []; + + // 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'))); + + // Toggle action. + $toggleurl = new moodle_url($this->baseurl, ['action' => 'toggle', 'id' => $range->id, 'sesskey' => sesskey()]); + $toggleicon = $range->enabled ? 't/hide' : 't/show'; + $togglestring = $range->enabled ? get_string('disable') : get_string('enable'); + + // Check if range is in use and add confirmation if disabling. + if ($range->enabled) { + $usage = \availability_ipaddress\helper::is_range_in_use($range->id); + 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)); + } else { + $actions[] = $OUTPUT->action_icon($toggleurl, new pix_icon($toggleicon, $togglestring)); + } + } else { + // Enabling doesn't need confirmation. + $actions[] = $OUTPUT->action_icon($toggleurl, new pix_icon($toggleicon, $togglestring)); + } + + // Delete action. + $deleteurl = new moodle_url($this->baseurl, [ + 'action' => 'delete', + 'id' => $range->id, + 'sesskey' => sesskey(), + ]); + + // Check if range is in use and add usage info to confirmation. + $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']); + } else { + $message = get_string('confirm_delete_range', 'availability_ipaddress'); + } + + $actions[] = $OUTPUT->action_icon($deleteurl, + new pix_icon('t/delete', get_string('delete')), + new confirm_action($message)); + + return implode(' ', $actions); + } + +} diff --git a/db/install.xml b/db/install.xml new file mode 100644 index 0000000..ac6d0bb --- /dev/null +++ b/db/install.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/db/upgrade.php b/db/upgrade.php new file mode 100644 index 0000000..672014c --- /dev/null +++ b/db/upgrade.php @@ -0,0 +1,94 @@ +. + +/** + * Upgrade script for availability_ipaddress. + * + * @package availability_ipaddress + * @copyright 04/08/2025 LdesignMedia.nl - Luuk Verhoeven + * @author Vincent Cornelis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Upgrade function. + * + * @param int $oldversion The old version of the plugin + * + * @return bool + */ +function xmldb_availability_ipaddress_upgrade(int $oldversion): bool { + global $DB; + + $dbman = $DB->get_manager(); + + if ($oldversion < 2025070400) { + // Define table availability_ipaddress_pre to be created. + $table = new xmldb_table('availability_ipaddress_pre'); + + // Adding fields to table availability_ipaddress_pre. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('ipaddresses', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('enabled', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table availability_ipaddress_pre. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Adding indexes to table availability_ipaddress_pre. + $table->add_index('enabled_sortorder', XMLDB_INDEX_NOTUNIQUE, ['enabled', 'sortorder']); + + // Conditionally launch create table for availability_ipaddress_pre. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Availability_ipaddress savepoint reached. + upgrade_plugin_savepoint(true, 2025070400, 'availability', 'ipaddress'); + } + + if ($oldversion < 2025080401) { + // Remove sortorder field from availability_ipaddress_pre table. + $table = new xmldb_table('availability_ipaddress_pre'); + + // Drop the index that includes sortorder. + $index = new xmldb_index('enabled_sortorder', XMLDB_INDEX_NOTUNIQUE, ['enabled', 'sortorder']); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + + // Drop the sortorder field. + $field = new xmldb_field('sortorder'); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Add a new index for enabled only. + $index = new xmldb_index('enabled', XMLDB_INDEX_NOTUNIQUE, ['enabled']); + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Availability_ipaddress savepoint reached. + upgrade_plugin_savepoint(true, 2025080401, 'availability', 'ipaddress'); + } + + return true; +} diff --git a/lang/en/availability_ipaddress.php b/lang/en/availability_ipaddress.php index fea2e6e..6932a7a 100644 --- a/lang/en/availability_ipaddress.php +++ b/lang/en/availability_ipaddress.php @@ -24,6 +24,10 @@ * @author Luuk Verhoeven **/ +// We like comments and our own sorting. +// phpcs:disable moodle.Files.LangFilesOrdering.UnexpectedComment +// phpcs:disable moodle.Files.LangFilesOrdering.IncorrectOrder + $string['pluginname'] = 'IP address'; $string['description'] = 'Restrict access by ip-address or subnet'; $string['title'] = 'IP address'; @@ -37,3 +41,35 @@ $string['js:ipaddress'] = 'Require network address'; // Privacy provider. $string['privacy:metadata'] = 'The restriction by activity ipaddress plugin does not store any personal data.'; + +// Predefined ranges. +$string['setting:manage_predefined_ranges'] = 'IP address - Manage predefined IP ranges'; +$string['manage_predefined_ranges'] = 'Manage predefined IP ranges'; +$string['predefined_ranges'] = 'Predefined IP ranges'; +$string['custom_ipaddress'] = 'Custom IP address(es)'; +$string['use_predefined'] = 'Use predefined IP addresses'; +$string['range_name'] = 'Range name'; +$string['range_name_help'] = 'A descriptive name for this IP range, e.g., "Campus Network" or "Library Computers"'; +$string['ipaddresses'] = 'IP addresses'; +$string['ipaddresses_help'] = 'Enter IP addresses separated by commas. Supports single IPs (192.168.1.1), ranges (192.168.1.1-255), and subnets (192.168.1.0/24)'; +$string['ipaddresses_help_help'] = '

Enter one or more IP addresses or ranges, separated by commas.

+

Examples:

+'; +$string['enabled'] = 'Enabled'; +$string['sortorder'] = 'Sort order'; +$string['existing_ranges'] = 'Existing IP ranges'; +$string['range_created'] = 'IP range created successfully'; +$string['range_updated'] = 'IP range updated successfully'; +$string['range_deleted'] = 'IP range deleted successfully'; +$string['confirm_delete_range'] = 'Deleting this IP range will remove it from all restrictions where it is used. Are you sure you want to permanently delete it?'; +$string['range_in_use_count'] = 'This IP range is currently used in {$a} restriction(s).'; +$string['and_x_more'] = '... and {$a} more.'; +$string['confirm_disable_range'] = 'This IP range is currently in use. Disabling it will remove it from all restrictions where it is used. Are you sure you want to continue?'; +$string['range_in_use_title'] = 'IP Range In Use'; +$string['range_disabled_and_removed'] = 'IP range disabled and removed from {$a} restriction(s).'; +$string['range_deleted_and_removed'] = 'IP range deleted and removed from {$a} restriction(s).'; diff --git a/manage_ranges.php b/manage_ranges.php new file mode 100644 index 0000000..75f5649 --- /dev/null +++ b/manage_ranges.php @@ -0,0 +1,123 @@ +. + +/** + * Manage predefined IP address ranges. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @package availability_ipaddress + * @copyright 04/08/2025 LdesignMedia.nl - Luuk Verhoeven + * @author Vincent Cornelis + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); + +admin_externalpage_setup('availability_ipaddress_ranges'); + +$action = optional_param('action', '', PARAM_ALPHA); +$id = optional_param('id', 0, PARAM_INT); + +$PAGE->set_url('/availability/condition/ipaddress/manage_ranges.php'); +$PAGE->set_title(get_string('manage_predefined_ranges', 'availability_ipaddress')); +$PAGE->set_heading(get_string('manage_predefined_ranges', 'availability_ipaddress')); + +// Handle actions. +if ($action === 'delete' && confirm_sesskey()) { + // Remove from all restrictions before deleting. + $removed = \availability_ipaddress\helper::remove_range_from_restrictions($id); + + $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); + } else { + redirect($PAGE->url, get_string('range_deleted', 'availability_ipaddress'), null, + \core\output\notification::NOTIFY_SUCCESS); + } +} + +if ($action === 'toggle' && confirm_sesskey()) { + $record = $DB->get_record('availability_ipaddress_pre', ['id' => $id], '*', MUST_EXIST); + + $record->enabled = !$record->enabled; + $record->timemodified = time(); + $DB->update_record('availability_ipaddress_pre', $record); + + // If we just disabled the range, remove it from all restrictions. + 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); +} + +// Handle form submission for adding/editing. +if ($action === 'add' || $action === 'edit') { + $formurl = new moodle_url($PAGE->url, ['action' => $action, 'id' => $id]); + $form = new \availability_ipaddress\form\range_form($formurl, ['id' => $id]); + + if ($form->is_cancelled()) { + redirect($PAGE->url); + } + + if ($data = $form->get_data()) { + if ($data->id) { + // 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); + } 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); + } + } + + // Load data for editing. + if ($action === 'edit' && $id) { + $record = $DB->get_record('availability_ipaddress_pre', ['id' => $id], '*', MUST_EXIST); + $form->set_data($record); + } + + echo $OUTPUT->header(); + $form->display(); + echo $OUTPUT->footer(); + exit; +} + +// Display page. +echo $OUTPUT->header(); + +// Add new button. +echo $OUTPUT->single_button(new moodle_url($PAGE->url, ['action' => 'add']), get_string('add'), 'get'); + +// Create and display table. +$table = new \availability_ipaddress\table\ipranges_table('availability-ipaddress-ranges', $PAGE->url); +$table->set_sql_data(30); +$table->out(30, true); + +echo $OUTPUT->footer(); diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..88a4cc0 --- /dev/null +++ b/settings.php @@ -0,0 +1,46 @@ +. + +/** + * Settings for availability_ipaddress. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @package availability_ipaddress + * @copyright 04/08/2025 LdesignMedia.nl - Luuk Verhoeven + * @author Vincent Cornelis + */ + +defined('MOODLE_INTERNAL') || die(); + +global $ADMIN; + +if ($hassiteconfig) { + + // Add external page for managing IP ranges. + $ADMIN->add( + 'availabilitysettings', + new admin_externalpage( + 'availability_ipaddress_ranges', + 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, +// to avoid showing it in the settings tree, as we only add the external page. +$settings->visiblename = ''; diff --git a/version.php b/version.php index 426dec3..d764e5d 100644 --- a/version.php +++ b/version.php @@ -27,8 +27,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'availability_ipaddress'; -$plugin->version = 2025052100; -$plugin->release = '5.0.0'; +$plugin->version = 2025080401; +$plugin->release = '5.0.2'; $plugin->requires = 2016120500; $plugin->maturity = MATURITY_STABLE; $plugin->supported = [400, 500]; diff --git a/yui/build/moodle-availability_ipaddress-form/moodle-availability_ipaddress-form-debug.js b/yui/build/moodle-availability_ipaddress-form/moodle-availability_ipaddress-form-debug.js index 85e00e6..ef6cb18 100644 --- a/yui/build/moodle-availability_ipaddress-form/moodle-availability_ipaddress-form-debug.js +++ b/yui/build/moodle-availability_ipaddress-form/moodle-availability_ipaddress-form-debug.js @@ -43,9 +43,11 @@ M.availability_ipaddress.form = Y.Object(M.core_availability.plugin); * @method initInner * @param {Array} param Array of objects */ -M.availability_ipaddress.form.initInner = function() { +M.availability_ipaddress.form.initInner = function(param) { "use strict"; Y.log('M.availability_ipaddress 1.10'); + // Store predefined ranges from backend. + this.predefinedRanges = param || []; }; /** @@ -80,24 +82,68 @@ M.availability_ipaddress.form.getValue = function(field, node) { */ M.availability_ipaddress.form.getNode = function(json) { "use strict"; - var html, node, root, id; + var html, node, root, id, selectId, i, range; // Make sure we work with unique id. id = 'ipaddresses' + M.availability_ipaddress.form.instId; + selectId = 'predefined' + M.availability_ipaddress.form.instId; M.availability_ipaddress.form.instId += 1; // Create HTML structure. - html = ''; - html += ''; - html += ''; - node = Y.Node.create('' + html + ''); + html = '
'; + + // Add predefined ranges if available. + if (this.predefinedRanges && this.predefinedRanges.length > 0) { + html += '
'; + html += ''; + html += ''; + html += '
'; + + html += '
'; + html += ''; + } else { + html += '
'; + html += ''; + } + + html += ''; + html += '
'; + html += '
'; + + node = Y.Node.create('
' + html + '
'); // Set initial values, if specified. if (json.ipaddresses !== undefined) { node.one('input[name=ipaddresses]').set('value', json.ipaddresses); } + // Set selected predefined ranges if specified. + if (json.predefined_ranges !== undefined && this.predefinedRanges && this.predefinedRanges.length > 0) { + var select = node.one('select[name=predefined_ranges]'); + if (select) { + json.predefined_ranges.forEach(function(rangeId) { + var option = select.one('option[value="' + rangeId + '"]'); + if (option) { + option.set('selected', true); + } + }); + } + } + // Add event handlers (first time only). if (!M.availability_ipaddress.form.addedEvents) { M.availability_ipaddress.form.addedEvents = true; @@ -106,6 +152,11 @@ M.availability_ipaddress.form.getNode = function(json) { // Trigger the updating of the hidden availability data whenever the ipaddress field changes. M.core_availability.form.update(); }, '.availability_ipaddress input[name=ipaddresses]'); + + root.delegate('change', function() { + // Trigger the updating when predefined ranges are selected. + M.core_availability.form.update(); + }, '.availability_ipaddress select[name=predefined_ranges]'); } return node; @@ -120,6 +171,10 @@ M.availability_ipaddress.form.getNode = function(json) { 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() === '') { + return true; + } ipaddresses = ipaddresses.split(','); for (var i in ipaddresses) { @@ -134,7 +189,7 @@ M.availability_ipaddress.validateIpaddress = function(ipaddresses) { 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}-' + - '([0-1]?[0-9]?[0-9]?|2[0-4][0-9]|25[0-5]){1}$', + '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$', 'gm' ); @@ -178,6 +233,20 @@ M.availability_ipaddress.form.fillValue = function(value, node) { // with the structure used in the __construct and save functions // within condition.php. value.ipaddresses = this.getValue('ipaddresses', node); + + // Get selected predefined ranges. + var select = node.one('select[name=predefined_ranges]'); + if (select) { + var selectedRanges = []; + select.get('options').each(function(option) { + if (option.get('selected')) { + selectedRanges.push(parseInt(option.get('value'))); + } + }); + if (selectedRanges.length > 0) { + value.predefined_ranges = selectedRanges; + } + } }; /** @@ -190,8 +259,9 @@ M.availability_ipaddress.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); - // Basic ipaddresses checks. - if (M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) { + // Basic ipaddresses checks - only validate if not empty. + if (value.ipaddresses && value.ipaddresses.trim() !== '' && + M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) { errors.push('availability_ipaddress:error_ipaddress'); } }; diff --git a/yui/build/moodle-availability_ipaddress-form/moodle-availability_ipaddress-form-min.js b/yui/build/moodle-availability_ipaddress-form/moodle-availability_ipaddress-form-min.js index 12f5479..8aedf57 100644 --- a/yui/build/moodle-availability_ipaddress-form/moodle-availability_ipaddress-form-min.js +++ b/yui/build/moodle-availability_ipaddress-form/moodle-availability_ipaddress-form-min.js @@ -1 +1 @@ -YUI.add("moodle-availability_ipaddress-form",function(e,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=e.Object(M.core_availability.plugin),M.availability_ipaddress.form.initInner=function(){},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,i="ipaddresses"+M.availability_ipaddress.form.instId;return M.availability_ipaddress.form.instId+=1,a="",a+='",i=e.Node.create(''+(a+='')+""),d.ipaddresses!==undefined&&i.one("input[name=ipaddresses]").set("value",d.ipaddresses),M.availability_ipaddress.form.addedEvents||(M.availability_ipaddress.form.addedEvents=!0,e.one(".availability-field").delegate("valuechange",function(){M.core_availability.form.update()},".availability_ipaddress input[name=ipaddresses]")),i},M.availability_ipaddress.validateIpaddress=function(d){"use strict";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}-([0-1]?[0-9]?[0-9]?|2[0-4][0-9]|25[0-5]){1}$","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){d.ipaddresses=this.getValue("ipaddresses",a)},M.availability_ipaddress.form.fillErrors=function(d,a){"use strict";var i={};this.fillValue(i,a),!1===M.availability_ipaddress.validateIpaddress(i.ipaddresses)&&d.push("availability_ipaddress:error_ipaddress")}},"@VERSION@",{requires:["base","node","event","moodle-core_availability-form"]}); \ No newline at end of file +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='
',this.predefinedRanges&&0')+('")+('")+"
"+'
')+('")}else a=(a+='
')+'";return l=r.Node.create("
"+(a=(a+='')+"
"+"
")+"
"),d.ipaddresses!==undefined&&l.one("input[name=ipaddresses]").set("value",d.ipaddresses),d.predefined_ranges!==undefined&&this.predefinedRanges&&0' + - M.util.get_string('title', 'availability_ipaddress') + ' '; - html += ''; - node = Y.Node.create('' + html + ''); + html = '
'; + + // Add predefined ranges if available. + if (this.predefinedRanges && this.predefinedRanges.length > 0) { + html += '
'; + html += ''; + html += ''; + html += '
'; + + html += '
'; + html += ''; + } else { + html += '
'; + html += ''; + } + + html += ''; + html += '
'; + html += '
'; + + node = Y.Node.create('
' + html + '
'); // Set initial values, if specified. if (json.ipaddresses !== undefined) { node.one('input[name=ipaddresses]').set('value', json.ipaddresses); } + // Set selected predefined ranges if specified. + if (json.predefined_ranges !== undefined && this.predefinedRanges && this.predefinedRanges.length > 0) { + var select = node.one('select[name=predefined_ranges]'); + if (select) { + json.predefined_ranges.forEach(function(rangeId) { + var option = select.one('option[value="' + rangeId + '"]'); + if (option) { + option.set('selected', true); + } + }); + } + } + // Add event handlers (first time only). if (!M.availability_ipaddress.form.addedEvents) { M.availability_ipaddress.form.addedEvents = true; @@ -103,6 +149,11 @@ M.availability_ipaddress.form.getNode = function(json) { // Trigger the updating of the hidden availability data whenever the ipaddress field changes. M.core_availability.form.update(); }, '.availability_ipaddress input[name=ipaddresses]'); + + root.delegate('change', function() { + // Trigger the updating when predefined ranges are selected. + M.core_availability.form.update(); + }, '.availability_ipaddress select[name=predefined_ranges]'); } return node; @@ -116,6 +167,10 @@ M.availability_ipaddress.form.getNode = function(json) { */ 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() === '') { + return true; + } ipaddresses = ipaddresses.split(','); for (var i in ipaddresses) { @@ -129,7 +184,7 @@ M.availability_ipaddress.validateIpaddress = function(ipaddresses) { 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}-' + - '([0-1]?[0-9]?[0-9]?|2[0-4][0-9]|25[0-5]){1}$', + '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$', 'gm' ); @@ -168,6 +223,20 @@ M.availability_ipaddress.form.fillValue = function(value, node) { // with the structure used in the __construct and save functions // within condition.php. value.ipaddresses = this.getValue('ipaddresses', node); + + // Get selected predefined ranges. + var select = node.one('select[name=predefined_ranges]'); + if (select) { + var selectedRanges = []; + select.get('options').each(function(option) { + if (option.get('selected')) { + selectedRanges.push(parseInt(option.get('value'))); + } + }); + if (selectedRanges.length > 0) { + value.predefined_ranges = selectedRanges; + } + } }; /** @@ -180,8 +249,9 @@ M.availability_ipaddress.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); - // Basic ipaddresses checks. - if (M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) { + // Basic ipaddresses checks - only validate if not empty. + if (value.ipaddresses && value.ipaddresses.trim() !== '' && + M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) { errors.push('availability_ipaddress:error_ipaddress'); } }; diff --git a/yui/src/form/js/form.js b/yui/src/form/js/form.js index 7032a25..7620bfd 100644 --- a/yui/src/form/js/form.js +++ b/yui/src/form/js/form.js @@ -41,9 +41,11 @@ M.availability_ipaddress.form = Y.Object(M.core_availability.plugin); * @method initInner * @param {Array} param Array of objects */ -M.availability_ipaddress.form.initInner = function() { +M.availability_ipaddress.form.initInner = function(param) { "use strict"; Y.log('M.availability_ipaddress 1.10'); + // Store predefined ranges from backend. + this.predefinedRanges = param || []; }; /** @@ -78,24 +80,68 @@ M.availability_ipaddress.form.getValue = function(field, node) { */ M.availability_ipaddress.form.getNode = function(json) { "use strict"; - var html, node, root, id; + var html, node, root, id, selectId, i, range; // Make sure we work with unique id. id = 'ipaddresses' + M.availability_ipaddress.form.instId; + selectId = 'predefined' + M.availability_ipaddress.form.instId; M.availability_ipaddress.form.instId += 1; // Create HTML structure. - html = ''; - html += ''; - html += ''; - node = Y.Node.create('' + html + ''); + html = '
'; + + // Add predefined ranges if available. + if (this.predefinedRanges && this.predefinedRanges.length > 0) { + html += '
'; + html += ''; + html += ''; + html += '
'; + + html += '
'; + html += ''; + } else { + html += '
'; + html += ''; + } + + html += ''; + html += '
'; + html += '
'; + + node = Y.Node.create('
' + html + '
'); // Set initial values, if specified. if (json.ipaddresses !== undefined) { node.one('input[name=ipaddresses]').set('value', json.ipaddresses); } + // Set selected predefined ranges if specified. + if (json.predefined_ranges !== undefined && this.predefinedRanges && this.predefinedRanges.length > 0) { + var select = node.one('select[name=predefined_ranges]'); + if (select) { + json.predefined_ranges.forEach(function(rangeId) { + var option = select.one('option[value="' + rangeId + '"]'); + if (option) { + option.set('selected', true); + } + }); + } + } + // Add event handlers (first time only). if (!M.availability_ipaddress.form.addedEvents) { M.availability_ipaddress.form.addedEvents = true; @@ -104,6 +150,11 @@ M.availability_ipaddress.form.getNode = function(json) { // Trigger the updating of the hidden availability data whenever the ipaddress field changes. M.core_availability.form.update(); }, '.availability_ipaddress input[name=ipaddresses]'); + + root.delegate('change', function() { + // Trigger the updating when predefined ranges are selected. + M.core_availability.form.update(); + }, '.availability_ipaddress select[name=predefined_ranges]'); } return node; @@ -118,6 +169,10 @@ M.availability_ipaddress.form.getNode = function(json) { 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() === '') { + return true; + } ipaddresses = ipaddresses.split(','); for (var i in ipaddresses) { @@ -132,7 +187,7 @@ M.availability_ipaddress.validateIpaddress = function(ipaddresses) { 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}-' + - '([0-1]?[0-9]?[0-9]?|2[0-4][0-9]|25[0-5]){1}$', + '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$', 'gm' ); @@ -176,6 +231,20 @@ M.availability_ipaddress.form.fillValue = function(value, node) { // with the structure used in the __construct and save functions // within condition.php. value.ipaddresses = this.getValue('ipaddresses', node); + + // Get selected predefined ranges. + var select = node.one('select[name=predefined_ranges]'); + if (select) { + var selectedRanges = []; + select.get('options').each(function(option) { + if (option.get('selected')) { + selectedRanges.push(parseInt(option.get('value'))); + } + }); + if (selectedRanges.length > 0) { + value.predefined_ranges = selectedRanges; + } + } }; /** @@ -188,8 +257,9 @@ M.availability_ipaddress.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); - // Basic ipaddresses checks. - if (M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) { + // Basic ipaddresses checks - only validate if not empty. + if (value.ipaddresses && value.ipaddresses.trim() !== '' && + M.availability_ipaddress.validateIpaddress(value.ipaddresses) === false) { errors.push('availability_ipaddress:error_ipaddress'); } };