From 663fdbb356e5bf5f4be16d189c7e496e44d6cacc Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Mon, 29 Mar 2021 15:32:27 +0200 Subject: [PATCH 01/24] Refactor ArrayHelperTest --- .gitignore | 2 ++ Tests/ArrayHelperTest.php | 33 ++++++++------------------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 871b715c..fe5e21c9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ vendor/ composer.phar composer.lock phpunit.xml +/.phpunit.result.cache +/build/ diff --git a/Tests/ArrayHelperTest.php b/Tests/ArrayHelperTest.php index dcf732b0..a15c605b 100644 --- a/Tests/ArrayHelperTest.php +++ b/Tests/ArrayHelperTest.php @@ -57,7 +57,7 @@ public function seedTestFromObject() return array( 'Invalid input' => array( - // Array The array being input + // Object The object being input null, // Boolean Recurse through multiple dimensions null, @@ -2127,7 +2127,6 @@ public function seedTestToString() * @return void * * @dataProvider seedTestArrayUnique - * @covers Joomla\Utilities\ArrayHelper::arrayUnique * @since 1.0 */ public function testArrayUnique($input, $expected) @@ -2141,7 +2140,7 @@ public function testArrayUnique($input, $expected) /** * Tests conversion of object to string. * - * @param array $input The array being input + * @param object $input The object being input * @param boolean $recurse Recurse through multiple dimensions? * @param string $regex Regex to select only some attributes * @param string $expect The expected return value @@ -2150,8 +2149,6 @@ public function testArrayUnique($input, $expected) * @return void * * @dataProvider seedTestFromObject - * @covers Joomla\Utilities\ArrayHelper::fromObject - * @covers Joomla\Utilities\ArrayHelper::arrayFromObject * @since 1.0 */ public function testFromObject($input, $recurse, $regex, $expect, $defaults) @@ -2181,7 +2178,6 @@ public function testFromObject($input, $recurse, $regex, $expect, $defaults) * @return void * * @dataProvider seedTestAddColumn - * @covers Joomla\Utilities\ArrayHelper::addColumn * @since 1.5.0 */ public function testAddColumn($input, $column, $colName, $keyCol, $expect, $message) @@ -2200,7 +2196,6 @@ public function testAddColumn($input, $column, $colName, $keyCol, $expect, $mess * @return void * * @dataProvider seedTestDropColumn - * @covers Joomla\Utilities\ArrayHelper::dropColumn * @since 1.5.0 */ public function testDropColumn($input, $colName, $expect, $message) @@ -2220,7 +2215,6 @@ public function testDropColumn($input, $colName, $expect, $message) * @return void * * @dataProvider seedTestGetColumn - * @covers Joomla\Utilities\ArrayHelper::getColumn * @since 1.0 */ public function testGetColumn($input, $valueCol, $keyCol, $expect, $message) @@ -2242,7 +2236,6 @@ public function testGetColumn($input, $valueCol, $keyCol, $expect, $message) * @return void * * @dataProvider seedTestGetValue - * @covers Joomla\Utilities\ArrayHelper::getValue * @since 1.0 */ public function testGetValue($input, $index, $default, $type, $expect, $message, $defaults) @@ -2264,7 +2257,6 @@ public function testGetValue($input, $index, $default, $type, $expect, $message, * * @return void * - * @covers Joomla\Utilities\ArrayHelper::getValue * @since 1.3.1 */ public function testGetValueWithObjectImplementingArrayAccess() @@ -2284,8 +2276,7 @@ public function testGetValueWithObjectImplementingArrayAccess() /** * @testdox Verify that getValue() throws an \InvalidArgumentException when an object is given that doesn't implement \ArrayAccess * - * @covers Joomla\Utilities\ArrayHelper::getValue - * @expectedException \InvalidArgumentException + * @ expectedException \InvalidArgumentException * @since 1.3.1 */ public function testInvalidArgumentExceptionWithAnObjectNotImplementingArrayAccess() @@ -2296,6 +2287,8 @@ public function testInvalidArgumentExceptionWithAnObjectNotImplementingArrayAcce $object->age = 20; $object->address = null; + $this->expectException('\\InvalidArgumentException'); + /** @noinspection PhpParamsInspection */ ArrayHelper::getValue($object, 'string'); } @@ -2369,7 +2362,6 @@ public function testIsAssociative() * @return void * * @dataProvider seedTestPivot - * @covers Joomla\Utilities\ArrayHelper::pivot * @since 1.0 */ public function testPivot($source, $key, $expected) @@ -2395,7 +2387,6 @@ public function testPivot($source, $key, $expected) * @return void * * @dataProvider seedTestSortObject - * @covers Joomla\Utilities\ArrayHelper::sortObjects * @since 1.0 */ public function testSortObjects($input, $key, $direction, $casesensitive, $locale, $expect, $message, $defaults, $swappable_keys = array()) @@ -2409,15 +2400,12 @@ public function testSortObjects($input, $key, $direction, $casesensitive, $local if (empty($input)) { $this->markTestSkipped('Skip for MAC until PHP sort bug is fixed'); - - return; } - elseif ($locale != false && !setlocale(LC_COLLATE, $locale)) + + if ($locale != false && !setlocale(LC_COLLATE, $locale)) { // If the locale is not available, we can't have to transcode the string and can't reliably compare it. $this->markTestSkipped("Locale {$locale} is not available."); - - return; } if ($defaults) @@ -2445,7 +2433,7 @@ public function testSortObjects($input, $key, $direction, $casesensitive, $local /** * Test convert an array to all integers. * - * @param string $input The array being input + * @param mixed $input The value being input * @param string $default The default value * @param string $expect The expected return value * @param string $message The failure message @@ -2453,7 +2441,6 @@ public function testSortObjects($input, $key, $direction, $casesensitive, $local * @return void * * @dataProvider seedTestToInteger - * @covers Joomla\Utilities\ArrayHelper::toInteger * @since 1.0 */ public function testToInteger($input, $default, $expect, $message) @@ -2477,7 +2464,6 @@ public function testToInteger($input, $default, $expect, $message) * @return void * * @dataProvider seedTestToObject - * @covers Joomla\Utilities\ArrayHelper::toObject * @since 1.0 */ public function testToObject($input, $className, $expect, $message) @@ -2503,7 +2489,6 @@ public function testToObject($input, $className, $expect, $message) * @return void * * @dataProvider seedTestToString - * @covers Joomla\Utilities\ArrayHelper::toString * @since 1.0 */ public function testToString($input, $inner, $outer, $keepKey, $expect, $message, $defaults) @@ -2525,7 +2510,6 @@ public function testToString($input, $inner, $outer, $keepKey, $expect, $message * * @return void * - * @covers Joomla\Utilities\ArrayHelper::arraySearch * @since 1.0 */ public function testArraySearch() @@ -2550,7 +2534,6 @@ public function testArraySearch() * * @return void * - * @covers Joomla\Utilities\ArrayHelper::flatten * @since 1.0 */ public function testFlatten() From d2d2771c59246366e51a0f38e88214e9d1c43f68 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Tue, 30 Mar 2021 17:20:42 +0200 Subject: [PATCH 02/24] Feature - Add RegEx utility Docs - Re-organise documentation structure --- README.md | 385 +------------------------------------- Tests/ArrayHelperTest.php | 3 + Tests/RegExTest.php | 198 ++++++++++++++++++++ docs/array-helper.md | 385 ++++++++++++++++++++++++++++++++++++++ docs/ip-helper.md | 4 + docs/regex.md | 184 ++++++++++++++++++ src/RegEx.php | 98 ++++++++++ 7 files changed, 876 insertions(+), 381 deletions(-) create mode 100644 Tests/RegExTest.php create mode 100644 docs/array-helper.md create mode 100644 docs/ip-helper.md create mode 100644 docs/regex.md create mode 100644 src/RegEx.php diff --git a/README.md b/README.md index 981caf12..427e5892 100644 --- a/README.md +++ b/README.md @@ -5,388 +5,11 @@ [![Latest Unstable Version](https://poser.pugx.org/joomla/utilities/v/unstable)](https://packagist.org/packages/joomla/utilities) [![License](https://poser.pugx.org/joomla/utilities/license)](https://packagist.org/packages/joomla/utilities) -## Using ArrayHelper +The Utilities Package provides some useful tools that do not fit into specific packages. -### toInteger - -```php -use Joomla\Utilities\ArrayHelper; - -$input = array( - "width" => "100", - "height" => "200xxx", - "length" => "10.3" -); -$result = ArrayHelper::toInteger($input); -var_dump($result); -``` -Result: -``` -array(3) { - 'width' => - int(100) - 'height' => - int(200) - 'length' => - int(10) -} -``` - -### toObject - -```php -use Joomla\Utilities\ArrayHelper; - -class Book { - public $name; - public $author; - public $genre; - public $rating; -} -class Author { - public $name; - public $born; -} -$input = array( - "name" => "The Hitchhiker's Guide to the Galaxy", - "author" => array( - "name" => "Douglas Adams", - "born" => 1952, - "died" => 2001), - "genre" => "comic science fiction", - "rating" => 10 -); -$book = ArrayHelper::toObject($input, 'Book'); -var_dump($book); -``` -Result: -``` -class Book#1 (4) { - public $name => - string(36) "The Hitchhiker's Guide to the Galaxy" - public $author => - class Book#2 (6) { - public $name => - string(13) "Douglas Adams" - public $author => - NULL - public $genre => - NULL - public $rating => - NULL - public $born => - int(1952) - public $died => - int(2001) - } - public $genre => - string(21) "comic science fiction" - public $rating => - int(10) -} -``` - -### toString - -```php -use Joomla\Utilities\ArrayHelper; - -$input = array( - "fruit" => "apple", - "pi" => 3.14 -); -echo ArrayHelper::toString($input); -``` -Result: -``` -fruit="apple" pi="3.14" -``` - -### fromObject - -```php -use Joomla\Utilities\ArrayHelper; - -class Book { - public $name; - public $author; - public $genre; - public $rating; -} -class Author { - public $name; - public $born; -} - -$book = new Book(); -$book->name = "Harry Potter and the Philosopher's Stone"; -$book->author = new Author(); -$book->author->name = "J.K. Rowling"; -$book->author->born = 1965; -$book->genre = "fantasy"; -$book->rating = 10; - -$array = ArrayHelper::fromObject($book); -var_dump($array); -``` -Result: -``` -array(4) { - 'name' => - string(40) "Harry Potter and the Philosopher's Stone" - 'author' => - array(2) { - 'name' => - string(12) "J.K. Rowling" - 'born' => - int(1965) - } - 'genre' => - string(7) "fantasy" - 'rating' => - int(10) -} -``` - -### getColumn - -```php -use Joomla\Utilities\ArrayHelper; - -$rows = array( - array("name" => "John", "age" => 20), - array("name" => "Alex", "age" => 35), - array("name" => "Sarah", "age" => 27) -); -$names = ArrayHelper::getColumn($rows, 'name'); -var_dump($names); -``` -Result: -``` -array(3) { - [0] => - string(4) "John" - [1] => - string(4) "Alex" - [2] => - string(5) "Sarah" -} -``` - -### getValue -```php -use Joomla\Utilities\ArrayHelper; - -$city = array( - "name" => "Oslo", - "country" => "Norway" -); - -// Prints 'Oslo' -echo ArrayHelper::getValue($city, 'name'); - -// Prints 'unknown mayor' (no 'mayor' key is found in the array) -echo ArrayHelper::getValue($city, 'mayor', 'unknown mayor'); -``` - -### invert - -```php -use Joomla\Utilities\ArrayHelper; - -$input = array( - 'New' => array('1000', '1500', '1750'), - 'Used' => array('3000', '4000', '5000', '6000') -); -$output = ArrayHelper::invert($input); -var_dump($output); -``` -Result: -``` -array(7) { - [1000] => - string(3) "New" - [1500] => - string(3) "New" - [1750] => - string(3) "New" - [3000] => - string(4) "Used" - [4000] => - string(4) "Used" - [5000] => - string(4) "Used" - [6000] => - string(4) "Used" -} -``` - - -### isAssociative - -```php -use Joomla\Utilities\ArrayHelper; - -$user = array("id" => 46, "name" => "John"); -echo ArrayHelper::isAssociative($user) ? 'true' : 'false'; // true - -$letters = array("a", "b", "c"); -echo ArrayHelper::isAssociative($letters) ? 'true' : 'false'; // false -``` - -### pivot - -```php -use Joomla\Utilities\ArrayHelper; - -$movies = array( - array('year' => 1972, 'title' => 'The Godfather'), - array('year' => 2000, 'title' => 'Gladiator'), - array('year' => 2000, 'title' => 'Memento'), - array('year' => 1964, 'title' => 'Dr. Strangelove') -); -$pivoted = ArrayHelper::pivot($movies, 'year'); -var_dump($pivoted); -``` -Result: -``` -array(3) { - [1972] => - array(2) { - 'year' => - int(1972) - 'title' => - string(13) "The Godfather" - } - [2000] => - array(2) { - [0] => - array(2) { - 'year' => - int(2000) - 'title' => - string(9) "Gladiator" - } - [1] => - array(2) { - 'year' => - int(2000) - 'title' => - string(7) "Memento" - } - } - [1964] => - array(2) { - 'year' => - int(1964) - 'title' => - string(15) "Dr. Strangelove" - } -} -``` - -### sortObjects - -```php -use Joomla\Utilities\ArrayHelper; - -$members = array( - (object) array('first_name' => 'Carl', 'last_name' => 'Hopkins'), - (object) array('first_name' => 'Lisa', 'last_name' => 'Smith'), - (object) array('first_name' => 'Julia', 'last_name' => 'Adams') -); -$sorted = ArrayHelper::sortObjects($members, 'last_name', 1); -var_dump($sorted); -``` -Result: -``` -array(3) { - [0] => - class stdClass#3 (2) { - public $first_name => - string(5) "Julia" - public $last_name => - string(5) "Adams" - } - [1] => - class stdClass#1 (2) { - public $first_name => - string(4) "Carl" - public $last_name => - string(7) "Hopkins" - } - [2] => - class stdClass#2 (2) { - public $first_name => - string(4) "Lisa" - public $last_name => - string(5) "Smith" - } -} -``` - -### arrayUnique -```php -use Joomla\Utilities\ArrayHelper; - -$names = array( - array("first_name" => "John", "last_name" => "Adams"), - array("first_name" => "John", "last_name" => "Adams"), - array("first_name" => "John", "last_name" => "Smith"), - array("first_name" => "Sam", "last_name" => "Smith") -); -$unique = ArrayHelper::arrayUnique($names); -var_dump($unique); -``` -Result: -``` -array(3) { - [0] => - array(2) { - 'first_name' => - string(4) "John" - 'last_name' => - string(5) "Adams" - } - [2] => - array(2) { - 'first_name' => - string(4) "John" - 'last_name' => - string(5) "Smith" - } - [3] => - array(2) { - 'first_name' => - string(3) "Sam" - 'last_name' => - string(5) "Smith" - } -} -``` - -### flatten - -``` php -use Joomla\Utilities\ArrayHelper; - -$array = array( - 'flower' => array( - 'sakura' => 'samurai', - 'olive' => 'peace' - ) -); - -// Flatten the nested array and separate the keys by a dot (".") -$flattenend1 = ArrayHelper::flatten($array); - -echo $flattenend1['flower.sakura']; // 'samuari' - -// Custom separator -$flattenend2 = ArrayHelper::flatten($array, '/'); - -echo $flattenend2['flower/olive']; // 'peace' -``` +* [ArrayHelper](docs/array-helper.md) +* [IpHelper](docs/ip-helper.md) +* [RegEx](docs/regex.md) ## Installation via Composer diff --git a/Tests/ArrayHelperTest.php b/Tests/ArrayHelperTest.php index a15c605b..b8d0796b 100644 --- a/Tests/ArrayHelperTest.php +++ b/Tests/ArrayHelperTest.php @@ -4,6 +4,9 @@ * @license GNU General Public License version 2 or later; see LICENSE */ +namespace Joomla\Utilities\Tests; + +use ArrayObject; use Joomla\Utilities\ArrayHelper; use PHPUnit\Framework\TestCase; diff --git a/Tests/RegExTest.php b/Tests/RegExTest.php new file mode 100644 index 00000000..bcbace9a --- /dev/null +++ b/Tests/RegExTest.php @@ -0,0 +1,198 @@ + 'foobar', + 'digit' => '2008' + ), + $matches + ); + } + + public function testOptional() + { + $regex = 'a' . RegEx::optional('b') . 'c'; + + self::assertEquals( + array('result' => 'ac'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaacccc' + ) + ); + + self::assertEquals( + array('result' => 'abc'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaabcccc' + ) + ); + + self::assertEquals( + array(), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaabbcccc' + ) + ); + } + + public function testOneOrMore() + { + $regex = 'a' . RegEx::oneOrMore('b') . 'c'; + + self::assertEquals( + array(), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaacccc' + ) + ); + + self::assertEquals( + array('result' => 'abc'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaabcccc' + ) + ); + + self::assertEquals( + array('result' => 'abbc'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaabbcccc' + ) + ); + } + + public function testNoneOrMore() + { + $regex = 'a' . RegEx::noneOrMore('b') . 'c'; + + self::assertEquals( + array('result' => 'ac'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaacccc' + ) + ); + + self::assertEquals( + array('result' => 'abc'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaabcccc' + ) + ); + + self::assertEquals( + array('result' => 'abbc'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaabbcccc' + ) + ); + } + + public function testAnyOfList() + { + $regex = 'a' . RegEx::anyOf('1', '2', '3') . 'c'; + + self::assertEquals( + array(), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaacccc' + ) + ); + + self::assertEquals( + array('result' => 'a1c'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaa1cccc' + ) + ); + + self::assertEquals( + array('result' => 'a2c'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaa2cccc' + ) + ); + } + + public function testAnyOfArray() + { + $regex = 'a' . RegEx::anyOf(array('1', '2', '3')) . 'c'; + + self::assertEquals( + array(), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaacccc' + ) + ); + + self::assertEquals( + array('result' => 'a1c'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaa1cccc' + ) + ); + + self::assertEquals( + array('result' => 'a2c'), + RegEx::match( + '~' . RegEx::capture($regex, 'result') . '~', + 'aaaa2cccc' + ) + ); + } +} diff --git a/docs/array-helper.md b/docs/array-helper.md new file mode 100644 index 00000000..270be4b6 --- /dev/null +++ b/docs/array-helper.md @@ -0,0 +1,385 @@ +[The Utilities Package](../README.md) +# ArrayHelper + +## Using ArrayHelper + +### toInteger + +```php +use Joomla\Utilities\ArrayHelper; + +$input = array( + "width" => "100", + "height" => "200xxx", + "length" => "10.3" +); +$result = ArrayHelper::toInteger($input); +var_dump($result); +``` +Result: +``` +array(3) { + 'width' => + int(100) + 'height' => + int(200) + 'length' => + int(10) +} +``` + +### toObject + +```php +use Joomla\Utilities\ArrayHelper; + +class Book { + public $name; + public $author; + public $genre; + public $rating; +} +class Author { + public $name; + public $born; +} +$input = array( + "name" => "The Hitchhiker's Guide to the Galaxy", + "author" => array( + "name" => "Douglas Adams", + "born" => 1952, + "died" => 2001), + "genre" => "comic science fiction", + "rating" => 10 +); +$book = ArrayHelper::toObject($input, 'Book'); +var_dump($book); +``` +Result: +``` +class Book#1 (4) { + public $name => + string(36) "The Hitchhiker's Guide to the Galaxy" + public $author => + class Book#2 (6) { + public $name => + string(13) "Douglas Adams" + public $author => + NULL + public $genre => + NULL + public $rating => + NULL + public $born => + int(1952) + public $died => + int(2001) + } + public $genre => + string(21) "comic science fiction" + public $rating => + int(10) +} +``` + +### toString + +```php +use Joomla\Utilities\ArrayHelper; + +$input = array( + "fruit" => "apple", + "pi" => 3.14 +); +echo ArrayHelper::toString($input); +``` +Result: +``` +fruit="apple" pi="3.14" +``` + +### fromObject + +```php +use Joomla\Utilities\ArrayHelper; + +class Book { + public $name; + public $author; + public $genre; + public $rating; +} +class Author { + public $name; + public $born; +} + +$book = new Book(); +$book->name = "Harry Potter and the Philosopher's Stone"; +$book->author = new Author(); +$book->author->name = "J.K. Rowling"; +$book->author->born = 1965; +$book->genre = "fantasy"; +$book->rating = 10; + +$array = ArrayHelper::fromObject($book); +var_dump($array); +``` +Result: +``` +array(4) { + 'name' => + string(40) "Harry Potter and the Philosopher's Stone" + 'author' => + array(2) { + 'name' => + string(12) "J.K. Rowling" + 'born' => + int(1965) + } + 'genre' => + string(7) "fantasy" + 'rating' => + int(10) +} +``` + +### getColumn + +```php +use Joomla\Utilities\ArrayHelper; + +$rows = array( + array("name" => "John", "age" => 20), + array("name" => "Alex", "age" => 35), + array("name" => "Sarah", "age" => 27) +); +$names = ArrayHelper::getColumn($rows, 'name'); +var_dump($names); +``` +Result: +``` +array(3) { + [0] => + string(4) "John" + [1] => + string(4) "Alex" + [2] => + string(5) "Sarah" +} +``` + +### getValue +```php +use Joomla\Utilities\ArrayHelper; + +$city = array( + "name" => "Oslo", + "country" => "Norway" +); + +// Prints 'Oslo' +echo ArrayHelper::getValue($city, 'name'); + +// Prints 'unknown mayor' (no 'mayor' key is found in the array) +echo ArrayHelper::getValue($city, 'mayor', 'unknown mayor'); +``` + +### invert + +```php +use Joomla\Utilities\ArrayHelper; + +$input = array( + 'New' => array('1000', '1500', '1750'), + 'Used' => array('3000', '4000', '5000', '6000') +); +$output = ArrayHelper::invert($input); +var_dump($output); +``` +Result: +``` +array(7) { + [1000] => + string(3) "New" + [1500] => + string(3) "New" + [1750] => + string(3) "New" + [3000] => + string(4) "Used" + [4000] => + string(4) "Used" + [5000] => + string(4) "Used" + [6000] => + string(4) "Used" +} +``` + + +### isAssociative + +```php +use Joomla\Utilities\ArrayHelper; + +$user = array("id" => 46, "name" => "John"); +echo ArrayHelper::isAssociative($user) ? 'true' : 'false'; // true + +$letters = array("a", "b", "c"); +echo ArrayHelper::isAssociative($letters) ? 'true' : 'false'; // false +``` + +### pivot + +```php +use Joomla\Utilities\ArrayHelper; + +$movies = array( + array('year' => 1972, 'title' => 'The Godfather'), + array('year' => 2000, 'title' => 'Gladiator'), + array('year' => 2000, 'title' => 'Memento'), + array('year' => 1964, 'title' => 'Dr. Strangelove') +); +$pivoted = ArrayHelper::pivot($movies, 'year'); +var_dump($pivoted); +``` +Result: +``` +array(3) { + [1972] => + array(2) { + 'year' => + int(1972) + 'title' => + string(13) "The Godfather" + } + [2000] => + array(2) { + [0] => + array(2) { + 'year' => + int(2000) + 'title' => + string(9) "Gladiator" + } + [1] => + array(2) { + 'year' => + int(2000) + 'title' => + string(7) "Memento" + } + } + [1964] => + array(2) { + 'year' => + int(1964) + 'title' => + string(15) "Dr. Strangelove" + } +} +``` + +### sortObjects + +```php +use Joomla\Utilities\ArrayHelper; + +$members = array( + (object) array('first_name' => 'Carl', 'last_name' => 'Hopkins'), + (object) array('first_name' => 'Lisa', 'last_name' => 'Smith'), + (object) array('first_name' => 'Julia', 'last_name' => 'Adams') +); +$sorted = ArrayHelper::sortObjects($members, 'last_name', 1); +var_dump($sorted); +``` +Result: +``` +array(3) { + [0] => + class stdClass#3 (2) { + public $first_name => + string(5) "Julia" + public $last_name => + string(5) "Adams" + } + [1] => + class stdClass#1 (2) { + public $first_name => + string(4) "Carl" + public $last_name => + string(7) "Hopkins" + } + [2] => + class stdClass#2 (2) { + public $first_name => + string(4) "Lisa" + public $last_name => + string(5) "Smith" + } +} +``` + +### arrayUnique +```php +use Joomla\Utilities\ArrayHelper; + +$names = array( + array("first_name" => "John", "last_name" => "Adams"), + array("first_name" => "John", "last_name" => "Adams"), + array("first_name" => "John", "last_name" => "Smith"), + array("first_name" => "Sam", "last_name" => "Smith") +); +$unique = ArrayHelper::arrayUnique($names); +var_dump($unique); +``` +Result: +``` +array(3) { + [0] => + array(2) { + 'first_name' => + string(4) "John" + 'last_name' => + string(5) "Adams" + } + [2] => + array(2) { + 'first_name' => + string(4) "John" + 'last_name' => + string(5) "Smith" + } + [3] => + array(2) { + 'first_name' => + string(3) "Sam" + 'last_name' => + string(5) "Smith" + } +} +``` + +### flatten + +``` php +use Joomla\Utilities\ArrayHelper; + +$array = array( + 'flower' => array( + 'sakura' => 'samurai', + 'olive' => 'peace' + ) +); + +// Flatten the nested array and separate the keys by a dot (".") +$flattenend1 = ArrayHelper::flatten($array); + +echo $flattenend1['flower.sakura']; // 'samuari' + +// Custom separator +$flattenend2 = ArrayHelper::flatten($array, '/'); + +echo $flattenend2['flower/olive']; // 'peace' +``` diff --git a/docs/ip-helper.md b/docs/ip-helper.md new file mode 100644 index 00000000..dd496f08 --- /dev/null +++ b/docs/ip-helper.md @@ -0,0 +1,4 @@ +[The Utilities Package](../README.md) +# IpHelper + +## Using IpHelper diff --git a/docs/regex.md b/docs/regex.md new file mode 100644 index 00000000..eb51e468 --- /dev/null +++ b/docs/regex.md @@ -0,0 +1,184 @@ +[The Utilities Package](../README.md) +# RegEx + +Regular expressions can get quite complicated at times. +The RegEx package is designed to make them easier to create, more readable and easier to maintain. + +## Using RegEx + +As an example of the simple handling of RegEx, an implementation of a parser for HTTP URLs +according to [RFC 1738](https://www.ietf.org/rfc/rfc1738.txt) is shown here. +You find the BNF ([Backus-Naur-Form](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form)) +definition from Section 5 of the RFC in the comment for each element. + +```php +use Joomla\Utilities\RegEx; + +// For character classes, the native way is simplest +$digit = '[0-9]'; +$alpha = '[a-zA-Z]'; +$alphadigit = '[a-zA-Z0-9]'; +$hex = '[0-9a-fA-F]'; +$safe = '[$\-_.+]'; +$extra = '[!*\'(),]'; + +// BNF: digits = 1*digit +$digits = RegEx::oneOrMore($digit); + +// BNF: unreserved = alpha | digit | safe | extra +$unreserved = RegEx::anyOf($alpha, $digit, $safe, $extra); + +// BNF: escape = "%" hex hex +$escape = '%' . $hex . $hex; + +// BNF: uchar = unreserved | escape +$uchar = RegEx::anyOf($unreserved, $escape); + + +// BNF: domainlabel = alphadigit | alphadigit *[ alphadigit | "-" ] alphadigit +$domainlabel = RegEx::anyOf( + $alphadigit, + $alphadigit . RegEx::noneOrMore(RegEx::anyOf(array($alphadigit, '-'))) . $alphadigit +); + +// BNF: toplabel = alpha | alpha *[ alphadigit | "-" ] alphadigit +$toplabel = RegEx::anyOf( + $alpha, + $alpha . RegEx::noneOrMore(RegEx::anyOf(array($alphadigit, '-'))) . $alphadigit +); + +// Add the toplabel to the result with key 'tld' +$toplabel = RegEx::capture($toplabel, 'tld'); + +// BNF: hostname = *[ domainlabel "." ] toplabel +$hostname = RegEx::noneOrMore($domainlabel . '\.') . $toplabel; + +// Add the hostname to the result with key 'domain' +$hostname = RegEx::capture($hostname, 'domain'); + +// BNF: hostnumber = digits "." digits "." digits "." digits +$hostnumber = $digits . '\.' . $digits . '\.' . $digits . '\.' . $digits; + +// Add the hostnumber to the result with key 'ip' +$hostnumber = RegEx::capture($hostnumber, 'ip'); + +// BNF: host = hostname | hostnumber +$host = RegEx::anyOf($hostname, $hostnumber); + +// Add the host to the result with key 'host' +$host = RegEx::capture($host, 'host'); + +// BNF: port = digits +$port = $digits; + +// Add the port to the result with key 'port' +$port = RegEx::capture($port, 'port'); + +// BNF: hostport = host [ ":" port ] +$hostport = $host . RegEx::optional(':' . $port); + +// BNF: hsegment = *[ uchar | ";" | ":" | "@" | "&" | "=" ] +$hsegment = RegEx::noneOrMore(RegEx::anyOf($uchar, '[;:@&=]')); + +// BNF: hpath = hsegment *[ "/" hsegment ] +$hpath = $hsegment . RegEx::noneOrMore('/' . $hsegment); + +// Add the hpath to the result with key 'path' +$hpath = RegEx::capture($hpath, 'path'); + +// BNF: search = *[ uchar | ";" | ":" | "@" | "&" | "=" ] +$search = RegEx::noneOrMore(RegEx::anyOf(array($uchar, '[;:@&=]'))); + +// Add the search to the result with key 'query' +$search = RegEx::capture($search, 'query'); + +// BNF: httpurl = "http://" hostport [ "/" hpath [ "?" search ]] +$httpurl = 'http://' . $hostport . RegEx::optional('/' . $hpath) . RegEx::optional('\?' . $search); + +$regex = '~^' . $httpurl . '$~'; +$subject = 'http://www.example.com:8080/index.php?foo=bar'; + +$parts = RegEx::match($regex, $subject); +print_r($parts); +``` + +Result: + +``` +Array +( + [host] => www.example.com + [domain] => www.example.com + [tld] => com + [port] => 8080 + [path] => index.php + [query] => foo=bar +) +``` +### match + +As you can see from the example above, `RegEx::match()` returns the matches that have been +appropriately marked using `RegEx::capture()`. +Only the matches that have a value are returned. +If the Regular Expression does not match, the result is an empty array. + +### capture + +Assign a key to an expression. + +```php +use Joomla\Utilities\RegEx; + +$regex = RegEx::capture('[0-9]+', 'number'); +print_r(RegEx::match($regex, 'abc123def')); +``` +Result: +``` +Array +( + [number] => 123 +) +``` + +### optional + +Add a 'zero or one' quantifier to an expression. + +```php +use Joomla\Utilities\RegEx; + +print(RegEx::optional('regex')); // (?:regex)? +``` + +### oneOrMore + +Add a 'one or more' quantifier to an expression. + +```php +use Joomla\Utilities\RegEx; + +print(RegEx::oneOrMore('regex')); // (?:regex)+ +``` + +### noneOrMore + +Add a 'zero or more' quantifier to an expression. + +```php +use Joomla\Utilities\RegEx; + +print(RegEx::noneOrMore('regex')); // (?:regex)* +``` + +### anyOf + +Define a list of alternative expressions. + +```php +use Joomla\Utilities\RegEx; + +print(RegEx::anyOf('a', 'b', 'c')); // (?:a|b|c) + +$array = array('a', 'b', 'c'); +print(RegEx::anyOf($array)); // (?:a|b|c) +``` diff --git a/src/RegEx.php b/src/RegEx.php new file mode 100644 index 00000000..3809de9f --- /dev/null +++ b/src/RegEx.php @@ -0,0 +1,98 @@ +' . $regex . ')'; + } + + /** + * Add a 'zero or one' quantifier to an expression. + * + * @param string $regex The Regular Expression to match + * + * @return string The modified Regular Expression + */ + public static function optional($regex) + { + return '(?:' . $regex . ')?'; + } + + /** + * Add a 'one or more' quantifier to an expression. + * + * @param string $regex The Regular Expression to match + * + * @return string The modified Regular Expression + */ + public static function oneOrMore($regex) + { + return '(?:' . $regex . ')+'; + } + + /** + * Add a 'zero or more' quantifier to an expression. + * + * @param string $regex The Regular Expression to match + * + * @return string The modified Regular Expression + */ + public static function noneOrMore($regex) + { + return '(?:' . $regex . ')*'; + } + + /** + * Define a list of alternative expressions. + * + * @param string|array $regexList A list of Regular Expressions to choose from + * + * @return string The modified Regular Expression + */ + public static function anyOf($regexList) + { + if (is_string($regexList)) + { + $regexList = func_get_args(); + } + + return '(?:' . implode('|', $regexList) . ')'; + } +} From 66f808388c13828ad55e1f53bf9f2e43803dbc38 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Tue, 30 Mar 2021 18:01:13 +0200 Subject: [PATCH 03/24] Style - Fix codestyle issues --- Tests/ArrayHelperTest.php | 9 +++++++-- src/RegEx.php | 16 ++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Tests/ArrayHelperTest.php b/Tests/ArrayHelperTest.php index b8d0796b..24b99458 100644 --- a/Tests/ArrayHelperTest.php +++ b/Tests/ArrayHelperTest.php @@ -2279,7 +2279,7 @@ public function testGetValueWithObjectImplementingArrayAccess() /** * @testdox Verify that getValue() throws an \InvalidArgumentException when an object is given that doesn't implement \ArrayAccess * - * @ expectedException \InvalidArgumentException + * @expectedException \InvalidArgumentException * @since 1.3.1 */ public function testInvalidArgumentExceptionWithAnObjectNotImplementingArrayAccess() @@ -2290,7 +2290,12 @@ public function testInvalidArgumentExceptionWithAnObjectNotImplementingArrayAcce $object->age = 20; $object->address = null; - $this->expectException('\\InvalidArgumentException'); + if (method_exists($this, 'expectException')) + { + /** @noinspection PhpLanguageLevelInspection */ + $this->expectException(\InvalidArgumentException::class); + } + /** @noinspection PhpParamsInspection */ ArrayHelper::getValue($object, 'string'); } diff --git a/src/RegEx.php b/src/RegEx.php index 3809de9f..f657fc14 100644 --- a/src/RegEx.php +++ b/src/RegEx.php @@ -15,19 +15,23 @@ */ abstract class RegEx { + /** + * Math the Regular Expression + * + * @param string $regex The Regular Expression + * @param string $subject The string to check + * + * @return array Captured values + */ public static function match($regex, $subject) { $match = array(); preg_match($regex, $subject, $match); - return array_filter( - $match, - static function ($value, $key) { + return array_filter($match, static function ($value, $key) { return !is_numeric($key) && !empty($value); - }, - ARRAY_FILTER_USE_BOTH - ); + }, ARRAY_FILTER_USE_BOTH); } /** From 9eb007b292d1ca37e15998ed1418803898d1ab33 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Tue, 30 Mar 2021 18:04:18 +0200 Subject: [PATCH 04/24] Tests - Remove call to expectedException (not supported in old PHPUnit versions) --- Tests/ArrayHelperTest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Tests/ArrayHelperTest.php b/Tests/ArrayHelperTest.php index 24b99458..e9217086 100644 --- a/Tests/ArrayHelperTest.php +++ b/Tests/ArrayHelperTest.php @@ -2290,12 +2290,6 @@ public function testInvalidArgumentExceptionWithAnObjectNotImplementingArrayAcce $object->age = 20; $object->address = null; - if (method_exists($this, 'expectException')) - { - /** @noinspection PhpLanguageLevelInspection */ - $this->expectException(\InvalidArgumentException::class); - } - /** @noinspection PhpParamsInspection */ ArrayHelper::getValue($object, 'string'); } From 9d21dd0c54976fc269a23730f9d621d851e11f43 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Tue, 30 Mar 2021 18:15:59 +0200 Subject: [PATCH 05/24] Compatibility - Add manual implementation for array_filter for PHP <5.6 --- src/RegEx.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/RegEx.php b/src/RegEx.php index f657fc14..bed1bc0f 100644 --- a/src/RegEx.php +++ b/src/RegEx.php @@ -29,8 +29,24 @@ public static function match($regex, $subject) preg_match($regex, $subject, $match); + // @todo Remove this block, once minimum PHP version is raised above the limit + if (version_compare(PHP_VERSION, '5.6.0', '<')) + { + $result = array(); + + foreach ($match as $key => $value) + { + if (!is_numeric($key) && !empty($value)) + { + $result[$key] = $value; + } + } + + return $result; + } + return array_filter($match, static function ($value, $key) { - return !is_numeric($key) && !empty($value); + return !is_numeric($key) && !empty($value); }, ARRAY_FILTER_USE_BOTH); } From 7ad3512ada980a250ff5e32f094c99c68de0e62a Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Tue, 30 Mar 2021 18:20:21 +0200 Subject: [PATCH 06/24] Compatibility - PHP 5.3 seems to be unable to deal with static closures --- src/RegEx.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RegEx.php b/src/RegEx.php index bed1bc0f..aa38f527 100644 --- a/src/RegEx.php +++ b/src/RegEx.php @@ -29,8 +29,8 @@ public static function match($regex, $subject) preg_match($regex, $subject, $match); - // @todo Remove this block, once minimum PHP version is raised above the limit - if (version_compare(PHP_VERSION, '5.6.0', '<')) + // @todo Remove this block, once minimum PHP version is raised above PHP 5.6.0 + if (PHP_VERSION_ID < 50600) { $result = array(); @@ -45,7 +45,7 @@ public static function match($regex, $subject) return $result; } - return array_filter($match, static function ($value, $key) { + return array_filter($match, function ($value, $key) { return !is_numeric($key) && !empty($value); }, ARRAY_FILTER_USE_BOTH); } From b85cacc4f8a8167da6d41fe4a5bc8a1e5e25f474 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Tue, 30 Mar 2021 18:23:10 +0200 Subject: [PATCH 07/24] Style - Closing parenthesis of a multi-line function call must be on a line by itself --- src/RegEx.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RegEx.php b/src/RegEx.php index aa38f527..cae22959 100644 --- a/src/RegEx.php +++ b/src/RegEx.php @@ -47,7 +47,8 @@ public static function match($regex, $subject) return array_filter($match, function ($value, $key) { return !is_numeric($key) && !empty($value); - }, ARRAY_FILTER_USE_BOTH); + }, ARRAY_FILTER_USE_BOTH + ); } /** From 805defaae19a180f9e14f7d798e8ca8e82e161a4 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Tue, 30 Mar 2021 18:25:38 +0200 Subject: [PATCH 08/24] Style - Multi-line function call not indented correctly --- src/RegEx.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/RegEx.php b/src/RegEx.php index cae22959..7a50ac72 100644 --- a/src/RegEx.php +++ b/src/RegEx.php @@ -45,9 +45,11 @@ public static function match($regex, $subject) return $result; } - return array_filter($match, function ($value, $key) { - return !is_numeric($key) && !empty($value); - }, ARRAY_FILTER_USE_BOTH + return array_filter( + $match, + function ($value, $key) { + return !is_numeric($key) && !empty($value); + }, ARRAY_FILTER_USE_BOTH ); } From b0b9556bf22a142e9d393490b19d0ebb33c2251d Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Tue, 30 Mar 2021 20:08:10 +0200 Subject: [PATCH 09/24] Build - Remove PHPUnit patch --- .drone.jsonnet | 2 -- .drone.yml | 22 +--------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 75966fec..108fc8e7 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -19,8 +19,6 @@ local composer(phpversion, params) = { commands: [ "php -v", "composer update " + params, - if phpversion == "8.0" then "wget https://ci.joomla.org/artifacts/phpunit8_php8_match.patch", - if phpversion == "8.0" then "patch -N -p0 < phpunit8_php8_match.patch" ] }; diff --git a/.drone.yml b/.drone.yml index abbbbe0c..3c383427 100644 --- a/.drone.yml +++ b/.drone.yml @@ -99,8 +99,6 @@ steps: commands: - php -v - composer update --prefer-stable - - "" - - "" volumes: - name: composer-cache path: /tmp/composer-cache @@ -129,8 +127,6 @@ steps: commands: - php -v - composer update --prefer-stable - - "" - - "" volumes: - name: composer-cache path: /tmp/composer-cache @@ -159,8 +155,6 @@ steps: commands: - php -v - composer update --prefer-stable - - "" - - "" volumes: - name: composer-cache path: /tmp/composer-cache @@ -189,8 +183,6 @@ steps: commands: - php -v - composer update --prefer-stable - - "" - - "" volumes: - name: composer-cache path: /tmp/composer-cache @@ -219,8 +211,6 @@ steps: commands: - php -v - composer update --prefer-stable - - "" - - "" volumes: - name: composer-cache path: /tmp/composer-cache @@ -249,8 +239,6 @@ steps: commands: - php -v - composer update --prefer-stable - - "" - - "" volumes: - name: composer-cache path: /tmp/composer-cache @@ -279,8 +267,6 @@ steps: commands: - php -v - composer update --prefer-stable - - "" - - "" volumes: - name: composer-cache path: /tmp/composer-cache @@ -309,8 +295,6 @@ steps: commands: - php -v - composer update --prefer-stable - - "" - - "" volumes: - name: composer-cache path: /tmp/composer-cache @@ -339,8 +323,6 @@ steps: commands: - php -v - composer update --prefer-stable - - "" - - "" volumes: - name: composer-cache path: /tmp/composer-cache @@ -369,8 +351,6 @@ steps: commands: - php -v - composer update --ignore-platform-reqs --prefer-stable - - wget https://ci.joomla.org/artifacts/phpunit8_php8_match.patch - - patch -N -p0 < phpunit8_php8_match.patch volumes: - name: composer-cache path: /tmp/composer-cache @@ -388,6 +368,6 @@ volumes: --- kind: signature -hmac: c8067167930c41f3511b5975c080a56617da9b3d7d082940094f8fe381fa0552 +hmac: 67a222df6e264e64c1bf1f4661ef9d236c08bb9e4943a9d9c5b00759b048f999 ... From 88e3d56aad5b33c7a854e2d2bfde749557bcb70e Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Wed, 31 Mar 2021 18:44:44 +0200 Subject: [PATCH 10/24] Tests - Add tests for IpHelper --- .gitignore | 1 + Tests/IpHelperTest.php | 263 +++++++++++++++++++++++++++++++++++++++++ composer.json | 2 +- src/IpHelper.php | 2 +- 4 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 Tests/IpHelperTest.php diff --git a/.gitignore b/.gitignore index fe5e21c9..698dc8ae 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ composer.lock phpunit.xml /.phpunit.result.cache /build/ +/work/ diff --git a/Tests/IpHelperTest.php b/Tests/IpHelperTest.php new file mode 100644 index 00000000..e0530ea5 --- /dev/null +++ b/Tests/IpHelperTest.php @@ -0,0 +1,263 @@ +backupServer = $_SERVER; + $this->backupEnv = $_ENV; + + unset($_SERVER['HTTP_CLIENT_IP']); + unset($_SERVER['HTTP_X_FORWARDED_FOR']); + unset($_SERVER['HTTP_X_FORWARDED']); + unset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']); + unset($_SERVER['HTTP_FORWARDED_FOR']); + unset($_SERVER['HTTP_FORWARDED']); + unset($_SERVER['REMOTE_ADDR']); + + IpHelper::setIp(null); + } + + /** + * Restore environment + */ + protected function tearDown() + { + $_SERVER = $this->backupServer; + $_ENV = $this->backupEnv; + } + + /** + * Sample client IPs + * + * @return array + */ + public function sampleClientIPs() + { + $indexes = array( + 'HTTP_X_FORWARDED_FOR', + 'HTTP_CLIENT_IP', + #'HTTP_X_FORWARDED', + #'HTTP_X_CLUSTER_CLIENT_IP', + #'HTTP_FORWARDED_FOR', + #'HTTP_FORWARDED', + 'REMOTE_ADDR', + ); + + // ip => normalised + $ips = array( + '127.0.0.1' => '127.0.0.1', + '192.168.178.32' => '192.168.178.32', + '10.194.95.79' => '10.194.95.79', + '75.184.124.93, 10.194.95.79' => '10.194.95.79', + '10.194.95.79, 75.184.124.93' => '75.184.124.93', + '0.0.0.0' => '0.0.0.0', + 'ff05::1' => 'ff05::1', + 'fake' => '', + ); + + $cases = array(); + + foreach ($indexes as $index) + { + foreach ($ips as $ip => $normalised) + { + $cases[] = array( + $index, + $ip, + $normalised + ); + } + } + + return $cases; + } + + /** + * @testdox IP address is retrieved from $_SERVER global + * + * @param string $index The index for the $_SERVER global + * @param string $ip The IP address in the global + * @param string $normalised The IP address to be returned + * + * @dataProvider sampleClientIPs + */ + public function testGetIpFromServerWithOverride($index, $ip, $normalised) + { + $_SERVER[$index] = $ip; + + IpHelper::setIp(null); + IpHelper::setAllowIpOverrides(true); + + $this->assertEquals($normalised, IpHelper::getIp()); + } + + /** + * @testdox IP address is retrieved from $_SERVER['REMOTE_ADDR'] if override is prohibited + * + * @param string $index The index for the $_SERVER global + * @param string $ip The IP address in the global + * @param string $normalised The IP address to be returned + * + * @dataProvider sampleClientIPs + */ + public function testGetIpFromServerWithoutOverride($index, $ip, $normalised) + { + $_SERVER[$index] = $ip; + $_SERVER['REMOTE_ADDR'] = '80.80.80.80'; + + IpHelper::setAllowIpOverrides(false); + + $this->assertEquals('80.80.80.80', IpHelper::getIp()); + } + + /** + * Sample IPs wit format information + * + * @return \string[][] + */ + public function sampleIPsWithFormat() + { + // ip => format + return array( + array('127.0.0.1', 'IPv4'), + array('::1', 'IPv6'), + array('::127.0.0.1', 'IPv6'), + array('fake:ip', 'invalid'), + ); + } + + /** + * @param string $ip The IP to check + * @param string $format The true format + * + * @dataProvider sampleIPsWithFormat + */ + public function testIsIp6($ip, $format) + { + $actual = IpHelper::isIPv6($ip); + $expected = $format === 'IPv6'; + + $this->assertEquals($expected, $actual); + } + + /** + * Sample IPs with IP Table information + * + * @return array[] + */ + public function sampleIPsWithTable() + { + // IP, IP Table, isInTable + return array( + 'IPv4 address - IPv4 address' => array(self::IPv4_ADDRESS, self::IPv4_ADDRESS, true), + + 'IPv4 address - IPv4 subnet' => array(self::IPv4_ADDRESS, self::IPv4_SUBNET, true), + 'IPv4 address - IPv4 network range' => array(self::IPv4_ADDRESS, self::IPv4_NETWORK_RANGE, true), + 'IPv4 address - IPv4 swapped range' => array(self::IPv4_ADDRESS, self::IPv4_SWAPPED_RANGE, true), + 'IPv4 address - IPv4 address/netmask' => array(self::IPv4_ADDRESS, self::IPv4_ADDRESS . '/' . self::IPv4_NETMASK, true), + 'IPv4 localhost - IPv4 subnets (list)' => array(self::IPv4_LOCALHOST, self::IPv4_SUBNET . ', ' . self::IPv4_LOCALHOST . '/8', true), + 'IPv4 localhost - IPv4 subnets (array)' => array(self::IPv4_LOCALHOST, array(self::IPv4_SUBNET, self::IPv4_LOCALHOST . '/8'), true), + + 'IPv4 address - 1 byte' => array(self::IPv4_LOCALHOST, '127.', true), + 'IPv4 address - 2 bytes' => array(self::IPv4_LOCALHOST, '127.0.', true), + 'IPv4 address - 3 bytes' => array(self::IPv4_LOCALHOST, '127.0.0.', true), + + 'IPv4 address - IPv6 expanded address' => array(self::IPv4_ADDRESS, self::IPv6_EXPANDED_ADDRESS, false), + 'IPv4 address - IPv6 subnet' => array(self::IPv4_ADDRESS, self::IPv6_SUBNET, false), + 'IPv4 address - IPv6 network range' => array(self::IPv4_ADDRESS, self::IPv6_NETWORK_RANGE, false), + + 'IPv4 any address - IPv4 subnet' => array(self::IPv4_ANY_ADDRESS, self::IPv4_SUBNET, false), + 'IPv4 localhost - IPv4 subnet' => array(self::IPv4_LOCALHOST, self::IPv4_SUBNET, false), + + 'empty - IPv4 subnet' => array(null, self::IPv4_SUBNET, false), + 'fake.ip - IPv4 subnet' => array('fake.ip', self::IPv4_SUBNET, false), + 'IPv4 address - empty range' => array(self::IPv4_ADDRESS, null, false), + 'IPv4 address - invalid.ip/range' => array(self::IPv4_ADDRESS, 'invalid.ip/range', false), + + 'IPv6 expanded address - IPv6 expanded address' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_EXPANDED_ADDRESS, true), + 'IPv6 expanded address - IPv6 compressed address' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_COMPRESSED_ADDRESS, true), + 'IPv6 compressed address - IPv6 expanded address' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_EXPANDED_ADDRESS, true), + 'IPv6 compressed address - IPv6 compressed address' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_COMPRESSED_ADDRESS, true), + + 'IPv6 expanded address - IPv6 subnet' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_SUBNET, true), + 'IPv6 expanded address - IPv6 network range' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_NETWORK_RANGE, true), + 'IPv6 expanded address - IPv6 swapped range' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_SWAPPED_RANGE, true), + 'IPv6 expanded address - IPv6 address/netmask' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_EXPANDED_ADDRESS . '/' . self::IPv6_NETMASK, true), + 'IPv6 compressed address - IPv6 subnet' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_SUBNET, true), + 'IPv6 compressed address - IPv6 network range' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_NETWORK_RANGE, true), + 'IPv6 compressed address - IPv6 swapped range' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_SWAPPED_RANGE, true), + 'IPv6 compressed address - IPv6 address/netmask' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_EXPANDED_ADDRESS . '/' . self::IPv6_NETMASK, true), + 'IPv6 localhost - IPv6 subnets (list)' => array(self::IPv6_LOCALHOST, self::IPv6_SUBNET . ', ' . self::IPv6_LOCALHOST . '/128', true), + 'IPv6 localhost - IPv6 subnets (array)' => array(self::IPv6_LOCALHOST, array(self::IPv6_SUBNET, self::IPv6_LOCALHOST . '/128'), true), + + 'IPv6 address - IPv4 address' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv4_ADDRESS, false), + 'IPv6 address - IPv4 subnet' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv4_SUBNET, false), + 'IPv6 address - IPv4 network range' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv4_NETWORK_RANGE, false), + + 'IPv6 any address - IPv6 subnet' => array(self::IPv6_ANY_ADDRESS, self::IPv6_SUBNET, false), + 'IPv6 localhost - IPv6 subnet' => array(self::IPv6_LOCALHOST, self::IPv6_SUBNET, false), + + 'empty - IPv6 subnet' => array(null, self::IPv6_SUBNET, false), + 'fake:ip - IPv6 subnet' => array('fake:ip', self::IPv6_SUBNET, false), + 'IPv6 address - empty range' => array(self::IPv6_COMPRESSED_ADDRESS, null, false), + 'IPv6 address - invalid:ip/range' => array(self::IPv6_COMPRESSED_ADDRESS, 'invalid:ip/range', false), + ); + } + + /** + * @param string $ip + * @param string $ipTable + * @param boolean $expected + * + * @dataProvider sampleIPsWithTable + */ + public function testIpInList($ip, $ipTable, $expected) + { + $this->assertEquals($expected, IpHelper::IPinList($ip, $ipTable)); + } +} diff --git a/composer.json b/composer.json index 855d7237..e48a9e43 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ }, "require-dev": { "joomla/coding-standards": "~2.0@alpha", - "phpunit/phpunit": "^4.8.35|^5.4.3|~6.0|^7.0|^8.0" + "phpunit/phpunit": "^4.8.35|^5.4.3|~6.0|^7.0" }, "autoload": { "psr-4": { diff --git a/src/IpHelper.php b/src/IpHelper.php index b66758a1..d69c8707 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -149,7 +149,7 @@ public static function IPinList($ip, $ipTable = '') // Sanity check if (!\function_exists('inet_pton')) { - return false; + return false; // @codeCoverageIgnore } // Get the IP's in_adds representation From 78d9c5353a71186108f0251d564c2508dc09d66e Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 13:42:52 +0200 Subject: [PATCH 11/24] Tests - Add test cases for partial invalid network range --- Tests/IpHelperTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/IpHelperTest.php b/Tests/IpHelperTest.php index e0530ea5..9122cdd8 100644 --- a/Tests/IpHelperTest.php +++ b/Tests/IpHelperTest.php @@ -218,6 +218,7 @@ public function sampleIPsWithTable() 'fake.ip - IPv4 subnet' => array('fake.ip', self::IPv4_SUBNET, false), 'IPv4 address - empty range' => array(self::IPv4_ADDRESS, null, false), 'IPv4 address - invalid.ip/range' => array(self::IPv4_ADDRESS, 'invalid.ip/range', false), + 'IPv4 address - partial invalid range' => array(self::IPv4_ADDRESS, self::IPv4_ADDRESS . '-invalid', false), 'IPv6 expanded address - IPv6 expanded address' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_EXPANDED_ADDRESS, true), 'IPv6 expanded address - IPv6 compressed address' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_COMPRESSED_ADDRESS, true), @@ -246,6 +247,7 @@ public function sampleIPsWithTable() 'fake:ip - IPv6 subnet' => array('fake:ip', self::IPv6_SUBNET, false), 'IPv6 address - empty range' => array(self::IPv6_COMPRESSED_ADDRESS, null, false), 'IPv6 address - invalid:ip/range' => array(self::IPv6_COMPRESSED_ADDRESS, 'invalid:ip/range', false), + 'IPv6 address - partial invalid range' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_COMPRESSED_ADDRESS . '-invalid', false), ); } From a53173f8bc33109a62561c5288c0a83bb8e8c53d Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 14:21:52 +0200 Subject: [PATCH 12/24] Tests - Remove code coverage directive --- src/IpHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index d69c8707..b66758a1 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -149,7 +149,7 @@ public static function IPinList($ip, $ipTable = '') // Sanity check if (!\function_exists('inet_pton')) { - return false; // @codeCoverageIgnore + return false; } // Get the IP's in_adds representation From 0c9b91da0c7c636b384501022a67d7be56ad8375 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 14:46:00 +0200 Subject: [PATCH 13/24] Refactoring - Deprecate IP cache, which made IpHelper a Singleton --- src/IpHelper.php | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index b66758a1..4042154a 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -9,22 +9,20 @@ namespace Joomla\Utilities; /** - * IpHelper is a utility class for processing IP addresses - * - * This class is adapted from the `FOFUtilsIp` class distributed with the Joomla! CMS as part of the FOF library by Akeeba Ltd. - * The original class is copyright of Nicholas K. Dionysopoulos / Akeeba Ltd. + * Utility class for processing IP addresses * * @since 1.6.0 */ -final class IpHelper +abstract class IpHelper { /** * The IP address of the current visitor * * @var string * @since 1.6.0 + * @deprecated 2.0 If you want to cache the IP address, you should handle that yourself. */ - private static $ip = null; + private static $ip; /** * Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers? @@ -35,15 +33,6 @@ final class IpHelper */ private static $allowIpOverrides = true; - /** - * Private constructor to prevent instantiation of this class - * - * @since 1.6.0 - */ - private function __construct() - { - } - /** * Get the current visitor's IP address * @@ -53,24 +42,19 @@ private function __construct() */ public static function getIp() { - if (self::$ip === null) + $ip = static::detectAndCleanIP(); + + if (!empty($ip) && ($ip != '0.0.0.0') && \function_exists('inet_pton') && \function_exists('inet_ntop')) { - $ip = static::detectAndCleanIP(); + $myIP = @inet_pton($ip); - if (!empty($ip) && ($ip != '0.0.0.0') && \function_exists('inet_pton') && \function_exists('inet_ntop')) + if ($myIP !== false) { - $myIP = @inet_pton($ip); - - if ($myIP !== false) - { - $ip = inet_ntop($myIP); - } + $ip = inet_ntop($myIP); } - - static::setIp($ip); } - return self::$ip; + return $ip; } /** @@ -81,6 +65,7 @@ public static function getIp() * @return void * * @since 1.6.0 + * @deprecated 2.0 If you want to cache the IP address, you should handle that yourself. */ public static function setIp($ip) { @@ -383,6 +368,7 @@ public static function IPinList($ip, $ipTable = '') * @return void * * @since 1.6.0 + * @deprecated 2.0 No replacement, this is never used */ public static function workaroundIPIssues() { From bbd587f53831a3090f5d1039b0759d878a80641d Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 14:54:18 +0200 Subject: [PATCH 14/24] Refactoring - Deprecate global allowOverride, which made IpHelper a Singleton --- src/IpHelper.php | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 4042154a..bc2131fc 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -29,7 +29,7 @@ abstract class IpHelper * * @var boolean * @since 1.6.0 - * @note The default value is false in version 2.0+ + * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. */ private static $allowIpOverrides = true; @@ -40,9 +40,14 @@ abstract class IpHelper * * @since 1.6.0 */ - public static function getIp() + public static function getIp($allowOverride = null) { - $ip = static::detectAndCleanIP(); + // Remove this block in 2.0 and change the parameter's default value from null to false + if ($allowOverride === null) { + $allowOverride = self::$allowIpOverrides; + } + + $ip = static::detectAndCleanIP($allowOverride); if (!empty($ip) && ($ip != '0.0.0.0') && \function_exists('inet_pton') && \function_exists('inet_ntop')) { @@ -402,10 +407,11 @@ public static function workaroundIPIssues() * @return void * * @since 1.6.0 + * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = $newState ? true : false; + self::$allowIpOverrides = (bool) $newState; } /** @@ -418,13 +424,15 @@ public static function setAllowIpOverrides($newState) * * The solution used is assuming that the last IP address is the external one. * + * @param boolean $allowOverride + * * @return string * * @since 1.6.0 */ - protected static function detectAndCleanIP() + protected static function detectAndCleanIP($allowOverride) { - $ip = static::detectIP(); + $ip = static::detectIP($allowOverride); if (strstr($ip, ',') !== false || strstr($ip, ' ') !== false) { @@ -450,23 +458,25 @@ protected static function detectAndCleanIP() /** * Gets the visitor's IP address * + * @param boolean $allowOverride + * * @return string * * @since 1.6.0 */ - protected static function detectIP() + protected static function detectIP($allowOverride) { // Normally the $_SERVER superglobal is set if (isset($_SERVER)) { // Do we have an x-forwarded-for HTTP header (e.g. NginX)? - if (self::$allowIpOverrides && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) + if ($allowOverride && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { return $_SERVER['HTTP_X_FORWARDED_FOR']; } // Do we have a client-ip header (e.g. non-transparent proxy)? - if (self::$allowIpOverrides && isset($_SERVER['HTTP_CLIENT_IP'])) + if ($allowOverride && isset($_SERVER['HTTP_CLIENT_IP'])) { return $_SERVER['HTTP_CLIENT_IP']; } @@ -488,13 +498,13 @@ protected static function detectIP() } // Do we have an x-forwarded-for HTTP header? - if (self::$allowIpOverrides && getenv('HTTP_X_FORWARDED_FOR')) + if ($allowOverride && getenv('HTTP_X_FORWARDED_FOR')) { return getenv('HTTP_X_FORWARDED_FOR'); } // Do we have a client-ip header? - if (self::$allowIpOverrides && getenv('HTTP_CLIENT_IP')) + if ($allowOverride && getenv('HTTP_CLIENT_IP')) { return getenv('HTTP_CLIENT_IP'); } From 87e0b9e6106e14ab345e287727fb9fe45fd54f2d Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 15:01:39 +0200 Subject: [PATCH 15/24] Refactoring - Remove check for existence of inet_ntop and/or inet_pton - they are always present since PHP 5.1 --- src/IpHelper.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index bc2131fc..27f93f86 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -36,6 +36,8 @@ abstract class IpHelper /** * Get the current visitor's IP address * + * @param boolean|null $allowOverride If true, HTTP headers are taken into account + * * @return string * * @since 1.6.0 @@ -49,7 +51,7 @@ public static function getIp($allowOverride = null) $ip = static::detectAndCleanIP($allowOverride); - if (!empty($ip) && ($ip != '0.0.0.0') && \function_exists('inet_pton') && \function_exists('inet_ntop')) + if (!empty($ip) && $ip != '0.0.0.0') { $myIP = @inet_pton($ip); @@ -136,12 +138,6 @@ public static function IPinList($ip, $ipTable = '') return false; } - // Sanity check - if (!\function_exists('inet_pton')) - { - return false; - } - // Get the IP's in_adds representation $myIP = @inet_pton($ip); From 2e0ad6edafd4379be54574d56448b2ac2c2ab8bc Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 15:05:22 +0200 Subject: [PATCH 16/24] Refactoring - Remove check for existence of getenv - it is always present since PHP 4 --- src/IpHelper.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 27f93f86..33ca6227 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -384,12 +384,9 @@ public static function workaroundIPIssues() { $_SERVER['JOOMLA_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR']; } - elseif (\function_exists('getenv')) + elseif (getenv('REMOTE_ADDR')) { - if (getenv('REMOTE_ADDR')) - { - $_SERVER['JOOMLA_REMOTE_ADDR'] = getenv('REMOTE_ADDR'); - } + $_SERVER['JOOMLA_REMOTE_ADDR'] = getenv('REMOTE_ADDR'); } $_SERVER['REMOTE_ADDR'] = $ip; @@ -484,15 +481,6 @@ protected static function detectIP($allowOverride) } } - /* - * This part is executed on PHP running as CGI, or on SAPIs which do not set the $_SERVER superglobal - * If getenv() is disabled, you're screwed - */ - if (!\function_exists('getenv')) - { - return ''; - } - // Do we have an x-forwarded-for HTTP header? if ($allowOverride && getenv('HTTP_X_FORWARDED_FOR')) { From fe93af7de071186047a56bd0d280518dcf2b158c Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 15:09:21 +0200 Subject: [PATCH 17/24] Refactoring - Use strpos instead of strstr (saves memory), use strict comparision --- src/IpHelper.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 33ca6227..8c5ab75a 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -51,7 +51,7 @@ public static function getIp($allowOverride = null) $ip = static::detectAndCleanIP($allowOverride); - if (!empty($ip) && $ip != '0.0.0.0') + if (!empty($ip) && $ip !== '0.0.0.0') { $myIP = @inet_pton($ip); @@ -154,7 +154,7 @@ public static function IPinList($ip, $ipTable = '') $ipExpression = trim($ipExpression); // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127 - if (strstr($ipExpression, '-')) + if (strpos($ipExpression, '-') !== false) { list($from, $to) = explode('-', $ipExpression, 2); @@ -191,7 +191,7 @@ public static function IPinList($ip, $ipTable = '') } } // Netmask or CIDR provided - elseif (strstr($ipExpression, '/')) + elseif (strpos($ipExpression, '/') !== false) { $binaryip = static::inetToBits($myIP); @@ -209,7 +209,7 @@ public static function IPinList($ip, $ipTable = '') continue; } - if ($ipv6 && strstr($maskbits, ':')) + if ($ipv6 && strpos($maskbits, ':') !== false) { // Perform an IPv6 CIDR check if (static::checkIPv6CIDR($myIP, $ipExpression)) @@ -221,7 +221,7 @@ public static function IPinList($ip, $ipTable = '') continue; } - if (!$ipv6 && strstr($maskbits, '.')) + if (!$ipv6 && strpos($maskbits, '.') !== false) { // Convert IPv4 netmask to CIDR $long = ip2long($maskbits); @@ -270,7 +270,7 @@ public static function IPinList($ip, $ipTable = '') continue; } - if ($ipCheck == $myIP) + if ($ipCheck === $myIP) { return true; } @@ -280,12 +280,12 @@ public static function IPinList($ip, $ipTable = '') // Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123] $dots = 0; - if (substr($ipExpression, -1) == '.') + if (substr($ipExpression, -1) === '.') { // Partial IP address. Convert to CIDR and re-match foreach (count_chars($ipExpression, 1) as $i => $val) { - if ($i == 46) + if ($i === 46) { $dots = $val; } @@ -351,7 +351,7 @@ public static function IPinList($ip, $ipTable = '') { $ip = @inet_pton(trim($ipExpression)); - if ($ip == $myIP) + if ($ip === $myIP) { return true; } @@ -427,7 +427,7 @@ protected static function detectAndCleanIP($allowOverride) { $ip = static::detectIP($allowOverride); - if (strstr($ip, ',') !== false || strstr($ip, ' ') !== false) + if (strpos($ip, ',') !== false || strpos($ip, ' ') !== false) { $ip = str_replace(' ', ',', $ip); $ip = str_replace(',,', ',', $ip); @@ -514,7 +514,7 @@ protected static function detectIP($allowOverride) */ protected static function inetToBits($inet) { - if (\strlen($inet) == 4) + if (\strlen($inet) === 4) { $unpacked = unpack('A4', $inet); } From 55f49b1806bc70360a406f4f5375490e02522b8a Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 16:38:49 +0200 Subject: [PATCH 18/24] Refactoring - Simplify and harden IP detection --- src/IpHelper.php | 288 +++++++++++++++++++++-------------------------- 1 file changed, 129 insertions(+), 159 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 8c5ab75a..6509d59a 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -19,7 +19,7 @@ abstract class IpHelper * The IP address of the current visitor * * @var string - * @since 1.6.0 + * @since 1.6.0 * @deprecated 2.0 If you want to cache the IP address, you should handle that yourself. */ private static $ip; @@ -28,7 +28,7 @@ abstract class IpHelper * Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers? * * @var boolean - * @since 1.6.0 + * @since 1.6.0 * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. */ private static $allowIpOverrides = true; @@ -36,7 +36,7 @@ abstract class IpHelper /** * Get the current visitor's IP address * - * @param boolean|null $allowOverride If true, HTTP headers are taken into account + * @param boolean $allowOverride If true, HTTP headers are taken into account * * @return string * @@ -44,24 +44,13 @@ abstract class IpHelper */ public static function getIp($allowOverride = null) { - // Remove this block in 2.0 and change the parameter's default value from null to false - if ($allowOverride === null) { - $allowOverride = self::$allowIpOverrides; - } - - $ip = static::detectAndCleanIP($allowOverride); - - if (!empty($ip) && $ip !== '0.0.0.0') + // @todo Remove this block in 2.0 and change the parameter's default value from null to false + if ($allowOverride === null) { - $myIP = @inet_pton($ip); - - if ($myIP !== false) - { - $ip = inet_ntop($myIP); - } + $allowOverride = self::$allowIpOverrides; } - return $ip; + return static::detectAndCleanIP($allowOverride); } /** @@ -71,7 +60,7 @@ public static function getIp($allowOverride = null) * * @return void * - * @since 1.6.0 + * @since 1.6.0 * @deprecated 2.0 If you want to cache the IP address, you should handle that yourself. */ public static function setIp($ip) @@ -82,7 +71,7 @@ public static function setIp($ip) /** * Is it an IPv6 IP address? * - * @param string $ip An IPv4 or IPv6 address + * @param string $ip An IPv4 or IPv6 address * * @return boolean * @@ -90,7 +79,7 @@ public static function setIp($ip) */ public static function isIPv6($ip) { - return strpos($ip, ':') !== false; + return filter_var(trim($ip), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false; } /** @@ -251,110 +240,112 @@ public static function IPinList($ip, $ipTable = '') return true; } } - else + elseif ($ipv6) { // IPv6: Only single IPs are supported - if ($ipv6) - { - $ipExpression = trim($ipExpression); + $ipExpression = trim($ipExpression); - if (!static::isIPv6($ipExpression)) - { - continue; - } - - $ipCheck = @inet_pton($ipExpression); + if (!static::isIPv6($ipExpression)) + { + continue; + } - if ($ipCheck === false) - { - continue; - } + $ipCheck = @inet_pton($ipExpression); - if ($ipCheck === $myIP) - { - return true; - } + if ($ipCheck === false) + { + continue; } - else + + if ($ipCheck === $myIP) { - // Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123] - $dots = 0; + return true; + } + } + else + { + // Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123] + $dots = 0; - if (substr($ipExpression, -1) === '.') + if (substr($ipExpression, -1) === '.') + { + // Partial IP address. Convert to CIDR and re-match + foreach (count_chars($ipExpression, 1) as $i => $val) { - // Partial IP address. Convert to CIDR and re-match - foreach (count_chars($ipExpression, 1) as $i => $val) + if ($i === 46) { - if ($i === 46) - { - $dots = $val; - } + $dots = $val; } + } - switch ($dots) - { - case 1: - $netmask = '255.0.0.0'; - $ipExpression .= '0.0.0'; + switch ($dots) + { + case 1: + $netmask = '255.0.0.0'; + $ipExpression .= '0.0.0'; - break; + break; - case 2: - $netmask = '255.255.0.0'; - $ipExpression .= '0.0'; + case 2: + $netmask = '255.255.0.0'; + $ipExpression .= '0.0'; - break; + break; - case 3: - $netmask = '255.255.255.0'; - $ipExpression .= '0'; + case 3: + $netmask = '255.255.255.0'; + $ipExpression .= '0'; - break; + break; - default: - $dots = 0; - } + default: + $dots = 0; + } - if ($dots) - { - $binaryip = static::inetToBits($myIP); + if ($dots) + { + $binaryip = static::inetToBits($myIP); - // Convert netmask to CIDR - $long = ip2long($netmask); - $base = ip2long('255.255.255.255'); - $maskbits = 32 - log(($long ^ $base) + 1, 2); + // Convert netmask to CIDR + $long = ip2long($netmask); + $base = ip2long('255.255.255.255'); + $maskbits = 32 - log(($long ^ $base) + 1, 2); - $net = @inet_pton($ipExpression); + $net = @inet_pton($ipExpression); - // Sanity check - if ($net === false) - { - continue; - } + // Sanity check + if ($net === false) + { + continue; + } - // Get the network's binary representation - $expectedNumberOfBits = $ipv6 ? 128 : 24; - $binarynet = str_pad(static::inetToBits($net), $expectedNumberOfBits, '0', STR_PAD_RIGHT); + // Get the network's binary representation + $expectedNumberOfBits = $ipv6 ? 128 : 24; + $binarynet = str_pad( + static::inetToBits($net), + $expectedNumberOfBits, + '0', + STR_PAD_RIGHT + ); - // Check the corresponding bits of the IP and the network - $ipNetBits = substr($binaryip, 0, $maskbits); - $netBits = substr($binarynet, 0, $maskbits); + // Check the corresponding bits of the IP and the network + $ipNetBits = substr($binaryip, 0, $maskbits); + $netBits = substr($binarynet, 0, $maskbits); - if ($ipNetBits === $netBits) - { - return true; - } + if ($ipNetBits === $netBits) + { + return true; } } + } - if (!$dots) - { - $ip = @inet_pton(trim($ipExpression)); + if (!$dots) + { + $ip = @inet_pton(trim($ipExpression)); - if ($ip === $myIP) - { - return true; - } + if ($ip === $myIP) + { + return true; } } } @@ -368,7 +359,7 @@ public static function IPinList($ip, $ipTable = '') * * @return void * - * @since 1.6.0 + * @since 1.6.0 * @deprecated 2.0 No replacement, this is never used */ public static function workaroundIPIssues() @@ -399,53 +390,51 @@ public static function workaroundIPIssues() * * @return void * - * @since 1.6.0 + * @since 1.6.0 * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = (bool) $newState; + self::$allowIpOverrides = (bool)$newState; } /** - * Gets the visitor's IP address. + * Get the visitor's IP address. * * Automatically handles reverse proxies reporting the IPs of intermediate devices, like load balancers. Examples: * - * - https://www.akeebabackup.com/support/admin-tools/13743-double-ip-adresses-in-security-exception-log-warnings.html * - https://stackoverflow.com/questions/2422395/why-is-request-envremote-addr-returning-two-ips * * The solution used is assuming that the last IP address is the external one. * * @param boolean $allowOverride * - * @return string + * @return string The validated IP address as provided. + * If no IP is available, an empty string is returned. * * @since 1.6.0 */ protected static function detectAndCleanIP($allowOverride) { - $ip = static::detectIP($allowOverride); + $rawIp = static::detectIP($allowOverride); + $ipList = preg_split('~,\s*~', $rawIp); - if (strpos($ip, ',') !== false || strpos($ip, ' ') !== false) - { - $ip = str_replace(' ', ',', $ip); - $ip = str_replace(',,', ',', $ip); - $ips = explode(',', $ip); - $ip = ''; + $ipList = array_reduce( + $ipList, + function ($list, $ip) { + $ip = filter_var(trim($ip), FILTER_VALIDATE_IP); - while (empty($ip) && !empty($ips)) - { - $ip = array_pop($ips); - $ip = trim($ip); - } - } - else - { - $ip = trim($ip); - } + if ($ip !== false) + { + $list[] = $ip; + } - return $ip; + return $list; + }, + array() + ); + + return (string) array_pop($ipList); } /** @@ -453,54 +442,35 @@ protected static function detectAndCleanIP($allowOverride) * * @param boolean $allowOverride * - * @return string + * @return string The IP address(es) as provided without validation. + * If no IP is available, an empty string is returned. * * @since 1.6.0 */ protected static function detectIP($allowOverride) { - // Normally the $_SERVER superglobal is set - if (isset($_SERVER)) + // Order matters! + $indexes = array( + 'REMOTE_ADDR', + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + ); + + if (!$allowOverride) { - // Do we have an x-forwarded-for HTTP header (e.g. NginX)? - if ($allowOverride && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) - { - return $_SERVER['HTTP_X_FORWARDED_FOR']; - } - - // Do we have a client-ip header (e.g. non-transparent proxy)? - if ($allowOverride && isset($_SERVER['HTTP_CLIENT_IP'])) - { - return $_SERVER['HTTP_CLIENT_IP']; - } - - // Normal, non-proxied server or server behind a transparent proxy - if (isset($_SERVER['REMOTE_ADDR'])) - { - return $_SERVER['REMOTE_ADDR']; - } + $ip = ArrayHelper::getValue($_SERVER, 'REMOTE_ADDR', getenv('REMOTE_ADDR')); } - - // Do we have an x-forwarded-for HTTP header? - if ($allowOverride && getenv('HTTP_X_FORWARDED_FOR')) - { - return getenv('HTTP_X_FORWARDED_FOR'); - } - - // Do we have a client-ip header? - if ($allowOverride && getenv('HTTP_CLIENT_IP')) + else { - return getenv('HTTP_CLIENT_IP'); - } + $ip = ''; - // Normal, non-proxied server or server behind a transparent proxy - if (getenv('REMOTE_ADDR')) - { - return getenv('REMOTE_ADDR'); + foreach ($indexes as $index) + { + $ip = ArrayHelper::getValue($_SERVER, $index, $ip); + } } - // Catch-all case for broken servers, apparently - return ''; + return $ip; } /** @@ -550,8 +520,8 @@ protected static function checkIPv6CIDR($ip, $cidrnet) $binaryip = static::inetToBits($ip); list($net, $maskbits) = explode('/', $cidrnet); - $net = inet_pton($net); - $binarynet = static::inetToBits($net); + $net = inet_pton($net); + $binarynet = static::inetToBits($net); $ipNetBits = substr($binaryip, 0, $maskbits); $netBits = substr($binarynet, 0, $maskbits); From bdcb305c2fa4ea1231b8a525ba5aa55bacdc0edb Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 20:00:34 +0200 Subject: [PATCH 19/24] Refactoring - Complete refactoring --- Tests/IpHelperTest.php | 10 +- src/IpHelper.php | 435 +++++++++++++++++------------------------ 2 files changed, 182 insertions(+), 263 deletions(-) diff --git a/Tests/IpHelperTest.php b/Tests/IpHelperTest.php index 9122cdd8..ba4b025e 100644 --- a/Tests/IpHelperTest.php +++ b/Tests/IpHelperTest.php @@ -59,7 +59,7 @@ protected function setUp() unset($_SERVER['HTTP_FORWARDED']); unset($_SERVER['REMOTE_ADDR']); - IpHelper::setIp(null); + IpHelper::setIP(null); } /** @@ -130,10 +130,10 @@ public function testGetIpFromServerWithOverride($index, $ip, $normalised) { $_SERVER[$index] = $ip; - IpHelper::setIp(null); + IpHelper::setIP(null); IpHelper::setAllowIpOverrides(true); - $this->assertEquals($normalised, IpHelper::getIp()); + $this->assertEquals($normalised, IpHelper::getIP()); } /** @@ -152,7 +152,7 @@ public function testGetIpFromServerWithoutOverride($index, $ip, $normalised) IpHelper::setAllowIpOverrides(false); - $this->assertEquals('80.80.80.80', IpHelper::getIp()); + $this->assertEquals('80.80.80.80', IpHelper::getIP()); } /** @@ -260,6 +260,6 @@ public function sampleIPsWithTable() */ public function testIpInList($ip, $ipTable, $expected) { - $this->assertEquals($expected, IpHelper::IPinList($ip, $ipTable)); + $this->assertEquals($expected, IpHelper::isInRanges($ip, $ipTable)); } } diff --git a/src/IpHelper.php b/src/IpHelper.php index 6509d59a..267d4d9e 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -42,7 +42,7 @@ abstract class IpHelper * * @since 1.6.0 */ - public static function getIp($allowOverride = null) + public static function getIP($allowOverride = null) { // @todo Remove this block in 2.0 and change the parameter's default value from null to false if ($allowOverride === null) @@ -63,7 +63,7 @@ public static function getIp($allowOverride = null) * @since 1.6.0 * @deprecated 2.0 If you want to cache the IP address, you should handle that yourself. */ - public static function setIp($ip) + public static function setIP($ip) { self::$ip = $ip; } @@ -85,273 +85,120 @@ public static function isIPv6($ip) /** * Checks if an IP is contained in a list of IPs or IP expressions * - * @param string $ip The IPv4/IPv6 address to check - * @param array|string $ipTable An IP expression (or a comma-separated or array list of IP expressions) to check against + * @param string $ip The IPv4/IPv6 address to check + * @param array|string $ipRanges A comma-separated list or array of IP ranges to check against. + * Range may be specified as from-to, CIDR or IP with netmask. * * @return boolean * * @since 1.6.0 */ - public static function IPinList($ip, $ipTable = '') + public static function isInRanges($ip, $ipRanges = '') { - // No point proceeding with an empty IP list - if (empty($ipTable)) + // Reject empty IPs or ANY_ADDRESS + if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { return false; } - // If the IP list is not an array, convert it to an array - if (!\is_array($ipTable)) - { - if (strpos($ipTable, ',') !== false) - { - $ipTable = explode(',', $ipTable); - $ipTable = array_map('trim', $ipTable); - } - else - { - $ipTable = trim($ipTable); - $ipTable = array($ipTable); - } - } - - // If no IP address is found, return false - if ($ip === '0.0.0.0') + // IP can not be in an empty range + if (empty($ipRanges)) { return false; } - // If no IP is given, return false - if (empty($ip)) + // If the IP list is provided as string, convert it to an array + if (!\is_array($ipRanges)) { - return false; + $ipRanges = preg_split('~,\s*~', $ipRanges); } - // Get the IP's in_adds representation - $myIP = @inet_pton($ip); - - // If the IP is in an unrecognisable format, quite - if ($myIP === false) - { - return false; - } - - $ipv6 = static::isIPv6($ip); - - foreach ($ipTable as $ipExpression) - { - $ipExpression = trim($ipExpression); - - // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127 - if (strpos($ipExpression, '-') !== false) - { - list($from, $to) = explode('-', $ipExpression, 2); + $ipRanges = array_reduce( + $ipRanges, + function ($list, $range) { + $range = trim($range); - if ($ipv6 && (!static::isIPv6($from) || !static::isIPv6($to))) + if (!empty($range)) { - // Do not apply IPv4 filtering on an IPv6 address - continue; + $list[] = $range; } - if (!$ipv6 && (static::isIPv6($from) || static::isIPv6($to))) - { - // Do not apply IPv6 filtering on an IPv4 address - continue; - } - - $from = @inet_pton(trim($from)); - $to = @inet_pton(trim($to)); - - // Sanity check - if (($from === false) || ($to === false)) - { - continue; - } - - // Swap from/to if they're in the wrong order - if ($from > $to) - { - list($from, $to) = array($to, $from); - } + return $list; + }, + array() + ); - if (($myIP >= $from) && ($myIP <= $to)) - { - return true; - } - } - // Netmask or CIDR provided - elseif (strpos($ipExpression, '/') !== false) + foreach ($ipRanges as $ipRange) + { + if (self::isInRange($ip, $ipRange)) { - $binaryip = static::inetToBits($myIP); - - list($net, $maskbits) = explode('/', $ipExpression, 2); + return true; + } + } - if ($ipv6 && !static::isIPv6($net)) - { - // Do not apply IPv4 filtering on an IPv6 address - continue; - } + return false; + } - if (!$ipv6 && static::isIPv6($net)) - { - // Do not apply IPv6 filtering on an IPv4 address - continue; - } + private static function isInRange($ip, $ipRange) + { + // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127 + if (strpos($ipRange, '-') !== false) + { + list($from, $to) = preg_split('~\s*-\s*~', $ipRange, 2); - if ($ipv6 && strpos($maskbits, ':') !== false) - { - // Perform an IPv6 CIDR check - if (static::checkIPv6CIDR($myIP, $ipExpression)) - { - return true; - } - - // If we didn't match it proceed to the next expression - continue; - } + return self::isInExplicitRange($ip, $from, $to); + } - if (!$ipv6 && strpos($maskbits, '.') !== false) - { - // Convert IPv4 netmask to CIDR - $long = ip2long($maskbits); - $base = ip2long('255.255.255.255'); - $maskbits = 32 - log(($long ^ $base) + 1, 2); - } + // Netmask or CIDR provided + if (strpos($ipRange, '/') !== false) + { + list($net, $mask) = explode('/', $ipRange, 2); - // Convert network IP to in_addr representation - $net = @inet_pton($net); + // CIDR + if (is_numeric($mask)) + { + return self::isInCidrRange($ip, $net, $mask); + } - // Sanity check - if ($net === false) - { - continue; - } + // Netmask + return self::isInNetmaskRange($ip, $net, $mask); + } - // Get the network's binary representation - $expectedNumberOfBits = $ipv6 ? 128 : 24; - $binarynet = str_pad(static::inetToBits($net), $expectedNumberOfBits, '0', STR_PAD_RIGHT); + // Partial IP address, i.e. 123.[123.[123.]] + if (!self::isIPv6($ip) && preg_match('~\.$~', $ipRange)) + { + $segments = explode('.', $ipRange); - // Check the corresponding bits of the IP and the network - $ipNetBits = substr($binaryip, 0, $maskbits); - $netBits = substr($binarynet, 0, $maskbits); + // Drop empty segment + array_pop($segments); - if ($ipNetBits === $netBits) - { - return true; - } - } - elseif ($ipv6) + if (count($segments) > 3) { - // IPv6: Only single IPs are supported - $ipExpression = trim($ipExpression); - - if (!static::isIPv6($ipExpression)) - { - continue; - } - - $ipCheck = @inet_pton($ipExpression); + return false; + } - if ($ipCheck === false) - { - continue; - } + $mask = count($segments) * 8; - if ($ipCheck === $myIP) - { - return true; - } - } - else + while (count($segments) < 4) { - // Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123] - $dots = 0; + $segments[] = 0; + } - if (substr($ipExpression, -1) === '.') - { - // Partial IP address. Convert to CIDR and re-match - foreach (count_chars($ipExpression, 1) as $i => $val) - { - if ($i === 46) - { - $dots = $val; - } - } - - switch ($dots) - { - case 1: - $netmask = '255.0.0.0'; - $ipExpression .= '0.0.0'; - - break; - - case 2: - $netmask = '255.255.0.0'; - $ipExpression .= '0.0'; - - break; - - case 3: - $netmask = '255.255.255.0'; - $ipExpression .= '0'; - - break; - - default: - $dots = 0; - } - - if ($dots) - { - $binaryip = static::inetToBits($myIP); - - // Convert netmask to CIDR - $long = ip2long($netmask); - $base = ip2long('255.255.255.255'); - $maskbits = 32 - log(($long ^ $base) + 1, 2); - - $net = @inet_pton($ipExpression); - - // Sanity check - if ($net === false) - { - continue; - } - - // Get the network's binary representation - $expectedNumberOfBits = $ipv6 ? 128 : 24; - $binarynet = str_pad( - static::inetToBits($net), - $expectedNumberOfBits, - '0', - STR_PAD_RIGHT - ); - - // Check the corresponding bits of the IP and the network - $ipNetBits = substr($binaryip, 0, $maskbits); - $netBits = substr($binarynet, 0, $maskbits); - - if ($ipNetBits === $netBits) - { - return true; - } - } - } + $prefix = implode('.', $segments); - if (!$dots) - { - $ip = @inet_pton(trim($ipExpression)); + return self::isInCidrRange($ip, $prefix, $mask); + } - if ($ip === $myIP) - { - return true; - } - } - } + // Range is a single IP + $binaryIp = self::toBits($ip); + $binaryRange = self::toBits($ipRange); + + if (empty($binaryIp) || empty($binaryRange)) + { + return false; } - return false; + return $binaryIp === $binaryRange; } /** @@ -360,11 +207,12 @@ public static function IPinList($ip, $ipTable = '') * @return void * * @since 1.6.0 + * @codeCoverageIgnore * @deprecated 2.0 No replacement, this is never used */ public static function workaroundIPIssues() { - $ip = static::getIp(); + $ip = static::getIP(); if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === $ip) { @@ -395,7 +243,7 @@ public static function workaroundIPIssues() */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = (bool)$newState; + self::$allowIpOverrides = (bool) $newState; } /** @@ -414,9 +262,9 @@ public static function setAllowIpOverrides($newState) * * @since 1.6.0 */ - protected static function detectAndCleanIP($allowOverride) + private static function detectAndCleanIP($allowOverride) { - $rawIp = static::detectIP($allowOverride); + $rawIp = static::detectIP($allowOverride); $ipList = preg_split('~,\s*~', $rawIp); $ipList = array_reduce( @@ -434,7 +282,7 @@ function ($list, $ip) { array() ); - return (string) array_pop($ipList); + return (string)array_pop($ipList); } /** @@ -447,7 +295,7 @@ function ($list, $ip) { * * @since 1.6.0 */ - protected static function detectIP($allowOverride) + private static function detectIP($allowOverride) { // Order matters! $indexes = array( @@ -476,56 +324,127 @@ protected static function detectIP($allowOverride) /** * Converts inet_pton output to bits string * - * @param string $inet The in_addr representation of an IPv4 or IPv6 address + * @param string $ip The IPv4 or IPv6 address * * @return string * * @since 1.6.0 */ - protected static function inetToBits($inet) + private static function toBits($ip) { - if (\strlen($inet) === 4) - { - $unpacked = unpack('A4', $inet); - } - else + $packedIp = inet_pton($ip); + + if ($packedIp === false) { - $unpacked = unpack('A16', $inet); + return ''; } + $length = self::isIPv6($ip) ? 16 : 4; + $unpacked = unpack('A' . $length, $packedIp); $unpacked = str_split($unpacked[1]); - $binaryip = ''; + $binaryIp = ''; foreach ($unpacked as $char) { - $binaryip .= str_pad(decbin(\ord($char)), 8, '0', STR_PAD_LEFT); + $binaryIp .= str_pad(decbin(\ord($char)), 8, '0', STR_PAD_LEFT); } - return $binaryip; + $binaryIp = str_pad($binaryIp, $length * 8, '0', STR_PAD_RIGHT); + + return $binaryIp; } /** - * Checks if an IPv6 address $ip is part of the IPv6 CIDR block $cidrnet + * Check if two IP addresses have the same IP format * - * @param string $ip The IPv6 address to check, e.g. 21DA:00D3:0000:2F3B:02AC:00FF:FE28:9C5A - * @param string $cidrnet The IPv6 CIDR block, e.g. 21DA:00D3:0000:2F3B::/64 + * @param string $ip1 The first IP address + * @param string $ip2 The second IP address + * + * @return boolean + */ + private static function ipVersionMatch($ip1, $ip2) + { + return self::isIPv6($ip1) === self::isIPv6($ip2); + } + + /** + * @param string $ip The IP address to check + * @param string $from Lower bound of the range + * @param string $to Upper bound of the range * * @return boolean + */ + private static function isInExplicitRange($ip, $from, $to) + { + if (!self::ipVersionMatch($ip, $from) || !self::ipVersionMatch($ip, $to)) + { + return false; + } + + $binaryFrom = self::toBits($from); + $binaryTo = self::toBits($to); + $binaryIp = self::toBits($ip); + + if (empty($binaryFrom) || empty($binaryTo) || empty($binaryIp)) + { + return false; + } + + // Swap from/to if they're in the wrong order + if ($binaryFrom > $binaryTo) + { + list($binaryFrom, $binaryTo) = array($binaryTo, $binaryFrom); + } + + return $binaryFrom <= $binaryIp && $binaryIp <= $binaryTo; + } + + /** + * @param string $ip + * @param string $prefix + * @param integer $mask * - * @since 1.6.0 + * @return boolean + */ + private static function isInCidrRange($ip, $prefix, $mask) + { + if (!self::ipVersionMatch($ip, $prefix)) + { + return false; + } + + $binaryIp = static::toBits($ip); + $binaryPrefix = static::toBits($prefix); + + if (empty($binaryIp) || empty($binaryPrefix)) + { + return false; + } + + $maskedIp = substr($binaryIp, 0, $mask); + $maskedPrefix = substr($binaryPrefix, 0, $mask); + + return $maskedIp === $maskedPrefix; + } + + /** + * @param string $ip + * @param string $prefix + * @param string $netmask + * + * @return boolean */ - protected static function checkIPv6CIDR($ip, $cidrnet) + private static function isInNetmaskRange($ip, $prefix, $netmask) { - $ip = inet_pton($ip); - $binaryip = static::inetToBits($ip); + $binaryMask = self::toBits($netmask); - list($net, $maskbits) = explode('/', $cidrnet); - $net = inet_pton($net); - $binarynet = static::inetToBits($net); + if (empty($binaryMask)) + { + return false; + } - $ipNetBits = substr($binaryip, 0, $maskbits); - $netBits = substr($binarynet, 0, $maskbits); + $mask = strlen(str_replace('0', '', $binaryMask)); - return $ipNetBits === $netBits; + return self::isInCidrRange($ip, $prefix, $mask); } } From cfabc8eae4e26363871265cbfab46bed09e26be1 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 20:03:58 +0200 Subject: [PATCH 20/24] Refactoring - Prevent inet_pton from emitting a warning --- src/IpHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 267d4d9e..e2bde8fb 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -322,7 +322,7 @@ private static function detectIP($allowOverride) } /** - * Converts inet_pton output to bits string + * Converts IP address to bits string * * @param string $ip The IPv4 or IPv6 address * @@ -332,7 +332,7 @@ private static function detectIP($allowOverride) */ private static function toBits($ip) { - $packedIp = inet_pton($ip); + $packedIp = @inet_pton($ip); if ($packedIp === false) { From 4a6c2871bcc05dcd6eaa345e4078288e0d7bab54 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 23:30:57 +0200 Subject: [PATCH 21/24] Style - Add comments --- src/IpHelper.php | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index e2bde8fb..45026464 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -139,6 +139,14 @@ function ($list, $range) { return false; } + /** + * Check if an IP is in a given range + * + * @param string $ip The IP to check + * @param string $ipRange The IP range; may be specified as from-to, CIDR or IP with netmask. + * + * @return bool + */ private static function isInRange($ip, $ipRange) { // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127 @@ -243,7 +251,7 @@ public static function workaroundIPIssues() */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = (bool) $newState; + self::$allowIpOverrides = (bool)$newState; } /** @@ -255,7 +263,7 @@ public static function setAllowIpOverrides($newState) * * The solution used is assuming that the last IP address is the external one. * - * @param boolean $allowOverride + * @param boolean $allowOverride If true, HTTP headers are taken into account * * @return string The validated IP address as provided. * If no IP is available, an empty string is returned. @@ -282,13 +290,13 @@ function ($list, $ip) { array() ); - return (string)array_pop($ipList); + return (string) array_pop($ipList); } /** * Gets the visitor's IP address * - * @param boolean $allowOverride + * @param boolean $allowOverride If true, HTTP headers are taken into account * * @return string The IP address(es) as provided without validation. * If no IP is available, an empty string is returned. @@ -400,9 +408,9 @@ private static function isInExplicitRange($ip, $from, $to) } /** - * @param string $ip - * @param string $prefix - * @param integer $mask + * @param string $ip The IP address to check + * @param string $prefix The prefix address + * @param integer $mask The length of the prefix * * @return boolean */ @@ -428,9 +436,9 @@ private static function isInCidrRange($ip, $prefix, $mask) } /** - * @param string $ip - * @param string $prefix - * @param string $netmask + * @param string $ip The IP address to check + * @param string $prefix The prefix address + * @param string $netmask The netmask * * @return boolean */ From fdc29aad8fbf822c3441a97ddc9f1531e08b356c Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 23:43:49 +0200 Subject: [PATCH 22/24] Style - Add CS fixes --- src/IpHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 45026464..5793ad35 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -145,7 +145,7 @@ function ($list, $range) { * @param string $ip The IP to check * @param string $ipRange The IP range; may be specified as from-to, CIDR or IP with netmask. * - * @return bool + * @return boolean */ private static function isInRange($ip, $ipRange) { @@ -251,7 +251,7 @@ public static function workaroundIPIssues() */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = (bool)$newState; + self::$allowIpOverrides = (bool) $newState; } /** From c62f29d009f2e0e0cd0478cfb7fdb80bd7b206ae Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Fri, 2 Apr 2021 03:52:07 +0200 Subject: [PATCH 23/24] Refactoring - Re-add IPinList() as proxy for isInRanges() --- src/IpHelper.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 5793ad35..66625ebb 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -90,10 +90,26 @@ public static function isIPv6($ip) * Range may be specified as from-to, CIDR or IP with netmask. * * @return boolean - * + * @deprecated 2.0 Use IpHelper::isInRanges() instead * @since 1.6.0 */ - public static function isInRanges($ip, $ipRanges = '') + public static function IPinList($ip, $ipRanges = '') + { + return self::isInRanges($ip, $ipRanges); + } + + /** + * Checks if an IP is contained in a list of IPs or IP expressions + * + * @param string $ip The IPv4/IPv6 address to check + * @param array|string $ipRanges A comma-separated list or array of IP ranges to check against. + * Range may be specified as from-to, CIDR or IP with netmask. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public static function isInRanges($ip, $ipRanges) { // Reject empty IPs or ANY_ADDRESS if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') From 119b8d0d1c405afe77ea948602d6214479539538 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 8 Apr 2021 13:53:06 +0200 Subject: [PATCH 24/24] Refactoring - Follow early return pattern, allow REMOTE_ADDR be set in environment --- src/IpHelper.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 66625ebb..be0b9c6f 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -330,16 +330,14 @@ private static function detectIP($allowOverride) if (!$allowOverride) { - $ip = ArrayHelper::getValue($_SERVER, 'REMOTE_ADDR', getenv('REMOTE_ADDR')); + return ArrayHelper::getValue($_SERVER, 'REMOTE_ADDR', getenv('REMOTE_ADDR')); } - else - { - $ip = ''; - foreach ($indexes as $index) - { - $ip = ArrayHelper::getValue($_SERVER, $index, $ip); - } + $ip = getenv('REMOTE_ADDR'); + + foreach ($indexes as $index) + { + $ip = ArrayHelper::getValue($_SERVER, $index, $ip); } return $ip;