MDL-56334 libraries: Add new internet protocol library to core
New library providing utilities for IP addresses, DNS and other internet-protocol-related information.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Contains a simple class providing some useful internet protocol-related functions.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2016 Jake Dallimore
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @author Jake Dallimore <[email protected]>
|
||||
*/
|
||||
|
||||
namespace core;
|
||||
|
||||
defined('MOODLE_INTERNAL') || exit();
|
||||
|
||||
/**
|
||||
* Static helper class providing some useful internet-protocol-related functions.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2016 Jake Dallimore
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @author Jake Dallimore <[email protected]>
|
||||
*/
|
||||
final class ip_utils {
|
||||
/**
|
||||
* Syntax checking for domain names, including fully qualified domain names.
|
||||
*
|
||||
* This function does not verify the existence of the domain name. It only verifies syntactic correctness.
|
||||
* This is based on RFC1034/1035 and does not provide support for validation of internationalised domain names (IDNs).
|
||||
* All IDNs must be prior-converted to their ascii-compatible encoding before being passed to this function.
|
||||
*
|
||||
* @param string $domainname the input string to check.
|
||||
* @return bool true if the string has valid syntax, false otherwise.
|
||||
*/
|
||||
public static function is_domain_name($domainname) {
|
||||
if (!is_string($domainname)) {
|
||||
return false;
|
||||
}
|
||||
// Usually the trailing dot (null label) is omitted, but is valid if supplied. We'll just remove it and validate as normal.
|
||||
$domainname = rtrim($domainname, '.');
|
||||
|
||||
// The entire name cannot exceed 253 ascii characters (255 octets, less the leading label-length byte and null label byte).
|
||||
if (strlen($domainname) > 253) {
|
||||
return false;
|
||||
}
|
||||
// Tertiary domain labels can have 63 octets max, and must not have begin or end with a hyphen.
|
||||
// The TLD label cannot begin with a number, but otherwise, is only loosely restricted here (TLD list is not checked).
|
||||
$domaintertiary = '([a-zA-Z0-9](([a-zA-Z0-9-]{0,61})[a-zA-Z0-9])?\.)*';
|
||||
$domaintoplevel = '([a-zA-Z](([a-zA-Z0-9-]*)[a-zA-Z0-9])?)';
|
||||
$address = '(' . $domaintertiary . $domaintoplevel . ')';
|
||||
$regexp = '#^' . $address . '$#i'; // Case insensitive matching.
|
||||
return preg_match($regexp, $domainname, $match) == true; // False for error, 0 for no match - we treat the same.
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the input string is a valid wildcard domain matching pattern.
|
||||
*
|
||||
* A domain matching pattern is essentially a domain name with a single, leading wildcard (*) label, and at least one other
|
||||
* label. The wildcard label is considered to match at least one label at or above (to the left of) its position in the string,
|
||||
* but will not match the trailing domain (everything to its right).
|
||||
*
|
||||
* The string must be dot-separated, and the whole pattern must follow the domain name syntax rules defined in RFC1034/1035.
|
||||
* Namely, the character type (ascii), total-length (253) and label-length (63) restrictions. This function only confirms
|
||||
* syntactic correctness. It does not check for the existence of the domain/subdomains.
|
||||
*
|
||||
* For example, the string '*.example.com' is a pattern deemed to match any direct subdomain of
|
||||
* example.com (such as test.example.com), any higher level subdomains (e.g. another.test.example.com) but will not match
|
||||
* the 'example.com' domain itself.
|
||||
*
|
||||
* @param string $pattern the string to check.
|
||||
* @return bool true if the input string is a valid domain wildcard matching pattern, false otherwise.
|
||||
*/
|
||||
public static function is_domain_matching_pattern($pattern) {
|
||||
if (!is_string($pattern)) {
|
||||
return false;
|
||||
}
|
||||
// Usually the trailing dot (null label) is omitted, but is valid if supplied. We'll just remove it and validate as normal.
|
||||
$pattern = rtrim($pattern, '.');
|
||||
|
||||
// The entire name cannot exceed 253 ascii characters (255 octets, less the leading label-length byte and null label byte).
|
||||
if (strlen($pattern) > 253) {
|
||||
return false;
|
||||
}
|
||||
// A valid pattern must left-positioned wildcard symbol (*).
|
||||
// Tertiary domain labels can have 63 octets max, and must not have begin or end with a hyphen.
|
||||
// The TLD label cannot begin with a number, but otherwise, is only loosely restricted here (TLD list is not checked).
|
||||
$wildcard = '((\*)\.){1}';
|
||||
$domaintertiary = '([a-zA-Z0-9](([a-zA-Z0-9-]{0,61})[a-zA-Z0-9])?\.)*';
|
||||
$domaintoplevel = '([a-zA-Z](([a-zA-Z0-9-]*)[a-zA-Z0-9])?)';
|
||||
$address = '(' . $wildcard . $domaintertiary . $domaintoplevel . ')';
|
||||
$regexp = '#^' . $address . '$#i'; // Case insensitive matching.
|
||||
return preg_match($regexp, $pattern, $match) == true; // False for error, 0 for no match - we treat the same.
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntax validation for IP addresses, supporting both IPv4 and Ipv6 formats.
|
||||
*
|
||||
* @param string $address the address to check.
|
||||
* @return bool true if the address is a valid IPv4 of IPv6 address, false otherwise.
|
||||
*/
|
||||
public static function is_ip_address($address) {
|
||||
return filter_var($address, FILTER_VALIDATE_IP) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntax validation for IPv4 addresses.
|
||||
*
|
||||
* @param string $address the address to check.
|
||||
* @return bool true if the address is a valid IPv4 address, false otherwise.
|
||||
*/
|
||||
public static function is_ipv4_address($address) {
|
||||
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntax checking for IPv4 address ranges.
|
||||
* Supports CIDR notation and last-group ranges.
|
||||
* Eg. 127.0.0.0/24 or 127.0.0.80-255
|
||||
*
|
||||
* @param string $addressrange the address range to check.
|
||||
* @return bool true if the string is a valid range representation, false otherwise.
|
||||
*/
|
||||
public static function is_ipv4_range($addressrange) {
|
||||
// Check CIDR notation.
|
||||
if (preg_match('#^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$#', $addressrange, $match)) {
|
||||
$address = "{$match[1]}.{$match[2]}.{$match[3]}.{$match[4]}";
|
||||
return self::is_ipv4_address($address) && $match[5] <= 32;
|
||||
}
|
||||
// Check last-group notation.
|
||||
if (preg_match('#^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})-(\d{1,3})$#', $addressrange, $match)) {
|
||||
$address = "{$match[1]}.{$match[2]}.{$match[3]}.{$match[4]}";
|
||||
return self::is_ipv4_address($address) && $match[5] <= 255 && $match[5] >= $match[4];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntax validation for IPv6 addresses.
|
||||
* This function does not check whether the address is assigned, only its syntactical correctness.
|
||||
*
|
||||
* @param string $address the address to check.
|
||||
* @return bool true if the address is a valid IPv6 address, false otherwise.
|
||||
*/
|
||||
public static function is_ipv6_address($address) {
|
||||
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntax validation for IPv6 address ranges.
|
||||
* Supports CIDR notation and last-group ranges.
|
||||
* Eg. fe80::d98c/64 or fe80::d98c-ffee
|
||||
*
|
||||
* @param string $addressrange the IPv6 address range to check.
|
||||
* @return bool true if the string is a valid range representation, false otherwise.
|
||||
*/
|
||||
public static function is_ipv6_range($addressrange) {
|
||||
// Check CIDR notation.
|
||||
$ipv6parts = explode('/', $addressrange);
|
||||
if (count($ipv6parts) == 2) {
|
||||
$range = (int)$ipv6parts[1];
|
||||
return self::is_ipv6_address($ipv6parts[0]) && (string)$range === $ipv6parts[1] && $range >= 0 && $range <= 128;
|
||||
}
|
||||
// Check last-group notation.
|
||||
$ipv6parts = explode('-', $addressrange);
|
||||
if (count($ipv6parts) == 2) {
|
||||
$addressparts = explode(':', $ipv6parts[0]);
|
||||
$rangestart = $addressparts[count($addressparts) - 1];
|
||||
$rangeend = $ipv6parts[1];
|
||||
return self::is_ipv6_address($ipv6parts[0]) && ctype_xdigit($rangestart) && ctype_xdigit($rangeend)
|
||||
&& strlen($rangeend) <= 4 && strlen($rangestart) <= 4 && hexdec($rangeend) >= hexdec($rangestart);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Contains the test class testing the \core\ip_utils static helper class functions.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2016 Jake Dallimore <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* This tests the static helper functions contained in the class '\core\ip_utils'.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2016 Jake Dallimore <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class core_ip_utils_testcase extends basic_testcase {
|
||||
/**
|
||||
* Test for \core\ip_utils::is_domain_name().
|
||||
*
|
||||
* @param string $domainname the domain name to validate.
|
||||
* @param bool $expected the expected result.
|
||||
* @dataProvider domain_name_data_provider
|
||||
*/
|
||||
public function test_is_domain_name($domainname, $expected) {
|
||||
$this->assertEquals($expected, \core\ip_utils::is_domain_name($domainname));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_is_domain_name().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function domain_name_data_provider() {
|
||||
return [
|
||||
["com", true],
|
||||
["i.net", true], // Single char, alpha tertiary domain.
|
||||
["0.org", true], // Single char, non-alpha tertiary domain.
|
||||
["0.a", true], // Single char, alpha top level domain.
|
||||
["0.1", false], // Single char, non-alpha top level domain.
|
||||
["example.com", true],
|
||||
["sub.example.com", true],
|
||||
["sub-domain.example-domain.net", true],
|
||||
["123.com", true],
|
||||
["123.a11", true],
|
||||
[str_repeat('sub.', 60) . "1-example.com", true], // Max length without null label is 253 octets = 253 ascii chars.
|
||||
[str_repeat('example', 9) . ".com", true], // Max number of octets per label is 63 = 63 ascii chars.
|
||||
["localhost", true],
|
||||
[" example.com", false],
|
||||
["example.com ", false],
|
||||
["example.com/", false],
|
||||
["*.example.com", false],
|
||||
["*example.com", false],
|
||||
["example.123", false],
|
||||
["-example.com", false],
|
||||
["example-.com", false],
|
||||
[".example.com", false],
|
||||
["127.0.0.1", false],
|
||||
[str_repeat('sub.', 60) . "11-example.com", false], // Name length is 254 chars, which exceeds the max allowed.
|
||||
[str_repeat('example', 9) . "1.com", false], // Label length is 64 chars, which exceed the max allowed.
|
||||
["example.com.", true], // Null label explicitly provided - this is valid.
|
||||
[".example.com.", false],
|
||||
["見.香港", false], // IDNs are invalid.
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \core\ip_utils::is_domain_matching_pattern().
|
||||
*
|
||||
* @param string $str the string to evaluate.
|
||||
* @param bool $expected the expected result.
|
||||
* @dataProvider domain_matching_patterns_data_provider
|
||||
*/
|
||||
public function test_is_domain_matching_pattern($str, $expected) {
|
||||
$this->assertEquals($expected, \core\ip_utils::is_domain_matching_pattern($str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_is_domain_matching_pattern().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function domain_matching_patterns_data_provider() {
|
||||
return [
|
||||
["*.com", true],
|
||||
["*.example.com", true],
|
||||
["*.example.com", true],
|
||||
["*.sub.example.com", true],
|
||||
["*.sub-domain.example-domain.com", true],
|
||||
["*." . str_repeat('sub.', 60) . "example.com", true], // Max number of domain name chars = 253.
|
||||
["*." . str_repeat('example', 9) . ".com", true], // Max number of domain name label chars = 63.
|
||||
["*com", false],
|
||||
["*example.com", false],
|
||||
[" *.example.com", false],
|
||||
["*.example.com ", false],
|
||||
["*-example.com", false],
|
||||
["*.-example.com", false],
|
||||
["*.example.com/", false],
|
||||
["sub.*.example.com", false],
|
||||
["sub.*example.com", false],
|
||||
["*.*.example.com", false],
|
||||
["example.com", false],
|
||||
["*." . str_repeat('sub.', 60) . "1example.com", false], // Name length is 254 chars, which exceeds the max allowed.
|
||||
["*." . str_repeat('example', 9) . "1.com", false], // Label length is 64 chars, which exceed the max allowed.
|
||||
["*.example.com.", true], // Null label explicitly provided - this is valid.
|
||||
[".*.example.com.", false],
|
||||
["*.香港", false], // IDNs are invalid.
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \core\ip_utils::is_ip_address().
|
||||
*
|
||||
* @param string $address the address to validate.
|
||||
* @param bool $expected the expected result.
|
||||
* @dataProvider ip_address_data_provider
|
||||
*/
|
||||
public function test_is_ip_address($address, $expected) {
|
||||
$this->assertEquals($expected, \core\ip_utils::is_ip_address($address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_is_ip_address().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function ip_address_data_provider() {
|
||||
return [
|
||||
["127.0.0.1", true],
|
||||
["10.1", false],
|
||||
["0.0.0.0", true],
|
||||
["255.255.255.255", true],
|
||||
["256.0.0.1", false],
|
||||
["256.0.0.1", false],
|
||||
["127.0.0.0/24", false],
|
||||
["127.0.0.0-255", false],
|
||||
["::", true],
|
||||
["::0", true],
|
||||
["0::", true],
|
||||
["0::0", true],
|
||||
["fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80", true],
|
||||
["fe80::ffff", true],
|
||||
["fe80::f", true],
|
||||
["fe80::", true],
|
||||
["0", false],
|
||||
["127.0.0.0/24", false],
|
||||
["fe80::fe80/128", false],
|
||||
["fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80/128", false],
|
||||
["fe80:", false],
|
||||
["fe80:: ", false],
|
||||
[" fe80::", false],
|
||||
["fe80::ddddd", false],
|
||||
["fe80::gggg", false],
|
||||
["fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80", false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \core\ip_utils::is_ipv4_address().
|
||||
*
|
||||
* @param string $address the address to validate.
|
||||
* @param bool $expected the expected result.
|
||||
* @dataProvider ipv4_address_data_provider
|
||||
*/
|
||||
public function test_is_ipv4_address($address, $expected) {
|
||||
$this->assertEquals($expected, \core\ip_utils::is_ipv4_address($address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_is_ipv4_address().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function ipv4_address_data_provider() {
|
||||
return [
|
||||
["127.0.0.1", true],
|
||||
["0.0.0.0", true],
|
||||
["255.255.255.255", true],
|
||||
[" 127.0.0.1", false],
|
||||
["127.0.0.1 ", false],
|
||||
["-127.0.0.1", false],
|
||||
["127.0.1", false],
|
||||
["127.0.0.0.1", false],
|
||||
["a.b.c.d", false],
|
||||
["localhost", false],
|
||||
["fe80::1", false],
|
||||
["256.0.0.1", false],
|
||||
["256.0.0.1", false],
|
||||
["127.0.0.0/24", false],
|
||||
["127.0.0.0-255", false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \core\ip_utils::is_ipv4_range().
|
||||
*
|
||||
* @param string $addressrange the address range to validate.
|
||||
* @param bool $expected the expected result.
|
||||
* @dataProvider ipv4_range_data_provider
|
||||
*/
|
||||
public function test_is_ipv4_range($addressrange, $expected) {
|
||||
$this->assertEquals($expected, \core\ip_utils::is_ipv4_range($addressrange));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_is_ipv4_range().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function ipv4_range_data_provider() {
|
||||
return [
|
||||
["127.0.0.1/24", true],
|
||||
["127.0.0.20-20", true],
|
||||
["127.0.0.20-50", true],
|
||||
["127.0.0.0-255", true],
|
||||
["127.0.0.1-1", true],
|
||||
["255.255.255.0-255", true],
|
||||
["127.0.0.1", false],
|
||||
["127.0", false],
|
||||
[" 127.0.0.0/24", false],
|
||||
["127.0.0.0/24 ", false],
|
||||
["a.b.c.d/24", false],
|
||||
["256.0.0.0-80", false],
|
||||
["127.0.0.0/a", false],
|
||||
["256.0.0.0/24", false],
|
||||
["127.0.0.0/-1", false],
|
||||
["127.0.0.0/33", false],
|
||||
["127.0.0.0-127.0.0.10", false],
|
||||
["127.0.0.30-20", false],
|
||||
["127.0.0.0-256", false],
|
||||
["fe80::fe80/64", false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \core\ip_utils::is_ipv6_address().
|
||||
*
|
||||
* @param string $address the address to validate.
|
||||
* @param bool $expected the expected result.
|
||||
* @dataProvider ipv6_address_data_provider
|
||||
*/
|
||||
public function test_is_ipv6_address($address, $expected) {
|
||||
$this->assertEquals($expected, \core\ip_utils::is_ipv6_address($address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_is_ipv6_address().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function ipv6_address_data_provider() {
|
||||
return [
|
||||
["::", true],
|
||||
["::0", true],
|
||||
["0::", true],
|
||||
["0::0", true],
|
||||
["fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80", true],
|
||||
["fe80::ffff", true],
|
||||
["fe80::f", true],
|
||||
["fe80::", true],
|
||||
["0", false],
|
||||
["127.0.0.0", false],
|
||||
["127.0.0.0/24", false],
|
||||
["fe80::fe80/128", false],
|
||||
["fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80/128", false],
|
||||
["fe80:", false],
|
||||
["fe80:: ", false],
|
||||
[" fe80::", false],
|
||||
["fe80::ddddd", false],
|
||||
["fe80::gggg", false],
|
||||
["fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80", false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \core\ip_utils::is_ipv6_range().
|
||||
*
|
||||
* @param string $addressrange the address range to validate.
|
||||
* @param bool $expected the expected result.
|
||||
* @dataProvider ipv6_range_data_provider
|
||||
*/
|
||||
public function test_is_ipv6_range($addressrange, $expected) {
|
||||
$this->assertEquals($expected, \core\ip_utils::is_ipv6_range($addressrange));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_is_ipv6_range().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function ipv6_range_data_provider() {
|
||||
return [
|
||||
["::/128", true],
|
||||
["::1/128", true],
|
||||
["fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80/128", true],
|
||||
["fe80::dddd/128", true],
|
||||
["fe80::/64", true],
|
||||
["fe80::dddd-ffff", true],
|
||||
["::0-ffff", true],
|
||||
["::a-ffff", true],
|
||||
["0", false],
|
||||
["::1", false],
|
||||
["fe80::fe80", false],
|
||||
["::/128 ", false],
|
||||
[" ::/128", false],
|
||||
["::/a", false],
|
||||
["::/-1", false],
|
||||
["fe80::fe80/129", false],
|
||||
["fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80", false],
|
||||
["fe80::bbbb-aaaa", false],
|
||||
["fe80::0-fffg", false],
|
||||
["fe80::0-fffff", false],
|
||||
["fe80::0 - ffff", false],
|
||||
[" fe80::0-ffff", false],
|
||||
["fe80::0-ffff ", false],
|
||||
["192.0.0.0/24", false],
|
||||
["fe80:::fe80/128", false],
|
||||
["fe80:::aaaa-dddd", false],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user