├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── iplimiter_screenshot.png ├── phpunit.xml ├── src ├── DatabaseInterface.php ├── DatabasePDO.php └── IPLimiter.php └── tests └── IPLimiterTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: syntaxseed 2 | custom: "https://github.com/syntaxseed#donatecontribute" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/node_modules 3 | **/public/hot 4 | **/public/storage 5 | **/storage/*.key 6 | **/vendor 7 | **/.idea 8 | **/.vscode 9 | **/.vagrant 10 | **Homestead.json 11 | **Homestead.yaml 12 | **npm-debug.log 13 | **yarn-error.log 14 | *.env 15 | *.env.* 16 | !*.env.example 17 | *.phpunit.result.cache 18 | 19 | **/tests/example.php 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Sherri Wheeler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IPLimiter 2 | ========================= 3 | 4 |
5 | GitHub tag (latest SemVer)   6 | PHP v7.3+   7 | PHP v8.0+   8 | PHP v8.1+   9 | License: MIT
10 | 11 | follow on Mastodon  Sponsor Project 13 |
14 | 15 | A lightweight, framework agnostic, PHP IP address logging library for tracking the # of attempts, and time of last attempt for various categories of events. An event is an IP address and category string combo. The library includes helpers for setting/getting ban status, deleting individual events or all events for a given IP, etc. 16 | 17 | Common uses include creating a tarpit or code-level gates/limits for any attempted action in your application. 18 | 19 | ![IPLimiter Screenshot](iplimiter_screenshot.png "Using IPLimiter for PHP IP event logging.") 20 | 21 | The IPLimiter constructor must be passed a connected PDO object to create a database table and log the events. 22 | 23 | The core function of the library is to execute a set of 'rules' which IPLimiter will then determine whether the IP address passes or fails the ruleset, and therefore whether they should be allowed to proceed. Rules can specifiy a max # of attempts, and how long it must be before the next attempt. And whether ban status matters for this rule. An example will follow. 24 | 25 | Licence: MIT. 26 | 27 | Author: Sherri Wheeler. 28 | 29 | Current Version: 2.0.4. 30 | 31 | 32 | Features 33 | -------- 34 | 35 | * Compliant with IPv4 and IPv6 addresses. 36 | * Simple to learn and use. 37 | * Track and run rules against: 38 | * Number of attempts. 39 | * Time since last attempt. 40 | * Ban status. 41 | * Reset # attempts after a given time has passed. 42 | * Flexible. IP Addresses and event strings can be anything. 43 | * Unit-Testing with PHPUnit. 44 | * Compliant with PHP 8.0+, 8.1+, 8.2+, 8.3+. 45 | 46 | 47 | Installation 48 | -------- 49 | 50 | Require with Composer: 51 | ``` 52 | composer require syntaxseed/iplimiter ^2.0 53 | ``` 54 | > **Major version 2 is not compatible with version 1.** 55 | 56 | Usage - Quick Start 57 | -------- 58 | 59 | First ensure you have a connected PDO object. (http://php.net/manual/en/book.pdo.php). Or implement a new class which implements the included `DatabaseInterface`. An implementation of a class for PDO connections is included (`DatabasePDO`). 60 | 61 | Import the namespaces into your application: 62 | ```php 63 | use Syntaxseed\IPLimiter\IPLimiter; 64 | use Syntaxseed\IPLimiter\DatabasePDO; 65 | ``` 66 | 67 | Initialize a PDO object and use it to create a new IPLimiter instance. The second parameter is your desired database table name for IPLimiter to use. There is a `DatabasePDO` class included which implements the `DatabaseInterface` required by IPLimiter. 68 | ```php 69 | $ipLimiter = new IPLimiter(new DatabasePDO($pdo), 'syntaxseed_iplimiter'); 70 | ``` 71 | 72 | If you aren't using PDO, you can implement a DB wrapper class which implements the 'Syntaxseed\IPLimiter\DatabaseInterface. 73 | 74 | **Create the IPLimiter table if it doesn't already exist:** 75 | 76 | This and future functions will use the DatabaseInterface object injected via the constructor. 77 | ```php 78 | $result = $ipLimiter->migrate(); 79 | ``` 80 | 81 | > **Only run this ONCE. It will create the initial tables for you.** 82 | 83 | **Log a new event.** 84 | 85 | An event has an IP address and a string 'category'. Working with events requires an event to have been set. 86 | ```php 87 | $ipLimiter->event('123.123.0.1', 'sendmail'); 88 | $ipLimiter->log(); 89 | ``` 90 | 91 | Or, you can method chain the initialization of the object, setting of event, and logging: 92 | ```php 93 | $ipLimiter = (new IPLimiter(new DatabasePDO($pdo), 'syntaxseed_iplimiter')) 94 | ->event('123.123.0.1', 'sendmail') 95 | ->log(); 96 | ``` 97 | 98 | **Get whether an event exists in the dabase.** 99 | ```php 100 | $ipLimiter->event('111.111.111.111', 'sendmail'); 101 | $isLogged = $ipLimiter->exists(); 102 | // $isLogged is false. 103 | $ipLimiter->log(); 104 | $isLogged = $ipLimiter->exists(); 105 | // $isLogged is true. 106 | ``` 107 | 108 | **Get or reset the # of attemps for a given event.** 109 | ```php 110 | $ipLimiter->event('123.123.0.1', 'sendmail'); 111 | $ipLimiter->log(); 112 | $ipLimiter->log(); 113 | $attempts = $ipLimiter->attempts(); 114 | // Returns 2. 115 | $ipLimiter->resetAttempts(); 116 | // Sets value to 0. 117 | ``` 118 | 119 | **Get or reset the time since last attempt.** 120 | ```php 121 | $ipLimiter->event('123.123.0.1', 'sendmail'); 122 | $ipLimiter->log(); 123 | $lastTime = $ipLimiter->last(false); 124 | // Returns the unix epoc time since last attempt. 125 | $lastSeconds = $ipLimiter->last(); 126 | // Returns the # of seconds since last attempt. 127 | ``` 128 | Note: You cannot rest the time since last attempt. If there is a record in the database for this event, then it has a timestamp. To solve this, just delete the event completely, which equates to 'never'. 129 | 130 | **Delete an event.** 131 | ```php 132 | $ipLimiter->event('123.123.0.1', 'sendmail'); 133 | $ipLimiter->log(); 134 | $ipLimiter->deleteEvent(); 135 | ``` 136 | 137 | **Delete ALL events for a given IP.** 138 | 139 | This function does NOT require an event to be set, instead, pass in the IP address. 140 | ```php 141 | $result = $ipLimiter->deleteIP('123.123.0.1'); 142 | // Returns false if no records were found/deleted. True otherwise. 143 | ``` 144 | 145 | **Manage ban status for an event.** 146 | 147 | Note that with this method, an IP is banned from individual categories of events, not banned system-wide. The ban/unBan methods return the current ban status, NOT whether the ban/unban set succeeded or not. 148 | ```php 149 | $ipLimiter->event('123.123.0.1', 'sendmail'); 150 | $ipLimiter->log(); 151 | $status = $ipLimiter->isBanned(); 152 | // Returns false, ie not currently banned. 153 | $status = $ipLimiter->ban(); 154 | // Now true, ie is currently banned. 155 | $status = $ipLimiter->unBan(); 156 | // Now false, ie not currently banned. 157 | ``` 158 | 159 | 160 | Rules 161 | -------- 162 | 163 | A core feature of IPLimiter is running an event against a ruleset to see if it passes. In this way your application can have different rules for various categories of actions. Here's an example: 164 | 165 | **Rule Example: Sending Mail** 166 | 167 | In our application, users can only send mail at most every 5 minutes (300 seconds). They can make at most 3 attempts at sending mail before the reset time. Ban status matters for this ruleset (ie some events might use ban status for other purposes but not for rules). Attempts get reset after an hour of no attempts (3600 seconds). 168 | 169 | Our ruleset in JSON format: 170 | ```php 171 | { 172 | "resetAtSeconds": 3600, 173 | "waitAtLeast": 300, 174 | "allowedAttempts": 3, 175 | "allowBanned":false 176 | } 177 | ``` 178 | This means: 179 | - If a record doesn't exist in the database, PASS. 180 | - If last attempt was at or older than (>=) 3600 seconds ago, reset attempts to 0. 181 | - If last attempt was more recent than (<) 300 seconds ago, FAIL. 182 | - If current attempts is more than (>) 3, FAIL. 183 | - If banned, FAIL. 184 | - Otherwise, PASS. 185 | 186 | Execute the ruleset for the currently set event (will fail): 187 | ```php 188 | $ipLimiter->event('111.222.333.444', 'sendmail'); 189 | $ipLimiter->log(); // User sent first mail. 190 | $ruleResult = $ipLimiter->rule('{ 191 | "resetAtSeconds":3600, 192 | "waitAtLeast":300, 193 | "allowedAttempts":3, 194 | "allowBanned":false 195 | }'); 196 | 197 | // $ruleResult is false because there was NO time since the last (log) event. 198 | ``` 199 | 200 | Execute a ruleset for the currently set event (will pass): 201 | ```php 202 | $ipLimiter->event('111.222.333.444', 'sendmail'); 203 | $ipLimiter->log(); // User sent first mail. 204 | $ruleResult = $ipLimiter->rule('{ 205 | "resetAtSeconds":3600, 206 | "waitAtLeast":-1, 207 | "allowedAttempts":3, 208 | "allowBanned":false 209 | }'); 210 | 211 | // $ruleResult is true because -1 means ignore time since last event, and only look at attempts. 1 <= 3 so PASS. 212 | ``` 213 | 214 | **TIP:** Parts of the ruleset "resetAtSeconds", "waitAtLeast", and "allowedAttempts" can be set to -1 to ignore this part. 215 | 216 | 217 | Contributing 218 | -------- 219 | * Pull requests are welcome and appreciated! Please be patient while I find time to review. 220 | * Donations: https://github.com/syntaxseed#donatecontribute 221 | 222 | 223 | Changelog 224 | -------- 225 | * v2.0.4 - Tested & updated for PHP 8.2 & 8.3. 226 | * v2.0.3 - Tested for PHP 8.1. 227 | * v2.0.2 - Tested for PHP 8.0, fix PSR formatting. 228 | * v2.0.0 - IPLimiter now expects a database object which implments the included DatabaseInterface. A PDO implementation of this is included. NOTE: Not compatible with version 1 due to database column type change. 229 | * v1.0.4 - Add screenshot to readme. 230 | * v1.0.3 - Allow method chaining on the event() and log() methods. 231 | * v1.0.2 - Improve readme. Better package description. 232 | * v1.0.1 - Fix readme. 233 | * v1.0.0 - Initial release. 234 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syntaxseed/iplimiter", 3 | "description": "A PHP IP logging library (framework agnostic) to track attempts and time since last attempt for an event by an IP address. Run an event against a ruleset to determine pass/fail. Supports banning.", 4 | "keywords": ["IP", "logging", "PDO", "limiting", "IP Address"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Sherri Wheeler", 9 | "homepage": "http://syntaxseed.com", 10 | "role": "Developer" 11 | } 12 | ], 13 | "type": "library", 14 | "require": { 15 | "php": "^8.0|^8.1|^8.2|^8.3" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^9.5" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Syntaxseed\\IPLimiter\\": "src/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "05f536b74fd3ef4d3ca45dbc8b37b6be", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "doctrine/instantiator", 12 | "version": "2.0.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/doctrine/instantiator.git", 16 | "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", 21 | "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^8.1" 26 | }, 27 | "require-dev": { 28 | "doctrine/coding-standard": "^11", 29 | "ext-pdo": "*", 30 | "ext-phar": "*", 31 | "phpbench/phpbench": "^1.2", 32 | "phpstan/phpstan": "^1.9.4", 33 | "phpstan/phpstan-phpunit": "^1.3", 34 | "phpunit/phpunit": "^9.5.27", 35 | "vimeo/psalm": "^5.4" 36 | }, 37 | "type": "library", 38 | "autoload": { 39 | "psr-4": { 40 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "MIT" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Marco Pivetta", 50 | "email": "ocramius@gmail.com", 51 | "homepage": "https://ocramius.github.io/" 52 | } 53 | ], 54 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 55 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 56 | "keywords": [ 57 | "constructor", 58 | "instantiate" 59 | ], 60 | "support": { 61 | "issues": "https://github.com/doctrine/instantiator/issues", 62 | "source": "https://github.com/doctrine/instantiator/tree/2.0.0" 63 | }, 64 | "funding": [ 65 | { 66 | "url": "https://www.doctrine-project.org/sponsorship.html", 67 | "type": "custom" 68 | }, 69 | { 70 | "url": "https://www.patreon.com/phpdoctrine", 71 | "type": "patreon" 72 | }, 73 | { 74 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 75 | "type": "tidelift" 76 | } 77 | ], 78 | "time": "2022-12-30T00:23:10+00:00" 79 | }, 80 | { 81 | "name": "myclabs/deep-copy", 82 | "version": "1.12.1", 83 | "source": { 84 | "type": "git", 85 | "url": "https://github.com/myclabs/DeepCopy.git", 86 | "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" 87 | }, 88 | "dist": { 89 | "type": "zip", 90 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", 91 | "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", 92 | "shasum": "" 93 | }, 94 | "require": { 95 | "php": "^7.1 || ^8.0" 96 | }, 97 | "conflict": { 98 | "doctrine/collections": "<1.6.8", 99 | "doctrine/common": "<2.13.3 || >=3 <3.2.2" 100 | }, 101 | "require-dev": { 102 | "doctrine/collections": "^1.6.8", 103 | "doctrine/common": "^2.13.3 || ^3.2.2", 104 | "phpspec/prophecy": "^1.10", 105 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" 106 | }, 107 | "type": "library", 108 | "autoload": { 109 | "files": [ 110 | "src/DeepCopy/deep_copy.php" 111 | ], 112 | "psr-4": { 113 | "DeepCopy\\": "src/DeepCopy/" 114 | } 115 | }, 116 | "notification-url": "https://packagist.org/downloads/", 117 | "license": [ 118 | "MIT" 119 | ], 120 | "description": "Create deep copies (clones) of your objects", 121 | "keywords": [ 122 | "clone", 123 | "copy", 124 | "duplicate", 125 | "object", 126 | "object graph" 127 | ], 128 | "support": { 129 | "issues": "https://github.com/myclabs/DeepCopy/issues", 130 | "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" 131 | }, 132 | "funding": [ 133 | { 134 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 135 | "type": "tidelift" 136 | } 137 | ], 138 | "time": "2024-11-08T17:47:46+00:00" 139 | }, 140 | { 141 | "name": "nikic/php-parser", 142 | "version": "v5.3.1", 143 | "source": { 144 | "type": "git", 145 | "url": "https://github.com/nikic/PHP-Parser.git", 146 | "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" 147 | }, 148 | "dist": { 149 | "type": "zip", 150 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", 151 | "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", 152 | "shasum": "" 153 | }, 154 | "require": { 155 | "ext-ctype": "*", 156 | "ext-json": "*", 157 | "ext-tokenizer": "*", 158 | "php": ">=7.4" 159 | }, 160 | "require-dev": { 161 | "ircmaxell/php-yacc": "^0.0.7", 162 | "phpunit/phpunit": "^9.0" 163 | }, 164 | "bin": [ 165 | "bin/php-parse" 166 | ], 167 | "type": "library", 168 | "extra": { 169 | "branch-alias": { 170 | "dev-master": "5.0-dev" 171 | } 172 | }, 173 | "autoload": { 174 | "psr-4": { 175 | "PhpParser\\": "lib/PhpParser" 176 | } 177 | }, 178 | "notification-url": "https://packagist.org/downloads/", 179 | "license": [ 180 | "BSD-3-Clause" 181 | ], 182 | "authors": [ 183 | { 184 | "name": "Nikita Popov" 185 | } 186 | ], 187 | "description": "A PHP parser written in PHP", 188 | "keywords": [ 189 | "parser", 190 | "php" 191 | ], 192 | "support": { 193 | "issues": "https://github.com/nikic/PHP-Parser/issues", 194 | "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" 195 | }, 196 | "time": "2024-10-08T18:51:32+00:00" 197 | }, 198 | { 199 | "name": "phar-io/manifest", 200 | "version": "2.0.4", 201 | "source": { 202 | "type": "git", 203 | "url": "https://github.com/phar-io/manifest.git", 204 | "reference": "54750ef60c58e43759730615a392c31c80e23176" 205 | }, 206 | "dist": { 207 | "type": "zip", 208 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", 209 | "reference": "54750ef60c58e43759730615a392c31c80e23176", 210 | "shasum": "" 211 | }, 212 | "require": { 213 | "ext-dom": "*", 214 | "ext-libxml": "*", 215 | "ext-phar": "*", 216 | "ext-xmlwriter": "*", 217 | "phar-io/version": "^3.0.1", 218 | "php": "^7.2 || ^8.0" 219 | }, 220 | "type": "library", 221 | "extra": { 222 | "branch-alias": { 223 | "dev-master": "2.0.x-dev" 224 | } 225 | }, 226 | "autoload": { 227 | "classmap": [ 228 | "src/" 229 | ] 230 | }, 231 | "notification-url": "https://packagist.org/downloads/", 232 | "license": [ 233 | "BSD-3-Clause" 234 | ], 235 | "authors": [ 236 | { 237 | "name": "Arne Blankerts", 238 | "email": "arne@blankerts.de", 239 | "role": "Developer" 240 | }, 241 | { 242 | "name": "Sebastian Heuer", 243 | "email": "sebastian@phpeople.de", 244 | "role": "Developer" 245 | }, 246 | { 247 | "name": "Sebastian Bergmann", 248 | "email": "sebastian@phpunit.de", 249 | "role": "Developer" 250 | } 251 | ], 252 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 253 | "support": { 254 | "issues": "https://github.com/phar-io/manifest/issues", 255 | "source": "https://github.com/phar-io/manifest/tree/2.0.4" 256 | }, 257 | "funding": [ 258 | { 259 | "url": "https://github.com/theseer", 260 | "type": "github" 261 | } 262 | ], 263 | "time": "2024-03-03T12:33:53+00:00" 264 | }, 265 | { 266 | "name": "phar-io/version", 267 | "version": "3.2.1", 268 | "source": { 269 | "type": "git", 270 | "url": "https://github.com/phar-io/version.git", 271 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" 272 | }, 273 | "dist": { 274 | "type": "zip", 275 | "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 276 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 277 | "shasum": "" 278 | }, 279 | "require": { 280 | "php": "^7.2 || ^8.0" 281 | }, 282 | "type": "library", 283 | "autoload": { 284 | "classmap": [ 285 | "src/" 286 | ] 287 | }, 288 | "notification-url": "https://packagist.org/downloads/", 289 | "license": [ 290 | "BSD-3-Clause" 291 | ], 292 | "authors": [ 293 | { 294 | "name": "Arne Blankerts", 295 | "email": "arne@blankerts.de", 296 | "role": "Developer" 297 | }, 298 | { 299 | "name": "Sebastian Heuer", 300 | "email": "sebastian@phpeople.de", 301 | "role": "Developer" 302 | }, 303 | { 304 | "name": "Sebastian Bergmann", 305 | "email": "sebastian@phpunit.de", 306 | "role": "Developer" 307 | } 308 | ], 309 | "description": "Library for handling version information and constraints", 310 | "support": { 311 | "issues": "https://github.com/phar-io/version/issues", 312 | "source": "https://github.com/phar-io/version/tree/3.2.1" 313 | }, 314 | "time": "2022-02-21T01:04:05+00:00" 315 | }, 316 | { 317 | "name": "phpunit/php-code-coverage", 318 | "version": "9.2.32", 319 | "source": { 320 | "type": "git", 321 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 322 | "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" 323 | }, 324 | "dist": { 325 | "type": "zip", 326 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", 327 | "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", 328 | "shasum": "" 329 | }, 330 | "require": { 331 | "ext-dom": "*", 332 | "ext-libxml": "*", 333 | "ext-xmlwriter": "*", 334 | "nikic/php-parser": "^4.19.1 || ^5.1.0", 335 | "php": ">=7.3", 336 | "phpunit/php-file-iterator": "^3.0.6", 337 | "phpunit/php-text-template": "^2.0.4", 338 | "sebastian/code-unit-reverse-lookup": "^2.0.3", 339 | "sebastian/complexity": "^2.0.3", 340 | "sebastian/environment": "^5.1.5", 341 | "sebastian/lines-of-code": "^1.0.4", 342 | "sebastian/version": "^3.0.2", 343 | "theseer/tokenizer": "^1.2.3" 344 | }, 345 | "require-dev": { 346 | "phpunit/phpunit": "^9.6" 347 | }, 348 | "suggest": { 349 | "ext-pcov": "PHP extension that provides line coverage", 350 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 351 | }, 352 | "type": "library", 353 | "extra": { 354 | "branch-alias": { 355 | "dev-main": "9.2.x-dev" 356 | } 357 | }, 358 | "autoload": { 359 | "classmap": [ 360 | "src/" 361 | ] 362 | }, 363 | "notification-url": "https://packagist.org/downloads/", 364 | "license": [ 365 | "BSD-3-Clause" 366 | ], 367 | "authors": [ 368 | { 369 | "name": "Sebastian Bergmann", 370 | "email": "sebastian@phpunit.de", 371 | "role": "lead" 372 | } 373 | ], 374 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 375 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 376 | "keywords": [ 377 | "coverage", 378 | "testing", 379 | "xunit" 380 | ], 381 | "support": { 382 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 383 | "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", 384 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" 385 | }, 386 | "funding": [ 387 | { 388 | "url": "https://github.com/sebastianbergmann", 389 | "type": "github" 390 | } 391 | ], 392 | "time": "2024-08-22T04:23:01+00:00" 393 | }, 394 | { 395 | "name": "phpunit/php-file-iterator", 396 | "version": "3.0.6", 397 | "source": { 398 | "type": "git", 399 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 400 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" 401 | }, 402 | "dist": { 403 | "type": "zip", 404 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", 405 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", 406 | "shasum": "" 407 | }, 408 | "require": { 409 | "php": ">=7.3" 410 | }, 411 | "require-dev": { 412 | "phpunit/phpunit": "^9.3" 413 | }, 414 | "type": "library", 415 | "extra": { 416 | "branch-alias": { 417 | "dev-master": "3.0-dev" 418 | } 419 | }, 420 | "autoload": { 421 | "classmap": [ 422 | "src/" 423 | ] 424 | }, 425 | "notification-url": "https://packagist.org/downloads/", 426 | "license": [ 427 | "BSD-3-Clause" 428 | ], 429 | "authors": [ 430 | { 431 | "name": "Sebastian Bergmann", 432 | "email": "sebastian@phpunit.de", 433 | "role": "lead" 434 | } 435 | ], 436 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 437 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 438 | "keywords": [ 439 | "filesystem", 440 | "iterator" 441 | ], 442 | "support": { 443 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 444 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" 445 | }, 446 | "funding": [ 447 | { 448 | "url": "https://github.com/sebastianbergmann", 449 | "type": "github" 450 | } 451 | ], 452 | "time": "2021-12-02T12:48:52+00:00" 453 | }, 454 | { 455 | "name": "phpunit/php-invoker", 456 | "version": "3.1.1", 457 | "source": { 458 | "type": "git", 459 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 460 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" 461 | }, 462 | "dist": { 463 | "type": "zip", 464 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 465 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 466 | "shasum": "" 467 | }, 468 | "require": { 469 | "php": ">=7.3" 470 | }, 471 | "require-dev": { 472 | "ext-pcntl": "*", 473 | "phpunit/phpunit": "^9.3" 474 | }, 475 | "suggest": { 476 | "ext-pcntl": "*" 477 | }, 478 | "type": "library", 479 | "extra": { 480 | "branch-alias": { 481 | "dev-master": "3.1-dev" 482 | } 483 | }, 484 | "autoload": { 485 | "classmap": [ 486 | "src/" 487 | ] 488 | }, 489 | "notification-url": "https://packagist.org/downloads/", 490 | "license": [ 491 | "BSD-3-Clause" 492 | ], 493 | "authors": [ 494 | { 495 | "name": "Sebastian Bergmann", 496 | "email": "sebastian@phpunit.de", 497 | "role": "lead" 498 | } 499 | ], 500 | "description": "Invoke callables with a timeout", 501 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 502 | "keywords": [ 503 | "process" 504 | ], 505 | "support": { 506 | "issues": "https://github.com/sebastianbergmann/php-invoker/issues", 507 | "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" 508 | }, 509 | "funding": [ 510 | { 511 | "url": "https://github.com/sebastianbergmann", 512 | "type": "github" 513 | } 514 | ], 515 | "time": "2020-09-28T05:58:55+00:00" 516 | }, 517 | { 518 | "name": "phpunit/php-text-template", 519 | "version": "2.0.4", 520 | "source": { 521 | "type": "git", 522 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 523 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" 524 | }, 525 | "dist": { 526 | "type": "zip", 527 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 528 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 529 | "shasum": "" 530 | }, 531 | "require": { 532 | "php": ">=7.3" 533 | }, 534 | "require-dev": { 535 | "phpunit/phpunit": "^9.3" 536 | }, 537 | "type": "library", 538 | "extra": { 539 | "branch-alias": { 540 | "dev-master": "2.0-dev" 541 | } 542 | }, 543 | "autoload": { 544 | "classmap": [ 545 | "src/" 546 | ] 547 | }, 548 | "notification-url": "https://packagist.org/downloads/", 549 | "license": [ 550 | "BSD-3-Clause" 551 | ], 552 | "authors": [ 553 | { 554 | "name": "Sebastian Bergmann", 555 | "email": "sebastian@phpunit.de", 556 | "role": "lead" 557 | } 558 | ], 559 | "description": "Simple template engine.", 560 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 561 | "keywords": [ 562 | "template" 563 | ], 564 | "support": { 565 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 566 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" 567 | }, 568 | "funding": [ 569 | { 570 | "url": "https://github.com/sebastianbergmann", 571 | "type": "github" 572 | } 573 | ], 574 | "time": "2020-10-26T05:33:50+00:00" 575 | }, 576 | { 577 | "name": "phpunit/php-timer", 578 | "version": "5.0.3", 579 | "source": { 580 | "type": "git", 581 | "url": "https://github.com/sebastianbergmann/php-timer.git", 582 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" 583 | }, 584 | "dist": { 585 | "type": "zip", 586 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 587 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 588 | "shasum": "" 589 | }, 590 | "require": { 591 | "php": ">=7.3" 592 | }, 593 | "require-dev": { 594 | "phpunit/phpunit": "^9.3" 595 | }, 596 | "type": "library", 597 | "extra": { 598 | "branch-alias": { 599 | "dev-master": "5.0-dev" 600 | } 601 | }, 602 | "autoload": { 603 | "classmap": [ 604 | "src/" 605 | ] 606 | }, 607 | "notification-url": "https://packagist.org/downloads/", 608 | "license": [ 609 | "BSD-3-Clause" 610 | ], 611 | "authors": [ 612 | { 613 | "name": "Sebastian Bergmann", 614 | "email": "sebastian@phpunit.de", 615 | "role": "lead" 616 | } 617 | ], 618 | "description": "Utility class for timing", 619 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 620 | "keywords": [ 621 | "timer" 622 | ], 623 | "support": { 624 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 625 | "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" 626 | }, 627 | "funding": [ 628 | { 629 | "url": "https://github.com/sebastianbergmann", 630 | "type": "github" 631 | } 632 | ], 633 | "time": "2020-10-26T13:16:10+00:00" 634 | }, 635 | { 636 | "name": "phpunit/phpunit", 637 | "version": "9.6.22", 638 | "source": { 639 | "type": "git", 640 | "url": "https://github.com/sebastianbergmann/phpunit.git", 641 | "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" 642 | }, 643 | "dist": { 644 | "type": "zip", 645 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", 646 | "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", 647 | "shasum": "" 648 | }, 649 | "require": { 650 | "doctrine/instantiator": "^1.5.0 || ^2", 651 | "ext-dom": "*", 652 | "ext-json": "*", 653 | "ext-libxml": "*", 654 | "ext-mbstring": "*", 655 | "ext-xml": "*", 656 | "ext-xmlwriter": "*", 657 | "myclabs/deep-copy": "^1.12.1", 658 | "phar-io/manifest": "^2.0.4", 659 | "phar-io/version": "^3.2.1", 660 | "php": ">=7.3", 661 | "phpunit/php-code-coverage": "^9.2.32", 662 | "phpunit/php-file-iterator": "^3.0.6", 663 | "phpunit/php-invoker": "^3.1.1", 664 | "phpunit/php-text-template": "^2.0.4", 665 | "phpunit/php-timer": "^5.0.3", 666 | "sebastian/cli-parser": "^1.0.2", 667 | "sebastian/code-unit": "^1.0.8", 668 | "sebastian/comparator": "^4.0.8", 669 | "sebastian/diff": "^4.0.6", 670 | "sebastian/environment": "^5.1.5", 671 | "sebastian/exporter": "^4.0.6", 672 | "sebastian/global-state": "^5.0.7", 673 | "sebastian/object-enumerator": "^4.0.4", 674 | "sebastian/resource-operations": "^3.0.4", 675 | "sebastian/type": "^3.2.1", 676 | "sebastian/version": "^3.0.2" 677 | }, 678 | "suggest": { 679 | "ext-soap": "To be able to generate mocks based on WSDL files", 680 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 681 | }, 682 | "bin": [ 683 | "phpunit" 684 | ], 685 | "type": "library", 686 | "extra": { 687 | "branch-alias": { 688 | "dev-master": "9.6-dev" 689 | } 690 | }, 691 | "autoload": { 692 | "files": [ 693 | "src/Framework/Assert/Functions.php" 694 | ], 695 | "classmap": [ 696 | "src/" 697 | ] 698 | }, 699 | "notification-url": "https://packagist.org/downloads/", 700 | "license": [ 701 | "BSD-3-Clause" 702 | ], 703 | "authors": [ 704 | { 705 | "name": "Sebastian Bergmann", 706 | "email": "sebastian@phpunit.de", 707 | "role": "lead" 708 | } 709 | ], 710 | "description": "The PHP Unit Testing framework.", 711 | "homepage": "https://phpunit.de/", 712 | "keywords": [ 713 | "phpunit", 714 | "testing", 715 | "xunit" 716 | ], 717 | "support": { 718 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 719 | "security": "https://github.com/sebastianbergmann/phpunit/security/policy", 720 | "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" 721 | }, 722 | "funding": [ 723 | { 724 | "url": "https://phpunit.de/sponsors.html", 725 | "type": "custom" 726 | }, 727 | { 728 | "url": "https://github.com/sebastianbergmann", 729 | "type": "github" 730 | }, 731 | { 732 | "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", 733 | "type": "tidelift" 734 | } 735 | ], 736 | "time": "2024-12-05T13:48:26+00:00" 737 | }, 738 | { 739 | "name": "sebastian/cli-parser", 740 | "version": "1.0.2", 741 | "source": { 742 | "type": "git", 743 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 744 | "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" 745 | }, 746 | "dist": { 747 | "type": "zip", 748 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", 749 | "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", 750 | "shasum": "" 751 | }, 752 | "require": { 753 | "php": ">=7.3" 754 | }, 755 | "require-dev": { 756 | "phpunit/phpunit": "^9.3" 757 | }, 758 | "type": "library", 759 | "extra": { 760 | "branch-alias": { 761 | "dev-master": "1.0-dev" 762 | } 763 | }, 764 | "autoload": { 765 | "classmap": [ 766 | "src/" 767 | ] 768 | }, 769 | "notification-url": "https://packagist.org/downloads/", 770 | "license": [ 771 | "BSD-3-Clause" 772 | ], 773 | "authors": [ 774 | { 775 | "name": "Sebastian Bergmann", 776 | "email": "sebastian@phpunit.de", 777 | "role": "lead" 778 | } 779 | ], 780 | "description": "Library for parsing CLI options", 781 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 782 | "support": { 783 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues", 784 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" 785 | }, 786 | "funding": [ 787 | { 788 | "url": "https://github.com/sebastianbergmann", 789 | "type": "github" 790 | } 791 | ], 792 | "time": "2024-03-02T06:27:43+00:00" 793 | }, 794 | { 795 | "name": "sebastian/code-unit", 796 | "version": "1.0.8", 797 | "source": { 798 | "type": "git", 799 | "url": "https://github.com/sebastianbergmann/code-unit.git", 800 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" 801 | }, 802 | "dist": { 803 | "type": "zip", 804 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", 805 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", 806 | "shasum": "" 807 | }, 808 | "require": { 809 | "php": ">=7.3" 810 | }, 811 | "require-dev": { 812 | "phpunit/phpunit": "^9.3" 813 | }, 814 | "type": "library", 815 | "extra": { 816 | "branch-alias": { 817 | "dev-master": "1.0-dev" 818 | } 819 | }, 820 | "autoload": { 821 | "classmap": [ 822 | "src/" 823 | ] 824 | }, 825 | "notification-url": "https://packagist.org/downloads/", 826 | "license": [ 827 | "BSD-3-Clause" 828 | ], 829 | "authors": [ 830 | { 831 | "name": "Sebastian Bergmann", 832 | "email": "sebastian@phpunit.de", 833 | "role": "lead" 834 | } 835 | ], 836 | "description": "Collection of value objects that represent the PHP code units", 837 | "homepage": "https://github.com/sebastianbergmann/code-unit", 838 | "support": { 839 | "issues": "https://github.com/sebastianbergmann/code-unit/issues", 840 | "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" 841 | }, 842 | "funding": [ 843 | { 844 | "url": "https://github.com/sebastianbergmann", 845 | "type": "github" 846 | } 847 | ], 848 | "time": "2020-10-26T13:08:54+00:00" 849 | }, 850 | { 851 | "name": "sebastian/code-unit-reverse-lookup", 852 | "version": "2.0.3", 853 | "source": { 854 | "type": "git", 855 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 856 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" 857 | }, 858 | "dist": { 859 | "type": "zip", 860 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 861 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 862 | "shasum": "" 863 | }, 864 | "require": { 865 | "php": ">=7.3" 866 | }, 867 | "require-dev": { 868 | "phpunit/phpunit": "^9.3" 869 | }, 870 | "type": "library", 871 | "extra": { 872 | "branch-alias": { 873 | "dev-master": "2.0-dev" 874 | } 875 | }, 876 | "autoload": { 877 | "classmap": [ 878 | "src/" 879 | ] 880 | }, 881 | "notification-url": "https://packagist.org/downloads/", 882 | "license": [ 883 | "BSD-3-Clause" 884 | ], 885 | "authors": [ 886 | { 887 | "name": "Sebastian Bergmann", 888 | "email": "sebastian@phpunit.de" 889 | } 890 | ], 891 | "description": "Looks up which function or method a line of code belongs to", 892 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 893 | "support": { 894 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 895 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" 896 | }, 897 | "funding": [ 898 | { 899 | "url": "https://github.com/sebastianbergmann", 900 | "type": "github" 901 | } 902 | ], 903 | "time": "2020-09-28T05:30:19+00:00" 904 | }, 905 | { 906 | "name": "sebastian/comparator", 907 | "version": "4.0.8", 908 | "source": { 909 | "type": "git", 910 | "url": "https://github.com/sebastianbergmann/comparator.git", 911 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a" 912 | }, 913 | "dist": { 914 | "type": "zip", 915 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", 916 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a", 917 | "shasum": "" 918 | }, 919 | "require": { 920 | "php": ">=7.3", 921 | "sebastian/diff": "^4.0", 922 | "sebastian/exporter": "^4.0" 923 | }, 924 | "require-dev": { 925 | "phpunit/phpunit": "^9.3" 926 | }, 927 | "type": "library", 928 | "extra": { 929 | "branch-alias": { 930 | "dev-master": "4.0-dev" 931 | } 932 | }, 933 | "autoload": { 934 | "classmap": [ 935 | "src/" 936 | ] 937 | }, 938 | "notification-url": "https://packagist.org/downloads/", 939 | "license": [ 940 | "BSD-3-Clause" 941 | ], 942 | "authors": [ 943 | { 944 | "name": "Sebastian Bergmann", 945 | "email": "sebastian@phpunit.de" 946 | }, 947 | { 948 | "name": "Jeff Welch", 949 | "email": "whatthejeff@gmail.com" 950 | }, 951 | { 952 | "name": "Volker Dusch", 953 | "email": "github@wallbash.com" 954 | }, 955 | { 956 | "name": "Bernhard Schussek", 957 | "email": "bschussek@2bepublished.at" 958 | } 959 | ], 960 | "description": "Provides the functionality to compare PHP values for equality", 961 | "homepage": "https://github.com/sebastianbergmann/comparator", 962 | "keywords": [ 963 | "comparator", 964 | "compare", 965 | "equality" 966 | ], 967 | "support": { 968 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 969 | "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" 970 | }, 971 | "funding": [ 972 | { 973 | "url": "https://github.com/sebastianbergmann", 974 | "type": "github" 975 | } 976 | ], 977 | "time": "2022-09-14T12:41:17+00:00" 978 | }, 979 | { 980 | "name": "sebastian/complexity", 981 | "version": "2.0.3", 982 | "source": { 983 | "type": "git", 984 | "url": "https://github.com/sebastianbergmann/complexity.git", 985 | "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" 986 | }, 987 | "dist": { 988 | "type": "zip", 989 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", 990 | "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", 991 | "shasum": "" 992 | }, 993 | "require": { 994 | "nikic/php-parser": "^4.18 || ^5.0", 995 | "php": ">=7.3" 996 | }, 997 | "require-dev": { 998 | "phpunit/phpunit": "^9.3" 999 | }, 1000 | "type": "library", 1001 | "extra": { 1002 | "branch-alias": { 1003 | "dev-master": "2.0-dev" 1004 | } 1005 | }, 1006 | "autoload": { 1007 | "classmap": [ 1008 | "src/" 1009 | ] 1010 | }, 1011 | "notification-url": "https://packagist.org/downloads/", 1012 | "license": [ 1013 | "BSD-3-Clause" 1014 | ], 1015 | "authors": [ 1016 | { 1017 | "name": "Sebastian Bergmann", 1018 | "email": "sebastian@phpunit.de", 1019 | "role": "lead" 1020 | } 1021 | ], 1022 | "description": "Library for calculating the complexity of PHP code units", 1023 | "homepage": "https://github.com/sebastianbergmann/complexity", 1024 | "support": { 1025 | "issues": "https://github.com/sebastianbergmann/complexity/issues", 1026 | "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" 1027 | }, 1028 | "funding": [ 1029 | { 1030 | "url": "https://github.com/sebastianbergmann", 1031 | "type": "github" 1032 | } 1033 | ], 1034 | "time": "2023-12-22T06:19:30+00:00" 1035 | }, 1036 | { 1037 | "name": "sebastian/diff", 1038 | "version": "4.0.6", 1039 | "source": { 1040 | "type": "git", 1041 | "url": "https://github.com/sebastianbergmann/diff.git", 1042 | "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" 1043 | }, 1044 | "dist": { 1045 | "type": "zip", 1046 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", 1047 | "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", 1048 | "shasum": "" 1049 | }, 1050 | "require": { 1051 | "php": ">=7.3" 1052 | }, 1053 | "require-dev": { 1054 | "phpunit/phpunit": "^9.3", 1055 | "symfony/process": "^4.2 || ^5" 1056 | }, 1057 | "type": "library", 1058 | "extra": { 1059 | "branch-alias": { 1060 | "dev-master": "4.0-dev" 1061 | } 1062 | }, 1063 | "autoload": { 1064 | "classmap": [ 1065 | "src/" 1066 | ] 1067 | }, 1068 | "notification-url": "https://packagist.org/downloads/", 1069 | "license": [ 1070 | "BSD-3-Clause" 1071 | ], 1072 | "authors": [ 1073 | { 1074 | "name": "Sebastian Bergmann", 1075 | "email": "sebastian@phpunit.de" 1076 | }, 1077 | { 1078 | "name": "Kore Nordmann", 1079 | "email": "mail@kore-nordmann.de" 1080 | } 1081 | ], 1082 | "description": "Diff implementation", 1083 | "homepage": "https://github.com/sebastianbergmann/diff", 1084 | "keywords": [ 1085 | "diff", 1086 | "udiff", 1087 | "unidiff", 1088 | "unified diff" 1089 | ], 1090 | "support": { 1091 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1092 | "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" 1093 | }, 1094 | "funding": [ 1095 | { 1096 | "url": "https://github.com/sebastianbergmann", 1097 | "type": "github" 1098 | } 1099 | ], 1100 | "time": "2024-03-02T06:30:58+00:00" 1101 | }, 1102 | { 1103 | "name": "sebastian/environment", 1104 | "version": "5.1.5", 1105 | "source": { 1106 | "type": "git", 1107 | "url": "https://github.com/sebastianbergmann/environment.git", 1108 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" 1109 | }, 1110 | "dist": { 1111 | "type": "zip", 1112 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", 1113 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", 1114 | "shasum": "" 1115 | }, 1116 | "require": { 1117 | "php": ">=7.3" 1118 | }, 1119 | "require-dev": { 1120 | "phpunit/phpunit": "^9.3" 1121 | }, 1122 | "suggest": { 1123 | "ext-posix": "*" 1124 | }, 1125 | "type": "library", 1126 | "extra": { 1127 | "branch-alias": { 1128 | "dev-master": "5.1-dev" 1129 | } 1130 | }, 1131 | "autoload": { 1132 | "classmap": [ 1133 | "src/" 1134 | ] 1135 | }, 1136 | "notification-url": "https://packagist.org/downloads/", 1137 | "license": [ 1138 | "BSD-3-Clause" 1139 | ], 1140 | "authors": [ 1141 | { 1142 | "name": "Sebastian Bergmann", 1143 | "email": "sebastian@phpunit.de" 1144 | } 1145 | ], 1146 | "description": "Provides functionality to handle HHVM/PHP environments", 1147 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1148 | "keywords": [ 1149 | "Xdebug", 1150 | "environment", 1151 | "hhvm" 1152 | ], 1153 | "support": { 1154 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1155 | "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" 1156 | }, 1157 | "funding": [ 1158 | { 1159 | "url": "https://github.com/sebastianbergmann", 1160 | "type": "github" 1161 | } 1162 | ], 1163 | "time": "2023-02-03T06:03:51+00:00" 1164 | }, 1165 | { 1166 | "name": "sebastian/exporter", 1167 | "version": "4.0.6", 1168 | "source": { 1169 | "type": "git", 1170 | "url": "https://github.com/sebastianbergmann/exporter.git", 1171 | "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" 1172 | }, 1173 | "dist": { 1174 | "type": "zip", 1175 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", 1176 | "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", 1177 | "shasum": "" 1178 | }, 1179 | "require": { 1180 | "php": ">=7.3", 1181 | "sebastian/recursion-context": "^4.0" 1182 | }, 1183 | "require-dev": { 1184 | "ext-mbstring": "*", 1185 | "phpunit/phpunit": "^9.3" 1186 | }, 1187 | "type": "library", 1188 | "extra": { 1189 | "branch-alias": { 1190 | "dev-master": "4.0-dev" 1191 | } 1192 | }, 1193 | "autoload": { 1194 | "classmap": [ 1195 | "src/" 1196 | ] 1197 | }, 1198 | "notification-url": "https://packagist.org/downloads/", 1199 | "license": [ 1200 | "BSD-3-Clause" 1201 | ], 1202 | "authors": [ 1203 | { 1204 | "name": "Sebastian Bergmann", 1205 | "email": "sebastian@phpunit.de" 1206 | }, 1207 | { 1208 | "name": "Jeff Welch", 1209 | "email": "whatthejeff@gmail.com" 1210 | }, 1211 | { 1212 | "name": "Volker Dusch", 1213 | "email": "github@wallbash.com" 1214 | }, 1215 | { 1216 | "name": "Adam Harvey", 1217 | "email": "aharvey@php.net" 1218 | }, 1219 | { 1220 | "name": "Bernhard Schussek", 1221 | "email": "bschussek@gmail.com" 1222 | } 1223 | ], 1224 | "description": "Provides the functionality to export PHP variables for visualization", 1225 | "homepage": "https://www.github.com/sebastianbergmann/exporter", 1226 | "keywords": [ 1227 | "export", 1228 | "exporter" 1229 | ], 1230 | "support": { 1231 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1232 | "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" 1233 | }, 1234 | "funding": [ 1235 | { 1236 | "url": "https://github.com/sebastianbergmann", 1237 | "type": "github" 1238 | } 1239 | ], 1240 | "time": "2024-03-02T06:33:00+00:00" 1241 | }, 1242 | { 1243 | "name": "sebastian/global-state", 1244 | "version": "5.0.7", 1245 | "source": { 1246 | "type": "git", 1247 | "url": "https://github.com/sebastianbergmann/global-state.git", 1248 | "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" 1249 | }, 1250 | "dist": { 1251 | "type": "zip", 1252 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", 1253 | "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", 1254 | "shasum": "" 1255 | }, 1256 | "require": { 1257 | "php": ">=7.3", 1258 | "sebastian/object-reflector": "^2.0", 1259 | "sebastian/recursion-context": "^4.0" 1260 | }, 1261 | "require-dev": { 1262 | "ext-dom": "*", 1263 | "phpunit/phpunit": "^9.3" 1264 | }, 1265 | "suggest": { 1266 | "ext-uopz": "*" 1267 | }, 1268 | "type": "library", 1269 | "extra": { 1270 | "branch-alias": { 1271 | "dev-master": "5.0-dev" 1272 | } 1273 | }, 1274 | "autoload": { 1275 | "classmap": [ 1276 | "src/" 1277 | ] 1278 | }, 1279 | "notification-url": "https://packagist.org/downloads/", 1280 | "license": [ 1281 | "BSD-3-Clause" 1282 | ], 1283 | "authors": [ 1284 | { 1285 | "name": "Sebastian Bergmann", 1286 | "email": "sebastian@phpunit.de" 1287 | } 1288 | ], 1289 | "description": "Snapshotting of global state", 1290 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1291 | "keywords": [ 1292 | "global state" 1293 | ], 1294 | "support": { 1295 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1296 | "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" 1297 | }, 1298 | "funding": [ 1299 | { 1300 | "url": "https://github.com/sebastianbergmann", 1301 | "type": "github" 1302 | } 1303 | ], 1304 | "time": "2024-03-02T06:35:11+00:00" 1305 | }, 1306 | { 1307 | "name": "sebastian/lines-of-code", 1308 | "version": "1.0.4", 1309 | "source": { 1310 | "type": "git", 1311 | "url": "https://github.com/sebastianbergmann/lines-of-code.git", 1312 | "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" 1313 | }, 1314 | "dist": { 1315 | "type": "zip", 1316 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", 1317 | "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", 1318 | "shasum": "" 1319 | }, 1320 | "require": { 1321 | "nikic/php-parser": "^4.18 || ^5.0", 1322 | "php": ">=7.3" 1323 | }, 1324 | "require-dev": { 1325 | "phpunit/phpunit": "^9.3" 1326 | }, 1327 | "type": "library", 1328 | "extra": { 1329 | "branch-alias": { 1330 | "dev-master": "1.0-dev" 1331 | } 1332 | }, 1333 | "autoload": { 1334 | "classmap": [ 1335 | "src/" 1336 | ] 1337 | }, 1338 | "notification-url": "https://packagist.org/downloads/", 1339 | "license": [ 1340 | "BSD-3-Clause" 1341 | ], 1342 | "authors": [ 1343 | { 1344 | "name": "Sebastian Bergmann", 1345 | "email": "sebastian@phpunit.de", 1346 | "role": "lead" 1347 | } 1348 | ], 1349 | "description": "Library for counting the lines of code in PHP source code", 1350 | "homepage": "https://github.com/sebastianbergmann/lines-of-code", 1351 | "support": { 1352 | "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", 1353 | "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" 1354 | }, 1355 | "funding": [ 1356 | { 1357 | "url": "https://github.com/sebastianbergmann", 1358 | "type": "github" 1359 | } 1360 | ], 1361 | "time": "2023-12-22T06:20:34+00:00" 1362 | }, 1363 | { 1364 | "name": "sebastian/object-enumerator", 1365 | "version": "4.0.4", 1366 | "source": { 1367 | "type": "git", 1368 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1369 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" 1370 | }, 1371 | "dist": { 1372 | "type": "zip", 1373 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", 1374 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", 1375 | "shasum": "" 1376 | }, 1377 | "require": { 1378 | "php": ">=7.3", 1379 | "sebastian/object-reflector": "^2.0", 1380 | "sebastian/recursion-context": "^4.0" 1381 | }, 1382 | "require-dev": { 1383 | "phpunit/phpunit": "^9.3" 1384 | }, 1385 | "type": "library", 1386 | "extra": { 1387 | "branch-alias": { 1388 | "dev-master": "4.0-dev" 1389 | } 1390 | }, 1391 | "autoload": { 1392 | "classmap": [ 1393 | "src/" 1394 | ] 1395 | }, 1396 | "notification-url": "https://packagist.org/downloads/", 1397 | "license": [ 1398 | "BSD-3-Clause" 1399 | ], 1400 | "authors": [ 1401 | { 1402 | "name": "Sebastian Bergmann", 1403 | "email": "sebastian@phpunit.de" 1404 | } 1405 | ], 1406 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1407 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1408 | "support": { 1409 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1410 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" 1411 | }, 1412 | "funding": [ 1413 | { 1414 | "url": "https://github.com/sebastianbergmann", 1415 | "type": "github" 1416 | } 1417 | ], 1418 | "time": "2020-10-26T13:12:34+00:00" 1419 | }, 1420 | { 1421 | "name": "sebastian/object-reflector", 1422 | "version": "2.0.4", 1423 | "source": { 1424 | "type": "git", 1425 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1426 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" 1427 | }, 1428 | "dist": { 1429 | "type": "zip", 1430 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1431 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1432 | "shasum": "" 1433 | }, 1434 | "require": { 1435 | "php": ">=7.3" 1436 | }, 1437 | "require-dev": { 1438 | "phpunit/phpunit": "^9.3" 1439 | }, 1440 | "type": "library", 1441 | "extra": { 1442 | "branch-alias": { 1443 | "dev-master": "2.0-dev" 1444 | } 1445 | }, 1446 | "autoload": { 1447 | "classmap": [ 1448 | "src/" 1449 | ] 1450 | }, 1451 | "notification-url": "https://packagist.org/downloads/", 1452 | "license": [ 1453 | "BSD-3-Clause" 1454 | ], 1455 | "authors": [ 1456 | { 1457 | "name": "Sebastian Bergmann", 1458 | "email": "sebastian@phpunit.de" 1459 | } 1460 | ], 1461 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1462 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1463 | "support": { 1464 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1465 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" 1466 | }, 1467 | "funding": [ 1468 | { 1469 | "url": "https://github.com/sebastianbergmann", 1470 | "type": "github" 1471 | } 1472 | ], 1473 | "time": "2020-10-26T13:14:26+00:00" 1474 | }, 1475 | { 1476 | "name": "sebastian/recursion-context", 1477 | "version": "4.0.5", 1478 | "source": { 1479 | "type": "git", 1480 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1481 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" 1482 | }, 1483 | "dist": { 1484 | "type": "zip", 1485 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", 1486 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", 1487 | "shasum": "" 1488 | }, 1489 | "require": { 1490 | "php": ">=7.3" 1491 | }, 1492 | "require-dev": { 1493 | "phpunit/phpunit": "^9.3" 1494 | }, 1495 | "type": "library", 1496 | "extra": { 1497 | "branch-alias": { 1498 | "dev-master": "4.0-dev" 1499 | } 1500 | }, 1501 | "autoload": { 1502 | "classmap": [ 1503 | "src/" 1504 | ] 1505 | }, 1506 | "notification-url": "https://packagist.org/downloads/", 1507 | "license": [ 1508 | "BSD-3-Clause" 1509 | ], 1510 | "authors": [ 1511 | { 1512 | "name": "Sebastian Bergmann", 1513 | "email": "sebastian@phpunit.de" 1514 | }, 1515 | { 1516 | "name": "Jeff Welch", 1517 | "email": "whatthejeff@gmail.com" 1518 | }, 1519 | { 1520 | "name": "Adam Harvey", 1521 | "email": "aharvey@php.net" 1522 | } 1523 | ], 1524 | "description": "Provides functionality to recursively process PHP variables", 1525 | "homepage": "https://github.com/sebastianbergmann/recursion-context", 1526 | "support": { 1527 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1528 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" 1529 | }, 1530 | "funding": [ 1531 | { 1532 | "url": "https://github.com/sebastianbergmann", 1533 | "type": "github" 1534 | } 1535 | ], 1536 | "time": "2023-02-03T06:07:39+00:00" 1537 | }, 1538 | { 1539 | "name": "sebastian/resource-operations", 1540 | "version": "3.0.4", 1541 | "source": { 1542 | "type": "git", 1543 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1544 | "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" 1545 | }, 1546 | "dist": { 1547 | "type": "zip", 1548 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", 1549 | "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", 1550 | "shasum": "" 1551 | }, 1552 | "require": { 1553 | "php": ">=7.3" 1554 | }, 1555 | "require-dev": { 1556 | "phpunit/phpunit": "^9.0" 1557 | }, 1558 | "type": "library", 1559 | "extra": { 1560 | "branch-alias": { 1561 | "dev-main": "3.0-dev" 1562 | } 1563 | }, 1564 | "autoload": { 1565 | "classmap": [ 1566 | "src/" 1567 | ] 1568 | }, 1569 | "notification-url": "https://packagist.org/downloads/", 1570 | "license": [ 1571 | "BSD-3-Clause" 1572 | ], 1573 | "authors": [ 1574 | { 1575 | "name": "Sebastian Bergmann", 1576 | "email": "sebastian@phpunit.de" 1577 | } 1578 | ], 1579 | "description": "Provides a list of PHP built-in functions that operate on resources", 1580 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1581 | "support": { 1582 | "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" 1583 | }, 1584 | "funding": [ 1585 | { 1586 | "url": "https://github.com/sebastianbergmann", 1587 | "type": "github" 1588 | } 1589 | ], 1590 | "time": "2024-03-14T16:00:52+00:00" 1591 | }, 1592 | { 1593 | "name": "sebastian/type", 1594 | "version": "3.2.1", 1595 | "source": { 1596 | "type": "git", 1597 | "url": "https://github.com/sebastianbergmann/type.git", 1598 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" 1599 | }, 1600 | "dist": { 1601 | "type": "zip", 1602 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", 1603 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", 1604 | "shasum": "" 1605 | }, 1606 | "require": { 1607 | "php": ">=7.3" 1608 | }, 1609 | "require-dev": { 1610 | "phpunit/phpunit": "^9.5" 1611 | }, 1612 | "type": "library", 1613 | "extra": { 1614 | "branch-alias": { 1615 | "dev-master": "3.2-dev" 1616 | } 1617 | }, 1618 | "autoload": { 1619 | "classmap": [ 1620 | "src/" 1621 | ] 1622 | }, 1623 | "notification-url": "https://packagist.org/downloads/", 1624 | "license": [ 1625 | "BSD-3-Clause" 1626 | ], 1627 | "authors": [ 1628 | { 1629 | "name": "Sebastian Bergmann", 1630 | "email": "sebastian@phpunit.de", 1631 | "role": "lead" 1632 | } 1633 | ], 1634 | "description": "Collection of value objects that represent the types of the PHP type system", 1635 | "homepage": "https://github.com/sebastianbergmann/type", 1636 | "support": { 1637 | "issues": "https://github.com/sebastianbergmann/type/issues", 1638 | "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" 1639 | }, 1640 | "funding": [ 1641 | { 1642 | "url": "https://github.com/sebastianbergmann", 1643 | "type": "github" 1644 | } 1645 | ], 1646 | "time": "2023-02-03T06:13:03+00:00" 1647 | }, 1648 | { 1649 | "name": "sebastian/version", 1650 | "version": "3.0.2", 1651 | "source": { 1652 | "type": "git", 1653 | "url": "https://github.com/sebastianbergmann/version.git", 1654 | "reference": "c6c1022351a901512170118436c764e473f6de8c" 1655 | }, 1656 | "dist": { 1657 | "type": "zip", 1658 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", 1659 | "reference": "c6c1022351a901512170118436c764e473f6de8c", 1660 | "shasum": "" 1661 | }, 1662 | "require": { 1663 | "php": ">=7.3" 1664 | }, 1665 | "type": "library", 1666 | "extra": { 1667 | "branch-alias": { 1668 | "dev-master": "3.0-dev" 1669 | } 1670 | }, 1671 | "autoload": { 1672 | "classmap": [ 1673 | "src/" 1674 | ] 1675 | }, 1676 | "notification-url": "https://packagist.org/downloads/", 1677 | "license": [ 1678 | "BSD-3-Clause" 1679 | ], 1680 | "authors": [ 1681 | { 1682 | "name": "Sebastian Bergmann", 1683 | "email": "sebastian@phpunit.de", 1684 | "role": "lead" 1685 | } 1686 | ], 1687 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1688 | "homepage": "https://github.com/sebastianbergmann/version", 1689 | "support": { 1690 | "issues": "https://github.com/sebastianbergmann/version/issues", 1691 | "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" 1692 | }, 1693 | "funding": [ 1694 | { 1695 | "url": "https://github.com/sebastianbergmann", 1696 | "type": "github" 1697 | } 1698 | ], 1699 | "time": "2020-09-28T06:39:44+00:00" 1700 | }, 1701 | { 1702 | "name": "theseer/tokenizer", 1703 | "version": "1.2.3", 1704 | "source": { 1705 | "type": "git", 1706 | "url": "https://github.com/theseer/tokenizer.git", 1707 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" 1708 | }, 1709 | "dist": { 1710 | "type": "zip", 1711 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 1712 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 1713 | "shasum": "" 1714 | }, 1715 | "require": { 1716 | "ext-dom": "*", 1717 | "ext-tokenizer": "*", 1718 | "ext-xmlwriter": "*", 1719 | "php": "^7.2 || ^8.0" 1720 | }, 1721 | "type": "library", 1722 | "autoload": { 1723 | "classmap": [ 1724 | "src/" 1725 | ] 1726 | }, 1727 | "notification-url": "https://packagist.org/downloads/", 1728 | "license": [ 1729 | "BSD-3-Clause" 1730 | ], 1731 | "authors": [ 1732 | { 1733 | "name": "Arne Blankerts", 1734 | "email": "arne@blankerts.de", 1735 | "role": "Developer" 1736 | } 1737 | ], 1738 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1739 | "support": { 1740 | "issues": "https://github.com/theseer/tokenizer/issues", 1741 | "source": "https://github.com/theseer/tokenizer/tree/1.2.3" 1742 | }, 1743 | "funding": [ 1744 | { 1745 | "url": "https://github.com/theseer", 1746 | "type": "github" 1747 | } 1748 | ], 1749 | "time": "2024-03-03T12:36:25+00:00" 1750 | } 1751 | ], 1752 | "aliases": [], 1753 | "minimum-stability": "stable", 1754 | "stability-flags": [], 1755 | "prefer-stable": false, 1756 | "prefer-lowest": false, 1757 | "platform": { 1758 | "php": "^8.0|^8.1|^8.2|^8.3" 1759 | }, 1760 | "platform-dev": [], 1761 | "plugin-api-version": "2.3.0" 1762 | } 1763 | -------------------------------------------------------------------------------- /iplimiter_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syntaxseed/iplimiter/be8e12c88fd933631e056dd87a7e1a64132bef2e/iplimiter_screenshot.png -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/DatabaseInterface.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 22 | } 23 | 24 | public function executePrepared(string $statement, array $values): ?int 25 | { 26 | try { 27 | $stmt = $this->pdo->prepare($statement); 28 | $stmt->execute($values); 29 | return $stmt->rowCount(); 30 | } catch (\PDOException $e) { 31 | throw new \Exception($e->getMessage()); 32 | } 33 | return null; 34 | } 35 | 36 | public function fetchPrepared(string $statement, array $values): array 37 | { 38 | try { 39 | $stmt = $this->pdo->prepare($statement); 40 | $stmt->execute($values); 41 | $result = $stmt->fetchAll(\PDO::FETCH_ASSOC); 42 | return $result; 43 | } catch (\PDOException $e) { 44 | throw new \Exception($e->getMessage()); 45 | return []; 46 | } 47 | } 48 | 49 | public function executeSQL(string $sql): int 50 | { 51 | try { 52 | return $this->pdo->exec($sql); 53 | } catch (\PDOException $e) { 54 | throw new \Exception($e->getMessage()); 55 | return 0; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/IPLimiter.php: -------------------------------------------------------------------------------- 1 | lastError = 'IPLimiter requires a table name as the second parameter.'; 38 | throw new \Exception($this->lastError); 39 | } 40 | $this->db = $db; 41 | $this->tableName = $tableName; 42 | } 43 | 44 | /** 45 | * Set up IPLimiter for a given IP address and event category. All other method 46 | * calls will use these. 47 | * 48 | * @param string $ip 49 | * @param string $category 50 | * @return void 51 | */ 52 | public function event($ip, $category) 53 | { 54 | $this->ip = $ip; 55 | $this->category = $category; 56 | return $this; 57 | } 58 | 59 | /** 60 | * Log an action by an IP for a given category. 61 | * 62 | * @return IPLimiter 63 | * @throws Exception 64 | */ 65 | public function log() 66 | { 67 | $this->checkEvent(); 68 | 69 | try { 70 | $sql = 'INSERT INTO '.$this->tableName.' (ip, category, lastattempt) VALUES (:ip, :category, '.time().') ON DUPLICATE KEY UPDATE attempts=attempts+1, lastattempt='.time().';'; 71 | $result = $this->db->executePrepared($sql, [$this->ip, $this->category]); 72 | } catch (\Exception $e) { 73 | $this->lastError = $e->getMessage(); 74 | throw $e; 75 | } 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Delete the event (IP/category) record from the DB. 82 | * 83 | * @return bool 84 | * @throws Exception 85 | */ 86 | public function deleteEvent() 87 | { 88 | $this->checkEvent(); 89 | 90 | try { 91 | $sql = 'DELETE FROM '.$this->tableName.' WHERE ip=:ip AND category=:category;'; 92 | $result = $this->db->executePrepared($sql, [$this->ip, $this->category]); 93 | } catch (\Exception $e) { 94 | $this->lastError = $e->getMessage(); 95 | throw $e; 96 | } 97 | 98 | if (is_null($result) || $result < 1) { 99 | return false; 100 | } 101 | return true; 102 | } 103 | 104 | /** 105 | * Delete all event records for a given IP from the DB. 106 | * 107 | * @return bool 108 | * @throws Exception 109 | */ 110 | public function deleteIP($ip) 111 | { 112 | try { 113 | $sql = 'DELETE FROM '.$this->tableName.' WHERE ip=:ip;'; 114 | $result = $this->db->executePrepared($sql, [$ip]); 115 | } catch (\Exception $e) { 116 | $this->lastError = $e->getMessage(); 117 | throw $e; 118 | } 119 | 120 | if (is_null($result) || $result < 1) { 121 | return false; 122 | } 123 | return true; 124 | } 125 | 126 | /** 127 | * Check if an IP and category have been set. 128 | * 129 | * @return void 130 | * @throws Exception 131 | */ 132 | protected function checkEvent() 133 | { 134 | if (!isset($this->ip) || !isset($this->category)) { 135 | $this->lastError = "IPLimiter: IP and category have not been set. Use setup() method."; 136 | throw new \Exception($this->lastError); 137 | exit(); 138 | } 139 | } 140 | 141 | /** 142 | * Determine whether a record exists in the DB for this event. 143 | * 144 | * @return bool 145 | * @throws Exception 146 | */ 147 | public function exists() 148 | { 149 | $this->checkEvent(); 150 | 151 | try { 152 | $sql = 'SELECT (1) as found FROM '.$this->tableName.' WHERE ip=:ip AND category=:category;'; 153 | $result = $this->db->fetchPrepared($sql, [$this->ip, $this->category]); 154 | } catch (\Exception $e) { 155 | $this->lastError = $e->getMessage(); 156 | throw $e; 157 | } 158 | 159 | if (empty($result)) { 160 | return false; 161 | } 162 | return ($result[0]['found'] == "1"); 163 | } 164 | 165 | /** 166 | * Get attempts by an IP on a given category. 167 | * 168 | * @return int|null 169 | * @throws Exception 170 | */ 171 | public function attempts() 172 | { 173 | $this->checkEvent(); 174 | 175 | try { 176 | $sql = 'SELECT attempts FROM '.$this->tableName.' WHERE ip=:ip AND category=:category;'; 177 | $result = $this->db->fetchPrepared($sql, [$this->ip, $this->category]); 178 | } catch (\Exception $e) { 179 | $this->lastError = $e->getMessage(); 180 | throw $e; 181 | } 182 | 183 | if (empty($result)) { 184 | return null; 185 | } 186 | 187 | return $result[0]['attempts']; 188 | } 189 | 190 | /** 191 | * Reset the attempts for a given IP/Category. 192 | * 193 | * @return bool 194 | * @throws Exception 195 | */ 196 | public function resetAttempts() 197 | { 198 | $this->checkEvent(); 199 | 200 | try { 201 | $sql = 'UPDATE '.$this->tableName.' SET attempts=0 WHERE ip=:ip AND category=:category;'; 202 | $result = $this->db->executePrepared($sql, [$this->ip, $this->category]); 203 | } catch (\Exception $e) { 204 | $this->lastError = $e->getMessage(); 205 | throw $e; 206 | } 207 | 208 | if (is_null($result) || $result != 1) { 209 | return false; 210 | } 211 | return true; 212 | } 213 | 214 | /** 215 | * Get last attempt by the IP address in the given category. 216 | * 217 | * @param bool $asSecondsAgo 218 | * @return int|null 219 | * @throws Exception 220 | */ 221 | public function last($asSecondsAgo=true) 222 | { 223 | $this->checkEvent(); 224 | 225 | try { 226 | $sql = 'SELECT lastattempt FROM '.$this->tableName.' WHERE ip=:ip AND category=:category;'; 227 | $result = $this->db->fetchPrepared($sql, [$this->ip, $this->category]); 228 | } catch (\Exception $e) { 229 | $this->lastError = $e->getMessage(); 230 | throw $e; 231 | } 232 | 233 | 234 | 235 | if (empty($result)) { 236 | return null; 237 | } 238 | 239 | $lastAttempt = intval($result[0]['lastattempt']); 240 | 241 | if ($asSecondsAgo) { 242 | $lastAttempt = time() - $lastAttempt; 243 | } 244 | 245 | return $lastAttempt; 246 | } 247 | 248 | 249 | /** 250 | * Execute an IPLimiter rule on a given IP/category. 251 | * This allows associating certian categories of events to a given ruleset, 252 | * then easily verify if a current attempt either passes or fails that rule. 253 | * And ensuring that attempts are reset after a given cool off period. 0 for always. -1 for never. 254 | * 255 | * Rule format JSON: 256 | * { 257 | * "resetAtSeconds": 3600, 258 | * "waitAtLeast": 360, 259 | * "allowedAttempts": 5, 260 | * "allowBanned":false 261 | * } 262 | * Means: 263 | * - If record doesn't exist in the database, PASS. 264 | * - If last attempt was at or older than (>=) "resetAtSeconds" seconds ago, reset attempts to 0. -1 for don't reset. 265 | * - If last attempt was more recent than (<) "waitAtLeast" seconds ago, FAIL. -1 for ignore. 266 | * - If current attempts is more than (>) "allowedAttempts", FAIL. -1 for ignore. 267 | * - If banned, FAIL if "allowBanned" is false. Use true to ignore banned status. 268 | * - Otherwise, PASS. 269 | * 270 | * @param json $rule 271 | * @return bool 272 | * @throws Exception 273 | */ 274 | public function rule($rule) 275 | { 276 | $this->checkEvent(); 277 | 278 | if (!$this->isValidRule($rule)) { 279 | throw new \Exception($this->lastError); 280 | } 281 | 282 | $rule = json_decode($rule); 283 | 284 | // RULE STEP 0: Check if there is even a record in the DB. 285 | if (!$this->exists()) { 286 | return true; // Does not exist in DB. 287 | } 288 | 289 | // RULE STEP 1: If last attempt was >= "resetAtSeconds" seconds ago, set attempts to 0. -1 for don't reset. 290 | $last = $this->last(); 291 | if (($rule->resetAtSeconds >= 0) && ($last >= $rule->resetAtSeconds)) { 292 | $this->resetAttempts(); 293 | } 294 | 295 | // RULE STEP 2: If last attempt was < "waitAtLeast" seconds ago, FAIL. -1 for ignore. 296 | $last = $this->last(); 297 | if (($rule->waitAtLeast >= 0) && ($last < $rule->waitAtLeast)) { 298 | return false; 299 | } 300 | 301 | // RULE STEP 3: If current attempts is > "attempts", FAIL. -1 for ignore. 302 | $attempts = $this->attempts(); 303 | if (($rule->allowedAttempts >= 0) && ($attempts > $rule->allowedAttempts)) { 304 | return false; 305 | } 306 | 307 | // RULE STEP 4: If banned, FAIL if "allowBanned" is false. 308 | $banned = $this->isBanned(); 309 | if (!$rule->allowBanned && $banned) { 310 | return false; 311 | } 312 | 313 | // RULE STEP 5: Otherwise, PASS. 314 | return true; 315 | } 316 | 317 | /** 318 | * Determine if a Rule string is a valid format. 319 | * 320 | * @param string $ruleJSON 321 | * @return boolean 322 | */ 323 | private function isValidRule($ruleJSON) 324 | { 325 | $rule = json_decode($ruleJSON); 326 | 327 | if (json_last_error() !== JSON_ERROR_NONE) { 328 | $this->lastError = 'IPLimiter rule is not valid json.'; 329 | return false; 330 | } 331 | 332 | if (!isset($rule->resetAtSeconds) || !is_int($rule->resetAtSeconds) || ($rule->resetAtSeconds < -1)) { 333 | $this->lastError = 'IPLimiter invalid rule. Bad "resetAtSeconds" value.'; 334 | return false; 335 | } 336 | 337 | if (!isset($rule->waitAtLeast) || !is_int($rule->waitAtLeast) || ($rule->waitAtLeast < -1)) { 338 | $this->lastError = 'IPLimiter invalid rule. Bad "waitAtLeast" value.'; 339 | return false; 340 | } 341 | 342 | if (!isset($rule->allowedAttempts) || !is_int($rule->allowedAttempts) || ($rule->allowedAttempts < -1)) { 343 | $this->lastError = 'IPLimiter invalid rule. Bad "allowedAttempts" value.'; 344 | return false; 345 | } 346 | 347 | if (!isset($rule->allowBanned) || !is_bool($rule->allowBanned)) { 348 | $this->lastError = 'IPLimiter invalid rule. Bad "allowBanned" value.'; 349 | return false; 350 | } 351 | 352 | return true; 353 | } 354 | 355 | /** 356 | * Set an IP as banned for the event category. Returns ban status. 357 | * 358 | * @return bool 359 | * @throws Exception 360 | */ 361 | public function ban() 362 | { 363 | $this->checkEvent(); 364 | 365 | if ($this->isBanned()) { 366 | return true; 367 | } 368 | 369 | try { 370 | $sql = 'UPDATE '.$this->tableName.' SET banned=1 WHERE ip=:ip AND category=:category;'; 371 | $result = $this->db->executePrepared($sql, [$this->ip, $this->category]); 372 | } catch (\Exception $e) { 373 | $this->lastError = $e->getMessage(); 374 | throw $e; 375 | } 376 | 377 | if (is_null($result) || $result != 1) { 378 | return false; 379 | } 380 | return true; 381 | } 382 | 383 | /** 384 | * Get whether this IP is banned for this event category. 385 | * 386 | * @return bool 387 | * @throws Exception 388 | */ 389 | public function isBanned() 390 | { 391 | $this->checkEvent(); 392 | 393 | try { 394 | $sql = 'SELECT banned FROM '.$this->tableName.' WHERE ip=:ip AND category=:category;'; 395 | $result = $this->db->fetchPrepared($sql, [$this->ip, $this->category]); 396 | } catch (\Exception $e) { 397 | $this->lastError = $e->getMessage(); 398 | throw $e; 399 | } 400 | 401 | if (empty($result)) { 402 | return false; 403 | } 404 | 405 | return ($result[0]['banned'] == 1); 406 | } 407 | 408 | /** 409 | * Set an IP as banned for the event category. Returns ban status. 410 | * 411 | * @return bool 412 | * @throws Exception 413 | */ 414 | public function unBan() 415 | { 416 | $this->checkEvent(); 417 | 418 | if (!$this->isBanned()) { 419 | return false; 420 | } 421 | 422 | try { 423 | $sql = 'UPDATE '.$this->tableName.' SET banned=0 WHERE ip=:ip AND category=:category;'; 424 | $result = $this->db->executePrepared($sql, [$this->ip, $this->category]); 425 | } catch (\Exception $e) { 426 | $this->lastError = $e->getMessage(); 427 | throw $e; 428 | } 429 | 430 | if (is_null($result) || $result != 1) { 431 | return true; 432 | } 433 | 434 | return false; 435 | } 436 | 437 | 438 | /** 439 | * Getter for the lastError encountered by this object. 440 | * 441 | * @return string 442 | */ 443 | public function getLastError() 444 | { 445 | return $this->lastError; 446 | } 447 | 448 | /** 449 | * Creates the table needed for this package if it doesn't already exist. 450 | * 451 | * @return bool 452 | */ 453 | public function migrate() 454 | { 455 | try { 456 | $sql = <<tableName}` ( 458 | `ip` varchar(39) COLLATE ascii_general_ci NOT NULL, 459 | `category` varchar(128) COLLATE utf8mb4_general_ci NOT NULL, 460 | `attempts` int(10) unsigned NOT NULL DEFAULT '1', 461 | `lastattempt` bigint(20) NOT NULL, 462 | `banned` tinyint(1) NOT NULL DEFAULT '0', 463 | PRIMARY KEY (`ip`, `category`) 464 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; 465 | EOF; 466 | $this->db->executeSQL($sql); 467 | } catch (\Exception $e) { 468 | $this->lastError = $e->getMessage(); 469 | throw $e; 470 | } 471 | return true; 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /tests/IPLimiterTest.php: -------------------------------------------------------------------------------- 1 | PDO::ERRMODE_EXCEPTION, 25 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, 26 | PDO::ATTR_EMULATE_PREPARES => false, 27 | ]; 28 | 29 | try { 30 | self::$pdo = new PDO($dsn, $user, $pass, $options); 31 | } catch (\PDOException $e) { 32 | throw new \PDOException($e->getMessage(), (int)$e->getCode()); 33 | } 34 | } 35 | 36 | /** 37 | * Called before all tests in this class are run. 38 | */ 39 | public static function setUpBeforeClass(): void 40 | { 41 | self::connectToDB(); 42 | } 43 | 44 | /** 45 | * Called before each test method is run. 46 | */ 47 | protected function setUp(): void 48 | { 49 | } 50 | 51 | /** 52 | * Test migrating the database table. 53 | */ 54 | public function testMigrateDatabaseTable() 55 | { 56 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 57 | $result = $ipLimiter->migrate(); 58 | $this->assertTrue($result); 59 | } 60 | 61 | /** 62 | * Test with a missing connection object. 63 | */ 64 | public function testNoConnectionGivesException() 65 | { 66 | $this->expectException(\Exception::class); 67 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(new PDO(null)), 'syntaxseed_iplimiter'); 68 | } 69 | 70 | /** 71 | * Basic test of valid class instantiation. 72 | */ 73 | public function testValidClassInstantiation() 74 | { 75 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 76 | $this->assertTrue(is_object($ipLimiter)); 77 | } 78 | 79 | /** 80 | * Test logging a visit and get the count for it. 81 | */ 82 | public function testBasicLogging() 83 | { 84 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 85 | $ipLimiter->event('0.0.0.1', 'phpunit'); 86 | $ipLimiter->log(); 87 | 88 | $attempts = $ipLimiter->attempts(); 89 | $this->assertEquals(1, $attempts); 90 | 91 | $result = $ipLimiter->deleteEvent(); 92 | } 93 | 94 | /** 95 | * Test logging a visit and get the count for it. 96 | */ 97 | public function testMethodChainedLogging() 98 | { 99 | $ipLimiter = (new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter')) 100 | ->event('0.0.0.1', 'phpunit') 101 | ->log() 102 | ->log(); 103 | 104 | $attempts = $ipLimiter->attempts(); 105 | $this->assertEquals(2, $attempts); 106 | 107 | $result = $ipLimiter->deleteEvent(); 108 | } 109 | 110 | /** 111 | * Test resetting the attempts count for an ip/category (event). 112 | */ 113 | public function testResettingAttempts() 114 | { 115 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 116 | $ipLimiter->event('0.0.0.1', 'phpunit'); 117 | 118 | $ipLimiter->log(); 119 | $attempts = $ipLimiter->attempts(); 120 | $this->assertGreaterThan(0, $attempts); 121 | 122 | $ipLimiter->resetAttempts(); 123 | $attempts = $ipLimiter->attempts(); 124 | 125 | $this->assertEquals(0, $attempts); 126 | } 127 | 128 | /** 129 | * Test deleting an event. 130 | */ 131 | public function testDeleteEvent() 132 | { 133 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 134 | $ipLimiter->event('0.0.0.2', 'phpunit'); 135 | 136 | // Delete record that doesn't exit. 137 | $result = $ipLimiter->deleteEvent(); 138 | $this->assertFalse($result); 139 | 140 | // Actually create an event. 141 | $ipLimiter->log(); 142 | 143 | // Delete record that does exist. 144 | $result = $ipLimiter->deleteEvent(); 145 | $this->assertTrue($result); 146 | } 147 | 148 | /** 149 | * Test getting the time of the last attempt. 150 | */ 151 | public function testLastEventTime() 152 | { 153 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 154 | $ipLimiter->event('0.0.0.1', 'phpunit'); 155 | //$ipLimiter->log(); 156 | $last = $ipLimiter->last(false); 157 | $lastSecsAgo = $ipLimiter->last(); 158 | 159 | $this->assertIsInt($last); 160 | $this->assertLessThan($last, $lastSecsAgo); 161 | } 162 | 163 | /** 164 | * Test getting if an event exists. 165 | */ 166 | public function testEventExists() 167 | { 168 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 169 | $ipLimiter->event('9.9.9.9', 'phpunit'); 170 | $ipLimiter->deleteEvent(); 171 | 172 | // Check record that does not exist. 173 | $result = $ipLimiter->exists(); 174 | $this->assertFalse($result); 175 | 176 | // Actually create an event. 177 | $ipLimiter->log(); 178 | 179 | // Check record that does exist. 180 | $result = $ipLimiter->exists(); 181 | $this->assertTrue($result); 182 | } 183 | 184 | /** 185 | * Test running a rule set. 186 | */ 187 | public function testRules() 188 | { 189 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 190 | $ipLimiter->event('0.0.0.3', 'phpunit'); 191 | $ipLimiter->deleteEvent(); 192 | $ipLimiter->log(); 193 | $ipLimiter->log(); 194 | $ipLimiter->log(); 195 | 196 | // Test if last attempt was at least 60 seconds ago (fails). 197 | $ruleResult = $ipLimiter->rule('{ 198 | "resetAtSeconds":-1, 199 | "waitAtLeast":60, 200 | "allowedAttempts":-1, 201 | "allowBanned":true 202 | }'); 203 | $this->assertFalse($ruleResult); 204 | 205 | // Test if there has been a max of 3 attempts (passes). 206 | $ruleResult = $ipLimiter->rule('{ 207 | "resetAtSeconds":-1, 208 | "waitAtLeast":-1, 209 | "allowedAttempts":3, 210 | "allowBanned":true 211 | }'); 212 | $this->assertTrue($ruleResult); 213 | 214 | // Reset attempts count if last attempt was at least 0 seconds ago. 215 | // Then test if there has been a max of 1 attempt. 216 | $ruleResult = $ipLimiter->rule('{ 217 | "resetAtSeconds":0, 218 | "waitAtLeast":-1, 219 | "allowedAttempts":1, 220 | "allowBanned":true 221 | }'); 222 | $this->assertTrue($ruleResult); 223 | 224 | // Don't allow banned IPs regardless of relaxed rules. 225 | $ipLimiter->ban(); 226 | $ruleResult = $ipLimiter->rule('{ 227 | "resetAtSeconds":-1, 228 | "waitAtLeast":0, 229 | "allowedAttempts":99999, 230 | "allowBanned":false 231 | }'); 232 | $this->assertFalse($ruleResult); 233 | } 234 | 235 | /** 236 | * Test banning an IP for the event category. 237 | */ 238 | public function testBanning() 239 | { 240 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 241 | $ipLimiter->event('0.0.0.1', 'phpunit'); 242 | $ipLimiter->log(); 243 | 244 | $isBanned = $ipLimiter->ban(); 245 | $this->assertTrue($isBanned); 246 | 247 | $isBanned = $ipLimiter->isBanned(); 248 | $this->assertTrue($isBanned); 249 | 250 | $isBanned = $ipLimiter->unBan(); 251 | $this->assertFalse($isBanned); 252 | 253 | $isBanned = $ipLimiter->isBanned(); 254 | $this->assertFalse($isBanned); 255 | } 256 | 257 | /** 258 | * Test deleting all events for an IP. 259 | */ 260 | public function testDeleteIp() 261 | { 262 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 263 | $ipLimiter->event('0.0.0.1', 'phpunit'); 264 | $ipLimiter->log(); 265 | $ipLimiter->event('0.0.0.1', 'otherevent'); 266 | $ipLimiter->log(); 267 | 268 | $result = $ipLimiter->deleteIP('0.0.0.1'); 269 | $this->assertTrue($result); 270 | 271 | $attempts = $ipLimiter->attempts(); 272 | $this->assertEquals(0, $attempts); 273 | } 274 | 275 | public function testIpV6Address() 276 | { 277 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 278 | $ipLimiter->event('2001:0db8:0000:0000:0000:ff00:0042:8329', 'phpunit'); 279 | $ipLimiter->log(); 280 | $ipLimiter->log(); 281 | $attempts = $ipLimiter->attempts(); 282 | 283 | $this->assertEquals(2, $attempts); 284 | $ipLimiter->deleteEvent(); 285 | } 286 | 287 | 288 | /** 289 | * Called after all tests in this class are run. 290 | */ 291 | public static function tearDownAfterClass(): void 292 | { 293 | $ipLimiter = new Syntaxseed\IPLimiter\IPLimiter(new DatabasePDO(self::$pdo), 'syntaxseed_iplimiter'); 294 | $ipLimiter->deleteIP('0.0.0.1'); 295 | $ipLimiter->deleteIP('0.0.0.2'); 296 | $ipLimiter->deleteIP('0.0.0.3'); 297 | $ipLimiter->deleteIP('9.9.9.9'); 298 | self::$pdo = null; 299 | unset($ipLimiter); 300 | } 301 | } 302 | --------------------------------------------------------------------------------