Commit:    35218a282579109bb365f0ae5da42c983487b688
Author:    Peter Kokot <peterko...@gmail.com>         Mon, 3 Dec 2018 03:08:45 
+0100
Parents:   83d6860af9a4554a234177d1d38f37907fcb4b4f
Branches:  master

Link:       
http://git.php.net/?p=web/bugs.git;a=commitdiff;h=35218a282579109bb365f0ae5da42c983487b688

Log:
Refactor numerical Captcha package into class

The Text_CAPTCHA_Numeral can be integrated in the project source code
instead of using a separate unmaintained dependency.

This patch also adds a simple unit test.

Changed paths:
  M  .gitignore
  M  README.md
  M  composer.json
  M  composer.lock
  A  phpunit.xml.dist
  A  src/Utils/Captcha.php
  M  templates/addghpull.php
  M  templates/addpatch.php
  A  tests/Utils/CaptchaTest.php
  M  www/bug-pwd-finder.php
  M  www/bug.php
  M  www/gh-pull-add.php
  M  www/patch-add.php
  M  www/report.php

diff --git a/.gitignore b/.gitignore
index 56b323f..6c2d011 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,8 @@
 # Configuration file
 /local_config.php
 
+# Local specific PHPUnit configuration
+/phpunit.xml
+
 # Generated by Composer
 /vendor/
diff --git a/README.md b/README.md
index 5170796..564aa47 100644
--- a/README.md
+++ b/README.md
@@ -21,10 +21,19 @@ composer install
 
 ```bash
 pear channel-update pear.php.net
-pear install --alldeps Text_CAPTCHA_Numeral Text_Diff HTTP_Upload-1.0.0b4
+pear install --alldeps Text_Diff HTTP_Upload-1.0.0b4
 ```
 
 * Database:
 
 Create a new database using `sql/database.sql`, create database schema
 `sql/schema.sql` and insert fixtures using `sql/fixtures.sql`.
+
+## Tests
+
+Application unit tests can be executed in development environment after
+installing dependencies by running `phpunit`:
+
+```bash
+phpunit
+```
diff --git a/composer.json b/composer.json
index 3063851..ddb3fa3 100644
--- a/composer.json
+++ b/composer.json
@@ -27,5 +27,18 @@
         "ext-pdo": "*",
         "ext-pdo_mysql": "*",
         "ext-session": "*"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^7.4"
+    },
+    "autoload": {
+        "psr-4": {
+            "App\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "App\\Tests\\": "tests/"
+        }
     }
 }
diff --git a/composer.lock b/composer.lock
index 4405657..b0475da 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,9 +4,1418 @@
         "Read more about it at 
https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies";,
         "This file is @generated automatically"
     ],
-    "content-hash": "ec56b1b2037ec16e6e8fd80f4c1b8f1a",
+    "content-hash": "bb74d235e97b99c5049cfbbdbb6a3654",
     "packages": [],
-    "packages-dev": [],
+    "packages-dev": [
+        {
+            "name": "doctrine/instantiator",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/instantiator.git";,
+                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda";,
+                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "athletic/athletic": "~0.1.8",
+                "ext-pdo": "*",
+                "ext-phar": "*",
+                "phpunit/phpunit": "^6.2.3",
+                "squizlabs/php_codesniffer": "^3.0.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "ocram...@gmail.com",
+                    "homepage": "http://ocramius.github.com/";
+                }
+            ],
+            "description": "A small, lightweight utility to instantiate 
objects in PHP without invoking their constructors",
+            "homepage": "https://github.com/doctrine/instantiator";,
+            "keywords": [
+                "constructor",
+                "instantiate"
+            ],
+            "time": "2017-07-22T11:58:36+00:00"
+        },
+        {
+            "name": "myclabs/deep-copy",
+            "version": "1.8.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/myclabs/DeepCopy.git";,
+                "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8";,
+                "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "replace": {
+                "myclabs/deep-copy": "self.version"
+            },
+            "require-dev": {
+                "doctrine/collections": "^1.0",
+                "doctrine/common": "^2.6",
+                "phpunit/phpunit": "^7.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "DeepCopy\\": "src/DeepCopy/"
+                },
+                "files": [
+                    "src/DeepCopy/deep_copy.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "MIT"
+            ],
+            "description": "Create deep copies (clones) of your objects",
+            "keywords": [
+                "clone",
+                "copy",
+                "duplicate",
+                "object",
+                "object graph"
+            ],
+            "time": "2018-06-11T23:09:50+00:00"
+        },
+        {
+            "name": "phar-io/manifest",
+            "version": "1.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/manifest.git";,
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4";,
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-phar": "*",
+                "phar-io/version": "^2.0",
+                "php": "^5.6 || ^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "a...@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebast...@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Component for reading phar.io manifest information 
from a PHP Archive (PHAR)",
+            "time": "2018-07-08T19:23:20+00:00"
+        },
+        {
+            "name": "phar-io/version",
+            "version": "2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/version.git";,
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6";,
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.6 || ^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "a...@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebast...@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Library for handling version information and 
constraints",
+            "time": "2018-07-08T19:19:57+00:00"
+        },
+        {
+            "name": "phpdocumentor/reflection-common",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/ReflectionCommon.git";,
+                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6";,
+                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": [
+                        "src"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jaap van Otterdijk",
+                    "email": "opensou...@ijaap.nl"
+                }
+            ],
+            "description": "Common reflection classes used by phpdocumentor to 
reflect the code structure",
+            "homepage": "http://www.phpdoc.org";,
+            "keywords": [
+                "FQSEN",
+                "phpDocumentor",
+                "phpdoc",
+                "reflection",
+                "static analysis"
+            ],
+            "time": "2017-09-11T18:02:19+00:00"
+        },
+        {
+            "name": "phpdocumentor/reflection-docblock",
+            "version": "4.3.0",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/phpDocumentor/ReflectionDocBlock.git";,
+                "reference": "94fd0001232e47129dd3504189fa1c7225010d08"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08";,
+                "reference": "94fd0001232e47129dd3504189fa1c7225010d08",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0",
+                "phpdocumentor/reflection-common": "^1.0.0",
+                "phpdocumentor/type-resolver": "^0.4.0",
+                "webmozart/assert": "^1.0"
+            },
+            "require-dev": {
+                "doctrine/instantiator": "~1.0.5",
+                "mockery/mockery": "^1.0",
+                "phpunit/phpunit": "^6.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": [
+                        "src/"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "m...@mikevanriel.com"
+                }
+            ],
+            "description": "With this component, a library can provide support 
for annotations via DocBlocks or otherwise retrieve information that is 
embedded in a DocBlock.",
+            "time": "2017-11-30T07:14:17+00:00"
+        },
+        {
+            "name": "phpdocumentor/type-resolver",
+            "version": "0.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/TypeResolver.git";,
+                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7";,
+                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.5 || ^7.0",
+                "phpdocumentor/reflection-common": "^1.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "^0.9.4",
+                "phpunit/phpunit": "^5.2||^4.8.24"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": [
+                        "src/"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "m...@mikevanriel.com"
+                }
+            ],
+            "time": "2017-07-14T14:27:02+00:00"
+        },
+        {
+            "name": "phpspec/prophecy",
+            "version": "1.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpspec/prophecy.git";,
+                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06";,
+                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.0.2",
+                "php": "^5.3|^7.0",
+                "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
+                "sebastian/comparator": "^1.1|^2.0|^3.0",
+                "sebastian/recursion-context": "^1.0|^2.0|^3.0"
+            },
+            "require-dev": {
+                "phpspec/phpspec": "^2.5|^3.2",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.8.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Prophecy\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Konstantin Kudryashov",
+                    "email": "ever....@gmail.com",
+                    "homepage": "http://everzet.com";
+                },
+                {
+                    "name": "Marcello Duarte",
+                    "email": "marcello.dua...@gmail.com"
+                }
+            ],
+            "description": "Highly opinionated mocking framework for PHP 5.3+",
+            "homepage": "https://github.com/phpspec/prophecy";,
+            "keywords": [
+                "Double",
+                "Dummy",
+                "fake",
+                "mock",
+                "spy",
+                "stub"
+            ],
+            "time": "2018-08-05T17:53:17+00:00"
+        },
+        {
+            "name": "phpunit/php-code-coverage",
+            "version": "6.1.4",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/php-code-coverage.git";,
+                "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d";,
+                "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-xmlwriter": "*",
+                "php": "^7.1",
+                "phpunit/php-file-iterator": "^2.0",
+                "phpunit/php-text-template": "^1.2.1",
+                "phpunit/php-token-stream": "^3.0",
+                "sebastian/code-unit-reverse-lookup": "^1.0.1",
+                "sebastian/environment": "^3.1 || ^4.0",
+                "sebastian/version": "^2.0.1",
+                "theseer/tokenizer": "^1.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0"
+            },
+            "suggest": {
+                "ext-xdebug": "^2.6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "6.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that provides collection, processing, and 
rendering functionality for PHP code coverage information.",
+            "homepage": 
"https://github.com/sebastianbergmann/php-code-coverage";,
+            "keywords": [
+                "coverage",
+                "testing",
+                "xunit"
+            ],
+            "time": "2018-10-31T16:06:48+00:00"
+        },
+        {
+            "name": "phpunit/php-file-iterator",
+            "version": "2.0.2",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/php-file-iterator.git";,
+                "reference": "050bedf145a257b1ff02746c31894800e5122946"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946";,
+                "reference": "050bedf145a257b1ff02746c31894800e5122946",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "FilterIterator implementation that filters files 
based on a list of suffixes.",
+            "homepage": 
"https://github.com/sebastianbergmann/php-file-iterator/";,
+            "keywords": [
+                "filesystem",
+                "iterator"
+            ],
+            "time": "2018-09-13T20:33:42+00:00"
+        },
+        {
+            "name": "phpunit/php-text-template",
+            "version": "1.2.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/php-text-template.git";,
+                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686";,
+                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Simple template engine.",
+            "homepage": 
"https://github.com/sebastianbergmann/php-text-template/";,
+            "keywords": [
+                "template"
+            ],
+            "time": "2015-06-21T13:50:34+00:00"
+        },
+        {
+            "name": "phpunit/php-timer",
+            "version": "2.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-timer.git";,
+                "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f";,
+                "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Utility class for timing",
+            "homepage": "https://github.com/sebastianbergmann/php-timer/";,
+            "keywords": [
+                "timer"
+            ],
+            "time": "2018-02-01T13:07:23+00:00"
+        },
+        {
+            "name": "phpunit/php-token-stream",
+            "version": "3.0.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/php-token-stream.git";,
+                "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18";,
+                "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Wrapper around PHP's tokenizer extension.",
+            "homepage": 
"https://github.com/sebastianbergmann/php-token-stream/";,
+            "keywords": [
+                "tokenizer"
+            ],
+            "time": "2018-10-30T05:52:18+00:00"
+        },
+        {
+            "name": "phpunit/phpunit",
+            "version": "7.4.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phpunit.git";,
+                "reference": "b1be2c8530c4c29c3519a052c9fb6cee55053bbd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b1be2c8530c4c29c3519a052c9fb6cee55053bbd";,
+                "reference": "b1be2c8530c4c29c3519a052c9fb6cee55053bbd",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.1",
+                "ext-dom": "*",
+                "ext-json": "*",
+                "ext-libxml": "*",
+                "ext-mbstring": "*",
+                "ext-xml": "*",
+                "myclabs/deep-copy": "^1.7",
+                "phar-io/manifest": "^1.0.2",
+                "phar-io/version": "^2.0",
+                "php": "^7.1",
+                "phpspec/prophecy": "^1.7",
+                "phpunit/php-code-coverage": "^6.0.7",
+                "phpunit/php-file-iterator": "^2.0.1",
+                "phpunit/php-text-template": "^1.2.1",
+                "phpunit/php-timer": "^2.0",
+                "sebastian/comparator": "^3.0",
+                "sebastian/diff": "^3.0",
+                "sebastian/environment": "^3.1 || ^4.0",
+                "sebastian/exporter": "^3.1",
+                "sebastian/global-state": "^2.0",
+                "sebastian/object-enumerator": "^3.0.3",
+                "sebastian/resource-operations": "^2.0",
+                "sebastian/version": "^2.0.1"
+            },
+            "conflict": {
+                "phpunit/phpunit-mock-objects": "*"
+            },
+            "require-dev": {
+                "ext-pdo": "*"
+            },
+            "suggest": {
+                "ext-soap": "*",
+                "ext-xdebug": "*",
+                "phpunit/php-invoker": "^2.0"
+            },
+            "bin": [
+                "phpunit"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "7.4-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "The PHP Unit Testing framework.",
+            "homepage": "https://phpunit.de/";,
+            "keywords": [
+                "phpunit",
+                "testing",
+                "xunit"
+            ],
+            "time": "2018-11-14T16:52:02+00:00"
+        },
+        {
+            "name": "sebastian/code-unit-reverse-lookup",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/code-unit-reverse-lookup.git";,
+                "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18";,
+                "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.6 || ^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^5.7 || ^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Looks up which function or method a line of code 
belongs to",
+            "homepage": 
"https://github.com/sebastianbergmann/code-unit-reverse-lookup/";,
+            "time": "2017-03-04T06:30:41+00:00"
+        },
+        {
+            "name": "sebastian/comparator",
+            "version": "3.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/comparator.git";,
+                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da";,
+                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1",
+                "sebastian/diff": "^3.0",
+                "sebastian/exporter": "^3.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthej...@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "git...@wallbash.com"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschus...@2bepublished.at"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Provides the functionality to compare PHP values 
for equality",
+            "homepage": "https://github.com/sebastianbergmann/comparator";,
+            "keywords": [
+                "comparator",
+                "compare",
+                "equality"
+            ],
+            "time": "2018-07-12T15:12:46+00:00"
+        },
+        {
+            "name": "sebastian/diff",
+            "version": "3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/diff.git";,
+                "reference": "366541b989927187c4ca70490a35615d3fef2dce"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/diff/zipball/366541b989927187c4ca70490a35615d3fef2dce";,
+                "reference": "366541b989927187c4ca70490a35615d3fef2dce",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0",
+                "symfony/process": "^2 || ^3.3 || ^4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Kore Nordmann",
+                    "email": "m...@kore-nordmann.de"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Diff implementation",
+            "homepage": "https://github.com/sebastianbergmann/diff";,
+            "keywords": [
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
+            ],
+            "time": "2018-06-10T07:54:39+00:00"
+        },
+        {
+            "name": "sebastian/environment",
+            "version": "4.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/environment.git";,
+                "reference": "febd209a219cea7b56ad799b30ebbea34b71eb8f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/environment/zipball/febd209a219cea7b56ad799b30ebbea34b71eb8f";,
+                "reference": "febd209a219cea7b56ad799b30ebbea34b71eb8f",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Provides functionality to handle HHVM/PHP 
environments",
+            "homepage": "http://www.github.com/sebastianbergmann/environment";,
+            "keywords": [
+                "Xdebug",
+                "environment",
+                "hhvm"
+            ],
+            "time": "2018-11-25T09:31:21+00:00"
+        },
+        {
+            "name": "sebastian/exporter",
+            "version": "3.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/exporter.git";,
+                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937";,
+                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0",
+                "sebastian/recursion-context": "^3.0"
+            },
+            "require-dev": {
+                "ext-mbstring": "*",
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.1.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthej...@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "git...@wallbash.com"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschus...@2bepublished.at"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "ahar...@php.net"
+                }
+            ],
+            "description": "Provides the functionality to export PHP variables 
for visualization",
+            "homepage": "http://www.github.com/sebastianbergmann/exporter";,
+            "keywords": [
+                "export",
+                "exporter"
+            ],
+            "time": "2017-04-03T13:19:02+00:00"
+        },
+        {
+            "name": "sebastian/global-state",
+            "version": "2.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/global-state.git";,
+                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4";,
+                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "suggest": {
+                "ext-uopz": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Snapshotting of global state",
+            "homepage": "http://www.github.com/sebastianbergmann/global-state";,
+            "keywords": [
+                "global state"
+            ],
+            "time": "2017-04-27T15:39:26+00:00"
+        },
+        {
+            "name": "sebastian/object-enumerator",
+            "version": "3.0.3",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/object-enumerator.git";,
+                "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5";,
+                "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0",
+                "sebastian/object-reflector": "^1.1.1",
+                "sebastian/recursion-context": "^3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Traverses array structures and object graphs to 
enumerate all referenced objects",
+            "homepage": 
"https://github.com/sebastianbergmann/object-enumerator/";,
+            "time": "2017-08-03T12:35:26+00:00"
+        },
+        {
+            "name": "sebastian/object-reflector",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/object-reflector.git";,
+                "reference": "773f97c67f28de00d397be301821b06708fca0be"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be";,
+                "reference": "773f97c67f28de00d397be301821b06708fca0be",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Allows reflection of object attributes, including 
inherited and non-public ones",
+            "homepage": 
"https://github.com/sebastianbergmann/object-reflector/";,
+            "time": "2017-03-29T09:07:27+00:00"
+        },
+        {
+            "name": "sebastian/recursion-context",
+            "version": "3.0.0",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/recursion-context.git";,
+                "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8";,
+                "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthej...@gmail.com"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "ahar...@php.net"
+                }
+            ],
+            "description": "Provides functionality to recursively process PHP 
variables",
+            "homepage": 
"http://www.github.com/sebastianbergmann/recursion-context";,
+            "time": "2017-03-03T06:23:57+00:00"
+        },
+        {
+            "name": "sebastian/resource-operations",
+            "version": "2.0.1",
+            "source": {
+                "type": "git",
+                "url": 
"https://github.com/sebastianbergmann/resource-operations.git";,
+                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9";,
+                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de"
+                }
+            ],
+            "description": "Provides a list of PHP built-in functions that 
operate on resources",
+            "homepage": 
"https://www.github.com/sebastianbergmann/resource-operations";,
+            "time": "2018-10-04T04:07:39+00:00"
+        },
+        {
+            "name": "sebastian/version",
+            "version": "2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/version.git";,
+                "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019";,
+                "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebast...@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that helps with managing the version 
number of Git-hosted PHP projects",
+            "homepage": "https://github.com/sebastianbergmann/version";,
+            "time": "2016-10-03T07:35:21+00:00"
+        },
+        {
+            "name": "theseer/tokenizer",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/tokenizer.git";,
+                "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b";,
+                "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-tokenizer": "*",
+                "ext-xmlwriter": "*",
+                "php": "^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "a...@blankerts.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A small library for converting tokenized PHP 
source code into XML and potentially other formats",
+            "time": "2017-04-07T12:08:54+00:00"
+        },
+        {
+            "name": "webmozart/assert",
+            "version": "1.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/webmozart/assert.git";,
+                "reference": "0df1908962e7a3071564e857d86874dad1ef204a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": 
"https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a";,
+                "reference": "0df1908962e7a3071564e857d86874dad1ef204a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.3 || ^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.6",
+                "sebastian/version": "^1.0.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Webmozart\\Assert\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/";,
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschus...@gmail.com"
+                }
+            ],
+            "description": "Assertions to validate method input/output with 
nice error messages.",
+            "keywords": [
+                "assert",
+                "check",
+                "validate"
+            ],
+            "time": "2018-01-29T19:49:41+00:00"
+        }
+    ],
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": [],
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..a793290
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
+<phpunit
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+        
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.4/phpunit.xsd";
+        backupGlobals="false"
+        colors="true"
+        bootstrap="vendor/autoload.php">
+    <php>
+        <ini name="error_reporting" value="-1" />
+        <ini name="date.timezone" value="UTC" />
+    </php>
+
+    <testsuites>
+        <testsuite name="The PHP bugtracking system test suite">
+            <directory>tests/</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>
diff --git a/src/Utils/Captcha.php b/src/Utils/Captcha.php
new file mode 100644
index 0000000..01377aa
--- /dev/null
+++ b/src/Utils/Captcha.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace App\Utils;
+
+/**
+ * Captcha utility class for providing a simple math question with additions or
+ * subtractions to prevent spam.
+ */
+class Captcha
+{
+    /**
+     * First operand.
+     */
+    private $first;
+
+    /**
+     * Last operand.
+     */
+    private $last;
+
+    /**
+     * Highest possible operands value for randomization at initialization.
+     */
+    const MAX = 50;
+
+    /**
+     * Supported equation operations. Keys are operation symbols and values are
+     * class method names to execute.
+     */
+    const OPERATIONS = [
+        '+' => 'addition',
+        '-' => 'subtraction',
+    ];
+
+    /**
+     * Current operation.
+     */
+    private $operation;
+
+    /**
+     * Class constructor where operands random values and operation are set.
+     */
+    public function __construct()
+    {
+        $this->randomize();
+    }
+
+    /**
+     * Set random operands values and operation.
+     */
+    public function randomize()
+    {
+        $this->setFirst(rand(1, self::MAX));
+        $this->setLast(rand(1, self::MAX));
+        $this->setOperation(self::OPERATIONS[array_rand(self::OPERATIONS)]);
+    }
+
+    /**
+     * First operand number setter to override default random pick. Defined as 
a
+     * separate method for convenience when unit testing.
+     */
+    public function setFirst($number)
+    {
+        $this->first = $number;
+    }
+
+    /**
+     * Last operand number setter to override default random pick. Defined as a
+     * separate method for convenience when unit testing.
+     */
+    public function setLast($number)
+    {
+        $this->last = $number;
+    }
+
+    /**
+     * Set the operation. If provided operation is invalid it falls back to 
addition.
+     */
+    public function setOperation($operation)
+    {
+        $this->operation = in_array($operation, self::OPERATIONS) ? $operation 
: 'addition';
+    }
+
+    /**
+     * Get current question equation string for displaying it to the user.
+     */
+    public function getQuestion()
+    {
+        $this->sortOperands();
+
+        $symbol = array_search($this->operation, self::OPERATIONS);
+        $symbol = $symbol === false ? '+' : $symbol;
+
+        return $this->first.' '.$symbol.' '.$this->last.' = ?';
+    }
+
+    /**
+     * The correct current answer of the given equation question.
+     */
+    public function getAnswer()
+    {
+        $this->sortOperands();
+
+        return \call_user_func([Captcha::class, $this->operation], 
$this->first, $this->last);
+    }
+
+    /**
+     * When the current operation is subtraction, sort operands to have a 
bigger
+     * operand first. With this, negative results are omitted for simplicity 
and
+     * possible better user experience.
+     */
+    private function sortOperands()
+    {
+        $first = $this->first;
+        $last = $this->last;
+
+        if ($this->operation === 'subtraction') {
+            $this->first = $first > $last ? $first : $last;
+            $this->last = $first > $last ? $last : $first;
+        }
+    }
+
+    /**
+     * Addition of two operands.
+     */
+    private function addition($first, $last)
+    {
+        return $first + $last;
+    }
+
+    /**
+     * Subtraction of two operands.
+     */
+    private function subtraction($first, $last)
+    {
+        return $first - $last;
+    }
+}
diff --git a/templates/addghpull.php b/templates/addghpull.php
index 9884a4a..fb06dd1 100644
--- a/templates/addghpull.php
+++ b/templates/addghpull.php
@@ -23,8 +23,7 @@ if (!empty($errors)) {
 <table>
 <?php
 if (!$logged_in) {
-       $captcha = $numeralCaptcha->getOperation();
-       $_SESSION['answer'] = $numeralCaptcha->getAnswer();
+       $_SESSION['answer'] = $captcha->getAnswer();
 ?>
  <tr>
   <th class="form-label_left">
@@ -35,7 +34,7 @@ if (!$logged_in) {
   </td>
  </tr>
  <tr>
-  <th>Solve the problem:<br><?php echo $captcha; ?> = ?</th>
+  <th>Solve the problem:<br><?php echo $captcha->getQuestion(); ?></th>
   <td class="form-input"><input type="text" name="captcha"></td>
  </tr>
 <?php } ?>
diff --git a/templates/addpatch.php b/templates/addpatch.php
index 6e0a7f6..5285789 100644
--- a/templates/addpatch.php
+++ b/templates/addpatch.php
@@ -21,8 +21,7 @@ if (!empty($errors)) {
 <table>
 <?php
 if (!$logged_in) {
-       $captcha = $numeralCaptcha->getOperation();
-       $_SESSION['answer'] = $numeralCaptcha->getAnswer();
+       $_SESSION['answer'] = $captcha->getAnswer();
 ?>
  <tr>
   <th class="form-label_left">
@@ -33,7 +32,7 @@ if (!$logged_in) {
   </td>
  </tr>
  <tr>
-  <th>Solve the problem:<br><?php echo $captcha; ?> = ?</th>
+  <th>Solve the problem:<br><?php echo $captcha->getQuestion(); ?></th>
   <td class="form-input"><input type="text" name="captcha"></td>
  </tr>
 <?php } ?>
diff --git a/tests/Utils/CaptchaTest.php b/tests/Utils/CaptchaTest.php
new file mode 100644
index 0000000..681da7a
--- /dev/null
+++ b/tests/Utils/CaptchaTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Tests\Utils;
+
+use PHPUnit\Framework\TestCase;
+use App\Utils\Captcha;
+
+class CaptchaTest extends TestCase
+{
+    private $captcha;
+
+    public function setUp()
+    {
+        $this->captcha = new Captcha();
+    }
+
+    /**
+     * @dataProvider mathProvider
+     */
+    public function testGetQuestion($first, $last, $operation, $question, 
$expected)
+    {
+        $this->captcha->setFirst($first);
+        $this->captcha->setLast($last);
+        $this->captcha->setOperation($operation);
+
+        $this->assertEquals($question, $this->captcha->getQuestion());
+        $this->assertEquals($expected, $this->captcha->getAnswer());
+    }
+
+    public function mathProvider()
+    {
+        return [
+            [1, 2, 'addition', '1 + 2 = ?', 3],
+            [10, 50, 'subtraction', '50 - 10 = ?', 40],
+            [90, 50, 'subtraction', '90 - 50 = ?', 40],
+            [14, 14, 'subtraction', '14 - 14 = ?', 0],
+            [10, 5, 'multiplication', '10 + 5 = ?', 15],
+            [12, 2, 'foo', '12 + 2 = ?', 14],
+        ];
+    }
+}
diff --git a/www/bug-pwd-finder.php b/www/bug-pwd-finder.php
index 08a0fea..023597a 100644
--- a/www/bug-pwd-finder.php
+++ b/www/bug-pwd-finder.php
@@ -2,11 +2,14 @@
 
 /* Procedure for emailing a password reminder to a user */
 
+use App\Utils\Captcha;
+
+require_once __DIR__.'/../src/Utils/Captcha.php';
+
 // Start session (for captcha!)
 session_start();
 
-require_once 'Text/CAPTCHA/Numeral.php';
-$numeralCaptcha = new Text_CAPTCHA_Numeral();
+$captcha = new Captcha();
 
 // Obtain common includes
 require_once '../include/prepend.php';
@@ -72,8 +75,7 @@ if ($success) {
        display_bug_success($success);
 }
 
-$captcha = $numeralCaptcha->getOperation();
-$_SESSION['answer'] = $numeralCaptcha->getAnswer();
+$_SESSION['answer'] = $captcha->getAnswer();
 
 ?>
 
@@ -90,7 +92,7 @@ in the bug report.
 
 <form method="post" action="bug-pwd-finder.php">
 <p><b>Bug Report ID:</b> #<input type="text" size="20" name="id" value="<?php 
echo $bug_id; ?>">
-<p><b>Solve the problem:<br><?php echo $captcha; ?> = ? <input type="text" 
name="captcha"></p>
+<p><b>Solve the problem:<br><?php echo $captcha->getQuestion(); ?> <input 
type="text" name="captcha"></p>
 
 <input type="submit" value="Send"></p>
 </form>
diff --git a/www/bug.php b/www/bug.php
index 26183b1..46b5854 100644
--- a/www/bug.php
+++ b/www/bug.php
@@ -1,6 +1,8 @@
 <?php
 /* User interface for viewing and editing bug details */
 
+use App\Utils\Captcha;
+
 // Obtain common includes
 require_once '../include/prepend.php';
 
@@ -113,8 +115,8 @@ if ($edit == 1 && $is_trusted_developer && 
isset($_GET['delete_comment'])) {
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       require_once 'Text/CAPTCHA/Numeral.php';
-       $numeralCaptcha = new Text_CAPTCHA_Numeral();
+       require_once __DIR__.'/../src/Utils/Captcha.php';
+       $captcha = new Captcha();
 }
 
 $trytoforce = isset($_POST['trytoforce']) ? (int) $_POST['trytoforce'] : 0;
@@ -995,8 +997,7 @@ if ($edit == 1 || $edit == 2) { ?>
 echo $preview;
 
 if (!$logged_in) {
-       $captcha = $numeralCaptcha->getOperation();
-       $_SESSION['answer'] = $numeralCaptcha->getAnswer();
+       $_SESSION['answer'] = $captcha->getAnswer();
 ?>
        <table>
                <tr>
@@ -1006,7 +1007,7 @@ if (!$logged_in) {
                        </td>
                </tr>
                <tr>
-                       <th>Solve the problem:<br><?php echo 
htmlspecialchars($captcha); ?> = ?</th>
+                       <th>Solve the problem:<br><?php echo 
htmlspecialchars($captcha->getQuestion()); ?></th>
                        <td class="form-input"><input type="text" 
name="captcha"></td>
                </tr>
                <tr>
diff --git a/www/gh-pull-add.php b/www/gh-pull-add.php
index 616dda5..7dc2965 100644
--- a/www/gh-pull-add.php
+++ b/www/gh-pull-add.php
@@ -1,5 +1,7 @@
 <?php
 
+use App\Utils\Captcha;
+
 // Obtain common includes
 require_once '../include/prepend.php';
 require_once 'PEAR.php';
@@ -37,8 +39,8 @@ $is_trusted_developer = ($user_flags & BUGS_TRUSTED_DEV);
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       require_once 'Text/CAPTCHA/Numeral.php';
-       $numeralCaptcha = new Text_CAPTCHA_Numeral();
+       require_once __DIR__.'/../src/Utils/Captcha.php';
+       $captcha = new Captcha();
 }
 
 $show_bug_info = bugs_has_access($bug_id, $buginfo, $pw, $user_flags);
diff --git a/www/patch-add.php b/www/patch-add.php
index 368ce70..6fd7035 100644
--- a/www/patch-add.php
+++ b/www/patch-add.php
@@ -1,5 +1,7 @@
 <?php
 
+use App\Utils\Captcha;
+
 // Obtain common includes
 require_once '../include/prepend.php';
 
@@ -36,8 +38,8 @@ $is_trusted_developer = ($user_flags & BUGS_TRUSTED_DEV);
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       require_once 'Text/CAPTCHA/Numeral.php';
-       $numeralCaptcha = new Text_CAPTCHA_Numeral();
+       require_once __DIR__.'/../src/Utils/Captcha.php';
+       $captcha = new Captcha();
 }
 
 $show_bug_info = bugs_has_access($bug_id, $buginfo, $pw, $user_flags);
diff --git a/www/report.php b/www/report.php
index a840b5f..542bf87 100644
--- a/www/report.php
+++ b/www/report.php
@@ -1,5 +1,7 @@
 <?php
 
+use App\Utils\Captcha;
+
 // Obtain common includes
 require_once '../include/prepend.php';
 
@@ -23,8 +25,8 @@ require "{$ROOT_DIR}/include/php_versions.php";
 
 // captcha is not necessary if the user is logged in
 if (!$logged_in) {
-       require_once 'Text/CAPTCHA/Numeral.php';
-       $numeralCaptcha = new Text_CAPTCHA_Numeral();
+       require_once __DIR__.'/../src/Utils/Captcha.php';
+       $captcha = new Captcha();
 }
 
 $packageAffectedScript = <<<SCRIPT
@@ -532,8 +534,8 @@ display_bug_error($errors);
                        </tr>
 
 <?php if (!$logged_in) {
-       $captcha = $numeralCaptcha->getOperation();
-       $_SESSION['answer'] = $numeralCaptcha->getAnswer();
+       $_SESSION['answer'] = $captcha->getAnswer();
+
        if (!empty($_POST['captcha']) && empty($ok_to_submit_report)) {
                $captcha_label = '<strong>Solve this <em>new</em> 
problem:</strong>';
        } else {
@@ -541,7 +543,7 @@ display_bug_error($errors);
        }
 ?>
                        <tr>
-                               <th><?php echo $captcha_label; ?><br><?php echo 
htmlspecialchars($captcha); ?> = ?</th>
+                               <th><?php echo $captcha_label; ?><br><?php echo 
htmlspecialchars($captcha->getQuestion()); ?></th>
                                <td class="form-input"><input type="text" 
name="captcha" autocomplete="off"></td>
                        </tr>
 <?php } ?>
-- 
PHP Webmaster List Mailing List (http://www.php.net/)
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to