diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f60cb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +/logs/ +*.sqlite +*.cache +/config/config.php +/.idea/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3eec3a1 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +# Default port to start server on +PORT := 8888 + +.PHONY: clean +clean: + rm -rf ./vendor + +.PHONY: install +install: + composer install --prefer-dist + php db/database.php + +.PHONY: start-server +start-server: + composer install --prefer-dist + php db/database.php + php -S localhost:$(PORT) -t public/ public/index.php + +# linting based on https://github.com/dbfx/github-phplint +.PHONY: lint +lint: + # Make sure we don't lint test classes + composer install --prefer-dist --no-dev + + # Lint for installed PHP version + sh -c "! (find . -type f -name \"*.php\" -not -path \"./build/*\" -not -path \"./vendor/*\" $1 -exec php -l -n {} \; | grep -v \"No syntax errors detected\")" + + # Make devtools available again + composer install --prefer-dist + + # Lint with CodeSniffer + vendor/bin/phpcs --standard=phpcs.xml src/ + +.PHONY: api_test +api_test: +# If we pass the PFA_API_TEST variable with value 1, we can run the PFA API tests +ifeq ($(PFA_API_TEST),1) + newman run test/postman/scim-opf-pfa.postman_collection.json -e test/postman/scim-env.postman_environment.json +else + newman run test/postman/scim-opf.postman_collection.json -e test/postman/scim-env.postman_environment.json +endif + +.PHONY: unit_test +unit_test: + composer install --prefer-dist + vendor/bin/phpunit -c test/phpunit.xml --testdox + +.PHONY: fulltest +fulltest: lint api_test unit_test diff --git a/README.md b/README.md index 31990da..72cd51d 100644 --- a/README.md +++ b/README.md @@ -1 +1,82 @@ # scim-server-php + +**scim-server-php** is a PHP library making it easy to implement [SCIM v2.0](https://datatracker.ietf.org/wg/scim/documents/) server endpoints for various systems. + +It is built on the following IETF approved RFCs: [RFC7642](https://datatracker.ietf.org/doc/html/rfc7642), [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644) + +This is a **work in progress** project. It already works pretty well but some features will be added in the future and some bugs may still be arround 😉 + +The **scim-server-php** project currently includes the following: + +* A SCIM 2.0 server core library +* A [Postfix Admin](https://github.com/postfixadmin/postfixadmin) SCIM API + +**scim-server-php** also comes with an integrated Mock SCIM server based on a SQLite database. + +## SCIM 2.0 server core library + +This library provides: + +* Standard SCIM resources implementations (*Core User*, *Enterprise User* and *Groups*) +* Custom SCIM resource *Provisioning User* implementation +* Standard CRUD operation on above SCIM resources +* A HTTP server handling requests and responses on defined endpoints, based on the [Slim](https://www.slimframework.com/) framework +* A very simple JWT implementation + * When enabled, a JWT token is generated on the `/jwt` endpoint. You **must** therefore protect this endpoint. + * When enabled, this JWT token needs to be provided in all requests using the Bearer schema (`Authorization: Bearer `) +* An easily reusable code architecture for implementing SCIM servers + +## Postfix Admin SCIM API + +The [Postfix Admin](https://github.com/postfixadmin/postfixadmin) API enables SCIM server capabilities for [Postfix Admin](https://github.com/postfixadmin/postfixadmin). It uses the core library above. + +It supports standard GET, POST, PUT and DELETE operations on SCIM *Provisioning User* resources, which are translated in the corresponding operations on the [Postfix Admin](https://github.com/postfixadmin/postfixadmin) mailboxes. + +Example (null values removed for readability): + +``` +$ curl https://my.postfix.admin.url/Users/aaaa@bli.fr -H 'Authorization: Bearer ' +{ + "schemas":[ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:audriga:params:scim:schemas:extension:provisioning:2.0:User" + ], + "id":"aaaa@bli.fr", + "meta":{ + "resourceType":"User", + "created":"2022-05-27 12:45:08", + "location":"https://my.postfix.admin.url/Users/aaaa@bli.fr", + "updated":"2022-06-15 13:07:30" + }, + "userName":"aaaa@bli.fr", + "name":{ + "formatted":"Aaaa" + }, + "displayName":"Aaaa", + "active":"1", + "urn:audriga:params:scim:schemas:extension:provisioning:2.0:User":{ + "sizeQuota":51200000 + } +} +``` + +## Prerequisites +* **scim-server-php** requires PHP 7.4 +* Dependencies are managed with [composer](https://getcomposer.org/) + +## Installation +### Local installation +* Run `make install` to automatically install dependencies + +### Configuration +* To switch from the Mock SCIM server to the Postfix Admin SCIM API, you simply need to adapt the `public/index.php` file (include **one** of the following): + +``` +// Set up system-specific dependencies +$dependencies = require dirname(__DIR__) . '/src/Dependencies/mock-dependencies.php'; // include that line if you want to use the integrated mock SCIM server +$dependencies = require dirname(__DIR__) . '/src/Dependencies/pfa-dependencies.php'; // include that line if you want to use the Postfix Admin SCIM API +``` + +## Acknowledgements + +This software is part of the [Open Provisioning Framework](https://www.audriga.com/en/User_provisioning/Open_Provisioning_Framework) project that has received funding from the European Union's Horizon 2020 research and innovation program under grant agreement No. 871498. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..13bcca4 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "audriga/scim-opf", + "description": "An open provisioning framework using the SCIM protocol", + "type": "library", + "require": { + "slim/slim": "^4.10", + "illuminate/database": "^8.83", + "php": "^7.4", + "slim/php-view": "^3.1", + "monolog/monolog": "^2.4", + "tuupola/slim-jwt-auth": "^3.6", + "ramsey/uuid": "^4.2", + "slim/psr7": "^1.5", + "php-di/php-di": "^6.3" + }, + "autoload": { + "psr-4": { + "Opf\\": "src/" + } + }, + "authors": [ + { + "name": "Stanimir Bozhilov", + "email": "stanimir@audriga.com" + } + ], + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.6" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..ad07d83 --- /dev/null +++ b/composer.lock @@ -0,0 +1,5380 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "82173fe14187da8a6dbfc0213f0bfb5a", + "packages": [ + { + "name": "brick/math", + "version": "0.9.3", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", + "vimeo/psalm": "4.9.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.9.3" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/brick/math", + "type": "tidelift" + } + ], + "time": "2021-08-15T20:50:18+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89", + "reference": "8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "vimeo/psalm": "^4.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:16:43+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v5.5.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "83b609028194aa042ea33b5af2d41a7427de80e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/83b609028194aa042ea33b5af2d41a7427de80e6", + "reference": "83b609028194aa042ea33b5af2d41a7427de80e6", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4.8 <=9" + }, + "suggest": { + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v5.5.1" + }, + "time": "2021-11-08T20:18:51+00:00" + }, + { + "name": "illuminate/collections", + "version": "v8.83.5", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "5cf7ed1c0a1b8049576b29f5cab5c822149aaa91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/5cf7ed1c0a1b8049576b29f5cab5c822149aaa91", + "reference": "5cf7ed1c0a1b8049576b29f5cab5c822149aaa91", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.0", + "illuminate/macroable": "^8.0", + "php": "^7.3|^8.0" + }, + "suggest": { + "symfony/var-dumper": "Required to use the dump method (^5.4)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2022-02-15T14:40:58+00:00" + }, + { + "name": "illuminate/container", + "version": "v8.83.5", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "14062628d05f75047c5a1360b9350028427d568e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/14062628d05f75047c5a1360b9350028427d568e", + "reference": "14062628d05f75047c5a1360b9350028427d568e", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.0", + "php": "^7.3|^8.0", + "psr/container": "^1.0" + }, + "provide": { + "psr/container-implementation": "1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2022-02-02T21:03:35+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v8.83.5", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "5e0fd287a1b22a6b346a9f7cd484d8cf0234585d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/5e0fd287a1b22a6b346a9f7cd484d8cf0234585d", + "reference": "5e0fd287a1b22a6b346a9f7cd484d8cf0234585d", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0", + "psr/container": "^1.0", + "psr/simple-cache": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2022-01-13T14:47:47+00:00" + }, + { + "name": "illuminate/database", + "version": "v8.83.5", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "e0fa6cce0825d7054d6ff3a3efca678f6054e403" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/e0fa6cce0825d7054d6ff3a3efca678f6054e403", + "reference": "e0fa6cce0825d7054d6ff3a3efca678f6054e403", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/collections": "^8.0", + "illuminate/container": "^8.0", + "illuminate/contracts": "^8.0", + "illuminate/macroable": "^8.0", + "illuminate/support": "^8.0", + "php": "^7.3|^8.0", + "symfony/console": "^5.4" + }, + "suggest": { + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.13.3|^3.1.4).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "illuminate/console": "Required to use the database commands (^8.0).", + "illuminate/events": "Required to use the observers with Eloquent (^8.0).", + "illuminate/filesystem": "Required to use the migrations (^8.0).", + "illuminate/pagination": "Required to paginate the result set (^8.0).", + "symfony/finder": "Required to use Eloquent model factories (^5.4)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2022-02-22T14:55:52+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v8.83.5", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "aed81891a6e046fdee72edd497f822190f61c162" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/aed81891a6e046fdee72edd497f822190f61c162", + "reference": "aed81891a6e046fdee72edd497f822190f61c162", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2021-11-16T13:57:03+00:00" + }, + { + "name": "illuminate/support", + "version": "v8.83.5", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "79afba1609f944a1678986c9e2c4486ae25999a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/79afba1609f944a1678986c9e2c4486ae25999a6", + "reference": "79afba1609f944a1678986c9e2c4486ae25999a6", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.4|^2.0", + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/collections": "^8.0", + "illuminate/contracts": "^8.0", + "illuminate/macroable": "^8.0", + "nesbot/carbon": "^2.53.1", + "php": "^7.3|^8.0", + "voku/portable-ascii": "^1.6.1" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "suggest": { + "illuminate/filesystem": "Required to use the composer class (^8.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^1.3|^2.0.2).", + "ramsey/uuid": "Required to use Str::uuid() (^4.2.2).", + "symfony/process": "Required to use the composer class (^5.4).", + "symfony/var-dumper": "Required to use the dd function (^5.4).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2022-02-25T19:54:55+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "d7fd7450628561ba697b7097d86db72662f54aef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/d7fd7450628561ba697b7097d86db72662f54aef", + "reference": "d7fd7450628561ba697b7097d86db72662f54aef", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7", + "graylog2/gelf-php": "^1.4.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.3", + "phpspec/prophecy": "^1.6.1", + "phpstan/phpstan": "^0.12.91", + "phpunit/phpunit": "^8.5", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": ">=0.90@dev", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.4.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2022-03-14T12:44:37+00:00" + }, + { + "name": "nesbot/carbon", + "version": "2.57.0", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "4a54375c21eea4811dbd1149fe6b246517554e78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4a54375c21eea4811dbd1149fe6b246517554e78", + "reference": "4a54375c21eea4811dbd1149fe6b246517554e78", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "doctrine/dbal": "^2.0 || ^3.0", + "doctrine/orm": "^2.7", + "friendsofphp/php-cs-fixer": "^3.0", + "kylekatarnls/multi-tester": "^2.0", + "phpmd/phpmd": "^2.9", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.54 || ^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.14", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.x-dev", + "dev-master": "2.x-dev" + }, + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2022-02-13T18:13:33+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "opis/closure", + "version": "3.6.3", + "source": { + "type": "git", + "url": "https://github.com/opis/closure.git", + "reference": "3d81e4309d2a927abbe66df935f4bb60082805ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/closure/zipball/3d81e4309d2a927abbe66df935f4bb60082805ad", + "reference": "3d81e4309d2a927abbe66df935f4bb60082805ad", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "jeremeamia/superclosure": "^2.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.6.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Opis\\Closure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.", + "homepage": "https://opis.io/closure", + "keywords": [ + "anonymous functions", + "closure", + "function", + "serializable", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/opis/closure/issues", + "source": "https://github.com/opis/closure/tree/3.6.3" + }, + "time": "2022-01-27T09:35:39+00:00" + }, + { + "name": "php-di/invoker", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "cd6d9f267d1a3474bdddf1be1da079f01b942786" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/cd6d9f267d1a3474bdddf1be1da079f01b942786", + "reference": "cd6d9f267d1a3474bdddf1be1da079f01b942786", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "psr/container": "^1.0|^2.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.3" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2021-12-13T09:22:56+00:00" + }, + { + "name": "php-di/php-di", + "version": "6.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "b8126d066ce144765300ee0ab040c1ed6c9ef588" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/b8126d066ce144765300ee0ab040c1ed6c9ef588", + "reference": "b8126d066ce144765300ee0ab040c1ed6c9ef588", + "shasum": "" + }, + "require": { + "opis/closure": "^3.5.5", + "php": ">=7.2.0", + "php-di/invoker": "^2.0", + "php-di/phpdoc-reader": "^2.0.1", + "psr/container": "^1.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "doctrine/annotations": "~1.2", + "friendsofphp/php-cs-fixer": "^2.4", + "mnapoli/phpunit-easymock": "^1.2", + "ocramius/proxy-manager": "^2.0.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)", + "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~2.0)" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "DI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "support": { + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/6.3.5" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2021-09-02T09:49:58+00:00" + }, + { + "name": "php-di/phpdoc-reader", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PhpDocReader.git", + "reference": "66daff34cbd2627740ffec9469ffbac9f8c8185c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PhpDocReader/zipball/66daff34cbd2627740ffec9469ffbac9f8c8185c", + "reference": "66daff34cbd2627740ffec9469ffbac9f8c8185c", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "require-dev": { + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpDocReader\\": "src/PhpDocReader" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)", + "keywords": [ + "phpdoc", + "reflection" + ], + "support": { + "issues": "https://github.com/PHP-DI/PhpDocReader/issues", + "source": "https://github.com/PHP-DI/PhpDocReader/tree/2.2.1" + }, + "time": "2020-10-12T12:39:22+00:00" + }, + { + "name": "psr/container", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-handler/issues", + "source": "https://github.com/php-fig/http-server-handler/tree/master" + }, + "time": "2018-10-30T16:46:14+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/master" + }, + "time": "2018-10-30T17:12:04+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a", + "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8", + "symfony/polyfill-php81": "^1.23" + }, + "require-dev": { + "captainhook/captainhook": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "ergebnis/composer-normalize": "^2.6", + "fakerphp/faker": "^1.5", + "hamcrest/hamcrest-php": "^2", + "jangregor/phpstan-prophecy": "^0.8", + "mockery/mockery": "^1.3", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^0.12.32", + "phpstan/phpstan-mockery": "^0.12.5", + "phpstan/phpstan-phpunit": "^0.12.11", + "phpunit/phpunit": "^8.5 || ^9", + "psy/psysh": "^0.10.4", + "slevomat/coding-standard": "^6.3", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/1.2.2" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2021-10-10T03:01:02+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", + "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", + "shasum": "" + }, + "require": { + "brick/math": "^0.8 || ^0.9", + "ext-json": "*", + "php": "^7.2 || ^8.0", + "ramsey/collection": "^1.0", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php80": "^1.14" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "moontoast/math": "^1.1", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-mockery": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^8.5 || ^9", + "slevomat/coding-standard": "^7.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-ctype": "Enables faster processing of character classification using ctype functions.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.x-dev" + }, + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.2.3" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2021-09-25T23:10:38+00:00" + }, + { + "name": "slim/php-view", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/PHP-View.git", + "reference": "c9ec5e4027d113af35816098f9059ef5c3e3000c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/PHP-View/zipball/c9ec5e4027d113af35816098f9059ef5c3e3000c", + "reference": "c9ec5e4027d113af35816098f9059ef5c3e3000c", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0", + "slim/psr7": "^1", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Views\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Glenn Eggleton", + "email": "geggleto@gmail.com" + } + ], + "description": "Render PHP view scripts into a PSR-7 Response object.", + "keywords": [ + "framework", + "php", + "phtml", + "renderer", + "slim", + "template", + "view" + ], + "support": { + "issues": "https://github.com/slimphp/PHP-View/issues", + "source": "https://github.com/slimphp/PHP-View/tree/3.1.0" + }, + "time": "2021-02-03T14:28:55+00:00" + }, + { + "name": "slim/psr7", + "version": "1.5", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Psr7.git", + "reference": "a47b43a8da7c0208b4c228af0cb29ea36080635a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/a47b43a8da7c0208b4c228af0cb29ea36080635a", + "reference": "a47b43a8da7c0208b4c228af0cb29ea36080635a", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.5", + "php": "^7.3 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "ralouphie/getallheaders": "^3.0", + "symfony/polyfill-php80": "^1.23" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.2", + "ext-json": "*", + "http-interop/http-factory-tests": "^0.9.0", + "php-http/psr7-integration-tests": "dev-master", + "phpspec/prophecy": "^1.14", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/phpstan": "^0.12.99", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Psr7\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + } + ], + "description": "Strict PSR-7 implementation", + "homepage": "https://www.slimframework.com", + "keywords": [ + "http", + "psr-7", + "psr7" + ], + "support": { + "issues": "https://github.com/slimphp/Slim-Psr7/issues", + "source": "https://github.com/slimphp/Slim-Psr7/tree/1.5" + }, + "time": "2021-09-22T04:33:00+00:00" + }, + { + "name": "slim/slim", + "version": "4.10.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "0dfc7d2fdf2553b361d864d51af3fe8a6ad168b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/0dfc7d2fdf2553b361d864d51af3fe8a6ad168b0", + "reference": "0dfc7d2fdf2553b361d864d51af3fe8a6ad168b0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.2", + "ext-simplexml": "*", + "guzzlehttp/psr7": "^2.1", + "httpsoft/http-message": "^1.0", + "httpsoft/http-server-request": "^1.0", + "laminas/laminas-diactoros": "^2.8", + "nyholm/psr7": "^1.5", + "nyholm/psr7-server": "^1.0", + "phpspec/prophecy": "^1.15", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5", + "slim/http": "^1.2", + "slim/psr7": "^1.5", + "squizlabs/php_codesniffer": "^3.6" + }, + "suggest": { + "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", + "ext-xml": "Needed to support XML format in BodyParsingMiddleware", + "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim", + "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://www.slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "support": { + "docs": "https://www.slimframework.com/docs/v4/", + "forum": "https://discourse.slimframework.com/", + "irc": "irc://irc.freenode.net:6667/slimphp", + "issues": "https://github.com/slimphp/Slim/issues", + "rss": "https://www.slimframework.com/blog/feed.rss", + "slack": "https://slimphp.slack.com/", + "source": "https://github.com/slimphp/Slim", + "wiki": "https://github.com/slimphp/Slim/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/slimphp", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slim/slim", + "type": "tidelift" + } + ], + "time": "2022-03-14T14:18:23+00:00" + }, + { + "name": "symfony/console", + "version": "v5.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d8111acc99876953f52fe16d4c50eb60940d49ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d8111acc99876953f52fe16d4c50eb60940d49ad", + "reference": "d8111acc99876953f52fe16d4c50eb60940d49ad", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-24T12:45:35+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-07-12T14:48:14+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-23T21:10:46+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-06-05T21:20:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-04T08:16:47+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-13T13:58:11+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-04T16:48:04+00:00" + }, + { + "name": "symfony/string", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/92043b7d8383e48104e411bc9434b260dbeb5a10", + "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/translation", + "version": "v5.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "a7ca9fdfffb0174209440c2ffa1dee228e15d95b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/a7ca9fdfffb0174209440c2ffa1dee228e15d95b", + "reference": "a7ca9fdfffb0174209440c2ffa1dee228e15d95b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/translation-contracts": "^2.3" + }, + "conflict": { + "symfony/config": "<4.4", + "symfony/console": "<5.3", + "symfony/dependency-injection": "<5.0", + "symfony/http-kernel": "<5.0", + "symfony/twig-bundle": "<5.0", + "symfony/yaml": "<4.4" + }, + "provide": { + "symfony/translation-implementation": "2.3" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/http-client-contracts": "^1.1|^2.0|^3.0", + "symfony/http-kernel": "^5.0|^6.0", + "symfony/intl": "^4.4|^5.0|^6.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/service-contracts": "^1.1.2|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v5.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-02T12:56:28+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "d28150f0f44ce854e942b671fc2620a98aae1b1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/d28150f0f44ce854e942b671fc2620a98aae1b1e", + "reference": "d28150f0f44ce854e942b671fc2620a98aae1b1e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/translation-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v2.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-08-17T14:20:01+00:00" + }, + { + "name": "tuupola/callable-handler", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/tuupola/callable-handler.git", + "reference": "0bc7b88630ca753de9aba8f411046856f5ca6f8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tuupola/callable-handler/zipball/0bc7b88630ca753de9aba8f411046856f5ca6f8c", + "reference": "0bc7b88630ca753de9aba8f411046856f5ca6f8c", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0", + "psr/http-server-middleware": "^1.0" + }, + "require-dev": { + "overtrue/phplint": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "squizlabs/php_codesniffer": "^3.2", + "tuupola/http-factory": "^0.4.0|^1.0", + "zendframework/zend-diactoros": "^1.6.0|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tuupola\\Middleware\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mika Tuupola", + "email": "tuupola@appelsiini.net", + "homepage": "https://appelsiini.net/", + "role": "Developer" + } + ], + "description": "Compatibility layer for PSR-7 double pass and PSR-15 middlewares.", + "homepage": "https://github.com/tuupola/callable-handler", + "keywords": [ + "middleware", + "psr-15", + "psr-7" + ], + "support": { + "issues": "https://github.com/tuupola/callable-handler/issues", + "source": "https://github.com/tuupola/callable-handler/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/tuupola", + "type": "github" + } + ], + "time": "2020-09-09T08:31:54+00:00" + }, + { + "name": "tuupola/http-factory", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/tuupola/http-factory.git", + "reference": "ae3f8fbdd31cf2f1bbe920b38963c5e4d1e9c454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tuupola/http-factory/zipball/ae3f8fbdd31cf2f1bbe920b38963c5e4d1e9c454", + "reference": "ae3f8fbdd31cf2f1bbe920b38963c5e4d1e9c454", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0", + "psr/http-factory": "^1.0" + }, + "conflict": { + "nyholm/psr7": "<1.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9.0", + "overtrue/phplint": "^3.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tuupola\\Http\\Factory\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mika Tuupola", + "email": "tuupola@appelsiini.net", + "homepage": "https://appelsiini.net/", + "role": "Developer" + } + ], + "description": "Lightweight autodiscovering PSR-17 HTTP factories", + "homepage": "https://github.com/tuupola/http-factory", + "keywords": [ + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/tuupola/http-factory/issues", + "source": "https://github.com/tuupola/http-factory/tree/1.4.0" + }, + "funding": [ + { + "url": "https://github.com/tuupola", + "type": "github" + } + ], + "time": "2021-09-14T12:46:25+00:00" + }, + { + "name": "tuupola/slim-jwt-auth", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/tuupola/slim-jwt-auth.git", + "reference": "d9ed8bca77a0ef2a95ab48e65ddc26073b99c5ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tuupola/slim-jwt-auth/zipball/d9ed8bca77a0ef2a95ab48e65ddc26073b99c5ff", + "reference": "d9ed8bca77a0ef2a95ab48e65ddc26073b99c5ff", + "shasum": "" + }, + "require": { + "firebase/php-jwt": "^3.0|^4.0|^5.0", + "php": "^7.1|^8.0", + "psr/http-message": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "tuupola/callable-handler": "^0.3.0|^0.4.0|^1.0", + "tuupola/http-factory": "^0.4.0|^1.0.2" + }, + "require-dev": { + "equip/dispatch": "^2.0", + "laminas/laminas-diactoros": "^2.0", + "overtrue/phplint": "^1.0", + "phpstan/phpstan": "^0.12.43", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Tuupola\\Middleware\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mika Tuupola", + "email": "tuupola@appelsiini.net", + "homepage": "https://appelsiini.net/", + "role": "Developer" + } + ], + "description": "PSR-7 and PSR-15 JWT Authentication Middleware", + "homepage": "https://github.com/tuupola/slim-jwt-auth", + "keywords": [ + "auth", + "json", + "jwt", + "middleware", + "psr-15", + "psr-7" + ], + "support": { + "issues": "https://github.com/tuupola/slim-jwt-auth/issues", + "source": "https://github.com/tuupola/slim-jwt-auth/tree/3.6.0" + }, + "funding": [ + { + "url": "https://github.com/tuupola", + "type": "github" + } + ], + "time": "2022-01-12T11:15:02+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "87337c91b9dfacee02452244ee14ab3c43bc485a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/87337c91b9dfacee02452244ee14ab3c43bc485a", + "reference": "87337c91b9dfacee02452244ee14ab3c43bc485a", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "http://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/1.6.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2022-01-24T18:55:24+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-03-03T08:28:38+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2022-03-03T13:19:32+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.13.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + }, + "time": "2021-11-30T19:35:32+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "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": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@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" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0" + }, + "time": "2022-01-04T19:58:01+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.2", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0 || ^7.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" + }, + "time": "2021-12-08T12:19:24+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.15", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.13.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@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" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-03-07T09:28:20+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "sebastian@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" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.5.19", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "35ea4b7f3acabb26f4bb640f8c30866c401da807" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/35ea4b7f3acabb26f4bb640f8c30866c401da807", + "reference": "35ea4b7f3acabb26f4bb640f8c30866c401da807", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpspec/prophecy": "^1.12.1", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.5", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.3", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.0", + "sebastian/version": "^3.0.2" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0.1" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.19" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-03-15T09:57:31+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:49:45+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "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": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:52:38+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "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": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-11-11T14:18:36+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-14T08:28:10+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.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": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "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": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-03-15T09:54:48+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "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": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.6.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2021-12-12T21:44:58+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^7.4" + }, + "platform-dev": [], + "plugin-api-version": "2.2.0" +} diff --git a/config/Schema/coreGroupSchema.json b/config/Schema/coreGroupSchema.json new file mode 100644 index 0000000..8acebfc --- /dev/null +++ b/config/Schema/coreGroupSchema.json @@ -0,0 +1,74 @@ +{ + "id": "urn:ietf:params:scim:schemas:core:2.0:Group", + "name": "Group", + "description": "Group", + "attributes": [ + { + "name": "displayName", + "type": "string", + "multiValued": false, + "description": "A human-readable name for the Group. REQUIRED.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "members", + "type": "complex", + "multiValued": true, + "description": "A list of members of the Group.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "description": "Identifier of the member of this Group.", + "required": false, + "caseExact": false, + "mutability": "immutable", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "$ref", + "type": "reference", + "referenceTypes": [ + "User", + "Group" + ], + "multiValued": false, + "description": "The URI corresponding to a SCIM resource that is a member of this Group.", + "required": false, + "caseExact": false, + "mutability": "immutable", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the type of resource, e.g., 'User' or 'Group'.", + "required": false, + "caseExact": false, + "canonicalValues": [ + "User", + "Group" + ], + "mutability": "immutable", + "returned": "default", + "uniqueness": "none" + } + ], + "mutability": "readWrite", + "returned": "default" + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:Group" + } +} \ No newline at end of file diff --git a/config/Schema/coreUserSchema.json b/config/Schema/coreUserSchema.json new file mode 100644 index 0000000..d2cb415 --- /dev/null +++ b/config/Schema/coreUserSchema.json @@ -0,0 +1,773 @@ +{ + "id": "urn:ietf:params:scim:schemas:core:2.0:User", + "name": "User", + "description": "User Account", + "attributes": [ + { + "name": "userName", + "type": "string", + "multiValued": false, + "description": "Unique identifier for the User, typically used by the user to directly authenticate to the service provider. Each User MUST include a non-empty userName value. This identifier MUST be unique across the service provider's entire set of Users. REQUIRED.", + "required": true, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "server" + }, + { + "name": "name", + "type": "complex", + "multiValued": false, + "description": "The components of the user's real name. Providers MAY return just the full name as a single string in the formatted sub-attribute, or they MAY return just the individual component attributes using the other sub-attributes, or they MAY return both. If both variants are returned, they SHOULD be describing the same name, with the formatted name indicating how the component attributes should be combined.", + "required": false, + "subAttributes": [ + { + "name": "formatted", + "type": "string", + "multiValued": false, + "description": "The full name, including all middle names, titles, and suffixes as appropriate, formatted for display (e.g., 'Ms. Barbara J Jensen, III').", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "familyName", + "type": "string", + "multiValued": false, + "description": "The family name of the User, or last name in most Western languages (e.g., 'Jensen' given the full name 'Ms. Barbara J Jensen, III').", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "givenName", + "type": "string", + "multiValued": false, + "description": "The given name of the User, or first name in most Western languages (e.g., 'Barbara' given the full name 'Ms. Barbara J Jensen, III').", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "middleName", + "type": "string", + "multiValued": false, + "description": "The middle name(s) of the User (e.g., 'Jane' given the full name 'Ms. Barbara J Jensen, III').", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "honorificPrefix", + "type": "string", + "multiValued": false, + "description": "The honorific prefix(es) of the User, or title in most Western languages (e.g., 'Ms.' given the full name 'Ms. Barbara J Jensen, III').", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "honorificSuffix", + "type": "string", + "multiValued": false, + "description": "The honorific suffix(es) of the User, or suffix in most Western languages (e.g., 'III' given the full name 'Ms. Barbara J Jensen, III').", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "displayName", + "type": "string", + "multiValued": false, + "description": "The name of the User, suitable for display to end-users. The name SHOULD be the full name of the User being described, if known.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "nickName", + "type": "string", + "multiValued": false, + "description": "The casual way to address the user in real life, e.g., 'Bob' or 'Bobby' instead of 'Robert'. This attribute SHOULD NOT be used to represent a User's username (e.g., 'bjensen' or 'mpepperidge').", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "profileUrl", + "type": "reference", + "referenceTypes": [ + "external" + ], + "multiValued": false, + "description": "A fully qualified URL pointing to a page representing the User's online profile.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "title", + "type": "string", + "multiValued": false, + "description": "The user's title, such as \"Vice President.\"", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "userType", + "type": "string", + "multiValued": false, + "description": "Used to identify the relationship between the organization and the user. Typical values used might be 'Contractor', 'Employee', 'Intern', 'Temp', 'External', and 'Unknown', but any value may be used.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "preferredLanguage", + "type": "string", + "multiValued": false, + "description": "Indicates the User's preferred written or spoken language. Generally used for selecting a localized user interface; e.g., 'en_US' specifies the language English and country US.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "locale", + "type": "string", + "multiValued": false, + "description": "Used to indicate the User's default location for purposes of localizing items such as currency, date time format, or numerical representations.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "timezone", + "type": "string", + "multiValued": false, + "description": "The User's time zone in the 'Olson' time zone database format, e.g., 'America/Los_Angeles'.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "active", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating the User's administrative status.", + "required": false, + "mutability": "readWrite", + "returned": "default" + }, + { + "name": "password", + "type": "string", + "multiValued": false, + "description": "The User's cleartext password. This attribute is intended to be used as a means to specify an initial password when creating a new User or to reset an existing User's password.", + "required": false, + "caseExact": false, + "mutability": "writeOnly", + "returned": "never", + "uniqueness": "none" + }, + { + "name": "emails", + "type": "complex", + "multiValued": true, + "description": "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "description": "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the attribute's function, e.g., 'work' or 'home'.", + "required": false, + "caseExact": false, + "canonicalValues": [ + "work", + "home", + "other" + ], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred mailing address or primary email address. The primary attribute value 'true' MUST appear no more than once.", + "required": false, + "mutability": "readWrite", + "returned": "default" + } + ], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "phoneNumbers", + "type": "complex", + "multiValued": true, + "description": "Phone numbers for the User. The value SHOULD be canonicalized by the service provider according to the format specified in RFC 3966, e.g., 'tel:+1-201-555-0123'. Canonical type values of 'work', 'home', 'mobile', 'fax', 'pager', and 'other'.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "description": "Phone number of the User.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the attribute's function, e.g., 'work', 'home', 'mobile'.", + "required": false, + "caseExact": false, + "canonicalValues": [ + "work", + "home", + "mobile", + "fax", + "pager", + "other" + ], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred phone number or primary phone number. The primary attribute value 'true' MUST appear no more than once.", + "required": false, + "mutability": "readWrite", + "returned": "default" + } + ], + "mutability": "readWrite", + "returned": "default" + }, + { + "name": "ims", + "type": "complex", + "multiValued": true, + "description": "Instant messaging addresses for the User.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "description": "Instant messaging address for the User.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the attribute's function, e.g., 'aim', 'gtalk', 'xmpp'.", + "required": false, + "caseExact": false, + "canonicalValues": [ + "aim", + "gtalk", + "icq", + "xmpp", + "msn", + "skype", + "qq", + "yahoo" + ], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred messenger or primary messenger. The primary attribute value 'true' MUST appear no more than once.", + "required": false, + "mutability": "readWrite", + "returned": "default" + } + ], + "mutability": "readWrite", + "returned": "default" + }, + { + "name": "photos", + "type": "complex", + "multiValued": true, + "description": "URLs of photos of the User.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "reference", + "referenceTypes": [ + "external" + ], + "multiValued": false, + "description": "URL of a photo of the User.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the attribute's function, i.e., 'photo' or 'thumbnail'.", + "required": false, + "caseExact": false, + "canonicalValues": [ + "photo", + "thumbnail" + ], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred photo or thumbnail. The primary attribute value 'true' MUST appear no more than once.", + "required": false, + "mutability": "readWrite", + "returned": "default" + } + ], + "mutability": "readWrite", + "returned": "default" + }, + { + "name": "addresses", + "type": "complex", + "multiValued": true, + "description": "A physical mailing address for this User. Canonical type values of 'work', 'home', and 'other'. This attribute is a complex type with the following sub-attributes.", + "required": false, + "subAttributes": [ + { + "name": "formatted", + "type": "string", + "multiValued": false, + "description": "The full mailing address, formatted for display or use with a mailing label. This attribute MAY contain newlines.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "streetAddress", + "type": "string", + "multiValued": false, + "description": "The full street address component, which may include house number, street name, P.O. box, and multi-line extended street address information. This attribute MAY contain newlines.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "locality", + "type": "string", + "multiValued": false, + "description": "The city or locality component.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "region", + "type": "string", + "multiValued": false, + "description": "The state or region component.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "postalCode", + "type": "string", + "multiValued": false, + "description": "The zip code or postal code component.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "country", + "type": "string", + "multiValued": false, + "description": "The country name component.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the attribute's function, e.g., 'work' or 'home'.", + "required": false, + "caseExact": false, + "canonicalValues": [ + "work", + "home", + "other" + ], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "groups", + "type": "complex", + "multiValued": true, + "description": "A list of groups to which the user belongs, either through direct membership, through nested groups, or dynamically calculated.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "description": "The identifier of the User's group.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "$ref", + "type": "reference", + "referenceTypes": [ + "User", + "Group" + ], + "multiValued": false, + "description": "The URI of the corresponding 'Group' resource to which the user belongs.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the attribute's function, e.g., 'direct' or 'indirect'.", + "required": false, + "caseExact": false, + "canonicalValues": [ + "direct", + "indirect" + ], + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + } + ], + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "entitlements", + "type": "complex", + "multiValued": true, + "description": "A list of entitlements for the User that represent a thing the User has.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "description": "The value of an entitlement.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the attribute's function.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.", + "required": false, + "mutability": "readWrite", + "returned": "default" + } + ], + "mutability": "readWrite", + "returned": "default" + }, + { + "name": "roles", + "type": "complex", + "multiValued": true, + "description": "A list of roles for the User that collectively represent who the User is, e.g., 'Student', 'Faculty'.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "description": "The value of a role.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the attribute's function.", + "required": false, + "caseExact": false, + "canonicalValues": [], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.", + "required": false, + "mutability": "readWrite", + "returned": "default" + } + ], + "mutability": "readWrite", + "returned": "default" + }, + { + "name": "x509Certificates", + "type": "complex", + "multiValued": true, + "description": "A list of certificates issued to the User.", + "required": false, + "caseExact": false, + "subAttributes": [ + { + "name": "value", + "type": "binary", + "multiValued": false, + "description": "The value of an X.509 certificate.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "display", + "type": "string", + "multiValued": false, + "description": "A human-readable name, primarily used for display purposes. READ-ONLY.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the attribute's function.", + "required": false, + "caseExact": false, + "canonicalValues": [], + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "primary", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.", + "required": false, + "mutability": "readWrite", + "returned": "default" + } + ], + "mutability": "readWrite", + "returned": "default" + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User" + } +} \ No newline at end of file diff --git a/config/Schema/enterpriseUserSchema.json b/config/Schema/enterpriseUserSchema.json new file mode 100644 index 0000000..7ee374a --- /dev/null +++ b/config/Schema/enterpriseUserSchema.json @@ -0,0 +1,113 @@ +{ + "id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", + "name": "EnterpriseUser", + "description": "Enterprise User", + "attributes": [ + { + "name": "employeeNumber", + "type": "string", + "multiValued": false, + "description": "Numeric or alphanumeric identifier assigned to a person, typically based on order of hire or association with an organization.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "costCenter", + "type": "string", + "multiValued": false, + "description": "Identifies the name of a cost center.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "organization", + "type": "string", + "multiValued": false, + "description": "Identifies the name of an organization.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "division", + "type": "string", + "multiValued": false, + "description": "Identifies the name of a division.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "department", + "type": "string", + "multiValued": false, + "description": "Identifies the name of a department.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "manager", + "type": "complex", + "multiValued": false, + "description": "The User's manager. A complex type that optionally allows service providers to represent organizational hierarchy by referencing the 'id' attribute of another User.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "description": "The id of the SCIM resource representing the User's manager. REQUIRED.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "$ref", + "type": "reference", + "referenceTypes": [ + "User" + ], + "multiValued": false, + "description": "The URI of the SCIM resource representing the User's manager. REQUIRED.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "displayName", + "type": "string", + "multiValued": false, + "description": "The displayName of the User's manager. OPTIONAL and READ-ONLY.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + } + ], + "mutability": "readWrite", + "returned": "default" + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" + } +} \ No newline at end of file diff --git a/config/Schema/provisioningUserSchema.json b/config/Schema/provisioningUserSchema.json new file mode 100644 index 0000000..fca7626 --- /dev/null +++ b/config/Schema/provisioningUserSchema.json @@ -0,0 +1,22 @@ +{ + "id": "urn:audriga:params:scim:schemas:extension:provisioning:2.0:User", + "name": "ProvisioningUser", + "description": "Provisioning User", + "attributes": [ + { + "name": "sizeQuota", + "type": "int", + "multiValued": false, + "description": "Size quota assigned to a user, in bytes", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:audriga:params:scim:schemas:extension:provisioning:2.0:User" + } +} \ No newline at end of file diff --git a/config/Schema/resourceTypeSchema.json b/config/Schema/resourceTypeSchema.json new file mode 100644 index 0000000..f44cd28 --- /dev/null +++ b/config/Schema/resourceTypeSchema.json @@ -0,0 +1,102 @@ +{ + "id": "urn:ietf:params:scim:schemas:core:2.0:ResourceType", + "name": "ResourceType", + "description": "Specifies the schema that describes a SCIM resource type", + "attributes": [ + { + "name": "id", + "type": "string", + "multiValued": false, + "description": "The resource type's server unique id. May be the same as the 'name' attribute.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "name", + "type": "string", + "multiValued": false, + "description": "The resource type name. When applicable, service providers MUST specify the name, e.g., 'User'.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "description", + "type": "string", + "multiValued": false, + "description": "The resource type's human-readable description. When applicable, service providers MUST specify the description.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "endpoint", + "type": "reference", + "referenceTypes": [ + "uri" + ], + "multiValued": false, + "description": "The resource type's HTTP-addressable endpoint relative to the Base URL, e.g., '/Users'.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "schema", + "type": "reference", + "referenceTypes": [ + "uri" + ], + "multiValued": false, + "description": "The resource type's primary/base schema URI.", + "required": true, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "schemaExtensions", + "type": "complex", + "multiValued": false, + "description": "A list of URIs of the resource type's schema extensions.", + "required": true, + "mutability": "readOnly", + "returned": "default", + "subAttributes": [ + { + "name": "schema", + "type": "reference", + "referenceTypes": [ + "uri" + ], + "multiValued": false, + "description": "The URI of a schema extension.", + "required": true, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "required", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value that specifies whether or not the schema extension is required for the resource type. If true, a resource of this type MUST include this schema extension and also include any attributes declared as required in this schema extension. If false, a resource of this type MAY omit this schema extension.", + "required": true, + "mutability": "readOnly", + "returned": "default" + } + ] + } + ] +} \ No newline at end of file diff --git a/config/Schema/schemaSchema.json b/config/Schema/schemaSchema.json new file mode 100644 index 0000000..895872f --- /dev/null +++ b/config/Schema/schemaSchema.json @@ -0,0 +1,344 @@ +{ + "id": "urn:ietf:params:scim:schemas:core:2.0:Schema", + "name": "Schema", + "description": "Specifies the schema that describes a SCIM schema", + "attributes": [ + { + "name": "id", + "type": "string", + "multiValued": false, + "description": "The unique URI of the schema. When applicable, service providers MUST specify the URI.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "name", + "type": "string", + "multiValued": false, + "description": "The schema's human-readable name. When applicable, service providers MUST specify the name, e.g., 'User'.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "description", + "type": "string", + "multiValued": false, + "description": "The schema's human-readable name. When applicable, service providers MUST specify the name, e.g., 'User'.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "attributes", + "type": "complex", + "multiValued": true, + "description": "A complex attribute that includes the attributes of a schema.", + "required": true, + "mutability": "readOnly", + "returned": "default", + "subAttributes": [ + { + "name": "name", + "type": "string", + "multiValued": false, + "description": "The attribute's name.", + "required": true, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "The attribute's data type. Valid values include 'string', 'complex', 'boolean', 'decimal', 'integer', 'dateTime', 'reference'.", + "required": true, + "canonicalValues": [ + "string", + "complex", + "boolean", + "decimal", + "integer", + "dateTime", + "reference" + ], + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "multiValued", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating an attribute's plurality.", + "required": true, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "description", + "type": "string", + "multiValued": false, + "description": "A human-readable description of the attribute.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "required", + "type": "boolean", + "multiValued": false, + "description": "A boolean value indicating whether or not the attribute is required.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "canonicalValues", + "type": "string", + "multiValued": true, + "description": "A collection of canonical values. When applicable, service providers MUST specify the canonical types, e.g., 'work', 'home'.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "caseExact", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating whether or not a string attribute is case sensitive.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "mutability", + "type": "string", + "multiValued": false, + "description": "Indicates whether or not an attribute is modifiable.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "readOnly", + "readWrite", + "immutable", + "writeOnly" + ] + }, + { + "name": "returned", + "type": "string", + "multiValued": false, + "description": "Indicates when an attribute is returned in a response (e.g., to a query).", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "always", + "never", + "default", + "request" + ] + }, + { + "name": "uniqueness", + "type": "string", + "multiValued": false, + "description": "Indicates how unique a value must be.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "none", + "server", + "global" + ] + }, + { + "name": "referenceTypes", + "type": "string", + "multiValued": true, + "description": "Used only with an attribute of type 'reference'. Specifies a SCIM resourceType that a reference attribute MAY refer to, e.g., 'User'.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "subAttributes", + "type": "complex", + "multiValued": true, + "description": "Used to define the sub-attributes of a complex attribute.", + "required": false, + "mutability": "readOnly", + "returned": "default", + "subAttributes": [ + { + "name": "name", + "type": "string", + "multiValued": false, + "description": "The attribute's name.", + "required": true, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "The attribute's data type. Valid values include 'string', 'complex', 'boolean', 'decimal', 'integer', 'dateTime', 'reference'.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "string", + "complex", + "boolean", + "decimal", + "integer", + "dateTime", + "reference" + ] + }, + { + "name": "multiValued", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating an attribute's plurality.", + "required": true, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "description", + "type": "string", + "multiValued": false, + "description": "A human-readable description of the attribute.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "required", + "type": "boolean", + "multiValued": false, + "description": "A boolean value indicating whether or not the attribute is required.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "canonicalValues", + "type": "string", + "multiValued": true, + "description": "A collection of canonical values. When applicable, service providers MUST specify the canonical types, e.g., 'work', 'home'.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "caseExact", + "type": "boolean", + "multiValued": false, + "description": "A Boolean value indicating whether or not a string attribute is case sensitive.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "mutability", + "type": "string", + "multiValued": false, + "description": "Indicates whether or not an attribute is modifiable.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "readOnly", + "readWrite", + "immutable", + "writeOnly" + ] + }, + { + "name": "returned", + "type": "string", + "multiValued": false, + "description": "Indicates when an attribute is returned in a response (e.g., to a query).", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "always", + "never", + "default", + "request" + ] + }, + { + "name": "uniqueness", + "type": "string", + "multiValued": false, + "description": "Indicates how unique a value must be.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": [ + "none", + "server", + "global" + ] + }, + { + "name": "referenceTypes", + "type": "string", + "multiValued": false, + "description": "Used only with an attribute of type 'reference'. Specifies a SCIM resourceType that a reference attribute MAY refer to, e.g., 'User'.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/config/ServiceProviderConfig/serviceProviderConfig.json b/config/ServiceProviderConfig/serviceProviderConfig.json new file mode 100644 index 0000000..06d348a --- /dev/null +++ b/config/ServiceProviderConfig/serviceProviderConfig.json @@ -0,0 +1,51 @@ +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig" + ], + "documentationUri": null, + "patch": { + "supported": false + }, + "bulk": { + "supported": false, + "maxOperations": 0, + "maxPayloadSize": 0 + }, + "filter": { + "supported": true, + "maxResults": 200 + }, + "changePassword": { + "supported": false + }, + "sort": { + "supported": false + }, + "etag": { + "supported": false + }, + "authenticationSchemes": [ + { + "name": "HTTP Basic", + "description": "Authentication scheme using the HTTP Basic Standard", + "specUri": "http://www.rfc-editor.org/info/rfc2617", + "documentationUri": null, + "type": "httpbasic" + }, + { + "name": "OAuth Bearer Token", + "description": "Authentication scheme using the OAuth Bearer Token Standard", + "specUri": "http://www.rfc-editor.org/info/rfc6750", + "documentationUri": "http://example.com/help/oauth.html", + "type": "oauthbearertoken", + "primary": true + } + ], + "meta": { + "location": "https://example.com/v2/ServiceProviderConfig", + "resourceType": "ServiceProviderConfig", + "created": "2010-01-23T04:56:22Z", + "lastModified": "2011-05-13T04:42:34Z", + "version": "W\/\"3694e05e9dff594\"" + } +} \ No newline at end of file diff --git a/config/config.default.php b/config/config.default.php new file mode 100644 index 0000000..3105797 --- /dev/null +++ b/config/config.default.php @@ -0,0 +1,36 @@ + false, // Set to true when deploying in production + 'basePath' => null, // If you want to specify a base path for the Slim app, add it here (e.g., '/test/scim') + 'supportedResourceTypes' => ['User', 'Group'], // Specify all the supported SCIM ResourceTypes by their names here + + // SQLite DB settings + 'db' => [ + 'driver' => 'sqlite', // Type of DB + 'database' => 'db/scim-mock.sqlite' // DB name + ], + + // PFA MySQL DB settings + //'db' => [ + // 'driver' => 'sqlite', // Type of DB + // 'host' => 'localhost', // DB host + // 'port' => '3306', // Port on DB host + // 'database' => 'postfix', // DB name + // 'user' => 'postfix', // DB user + // 'password' => 'postfix123' // DB user's password + //], + + // Monolog settings + 'logger' => [ + 'name' => 'scim-opf', + 'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log', + 'level' => \Monolog\Logger::DEBUG, + ], + + // Bearer token settings + 'jwt' => [ + 'secure' => false, + 'secret' => 'secret' + ] +]; diff --git a/db/database.php b/db/database.php new file mode 100644 index 0000000..a0aca10 --- /dev/null +++ b/db/database.php @@ -0,0 +1,29 @@ +exec("DROP TABLE users"); +// $database->exec("DROP TABLE groups"); + +$user_db_sql = "CREATE TABLE IF NOT EXISTS users ( + id varchar(160) NOT NULL UNIQUE, + userName varchar(160) NOT NULL, + active BOOLEAN NOT NULL DEFAULT 1, + externalId varchar(160) NULL, + profileUrl varchar(160) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NULL +)"; + +$database->exec($user_db_sql); + +$group_db_sql = "CREATE TABLE IF NOT EXISTS groups ( + id varchar(160) NOT NULL UNIQUE, + displayName varchar(160) NOT NULL DEFAULT '', + members TEXT NOT NULL DEFAULT '', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NULL +)"; + +$database->exec($group_db_sql); diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..81d96ec --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,11 @@ + + + The coding standard for audriga. + + + + + + + + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..d5751b2 --- /dev/null +++ b/public/index.php @@ -0,0 +1,78 @@ +enableCompilation(__DIR__ . '/../var/cache'); +} + +// Set up a few Slim-related settings +$settings = [ + 'settings' => [ + 'determineRouteBeforeAppMiddleware' => false, + 'displayErrorDetails' => true, // set to false in production + 'addContentLengthHeader' => false, // Allow the web server to send the content-length header + ] +]; +$containerBuilder->addDefinitions($settings); + +// Set up common dependencies +$dependencies = require dirname(__DIR__) . '/src/Dependencies/dependencies.php'; +$dependencies($containerBuilder); + +// Set up system-specific dependencies +$dependencies = require dirname(__DIR__) . '/src/Dependencies/mock-dependencies.php'; +$dependencies($containerBuilder); + +// Build PHP-DI Container instance +$container = $containerBuilder->build(); + +// Instantiate the app +AppFactory::setContainer($container); +$app = AppFactory::create(); +$callableResolver = $app->getCallableResolver(); +$responseFactory = $app->getResponseFactory(); + +// Set our app's base path if it's configured +if (isset($config['basePath']) && !empty($config['basePath'])) { + $app->setBasePath($config['basePath']); +} + +// Set up the ORM +$eloquent = require dirname(__DIR__) . '/src/eloquent.php'; +$eloquent($app); + +// Register routes +$routes = require dirname(__DIR__) . '/src/routes.php'; +$routes($app); + +// Add Routing Middleware +$app->addRoutingMiddleware(); +$app->addBodyParsingMiddleware(); + +// Add JWT middleware +$app->addMiddleware($container->get(JwtAuthentication::class)); + +// Instantiate our custom Http error handler that we need further down below +$errorHandler = new HttpErrorHandler($callableResolver, $responseFactory); + +// Add error middleware +$errorMiddleware = $app->addErrorMiddleware( + $config['isInProduction'] ? false : true, + true, + true +); +$errorMiddleware->setDefaultErrorHandler($errorHandler); + +// Run app +$app->run(); diff --git a/src/Adapters/AbstractAdapter.php b/src/Adapters/AbstractAdapter.php new file mode 100644 index 0000000..3e905b6 --- /dev/null +++ b/src/Adapters/AbstractAdapter.php @@ -0,0 +1,11 @@ +group; + } + + public function setGroup(MockGroupDataAccess $group) + { + $this->group = $group; + } + + public function getId() + { + if (isset($this->group->id) && !empty($this->group->id)) { + return $this->group->id; + } + } + + public function setId($id) + { + if (isset($id) && !empty($id)) { + $this->group->id = $id; + } + } + + public function getCreatedAt() + { + if (isset($this->group->created_at) && !empty($this->group->created_at)) { + return $this->group->created_at; + } + } + + public function setCreatedAt($createdAt) + { + if (isset($createdAt) && !empty($createdAt)) { + $this->group->created_at = $createdAt; + } + } + + public function getDisplayName() + { + if (isset($this->group->displayName) && !empty($this->group->displayName)) { + return $this->group->displayName; + } + } + + public function setDisplayName($displayName) + { + if (isset($displayName) && !empty($displayName)) { + $this->group->displayName = $displayName; + } + } + + public function getMembers() + { + if (isset($this->group->members) && !empty($this->group->members)) { + return $this->group->members; + } + } + + public function setMembers($members) + { + if (isset($members) && !empty($members)) { + $this->group->members = $members; + } + } +} diff --git a/src/Adapters/Users/MockUserAdapter.php b/src/Adapters/Users/MockUserAdapter.php new file mode 100644 index 0000000..a27064c --- /dev/null +++ b/src/Adapters/Users/MockUserAdapter.php @@ -0,0 +1,106 @@ +user; + } + + public function setUser(MockUserDataAccess $user) + { + $this->user = $user; + } + + public function getId() + { + if (isset($this->user->id) && !empty($this->user->id)) { + return $this->user->id; + } + } + + public function setId($id) + { + if (isset($id) && !empty($id)) { + $this->user->id = $id; + } + } + + public function getUserName() + { + if (isset($this->user->userName) && !empty($this->user->userName)) { + return $this->user->userName; + } + } + + public function setUserName($userName) + { + if (isset($userName) && !empty($userName)) { + $this->user->userName = $userName; + } + } + + public function getCreatedAt() + { + if (isset($this->user->created_at) && !empty($this->user->created_at)) { + return $this->user->created_at; + } + } + + public function setCreatedAt($createdAt) + { + if (isset($createdAt) && !empty($createdAt)) { + $this->user->created_at = $createdAt; + } + } + + public function getActive() + { + if (isset($this->user->active) && !empty($this->user->active)) { + return boolval($this->user->active); + } + } + + public function setActive($active) + { + if (isset($active) && !empty($active)) { + $this->user->active = $active; + } + } + + public function getExternalId() + { + if (isset($this->user->externalId) && !empty($this->user->externalId)) { + return $this->user->externalId; + } + } + + public function setExternalId($externalId) + { + if (isset($externalId) && !empty($externalId)) { + $this->user->externalId = $externalId; + } + } + + public function getProfileUrl() + { + if (isset($this->user->profileUrl) && !empty($this->user->profileUrl)) { + return $this->user->profileUrl; + } + } + + public function setProfileUrl($profileUrl) + { + if (isset($profileUrl) && !empty($profileUrl)) { + $this->user->profileUrl = $profileUrl; + } + } +} diff --git a/src/Adapters/Users/PfaUserAdapter.php b/src/Adapters/Users/PfaUserAdapter.php new file mode 100644 index 0000000..15f03ee --- /dev/null +++ b/src/Adapters/Users/PfaUserAdapter.php @@ -0,0 +1,100 @@ +setId($pfaUser->getUserName()); + $provisioningUser->setUserName($pfaUser->getUserName()); + + $scimUserMeta = new Meta(); + $scimUserMeta->setResourceType("User"); + $scimUserMeta->setCreated($pfaUser->getCreated()); + $scimUserMeta->setLastModified($pfaUser->getModified()); + $provisioningUser->setMeta($scimUserMeta); + + $name = new Name(); + $name->setFormatted($pfaUser->getName()); + $provisioningUser->setName($name); + $provisioningUser->setDisplayName($pfaUser->getName()); + $provisioningUser->setActive($pfaUser->getActive()); + $scimUserPhoneNumbers = new MultiValuedAttribute(); + $scimUserPhoneNumbers->setValue($pfaUser->getPhone()); + $provisioningUser->setPhoneNumbers(array($scimUserPhoneNumbers)); + + $provisioningUser->setSizeQuota($pfaUser->getQuota()); + + return $provisioningUser; + } + + public function getPfaUser(?ProvisioningUser $provisioningUser): ?PfaUser + { + if ($provisioningUser === null) { + return null; + } + $pfaUser = new PfaUser(); + + if (filter_var($provisioningUser->getUserName(), FILTER_VALIDATE_EMAIL)) { + $pfaUser->setUserName($provisioningUser->getUserName()); + } elseif (filter_var($provisioningUser->getId(), FILTER_VALIDATE_EMAIL)) { + $pfaUser->setUserName($provisioningUser->getId()); + } else { + $pfaUser->setUserName($provisioningUser->getEmails()[0]->getValue()); + } + $pfaUser->setPassword($provisioningUser->getPassword()); + if ($provisioningUser->getName() !== null) { + if (!empty($provisioningUser->getName()->getFormatted())) { + $pfaUser->setName($provisioningUser->getName()->getFormatted()); + } else { + $formattedName = ""; + if (!empty($provisioningUser->getName()->getHonorificPrefix())) { + $formattedName = $formattedName . $provisioningUser->getName()->getHonorificPrefix() . " "; + } + if (!empty($provisioningUser->getName()->getGivenName())) { + $formattedName = $formattedName . $provisioningUser->getName()->getGivenName() . " "; + } + if (!empty($provisioningUser->getName()->getFamilyName())) { + $formattedName = $formattedName . $provisioningUser->getName()->getFamilyName() . " "; + } + if (!empty($provisioningUser->getName()->getHonorificSuffix())) { + $formattedName = $formattedName . $provisioningUser->getName()->getHonorificSuffix(); + } + $formattedName = trim($formattedName); + if (!empty($formattedName)) { + $pfaUser->setName($formattedName); + } else { + $pfaUser->setName($provisioningUser->getDisplayName()); + } + } + } else { + $pfaUser->setName($provisioningUser->getDisplayName()); + } + $pfaUser->setMaildir(Util::getDomainFromEmail($pfaUser->getUserName()) . "/" + . Util::getLocalPartFromEmail($pfaUser->getUserName()) . "/"); + // We default PFA quota to 0 (unlimited) if not set + $pfaUser->setQuota($provisioningUser->getSizeQuota()); + $pfaUser->setLocalPart(Util::getLocalPartFromEmail($pfaUser->getUserName())); + $pfaUser->setDomain(Util::getDomainFromEmail($pfaUser->getUserName())); + $pfaUser->setActive($provisioningUser->getActive()); + if (isset($provisioningUser->getPhoneNumbers()[0])) { + $pfaUser->setPhone($provisioningUser->getPhoneNumbers()[0]->getValue()); + } + + return $pfaUser; + } +} diff --git a/src/Controllers/Controller.php b/src/Controllers/Controller.php new file mode 100644 index 0000000..c2de520 --- /dev/null +++ b/src/Controllers/Controller.php @@ -0,0 +1,28 @@ +container = $container; + $this->logger = $this->container->get(Logger::class); + + $config = Util::getConfigFile(); + if (isset($config['basePath']) && !empty($config['basePath'])) { + $this->basePath = $config['basePath']; + } + } +} diff --git a/src/Controllers/Groups/CreateGroupAction.php b/src/Controllers/Groups/CreateGroupAction.php new file mode 100644 index 0000000..fc54e1b --- /dev/null +++ b/src/Controllers/Groups/CreateGroupAction.php @@ -0,0 +1,62 @@ +repository = $this->container->get('GroupsRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("CREATE Group"); + $this->logger->info($request->getBody()); + + $uri = $request->getUri(); + $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); + + try { + $group = $this->repository->create($request->getParsedBody()); + + if (isset($group) && !empty($group)) { + $scimGroup = $group->toSCIM(false, $baseUrl); + + $responseBody = json_encode($scimGroup, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 201); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } else { + $this->logger->error("Error creating group"); + $errorResponseBody = json_encode( + [ + "Errors" => [ + "description" => "Error creating group", "code" => 400 + ] + ] + ); + $response = new Response($status = 400); + $response->getBody()->write($errorResponseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } + } catch (\Exception $e) { + $this->logger->error("Error creating group: " . $e->getMessage()); + $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); + $response = new Response($status = 400); + $response->getBody()->write($errorResponseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } + } +} diff --git a/src/Controllers/Groups/DeleteGroupAction.php b/src/Controllers/Groups/DeleteGroupAction.php new file mode 100644 index 0000000..63b07b3 --- /dev/null +++ b/src/Controllers/Groups/DeleteGroupAction.php @@ -0,0 +1,32 @@ +repository = $this->container->get('GroupsRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("DELETE Group"); + $id = $request->getAttribute('id'); + $this->logger->info("ID: " . $id); + $deleteRes = $this->repository->delete($id); + if (!$deleteRes) { + $this->logger->info("Not found"); + return $response->withStatus(404); + } + $this->logger->info("Group deleted"); + + return $response->withStatus(200); + } +} diff --git a/src/Controllers/Groups/GetGroupAction.php b/src/Controllers/Groups/GetGroupAction.php new file mode 100644 index 0000000..883cecb --- /dev/null +++ b/src/Controllers/Groups/GetGroupAction.php @@ -0,0 +1,42 @@ +repository = $this->container->get('GroupsRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("GET Group"); + $uri = $request->getUri(); + $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); + + $id = $request->getAttribute('id'); + $this->logger->info("ID: " . $id); + $group = $this->repository->getOneById($id); + if (!isset($group) || empty($group)) { + $this->logger->info("Not found"); + return $response->withStatus(404); + } + + $scimGroup = $group->toSCIM(false, $baseUrl); + + $responseBody = json_encode($scimGroup, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 200); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } +} diff --git a/src/Controllers/Groups/ListGroupsAction.php b/src/Controllers/Groups/ListGroupsAction.php new file mode 100644 index 0000000..03549a6 --- /dev/null +++ b/src/Controllers/Groups/ListGroupsAction.php @@ -0,0 +1,45 @@ +repository = $this->container->get('GroupsRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("GET Groups"); + + $uri = $request->getUri(); + $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); + + $groups = []; + $groups = $this->repository->getAll(); + + $scimGroups = []; + if (!empty($groups)) { + foreach ($groups as $group) { + $scimGroups[] = $group->toSCIM(false, $baseUrl); + } + } + $scimGroupCollection = (new CoreCollection($scimGroups))->toSCIM(false); + + $responseBody = json_encode($scimGroupCollection, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 200); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } +} diff --git a/src/Controllers/Groups/UpdateGroupAction.php b/src/Controllers/Groups/UpdateGroupAction.php new file mode 100644 index 0000000..081bbc6 --- /dev/null +++ b/src/Controllers/Groups/UpdateGroupAction.php @@ -0,0 +1,65 @@ +repository = $this->container->get('GroupsRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("UPDATE Group"); + $this->logger->info($request->getBody()); + + $uri = $request->getUri(); + $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); + + $id = $request->getAttribute('id'); + $this->logger->info("ID: " . $id); + + $group = $this->repository->getOneById($id); + if (!isset($group) || empty($group)) { + $this->logger->info("Not found"); + return $response->withStatus(404); + } + + try { + $group = $this->repository->update($id, $request->getParsedBody()); + if (isset($group) && !empty($group)) { + $scimGroup = $group->toSCIM(false, $baseUrl); + + $responseBody = json_encode($scimGroup, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 201); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } else { + $this->logger->error("Error updating group"); + $errorResponseBody = json_encode(["Errors" => ["decription" => "Error updating group", "code" => 400]]); + $response = new Response($status = 400); + $response->getBody()->write($errorResponseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } + } catch (\Exception $e) { + $this->logger->error("Error updating group: " . $e->getMessage()); + $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); + $response = new Response($status = 400); + $response->getBody()->write($errorResponseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } + } +} diff --git a/src/Controllers/JWT/GenerateJWTAction.php b/src/Controllers/JWT/GenerateJWTAction.php new file mode 100644 index 0000000..92602fe --- /dev/null +++ b/src/Controllers/JWT/GenerateJWTAction.php @@ -0,0 +1,28 @@ + "admin", "password" => "admin"], $settings['secret']); + + $responseBody = json_encode(['Bearer' => $token], JSON_UNESCAPED_SLASHES); + + $response = new Response($status = 200); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/json'); + return $response; + } +} diff --git a/src/Controllers/ServiceProviders/ListResourceTypesAction.php b/src/Controllers/ServiceProviders/ListResourceTypesAction.php new file mode 100644 index 0000000..fb562cd --- /dev/null +++ b/src/Controllers/ServiceProviders/ListResourceTypesAction.php @@ -0,0 +1,78 @@ +logger->info("GET ResourceTypes"); + + $uri = $request->getUri(); + $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); + + // Check which resource types are supported via the config file and in this method further down below + // make sure to only return those that are indeed supported + $config = Util::getConfigFile(); + $supportedResourceTypes = $config['supportedResourceTypes']; + + $scimResourceTypes = []; + + if (in_array('User', $supportedResourceTypes)) { + $userResourceType = new CoreResourceType(); + $userResourceType->setId("User"); + $userResourceType->setName("User"); + $userResourceType->setEndpoint("/Users"); + $userResourceType->setDescription("User Account"); + $userResourceType->setSchema(Util::USER_SCHEMA); + + if (in_array('EnterpriseUser', $supportedResourceTypes)) { + $enterpriseUserSchemaExtension = new CoreSchemaExtension(); + $enterpriseUserSchemaExtension->setSchema(Util::ENTERPRISE_USER_SCHEMA); + $enterpriseUserSchemaExtension->setRequired(true); + + $userResourceType->setSchemaExtensions(array($enterpriseUserSchemaExtension)); + } + if (in_array('ProvisioningUser', $supportedResourceTypes)) { + $provisioningUserSchemaExtension = new CoreSchemaExtension(); + $provisioningUserSchemaExtension->setSchema(Util::PROVISIONING_USER_SCHEMA); + $provisioningUserSchemaExtension->setRequired(true); + + $userResourceType->setSchemaExtensions(array($provisioningUserSchemaExtension)); + } + + $scimResourceTypes[] = $userResourceType->toSCIM(false, $baseUrl); + } + + if (in_array('Group', $supportedResourceTypes)) { + $groupResourceType = new CoreResourceType(); + $groupResourceType->setId("Group"); + $groupResourceType->setName("Group"); + $groupResourceType->setEndpoint("/Groups"); + $groupResourceType->setDescription("Group"); + $groupResourceType->setSchema("urn:ietf:params:scim:schemas:core:2.0:Group"); + $groupResourceType->setSchemaExtensions([]); + + $scimResourceTypes[] = $groupResourceType->toSCIM(false, $baseUrl); + } + + $scimResourceTypeCollection = (new CoreCollection($scimResourceTypes))->toSCIM(false); + + $responseBody = json_encode($scimResourceTypeCollection, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 200); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } +} diff --git a/src/Controllers/ServiceProviders/ListSchemasAction.php b/src/Controllers/ServiceProviders/ListSchemasAction.php new file mode 100644 index 0000000..3de4cb8 --- /dev/null +++ b/src/Controllers/ServiceProviders/ListSchemasAction.php @@ -0,0 +1,61 @@ +logger->info("GET Schemas"); + + $config = Util::getConfigFile(); + $supportedSchemas = $config['supportedResourceTypes']; + $mandatorySchemas = ['Schema', 'ResourceType']; + + $scimSchemas = []; + + // We store the schemas that the SCIM server supports in separate JSON files + // That's why we try to read them here and add them to $scimSchemas, which + // in turn is then put into the SCIM response body + $pathToSchemasDir = dirname(__DIR__, 3) . '/config/Schema'; + $schemaFiles = scandir($pathToSchemasDir, SCANDIR_SORT_NONE); + + // If scandir() failed (i.e., it returned false), then return 404 (is this spec-compliant?) + if ($schemaFiles === false) { + $this->logger->info("No Schemas found"); + $response = new Response($status = 404); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + + return $response; + } + + foreach ($schemaFiles as $schemaFile) { + if (!in_array($schemaFile, array('.', '..'))) { + $scimSchemaJsonDecoded = json_decode(file_get_contents($pathToSchemasDir . '/' . $schemaFile), true); + + // Only return schemas that are either mandatory (like the 'Schema' and 'ResourceType' ones) + // or supported by the server + if (in_array($scimSchemaJsonDecoded['name'], array_merge($supportedSchemas, $mandatorySchemas))) { + $scimSchemas[] = $scimSchemaJsonDecoded; + } + } + } + + $scimSchemasCollection = (new CoreCollection($scimSchemas))->toSCIM(false); + + $responseBody = json_encode($scimSchemasCollection, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 200); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + + return $response; + } +} diff --git a/src/Controllers/ServiceProviders/ListServiceProviderConfigurationsAction.php b/src/Controllers/ServiceProviders/ListServiceProviderConfigurationsAction.php new file mode 100644 index 0000000..8cb8e2a --- /dev/null +++ b/src/Controllers/ServiceProviders/ListServiceProviderConfigurationsAction.php @@ -0,0 +1,37 @@ +logger->info("GET ServiceProviderConfigurations"); + + $pathToServiceProviderConfigurationFile = + dirname(__DIR__, 3) . '/config/ServiceProviderConfig/serviceProviderConfig.json'; + + $scimServiceProviderConfigurationFile = file_get_contents($pathToServiceProviderConfigurationFile); + + if ($scimServiceProviderConfigurationFile === false) { + $this->logger->info("No ServiceProviderConfiguration found"); + $response = new Response($status = 404); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + + return $response; + } + + $responseBody = $scimServiceProviderConfigurationFile; + $this->logger->info($responseBody); + $response = new Response($status = 200); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + + return $response; + } +} diff --git a/src/Controllers/Users/CreateUserAction.php b/src/Controllers/Users/CreateUserAction.php new file mode 100644 index 0000000..e20dc74 --- /dev/null +++ b/src/Controllers/Users/CreateUserAction.php @@ -0,0 +1,58 @@ +repository = $this->container->get('UsersRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("CREATE User"); + $this->logger->info($request->getBody()); + + $uri = $request->getUri(); + $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); + + try { + $user = $this->repository->create($request->getParsedBody()); + + if (isset($user) && !empty($user)) { + $this->logger->info("Created user / username=" . $user->getUserName() . " / ID=" . $user->getId()); + + $scimUser = $user->toSCIM(false, $baseUrl); + + $responseBody = json_encode($scimUser, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 201); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } else { + $this->logger->error("Error creating user"); + $errorResponseBody = json_encode(["Errors" => ["description" => "Error creating user", "code" => 400]]); + $response = new Response($status = 400); + $response->getBody()->write($errorResponseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } + } catch (\Exception $e) { + $this->logger->error("Error creating user: " . $e->getMessage()); + $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); + $response = new Response($status = 400); + $response->getBody()->write($errorResponseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } + } +} diff --git a/src/Controllers/Users/DeleteUserAction.php b/src/Controllers/Users/DeleteUserAction.php new file mode 100644 index 0000000..55b6025 --- /dev/null +++ b/src/Controllers/Users/DeleteUserAction.php @@ -0,0 +1,33 @@ +repository = $this->container->get('UsersRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("DELETE User"); + $id = $request->getAttribute('id'); + $this->logger->info("ID: " . $id); + $deleteRes = $this->repository->delete($id); + if (!$deleteRes) { + $this->logger->info("Not found"); + return $response->withStatus(404); + } + $this->logger->info("User deleted"); + + $response = $response->withHeader("Content-Type", "application/scim+json"); + return $response->withStatus(204); + } +} diff --git a/src/Controllers/Users/GetUserAction.php b/src/Controllers/Users/GetUserAction.php new file mode 100644 index 0000000..5bdb6cb --- /dev/null +++ b/src/Controllers/Users/GetUserAction.php @@ -0,0 +1,42 @@ +repository = $this->container->get('UsersRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("GET User"); + $uri = $request->getUri(); + $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); + + $id = $request->getAttribute('id'); + $this->logger->info("ID: " . $id); + $user = $this->repository->getOneById($id); + if (!isset($user) || empty($user)) { + $this->logger->info("Not found"); + return $response->withStatus(404); + } + + $scimUser = $user->toSCIM(false, $baseUrl); + + $responseBody = json_encode($scimUser, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 200); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } +} diff --git a/src/Controllers/Users/ListUsersAction.php b/src/Controllers/Users/ListUsersAction.php new file mode 100644 index 0000000..f4a6d1e --- /dev/null +++ b/src/Controllers/Users/ListUsersAction.php @@ -0,0 +1,59 @@ +repository = $this->container->get('UsersRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("GET Users"); + if (!empty($request->getQueryParams()['filter'])) { + $this->logger->info("Filter --> " . $request->getQueryParams()['filter']); + } + $uri = $request->getUri(); + $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); + + $userName = null; + $users = []; + if (!empty($request->getQueryParams('filter'))) { + $userName = Util::getUserNameFromFilter($request->getQueryParams()['filter']); + if (!empty($userName)) { + $user = $this->repository->getOneByUserName(); + if (isset($user) && !empty($user)) { + $users[] = $user; + } + } + } else { + $users = $this->repository->getAll(); + } + + $scimUsers = []; + if (!empty($users)) { + foreach ($users as $user) { + $scimUsers[] = $user->toSCIM(false, $baseUrl); + } + } + $scimUserCollection = (new CoreCollection($scimUsers))->toSCIM(false); + + $responseBody = json_encode($scimUserCollection, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 200); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } +} diff --git a/src/Controllers/Users/UpdateUserAction.php b/src/Controllers/Users/UpdateUserAction.php new file mode 100644 index 0000000..1499e65 --- /dev/null +++ b/src/Controllers/Users/UpdateUserAction.php @@ -0,0 +1,66 @@ +repository = $this->container->get('UsersRepository'); + } + + public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->logger->info("UPDATE User"); + $this->logger->info($request->getBody()); + + $uri = $request->getUri(); + $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); + + $id = $request->getAttribute('id'); + $this->logger->info("ID: " . $id); + + // Try to find a user with the supplied ID + // and if it doesn't exist, return a 404 + $user = $this->repository->getOneById($id); + if (!isset($user) || empty($user)) { + $this->logger->info("Not found"); + return $response->withStatus(404); + } + + try { + $user = $this->repository->update($id, $request->getParsedBody()); + if (isset($user) && !empty($user)) { + $scimUser = $user->toSCIM(false, $baseUrl); + + $responseBody = json_encode($scimUser, JSON_UNESCAPED_SLASHES); + $this->logger->info($responseBody); + $response = new Response($status = 200); + $response->getBody()->write($responseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } else { + $this->logger->error("Error updating user"); + $errorResponseBody = json_encode(["Errors" => ["decription" => "Error updating user", "code" => 400]]); + $response = new Response($status = 400); + $response->getBody()->write($errorResponseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } + } catch (\Exception $e) { + $this->logger->error("Error updating user: " . $e->getMessage()); + $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); + $response = new Response($status = 400); + $response->getBody()->write($errorResponseBody); + $response = $response->withHeader('Content-Type', 'application/scim+json'); + return $response; + } + } +} diff --git a/src/DataAccess/Groups/MockGroupDataAccess.php b/src/DataAccess/Groups/MockGroupDataAccess.php new file mode 100644 index 0000000..5297390 --- /dev/null +++ b/src/DataAccess/Groups/MockGroupDataAccess.php @@ -0,0 +1,60 @@ +id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid()); + $this->displayName = $data['displayName']; + $this->members = is_string($data['members']) ? $data['members'] : implode(",", $data['members']); + $this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) : new \DateTime('NOW'); + } + + public function fromSCIM($data) + { + $this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid()); + $this->displayName = $data['displayName']; + $this->members = is_string($data['members']) ? $data['members'] : implode(",", $data['members']); + $this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) : new \DateTime('NOW'); + } + + public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') + { + $data = [ + 'schemas' => $this->schemas, + 'id' => $this->id, + 'displayName' => $this->displayName, + 'members' => [], + 'meta' => [ + 'created' => Util::dateTime2string($this->created_at), + 'location' => $baseLocation . '/Groups/' . $this->id + ] + ]; + + if (!empty($this->members)) { + $data['members'] = explode(',', $this->members); + } + + if (isset($this->updated_at)) { + $data['meta']['updated'] = Util::dateTime2string($this->updated_at); + } + + if ($encode) { + $data = json_encode($data); + } + + return $data; + } +} diff --git a/src/DataAccess/Users/MockUserDataAccess.php b/src/DataAccess/Users/MockUserDataAccess.php new file mode 100644 index 0000000..59b8e0c --- /dev/null +++ b/src/DataAccess/Users/MockUserDataAccess.php @@ -0,0 +1,68 @@ +id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid()); + $this->userName = isset($data['userName']) ? $data['userName'] : null; + $this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) + : (isset($this->created_at) ? $this->created_at : new \DateTime('NOW')); + $this->active = isset($data['active']) ? $data['active'] : true; + + $this->externalId = isset($data['externalId']) ? $data['externalId'] : null; + $this->profileUrl = isset($data['profileUrl']) ? $data['profileUrl'] : null; + } + + public function fromSCIM($data) + { + $this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid()); + $this->userName = isset($data['userName']) ? $data['userName'] : null; + $this->created_at = isset($data['meta']) && isset($data['meta']['created']) + ? Util::string2dateTime($data['meta']['created']) + : (isset($this->created_at) ? $this->created_at : new \DateTime('NOW')); + $this->active = isset($data['active']) ? $data['active'] : true; + + $this->externalId = isset($data['externalId']) ? $data['externalId'] : null; + $this->profileUrl = isset($data['profileUrl']) ? $data['profileUrl'] : null; + } + + public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') + { + $data = [ + 'schemas' => $this->schemas, + 'id' => $this->id, + 'externalId' => $this->externalId, + 'meta' => [ + 'created' => Util::dateTime2string($this->created_at), + 'location' => $baseLocation . '/Users/' . $this->id + ], + 'userName' => $this->userName, + 'profileUrl' => $this->profileUrl, + 'active' => (bool) $this->active + ]; + + if (isset($this->updated_at)) { + $data['meta']['updated'] = Util::dateTime2string($this->updated_at); + } + + if ($encode) { + $data = json_encode($data); + } + + return $data; + } +} diff --git a/src/DataAccess/Users/PfaUserDataAccess.php b/src/DataAccess/Users/PfaUserDataAccess.php new file mode 100644 index 0000000..9992577 --- /dev/null +++ b/src/DataAccess/Users/PfaUserDataAccess.php @@ -0,0 +1,319 @@ +logger = new Logger(PfaUserDataAccess::class); + $this->logger->pushHandler(new StreamHandler(__DIR__ . '/../../../logs/app.log', Logger::DEBUG)); + + // Try to obtain a DSN via the Util class and complain with an Exception if there's no DSN + $dsn = Util::buildDbDsn(); + if (!isset($dsn)) { + throw new Exception("Can't obtain DSN to connect to DB"); + } + + $this->config = Util::getConfigFile(); + if (isset($this->config) && !empty($this->config)) { + if (isset($this->config['db']) && !empty($this->config['db'])) { + if ( + isset($this->config['db']['user']) + && !empty($this->config['db']['user']) + && isset($this->config['db']['password']) + && !empty($this->config['db']['password']) + ) { + $dbUsername = $this->config['db']['user']; + $dbPassword = $this->config['db']['password']; + } else { + throw new Exception("No DB username and/or password provided to connect to DB"); + } + } + } + + // Create the DB connection with PDO + $this->dbConnection = new PDO($dsn, $dbUsername, $dbPassword); + + // Tell PDO explicitly to throw exceptions on errors, so as to have more info when debugging DB operations + $this->dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } + + public function getAll(): ?array + { + if (isset($this->dbConnection)) { + // TODO: should we use a variable for the table name? + // PFA users are 'mailbox' entries (see also https://web.audriga.com/mantis/view.php?id=5806#c28866): + // - with a corresponding 'alias' entry + // - with the 'alias.address' value being part of the 'alias.goto' value + $selectStatement = $this->dbConnection->query("SELECT mailbox.* FROM mailbox INNER JOIN alias + WHERE mailbox.username = alias.address + AND alias.goto LIKE CONCAT('%', alias.address, '%')"); + if ($selectStatement) { + $pfaUsers = []; + $pfaUsersRaw = $selectStatement->fetchAll(PDO::FETCH_ASSOC); + foreach ($pfaUsersRaw as $user) { + $pfaUser = new PfaUser(); + $pfaUser->mapFromArray($user); + $pfaUsers[] = $pfaUser; + } + return $pfaUsers; + } + + $this->logger->error("Couldn't read all users from PFA. SELECT query to DB failed"); + } + + $this->logger->error("Couldn't connect to DB while attempting to read all users from PFA"); + return null; + } + + // TODO: In the case of PFA, it maybe makes sense to rename this to something like getOneByUsername, + // since username is the distinguishing property between users (mailboxes) in PFA and not ID + public function getOneById($id): ?PfaUser + { + if (isset($id) && !empty($id)) { + if (isset($this->dbConnection)) { + try { + // TODO: should we use a variable for the table name? + $selectOnePreparedStatement = $this->dbConnection->prepare( + "SELECT mailbox.* FROM mailbox INNER JOIN alias + WHERE mailbox.username = alias.address + AND alias.goto LIKE CONCAT('%', alias.address, '%') + AND mailbox.username = ?" + ); + + $selectRes = $selectOnePreparedStatement->execute([$id]); + + if ($selectRes) { + $pfaUsersRaw = $selectOnePreparedStatement->fetchAll(PDO::FETCH_ASSOC); + if ($pfaUsersRaw) { + $pfaUser = new PfaUser(); + $pfaUser->mapFromArray($pfaUsersRaw[0]); + return $pfaUser; + } else { + return null; + } + } else { + return null; + } + } catch (PDOException $e) { + $this->logger->error($e->getMessage()); + } + } + } + + $this->logger->error( + "Argument provided to getOneById in class " . PfaUserDataAccess::class . " is not set or empty" + ); + return null; + } + + public function create(PfaUser $userToCreate): ?PfaUser + { + $dateNow = date('Y-m-d H:i:s'); + $date2000 = '2000-01-01 00:00:00'; + + if (isset($this->dbConnection)) { + try { + // We want to commit both insert (in mailbox and in alias) in one single commit, + // and we want to abort both if something fails + $this->dbConnection->beginTransaction(); + + // TODO: should we use a variable for the table name? + $insertStatement = $this->dbConnection->prepare( + "INSERT INTO mailbox + (username, password, name, maildir, quota, local_part, domain, created, modified, active, phone, + email_other, token, token_validity, password_expiry) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + ); + + // When performing an INSERT into the mailbox table, maildir, domain and local_part + // as columns don't have default values. + // That's why here in the prepared statement we currently just give them the value of the empty string, + // so as to avoid the issue of MySQL complaining + // that nothing is provided for them when they have no default value + $insertRes1 = $insertStatement->execute([ + $userToCreate->getUserName(), + $userToCreate->getPassword() !== null ? $userToCreate->getPassword() : '', + $userToCreate->getName() !== null ? $userToCreate->getName() : '', + $userToCreate->getMaildir(), + $userToCreate->getQuota() !== null ? $userToCreate->getQuota() : 0, + $userToCreate->getLocalPart(), + $userToCreate->getDomain(), + $dateNow, + $dateNow, + $userToCreate->getActive(), + $userToCreate->getPhone() !== null ? $userToCreate->getPhone() : '', + $userToCreate->getEmailOther() !== null ? $userToCreate->getEmailOther() : '', + $userToCreate->getToken() !== null ? $userToCreate->getToken() : '', + $userToCreate->getTokenValidity() !== null ? $userToCreate->getTokenValidity() : $date2000, + $userToCreate->getPasswordExpiry() !== null ? $userToCreate->getPasswordExpiry() : $date2000 + ]); + + $insertStatement = $this->dbConnection->prepare( + "INSERT INTO alias + (address, goto, domain, created, modified, active) + VALUES (?, ?, ?, ?, ?, ?)" + ); + + // When performing an INSERT into the mailbox table, maildir, domain and local_part + // as columns don't have default values. + // That's why here in the prepared statement we currently just give them the value of the empty string, + // so as to avoid the issue of MySQL complaining + // that nothing is provided for them when they have no default value + $insertRes2 = $insertStatement->execute([ + $userToCreate->getUserName(), + $userToCreate->getUserName(), + $userToCreate->getDomain(), + $dateNow, + $dateNow, + $userToCreate->getActive() + ]); + + // In case the write was successful, return the user that was just written + if ($insertRes1 && $insertRes2) { + $this->dbConnection->commit(); + $this->logger->info("Created user " . $userToCreate->getUserName()); + return $this->getOneById($userToCreate->getUserName()); + //return $userToCreate; + } else { + // Otherwise, rollback and just return null + $this->dbConnection->rollBack(); + return null; + } + } catch (PDOException $e) { + $this->dbConnection->rollBack(); + $this->logger->error($e->getMessage()); + } + } else { + $this->logger->error("DB connection not available"); + } + $this->logger->error("Error creating user"); + return null; + } + + public function update(string $username, PfaUser $userToUpdate): ?PfaUser + { + $dateNow = date('Y-m-d H:i:s'); + + if (isset($this->dbConnection)) { + try { + $query = ""; + $values = array(); + if ($userToUpdate->getPassword() !== null) { + $query = $query . "password = ?, "; + $values[] = $userToUpdate->getPassword(); + } + if ($userToUpdate->getName() !== null) { + $query = $query . "name = ?, "; + $values[] = $userToUpdate->getName(); + } + if ($userToUpdate->getQuota() !== null) { + $query = $query . "quota = ?, "; + $values[] = $userToUpdate->getQuota(); + } + if ($userToUpdate->getActive() !== null) { + $query = $query . "active = ?, "; + $values[] = $userToUpdate->getActive(); + } + if ($userToUpdate->getEmailOther() !== null) { + $query = $query . "email_other = ?, "; + $values[] = $userToUpdate->getEmailOther(); + } + + if (empty($query)) { + $this->logger->error("No user properties to update"); + return null; + } + + $query = $query . "modified = ? "; + $values[] = $dateNow; + $values[] = $username; + + // Since in PFA the username column in the mailbox table is the primary and is unique, + // we use username in this case as an id that serves the purpose of a unique identifier + // TODO: should we use a variable for the table name? + $updateStatement = $this->dbConnection->prepare( + "UPDATE mailbox SET " . $query . " WHERE username = ?" + ); + + $updateRes = $updateStatement->execute($values); + + // In case the update was successful, return the user that was just updated + if ($updateRes) { + $this->logger->info("Updated user " . $username); + return $this->getOneById($username); + } else { + $this->logger->error("Error updating user " . $username); + return null; + } + } catch (PDOException $e) { + $this->logger->error($e->getMessage()); + } + } else { + $this->logger->error("Error updating user " . $username . " - DB connection unavailable"); + } + $this->logger->error("Error updating user " . $username); + return null; + } + + public function delete($username): bool + { + if (isset($this->dbConnection)) { + try { + // We want to commit both delete (in mailbox and in alias) in one single commit, + // and we want to abort both if something fails + $this->dbConnection->beginTransaction(); + + // TODO: should we use a variable for the table name? + $deleteStatement = $this->dbConnection->prepare( + "DELETE FROM mailbox WHERE username = ?" + ); + $deleteRes1 = $deleteStatement->execute([$username]); + + // TODO: should we use a variable for the table name? + $deleteStatement = $this->dbConnection->prepare( + "DELETE FROM alias WHERE address = ?" + ); + $deleteRes2 = $deleteStatement->execute([$username]); + + // In case the delete was successful, return true + if ($deleteRes1 && $deleteRes2) { + $this->dbConnection->commit(); + $this->logger->info("Deleted user " . $username); + return true; + //return $userToCreate; + } else { + // Otherwise, rollback and just return false + $this->dbConnection->rollBack(); + return false; + } + } catch (PDOException $e) { + $this->dbConnection->rollBack(); + $this->logger->error($e->getMessage()); + } + } else { + $this->logger->error("Error deleting user " . $username . " - DB connection unavailable"); + } + $this->logger->error("Error deleting user " . $username); + return false; + } +} diff --git a/src/Dependencies/dependencies.php b/src/Dependencies/dependencies.php new file mode 100644 index 0000000..9d707ae --- /dev/null +++ b/src/Dependencies/dependencies.php @@ -0,0 +1,59 @@ +addDefinitions([ + // Monolog + Monolog\Logger::class => function (ContainerInterface $c) { + $config = Util::getConfigFile(); + $settings = $config['logger']; + $logger = new Monolog\Logger($settings['name']); + $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level'])); + return $logger; + }, + + // JWT + 'JwtAuthentication' => function (ContainerInterface $c) { + $config = Util::getConfigFile(); + $settings = $config['jwt']; + $settings["logger"] = $c->get(Monolog\Logger::class); + $settings["attribute"] = "jwt"; + + // Don't ask for JWT when trying to obtain one + $basePath = ""; + if (isset($config) && !empty($config)) { + if (isset($config["basePath"]) && !empty($config["basePath"])) { + $basePath = $config["basePath"]; + } + } + $settings["ignore"] = [$basePath . "/jwt"]; + + if (!isset($settings['error'])) { + $settings["error"] = function ( + ResponseInterface $response, + $arguments + ) { + $data["status"] = "error"; + $data["message"] = $arguments["message"]; + return $response + ->withHeader("Content-Type", "application/json") + ->getBody()->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + }; + } + return new JwtAuthentication($settings); + }, + + // Controllers + Controller::class => function (ContainerInterface $c) { + return new Controller($c); + } + ]); +}; diff --git a/src/Dependencies/mock-dependencies.php b/src/Dependencies/mock-dependencies.php new file mode 100644 index 0000000..395f8d3 --- /dev/null +++ b/src/Dependencies/mock-dependencies.php @@ -0,0 +1,46 @@ +addDefinitions([ + // Repositories + 'UsersRepository' => function (ContainerInterface $c) { + return new MockUsersRepository($c); + }, + + 'GroupsRepository' => function (ContainerInterface $c) { + return new MockGroupsRepository($c); + }, + + // Data access classes + 'UsersDataAccess' => function () { + return new MockUserDataAccess(); + }, + + 'GroupsDataAccess' => function () { + return new MockGroupDataAccess(); + }, + + // Adapters + 'UsersAdapter' => function () { + return new MockUserAdapter(); + }, + + 'GroupsAdapter' => function () { + return new MockGroupAdapter(); + } + ]); +}; diff --git a/src/Dependencies/pfa-dependencies.php b/src/Dependencies/pfa-dependencies.php new file mode 100644 index 0000000..c7279ca --- /dev/null +++ b/src/Dependencies/pfa-dependencies.php @@ -0,0 +1,28 @@ +addDefinitions([ + // Repositories + 'UsersRepository' => function (ContainerInterface $c) { + return new PfaUsersRepository($c); + }, + + // Data access classes + 'UsersDataAccess' => function () { + return new PfaUserDataAccess(); + }, + + // Adapters + 'UsersAdapter' => function () { + return new PfaUserAdapter(); + } + ]); +}; diff --git a/src/Handlers/HttpErrorHandler.php b/src/Handlers/HttpErrorHandler.php new file mode 100644 index 0000000..39a3c4b --- /dev/null +++ b/src/Handlers/HttpErrorHandler.php @@ -0,0 +1,76 @@ +exception; + $statusCode = 500; + $type = self::SERVER_ERROR; + $description = 'An internal error has occurred while processing your request.'; + + if ($exception instanceof HttpException) { + $statusCode = $exception->getCode(); + $description = $exception->getMessage(); + + if ($exception instanceof HttpNotFoundException) { + $type = self::RESOURCE_NOT_FOUND; + } elseif ($exception instanceof HttpMethodNotAllowedException) { + $type = self::NOT_ALLOWED; + } elseif ($exception instanceof HttpUnauthorizedException) { + $type = self::UNAUTHENTICATED; + } elseif ($exception instanceof HttpForbiddenException) { + $type = self::UNAUTHENTICATED; + } elseif ($exception instanceof HttpBadRequestException) { + $type = self::BAD_REQUEST; + } elseif ($exception instanceof HttpNotImplementedException) { + $type = self::NOT_IMPLEMENTED; + } + } + + if ( + !($exception instanceof HttpException) + && ($exception instanceof Exception || $exception instanceof Throwable) + && $this->displayErrorDetails + ) { + $description = $exception->getMessage(); + } + + $error = [ + 'statusCode' => $statusCode, + 'error' => [ + 'type' => $type, + 'description' => $description, + ], + ]; + + $payload = json_encode($error, JSON_PRETTY_PRINT); + + $response = $this->responseFactory->createResponse($statusCode); + $response->getBody()->write($payload); + + return $response; + } +} diff --git a/src/Models/PFA/PfaUser.php b/src/Models/PFA/PfaUser.php new file mode 100644 index 0000000..6348ff0 --- /dev/null +++ b/src/Models/PFA/PfaUser.php @@ -0,0 +1,364 @@ + $value) { + if (strcasecmp($key, 'userName') === 0) { + $this->userName = $value; + continue; + } + if (strcasecmp($key, 'password') === 0) { + $this->password = $value; + continue; + } + if (strcasecmp($key, 'name') === 0) { + $this->name = $value; + continue; + } + if (strcasecmp($key, 'maildir') === 0) { + $this->maildir = $value; + continue; + } + if (strcasecmp($key, 'quota') === 0) { + $this->quota = $value; + continue; + } + if (strcasecmp($key, 'localpart') === 0) { + $this->localpart = $value; + continue; + } + if (strcasecmp($key, 'domain') === 0) { + $this->domain = $value; + continue; + } + if (strcasecmp($key, 'created') === 0) { + $this->created = $value; + continue; + } + if (strcasecmp($key, 'modified') === 0) { + $this->modified = $value; + continue; + } + if (strcasecmp($key, 'active') === 0) { + $this->active = $value; + continue; + } + if (strcasecmp($key, 'phone') === 0) { + $this->phone = $value; + continue; + } + if (strcasecmp($key, 'emailOther') === 0) { + $this->emailOther = $value; + continue; + } + if (strcasecmp($key, 'token') === 0) { + $this->token = $value; + continue; + } + if (strcasecmp($key, 'tokenValidity') === 0) { + $this->tokenValidity = $value; + continue; + } + if (strcasecmp($key, 'passwordExpiry') === 0) { + $this->passwordExpiry = $value; + continue; + } + $result = false; + } + } else { + $result = false; + } + return $result; + } + + /** + * @return string|null + */ + public function getUserName(): ?string + { + return $this->userName; + } + + /** + * @param string|null $userName + */ + public function setUserName(?string $userName): void + { + $this->userName = $userName; + } + + /** + * @return string|null + */ + public function getPassword(): ?string + { + return $this->password; + } + + /** + * @param string|null $password + */ + public function setPassword(?string $password): void + { + $this->password = $password; + } + + /** + * @return string|null + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * @param string|null $name + */ + public function setName(?string $name): void + { + $this->name = $name; + } + + /** + * @return string|null + */ + public function getMaildir(): ?string + { + return $this->maildir; + } + + /** + * @param string|null $maildir + */ + public function setMaildir(?string $maildir): void + { + $this->maildir = $maildir; + } + + /** + * @return int|null + */ + public function getQuota(): ?int + { + return $this->quota; + } + + /** + * @param int|null $quota + */ + public function setQuota(?int $quota): void + { + $this->quota = $quota; + } + + /** + * @return string|null + */ + public function getLocalPart(): ?string + { + return $this->localPart; + } + + /** + * @param string|null $localPart + */ + public function setLocalPart(?string $localPart): void + { + $this->localPart = $localPart; + } + + /** + * @return string|null + */ + public function getDomain(): ?string + { + return $this->domain; + } + + /** + * @param string|null $domain + */ + public function setDomain(?string $domain): void + { + $this->domain = $domain; + } + + /** + * @return string|null + */ + public function getCreated(): ?string + { + return $this->created; + } + + /** + * @param string|null $created + */ + public function setCreated(?string $created): void + { + $this->created = $created; + } + + /** + * @return string|null + */ + public function getModified(): ?string + { + return $this->modified; + } + + /** + * @param string|null $modified + */ + public function setModified(?string $modified): void + { + $this->modified = $modified; + } + + /** + * @return string|null + */ + public function getActive(): ?string + { + return $this->active; + } + + /** + * @param string|null $active + */ + public function setActive(?string $active): void + { + $this->active = $active; + } + + /** + * @return string|null + */ + public function getPhone(): ?string + { + return $this->phone; + } + + /** + * @param string|null $phone + */ + public function setPhone(?string $phone): void + { + $this->phone = $phone; + } + + /** + * @return string|null + */ + public function getEmailOther(): ?string + { + return $this->emailOther; + } + + /** + * @param string|null $emailOther + */ + public function setEmailOther(?string $emailOther): void + { + $this->emailOther = $emailOther; + } + + /** + * @return string|null + */ + public function getToken(): ?string + { + return $this->token; + } + + /** + * @param string|null $token + */ + public function setToken(?string $token): void + { + $this->token = $token; + } + + /** + * @return string|null + */ + public function getTokenValidity(): ?string + { + return $this->tokenValidity; + } + + /** + * @param string|null $tokenValidity + */ + public function setTokenValidity(?string $tokenValidity): void + { + $this->tokenValidity = $tokenValidity; + } + + /** + * @return string|null + */ + public function getPasswordExpiry(): ?string + { + return $this->passwordExpiry; + } + + /** + * @param string|null $passwordExpiry + */ + public function setPasswordExpiry(?string $passwordExpiry): void + { + $this->passwordExpiry = $passwordExpiry; + } +} diff --git a/src/Models/SCIM/Custom/Domains/Domain.php b/src/Models/SCIM/Custom/Domains/Domain.php new file mode 100644 index 0000000..7e92662 --- /dev/null +++ b/src/Models/SCIM/Custom/Domains/Domain.php @@ -0,0 +1,8 @@ +sizeQuota; + } + + /** + * @param int|null $sizeQuota + */ + public function setSizeQuota(?int $sizeQuota): void + { + $this->sizeQuota = $sizeQuota; + } + + public function fromSCIM($data) + { + parent::fromSCIM($data); + if (isset($data[Util::PROVISIONING_USER_SCHEMA])) { + $provisioningUserData = $data[Util::PROVISIONING_USER_SCHEMA]; + $this->setSizeQuota(isset($provisioningUserData['sizeQuota']) ? $provisioningUserData['sizeQuota'] : null); + } + } + + public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') + { + $data = parent::toSCIM($encode, $baseLocation); + $data['schemas'][] = Util::PROVISIONING_USER_SCHEMA; + $data[Util::PROVISIONING_USER_SCHEMA]['sizeQuota'] = $this->getSizeQuota(); + + if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) { + $data['meta']['updated'] = $this->getMeta()->getLastModified(); + } + + if ($encode) { + $data = json_encode($data); + } + + return $data; + } +} diff --git a/src/Models/SCIM/Standard/CommonEntity.php b/src/Models/SCIM/Standard/CommonEntity.php new file mode 100644 index 0000000..3a7dd80 --- /dev/null +++ b/src/Models/SCIM/Standard/CommonEntity.php @@ -0,0 +1,64 @@ + $schemas */ + private $schemas; + + /** @var string $id */ + private $id; + + /** @var string $externalId */ + private $externalId; + + /** @var \Opf\Models\SCIM\Meta $meta */ + private $meta; + + public function getSchemas() + { + return $this->schemas; + } + + public function setSchemas($schemas) + { + $this->schemas = $schemas; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getExternalId() + { + return $this->externalId; + } + + public function setExternalId($externalId) + { + $this->externalId = $externalId; + } + + public function getMeta() + { + return $this->meta; + } + + public function setMeta($meta) + { + $this->meta = $meta; + } +} diff --git a/src/Models/SCIM/Standard/CoreCollection.php b/src/Models/SCIM/Standard/CoreCollection.php new file mode 100644 index 0000000..3855eec --- /dev/null +++ b/src/Models/SCIM/Standard/CoreCollection.php @@ -0,0 +1,31 @@ +scimItems = $scimItems; + } + + public function toSCIM($encode = true) + { + $data = [ + 'totalResults' => count($this->scimItems), + 'startIndex' => 1, + 'schemas' => $this->schemas, + 'Resources' => $this->scimItems + ]; + + if ($encode) { + $data = json_encode($data); + } + + return $data; + } +} diff --git a/src/Models/SCIM/Standard/Groups/CoreGroup.php b/src/Models/SCIM/Standard/Groups/CoreGroup.php new file mode 100644 index 0000000..c5f40c4 --- /dev/null +++ b/src/Models/SCIM/Standard/Groups/CoreGroup.php @@ -0,0 +1,88 @@ +displayName; + } + + public function setDisplayName($displayName) + { + $this->displayName = $displayName; + } + + public function getMembers() + { + return $this->members; + } + + public function setMembers($members) + { + $this->members = $members; + } + + public function fromSCIM($data) + { + if (isset($data['id'])) { + $this->setId($data['id']); + } elseif ($this->getId() !== null) { + $this->setId($this->getId()); + } else { + $this->setId(Util::genUuid()); + } + + $this->setDisplayName(isset($data['displayName']) ? $data['displayName'] : null); + + $meta = new Meta(); + if (isset($data['meta']) && isset($data['meta']['created'])) { + $meta->setCreated(Util::string2dateTime($data['meta']['created'])); + } else { + $meta->setCreated(Util::dateTime2string(new \DateTime('NOW'))); + } + $this->setMeta($meta); + + $this->setMembers(isset($data['members']) ? $data['members'] : true); + + $this->setExternalId(isset($data['externalId']) ? $data['externalId'] : null); + } + + public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') + { + $data = [ + 'schemas' => [Util::GROUP_SCHEMA], + 'id' => $this->getId(), + 'externalId' => $this->getExternalId(), + 'meta' => [ + 'resourceType' => $this->getMeta()->getResourceType(), + 'created' => $this->getMeta()->getCreated(), + 'location' => $baseLocation . '/Groups/' . $this->getId(), + 'version' => $this->getMeta()->getVersion() + ], + 'displayName' => $this->getDisplayName(), + 'members' => $this->getMembers() + ]; + + if (null !== $this->getMeta()->getLastModified()) { + $data['meta']['updated'] = $this->getMeta()->getLastModified(); + } + + if ($encode) { + $data = json_encode($data); + } + + return $data; + } +} diff --git a/src/Models/SCIM/Standard/Meta.php b/src/Models/SCIM/Standard/Meta.php new file mode 100644 index 0000000..7323838 --- /dev/null +++ b/src/Models/SCIM/Standard/Meta.php @@ -0,0 +1,67 @@ +resourceType; + } + + public function setResourceType($resourceType) + { + $this->resourceType = $resourceType; + } + + public function getCreated() + { + return $this->created; + } + + public function setCreated($created) + { + $this->created = $created; + } + + public function getLastModified() + { + return $this->lastModified; + } + + public function setLastModified($lastModified) + { + $this->lastModified = $lastModified; + } + + public function getVersion() + { + return $this->version; + } + + public function setVersion($version) + { + $this->version = $version; + } +} diff --git a/src/Models/SCIM/Standard/MultiValuedAttribute.php b/src/Models/SCIM/Standard/MultiValuedAttribute.php new file mode 100644 index 0000000..32fdfe4 --- /dev/null +++ b/src/Models/SCIM/Standard/MultiValuedAttribute.php @@ -0,0 +1,84 @@ +type; + } + + public function setType($type) + { + $this->type = $type; + } + + public function getPrimary() + { + return $this->primary; + } + + public function setPrimary($primary) + { + $this->primary = $primary; + } + + public function getDisplay() + { + return $this->display; + } + + public function setDisplay($display) + { + $this->display = $display; + } + + public function getValue() + { + return $this->value; + } + + public function setValue($value) + { + $this->value = $value; + } + + public function getRef() + { + return $this->ref; + } + + public function setRef($ref) + { + $this->ref = $ref; + } + + public function jsonSerialize() + { + return (object)[ + "type" => $this->getType(), + "primary" => $this->getPrimary(), + "display" => $this->getDisplay(), + "value" => $this->getValue(), + "\$ref" => $this->getRef() + ]; + } +} diff --git a/src/Models/SCIM/Standard/Service/Attribute.php b/src/Models/SCIM/Standard/Service/Attribute.php new file mode 100644 index 0000000..578f3e8 --- /dev/null +++ b/src/Models/SCIM/Standard/Service/Attribute.php @@ -0,0 +1,187 @@ + $subAttributes */ + private $subAttributes; + + /** @var boolean $multiValued */ + private $multiValued; + + /** @var string $description */ + private $description; + + /** @var boolean $required */ + private $required; + + /** @var array $canonicalValues */ + private $canonicalValues; + + /** @var boolean $caseExact */ + private $caseExact; + + /** @var string $mutability */ + private $mutability; + + /** @var string $returned */ + private $returned; + + /** @var string $uniqueness */ + private $uniqueness; + + /** @var array $referenceTypes */ + private $referenceTypes; + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getType() + { + return $this->type; + } + + public function setType($type) + { + $this->type = $type; + } + + public function getSubAttributes() + { + return $this->subAttributes; + } + + public function setSubAttributes($subAttributes) + { + $this->subAttributes = $subAttributes; + } + + public function getMultiValued() + { + return $this->multiValued; + } + + public function setMultiValued($multiValued) + { + $this->multiValued = $multiValued; + } + + public function getDescription() + { + return $this->description; + } + + public function setDescription($description) + { + $this->description = $description; + } + + public function getRequired() + { + return $this->required; + } + + public function setRequired($required) + { + $this->required = $required; + } + + public function getCanonicalValues() + { + return $this->canonicalValues; + } + + public function setCanonicalValues($canonicalValues) + { + $this->canonicalValues = $canonicalValues; + } + + public function getCaseExact() + { + return $this->caseExact; + } + + public function setCaseExact($caseExact) + { + $this->caseExact = $caseExact; + } + + public function getMutability() + { + return $this->mutability; + } + + public function setMutability($mutability) + { + $this->mutability = $mutability; + } + + public function getReturned() + { + return $this->returned; + } + + public function setReturned($returned) + { + $this->returned = $returned; + } + + public function getUniqueness() + { + return $this->uniqueness; + } + + public function setUniqueness($uniqueness) + { + $this->uniqueness = $uniqueness; + } + + public function getReferenceTypes() + { + return $this->referenceTypes; + } + + public function setReferenceTypes($referenceTypes) + { + $this->referenceTypes = $referenceTypes; + } + + public function jsonSerialize() + { + return (object)[ + "name" => $this->getName(), + "type" => $this->getType(), + "subAttributes" => $this->getSubAttributes(), + "multiValued" => $this->getMultiValued(), + "description" => $this->getDescription(), + "required" => $this->getRequired(), + "canonicalValues" => $this->getCanonicalValues(), + "caseExact" => $this->getCaseExact(), + "mutability" => $this->getMutability(), + "returned" => $this->getReturned(), + "uniqueness" => $this->getUniqueness(), + "referenceTypes" => $this->getReferenceTypes() + ]; + } +} diff --git a/src/Models/SCIM/Standard/Service/AuthenticationScheme.php b/src/Models/SCIM/Standard/Service/AuthenticationScheme.php new file mode 100644 index 0000000..fbddd01 --- /dev/null +++ b/src/Models/SCIM/Standard/Service/AuthenticationScheme.php @@ -0,0 +1,71 @@ +type; + } + + public function setType($type) + { + $this->type = $type; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getDescription() + { + return $this->description; + } + + public function setDescription($description) + { + $this->description = $description; + } + + public function getSpecUri() + { + return $this->specUri; + } + + public function setSpecUri($specUri) + { + $this->specUri = $specUri; + } + + public function getDocumentationUri() + { + return $this->documentationUri; + } + + public function setDocumentationUri($documentationUri) + { + $this->documentationUri = $documentationUri; + } +} diff --git a/src/Models/SCIM/Standard/Service/Bulk.php b/src/Models/SCIM/Standard/Service/Bulk.php new file mode 100644 index 0000000..f95ce2a --- /dev/null +++ b/src/Models/SCIM/Standard/Service/Bulk.php @@ -0,0 +1,22 @@ +maxOperations; + } + + public function setMaxOperations($maxOperations) + { + $this->maxOperations = $maxOperations; + } +} diff --git a/src/Models/SCIM/Standard/Service/CoreResourceType.php b/src/Models/SCIM/Standard/Service/CoreResourceType.php new file mode 100644 index 0000000..0a318e6 --- /dev/null +++ b/src/Models/SCIM/Standard/Service/CoreResourceType.php @@ -0,0 +1,124 @@ + */ + private $schemas = [Util::RESOURCE_TYPE_SCHEMA]; + + /** @var string */ + private $id; + + /** @var string */ + private $name; + + /** @var string */ + private $description; + + /** @var string */ + private $endpoint; + + /** @var string */ + private $schema; + + /** @var array */ + private $schemaExtensions; + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getDescription() + { + return $this->description; + } + + public function setDescription($description) + { + $this->description = $description; + } + + public function getEndpoint() + { + return $this->endpoint; + } + + public function setEndpoint($endpoint) + { + $this->endpoint = $endpoint; + } + + public function getSchema() + { + return $this->schema; + } + + public function setSchema($schema) + { + $this->schema = $schema; + } + + public function getSchemaExtensions() + { + return $this->schemaExtensions; + } + + public function setSchemaExtensions($schemaExtensions) + { + $this->schemaExtensions = $schemaExtensions; + } + + public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') + { + if (isset($this->schemaExtensions) && !empty($this->schemaExtensions)) { + $transformedSchemaExtensions = []; + + foreach ($this->schemaExtensions as $schemaExtension) { + $transformedSchemaExtensions[] = $schemaExtension->toSCIM(); + } + + $this->setSchemaExtensions($transformedSchemaExtensions); + } + + $data = [ + 'schemas' => $this->schemas, + 'id' => $this->id, + 'name' => $this->name, + 'description' => $this->description, + 'endpoint' => $this->endpoint, + 'schema' => $this->schema, + 'schemaExtensions' => $this->schemaExtensions, + 'meta' => [ + 'location' => $baseLocation . '/' . $this->id, + 'resourceType' => 'ResourceType' + ] + ]; + + if ($encode) { + $data = json_encode($data, JSON_UNESCAPED_SLASHES); + } + + return $data; + } +} diff --git a/src/Models/SCIM/Standard/Service/CoreSchema.php b/src/Models/SCIM/Standard/Service/CoreSchema.php new file mode 100644 index 0000000..e876781 --- /dev/null +++ b/src/Models/SCIM/Standard/Service/CoreSchema.php @@ -0,0 +1,79 @@ + $attributes */ + private $attributes; + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getDescription() + { + return $this->description; + } + + public function setDescription($description) + { + $this->description = $description; + } + + public function getAttributes() + { + return $this->attributes; + } + + public function setAttributes($attributes) + { + $this->attributes = $attributes; + } + + public function toSCIM($encode = true) + { + $data = [ + "id" => $this->id, + "name" => $this->name, + "description" => $this->description, + "attributes" => $this->attributes + ]; + + if ($encode) { + $data = json_encode($data, JSON_UNESCAPED_SLASHES); + } + + return $data; + } +} diff --git a/src/Models/SCIM/Standard/Service/CoreSchemaExtension.php b/src/Models/SCIM/Standard/Service/CoreSchemaExtension.php new file mode 100644 index 0000000..ecd3e79 --- /dev/null +++ b/src/Models/SCIM/Standard/Service/CoreSchemaExtension.php @@ -0,0 +1,46 @@ +schema; + } + + public function setSchema($schema) + { + $this->schema = $schema; + } + + public function getRequired() + { + return $this->required; + } + + public function setRequired($required) + { + $this->required = $required; + } + + public function toSCIM($encode = false) + { + $data = [ + 'schema' => $this->schema, + 'required' => $this->required + ]; + + if ($encode) { + $data = json_encode($data, JSON_UNESCAPED_SLASHES); + } + + return $data; + } +} diff --git a/src/Models/SCIM/Standard/Service/CoreServiceProviderConfiguration.php b/src/Models/SCIM/Standard/Service/CoreServiceProviderConfiguration.php new file mode 100644 index 0000000..9f82b5b --- /dev/null +++ b/src/Models/SCIM/Standard/Service/CoreServiceProviderConfiguration.php @@ -0,0 +1,146 @@ + $schemas */ + private $schemas = [Util::SERVICE_PROVIDER_CONFIGURATION_SCHEMA]; + + /** @var string $documentationUri */ + private $documentationUri; + + /** @var \Opf\Models\SCIM\SupportableConfigProperty $patch */ + private $patch; + + /** @var \Opf\Models\SCIM\Bulk $bulk */ + private $bulk; + + /** @var \Opf\Models\SCIM\Filter $filter */ + private $filter; + + /** @var \Opf\Models\SCIM\SupportableConfigProperty $changePassword */ + private $changePassword; + + /** @var \Opf\Models\SCIM\SupportableConfigProperty $sort */ + private $sort; + + /** @var \Opf\Models\SCIM\SupportableConfigProperty $etag */ + private $etag; + + /** @var array<\Opf\Models\SCIM\AuthenticationScheme> $authenticationSchemes */ + private $authenticationSchemes; + + public function getSchemas() + { + return $this->schemas; + } + + public function setSchemas($schemas) + { + $this->schemas = $schemas; + } + + public function getDocumentationUri() + { + return $this->documentationUri; + } + + public function setDocumentationUri($documentationUri) + { + $this->documentationUri = $documentationUri; + } + + public function getPatch() + { + return $this->patch; + } + + public function setPatch($patch) + { + $this->patch = $patch; + } + + public function getBulk() + { + return $this->bulk; + } + + public function setBulk($bulk) + { + $this->bulk = $bulk; + } + + public function getFilter() + { + return $this->filter; + } + + public function setFilter($filter) + { + $this->filter = $filter; + } + + public function getChangePassword() + { + return $this->changePassword; + } + + public function setChangePassword($changePassword) + { + $this->changePassword = $changePassword; + } + + public function getSort() + { + return $this->sort; + } + + public function setSort($sort) + { + $this->sort = $sort; + } + + public function getEtag() + { + return $this->etag; + } + + public function setEtag($etag) + { + $this->etag = $etag; + } + + public function getAuthenticationSchemes() + { + return $this->authenticationSchemes; + } + + public function setAuthenticationSchemes($authenticationSchemes) + { + $this->authenticationSchemes = $authenticationSchemes; + } + + public function toSCIM($encode = true) + { + $data = [ + "schemas" => $this->schemas, + "documentationUri" => $this->documentationUri, + "patch" => $this->patch, + "bulk" => $this->bulk, + "filter" => $this->filter, + "changePassword" => $this->changePassword, + "sort" => $this->sort, + "etag" => $this->etag, + "authenticationSchemes" => $this->authenticationSchemes + ]; + + if ($encode) { + $data = json_encode($data, JSON_UNESCAPED_SLASHES); + } + + return $data; + } +} diff --git a/src/Models/SCIM/Standard/Service/Filter.php b/src/Models/SCIM/Standard/Service/Filter.php new file mode 100644 index 0000000..76c6102 --- /dev/null +++ b/src/Models/SCIM/Standard/Service/Filter.php @@ -0,0 +1,19 @@ +maxResults; + } + + public function setMaxResults($maxResults) + { + $this->maxResults = $maxResults; + } +} diff --git a/src/Models/SCIM/Standard/Service/SupportableConfigProperty.php b/src/Models/SCIM/Standard/Service/SupportableConfigProperty.php new file mode 100644 index 0000000..ebffb70 --- /dev/null +++ b/src/Models/SCIM/Standard/Service/SupportableConfigProperty.php @@ -0,0 +1,19 @@ +supported; + } + + public function setSupported($supported) + { + $this->supported = $supported; + } +} diff --git a/src/Models/SCIM/Standard/Users/Address.php b/src/Models/SCIM/Standard/Users/Address.php new file mode 100644 index 0000000..6d7273b --- /dev/null +++ b/src/Models/SCIM/Standard/Users/Address.php @@ -0,0 +1,103 @@ +formatted; + } + + public function setFormatted($formatted) + { + $this->formatted = $formatted; + } + + public function getStreetAddress() + { + return $this->streetAddress; + } + + public function setStreetAddress($streetAddress) + { + $this->streetAddress = $streetAddress; + } + + public function getLocality() + { + return $this->locality; + } + + public function setLocality($locality) + { + $this->locality = $locality; + } + + public function getRegion() + { + return $this->region; + } + + public function setRegion($region) + { + $this->region = $region; + } + + public function getPostalCode() + { + return $this->postalCode; + } + + public function setPostalCode($postalCode) + { + $this->postalCode = $postalCode; + } + + public function getCountry() + { + return $this->country; + } + + public function setCountry($country) + { + $this->country = $country; + } + + public function jsonSerialize(): mixed + { + return (object)[ + "type" => $this->getType(), + "primary" => $this->getPrimary(), + "display" => $this->getDisplay(), + "value" => $this->getValue(), + "\$ref" => $this->getRef(), + "formatted" => $this->getFormatted(), + "streetAddress" => $this->getStreetAddress(), + "locality" => $this->getLocality(), + "region" => $this->getRegion(), + "postalCode" => $this->getPostalCode(), + "country" => $this->getCountry() + ]; + } +} diff --git a/src/Models/SCIM/Standard/Users/CoreUser.php b/src/Models/SCIM/Standard/Users/CoreUser.php new file mode 100644 index 0000000..a5de57d --- /dev/null +++ b/src/Models/SCIM/Standard/Users/CoreUser.php @@ -0,0 +1,403 @@ +userName; + } + + public function setUserName($userName) + { + $this->userName = $userName; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getDisplayName() + { + return $this->displayName; + } + + public function setDisplayName($displayName) + { + $this->displayName = $displayName; + } + + public function getNickName() + { + return $this->nickName; + } + + public function setNickName($nickName) + { + $this->nickName = $nickName; + } + + public function getProfileUrl() + { + return $this->profileUrl; + } + + public function setProfileUrl($profileUrl) + { + $this->profileUrl = $profileUrl; + } + + public function getTitle() + { + return $this->title; + } + + public function setTitle($title) + { + $this->title = $title; + } + + public function getUserType() + { + return $this->userType; + } + + public function setUserType($userType) + { + $this->userType = $userType; + } + + public function getPreferredLanguage() + { + return $this->preferredLanguage; + } + + public function setPreferredLanguage($preferredLanguage) + { + $this->preferredLanguage = $preferredLanguage; + } + + public function getLocale() + { + return $this->locale; + } + + public function setLocale($locale) + { + $this->locale = $locale; + } + + public function getTimezone() + { + return $this->timezone; + } + + public function setTimezone($timezone) + { + $this->timezone = $timezone; + } + + public function getActive() + { + return $this->active; + } + + public function setActive($active) + { + $this->active = $active; + } + + public function getPassword() + { + return $this->password; + } + + public function setPassword($password) + { + $this->password = $password; + } + + public function getEmails() + { + return $this->emails; + } + + public function setEmails($emails) + { + $this->emails = $emails; + } + + public function getPhoneNumbers() + { + return $this->phoneNumbers; + } + + public function setPhoneNumbers($phoneNumbers) + { + $this->phoneNumbers = $phoneNumbers; + } + + public function getIms() + { + return $this->ims; + } + + public function setIms($ims) + { + $this->ims = $ims; + } + + public function getPhotos() + { + return $this->photos; + } + + public function setPhotos($photos) + { + $this->photos = $photos; + } + + public function getAddresses() + { + return $this->addresses; + } + + public function setAddresses($addresses) + { + $this->addresses = $addresses; + } + + public function getGroups() + { + return $this->groups; + } + + public function setGroups($groups) + { + $this->groups = $groups; + } + + public function getEntitlements() + { + return $this->entitlements; + } + + public function setEntitlements($entitlements) + { + $this->entitlements = $entitlements; + } + + public function getRoles() + { + return $this->roles; + } + + public function setRoles($roles) + { + $this->roles = $roles; + } + + public function getX509Certificates() + { + return $this->x509Certificates; + } + + public function setX509Certificates($x509Certificates) + { + $this->x509Certificates = $x509Certificates; + } + + // TODO: Finish the implementation of this method for all properties + // TODO: Maybe rename this method to a more meaningful one, + // since it's currently just translating a SCIM JSON object to a PHP SCIM object + public function fromSCIM($data) + { + if (isset($data['id'])) { + $this->setId($data['id']); + } elseif ($this->getId() !== null) { + $this->setId($this->getId()); + } else { + $this->setId(Util::genUuid()); + } + + $this->setUserName(isset($data['userName']) ? $data['userName'] : null); + + $name = new Name(); + $name->setFamilyName($data['name']['familyName']); + $name->setFormatted($data['name']['formatted']); + $name->setGivenName($data['name']['givenName']); + $name->setHonorificPrefix($data['name']['honorificPrefix']); + $name->setHonorificSuffix($data['name']['honorificSuffix']); + $this->setName($name); + + $meta = new Meta(); + if (isset($data['meta']) && isset($data['meta']['created'])) { + $meta->setCreated(Util::string2dateTime($data['meta']['created'])); + } else { + $meta->setCreated(Util::dateTime2string(new \DateTime('NOW'))); + } + $this->setMeta($meta); + + $this->setActive(isset($data['active']) ? $data['active'] : true); + + $emails = []; + if (isset($data['emails']) && !empty($data['emails'])) { + foreach ($data['emails'] as $email) { + $scimEmail = new MultiValuedAttribute(); + $scimEmail->setType(isset($email['type']) && !empty($email['type']) ? $email['type'] : null); + $scimEmail->setPrimary( + isset($email['primary']) && !empty($email['primary']) ? $email['primary'] : null + ); + $scimEmail->setDisplay( + isset($email['display']) && !empty($email['display']) ? $email['display'] : null + ); + $scimEmail->setValue(isset($email['value']) && !empty($email['value']) ? $email['value'] : null); + + $emails[] = $scimEmail; + } + } + $this->setEmails($emails); + + $this->setDisplayName(isset($data['displayName']) ? $data['displayName'] : null); + $this->setPassword(isset($data['password']) ? strval($data['password']) : null); + + $this->setExternalId(isset($data['externalId']) ? $data['externalId'] : null); + $this->setProfileUrl(isset($data['profileUrl']) ? $data['profileUrl'] : null); + } + + // TODO: Maybe rename this method to a more meaningful one, + // since it's currently just translating a SCIM PHP object to a JSON SCIM object + public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') + { + $data = [ + 'schemas' => [Util::USER_SCHEMA], + 'id' => $this->getId(), + 'externalId' => $this->getExternalId(), + 'meta' => null !== $this->getMeta() ? [ + 'resourceType' => null !== $this->getMeta()->getResourceType() + ? $this->getMeta()->getResourceType() : null, + 'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null, + 'location' => $baseLocation . '/Users/' . $this->getId(), + 'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null + ] : null, + 'userName' => $this->getUserName(), + 'name' => null !== $this->getName() ? [ + 'formatted' => null !== $this->getName()->getFormatted() ? $this->getName()->getFormatted() : null, + 'familyName' => null !== $this->getName()->getFamilyName() ? $this->getName()->getFamilyName() : null, + 'givenName' => null !== $this->getName()->getGivenName() ? $this->getName()->getGivenName() : null, + 'middleName' => null !== $this->getName()->getMiddleName() ? $this->getName()->getMiddleName() : null, + 'honorificPrefix' => null !== $this->getName()->getHonorificPrefix() + ? $this->getName()->getHonorificPrefix() + : null, + 'honorificSuffix' => null !== $this->getName()->getHonorificSuffix() + ? $this->getName()->getHonorificSuffix() + : null + ] : null, + 'displayName' => $this->getDisplayName(), + 'nickName' => $this->getNickName(), + 'profileUrl' => $this->getProfileUrl(), + 'title' => $this->getTitle(), + 'userType' => $this->getUserType(), + 'preferredLanguage' => $this->getPreferredLanguage(), + 'locale' => $this->getLocale(), + 'timezone' => $this->getTimezone(), + 'active' => $this->getActive(), + 'password' => $this->getPassword(), + 'emails' => $this->getEmails(), + 'phoneNumbers' => $this->getPhoneNumbers(), + 'ims' => $this->getIms(), + 'photos' => $this->getPhotos(), + 'addresses' => $this->getAddresses(), + 'groups' => $this->getGroups(), + 'entitlements' => $this->getEntitlements(), + 'roles' => $this->getRoles(), + 'x509Certificates' => $this->getX509Certificates() + ]; + + if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) { + $data['meta']['updated'] = $this->getMeta()->getLastModified(); + } + + if ($encode) { + $data = json_encode($data); + } + + return $data; + } +} diff --git a/src/Models/SCIM/Standard/Users/EnterpriseUser.php b/src/Models/SCIM/Standard/Users/EnterpriseUser.php new file mode 100644 index 0000000..cc0e853 --- /dev/null +++ b/src/Models/SCIM/Standard/Users/EnterpriseUser.php @@ -0,0 +1,156 @@ +employeeNumber; + } + + public function setEmployeeNumber($employeeNumber) + { + $this->employeeNumber = $employeeNumber; + } + + public function getCostCenter() + { + return $this->costCenter; + } + + public function setCostCenter($costCenter) + { + $this->costCenter = $costCenter; + } + + public function getOrganization() + { + return $this->organization; + } + + public function setOrganization($organization) + { + $this->organization = $organization; + } + + public function getDivision() + { + return $this->division; + } + + public function setDivision($division) + { + $this->division = $division; + } + + public function getDepartment() + { + return $this->department; + } + + public function setDepartment($department) + { + $this->department = $department; + } + + public function getManager() + { + return $this->manager; + } + + public function setManager($manager) + { + $this->manager = $manager; + } + + public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') + { + $data = [ + 'schemas' => [Util::ENTERPRISE_USER_SCHEMA], + 'id' => $this->getId(), + 'externalId' => $this->getExternalId(), + 'meta' => null !== $this->getMeta() ? [ + 'resourceType' => null !== $this->getMeta()->getResourceType() + ? $this->getMeta()->getResourceType() : null, + 'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null, + 'location' => $baseLocation . '/Users/' . $this->getId(), + 'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null + ] : null, + 'userName' => $this->getUserName(), + 'name' => null !== $this->getName() ? [ + 'formatted' => null !== $this->getName()->getFormatted() ? $this->getName()->getFormatted() : null, + 'familyName' => null !== $this->getName()->getFamilyName() ? $this->getName()->getFamilyName() : null, + 'givenName' => null !== $this->getName()->getGivenName() ? $this->getName()->getGivenName() : null, + 'middleName' => null !== $this->getName()->getMiddleName() ? $this->getName()->getMiddleName() : null, + 'honorificPrefix' => null !== $this->getName()->getHonorificPrefix() + ? $this->getName()->getHonorificPrefix() + : null, + 'honorificSuffix' => null !== $this->getName()->getHonorificSuffix() + ? $this->getName()->getHonorificSuffix() + : null + ] : null, + 'displayName' => $this->getDisplayName(), + 'nickName' => $this->getNickName(), + 'profileUrl' => $this->getProfileUrl(), + 'title' => $this->getTitle(), + 'userType' => $this->getUserType(), + 'preferredLanguage' => $this->getPreferredLanguage(), + 'locale' => $this->getLocale(), + 'timezone' => $this->getTimezone(), + 'active' => $this->getActive(), + 'password' => $this->getPassword(), + 'emails' => $this->getEmails(), + 'phoneNumbers' => $this->getPhoneNumbers(), + 'ims' => $this->getIms(), + 'photos' => $this->getPhotos(), + 'addresses' => $this->getAddresses(), + 'groups' => $this->getGroups(), + 'entitlements' => $this->getEntitlements(), + 'roles' => $this->getRoles(), + 'x509Certificates' => $this->getX509Certificates(), + 'employeeNumber' => $this->getEmployeeNumber(), + 'costCenter' => $this->getCostCenter(), + 'organization' => $this->getOrganization(), + 'division' => $this->getDivision(), + 'department' => $this->getDepartment(), + 'manager' => null !== $this->getManager() ? [ + 'value' => null !== $this->getManager()->getValue() ? $this->getManager()->getValue() : null, + '\$ref' => null !== $this->getManager()->getRef() ? $this->getManager()->getRef() : null, + 'displayName' => null !== $this->getManager()->getDisplayName() + ? $this->getManager()->getDisplayName() + : null + ] : null + ]; + + if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) { + $data['meta']['updated'] = $this->getMeta()->getLastModified(); + } + + if ($encode) { + $data = json_encode($data); + } + + return $data; + } +} diff --git a/src/Models/SCIM/Standard/Users/Manager.php b/src/Models/SCIM/Standard/Users/Manager.php new file mode 100644 index 0000000..5c14190 --- /dev/null +++ b/src/Models/SCIM/Standard/Users/Manager.php @@ -0,0 +1,45 @@ +value; + } + + public function setValue($value) + { + $this->value = $value; + } + + public function getRef() + { + return $this->ref; + } + + public function setRef($ref) + { + $this->ref = $ref; + } + + public function getDisplayName() + { + return $this->displayName; + } + + public function setDisplayName($displayName) + { + $this->displayName = $displayName; + } +} diff --git a/src/Models/SCIM/Standard/Users/Name.php b/src/Models/SCIM/Standard/Users/Name.php new file mode 100644 index 0000000..fa60ad1 --- /dev/null +++ b/src/Models/SCIM/Standard/Users/Name.php @@ -0,0 +1,84 @@ +formatted; + } + + public function setFormatted($formatted) + { + $this->formatted = $formatted; + } + + public function getFamilyName() + { + return $this->familyName; + } + + public function setFamilyName($familyName) + { + $this->familyName = $familyName; + } + + public function getGivenName() + { + return $this->givenName; + } + + public function setGivenName($givenName) + { + $this->givenName = $givenName; + } + + public function getMiddleName() + { + return $this->middleName; + } + + public function setMiddleName($middleName) + { + $this->middleName = $middleName; + } + + public function getHonorificPrefix() + { + return $this->honorificPrefix; + } + + public function setHonorificPrefix($honorificPrefix) + { + $this->honorificPrefix = $honorificPrefix; + } + + public function getHonorificSuffix() + { + return $this->honorificSuffix; + } + + public function setHonorificSuffix($honorificSuffix) + { + $this->honorificSuffix = $honorificSuffix; + } +} diff --git a/src/Repositories/Groups/MockGroupsRepository.php b/src/Repositories/Groups/MockGroupsRepository.php new file mode 100644 index 0000000..60ecd7c --- /dev/null +++ b/src/Repositories/Groups/MockGroupsRepository.php @@ -0,0 +1,137 @@ +dataAccess = $this->container->get('GroupsDataAccess'); + $this->adapter = $this->container->get('GroupsAdapter'); + } + + public function getAll(): array + { + // Read all mock groups from the database + $mockGroups = $this->dataAccess::all(); + $scimGroups = []; + + // Transform each mock group to a SCIM group via the injected adapter + foreach ($mockGroups as $mockGroup) { + $this->adapter->setGroup($mockGroup); + + $scimGroup = new CoreGroup(); + $scimGroup->setId($this->adapter->getId()); + $scimGroup->setDisplayName($this->adapter->getDisplayName()); + $scimGroup->setMembers($this->adapter->getMembers()); + + $scimGroupMeta = new Meta(); + $scimGroupMeta->setCreated($this->adapter->getCreatedAt()); + + $scimGroup->setMeta($scimGroupMeta); + + $scimGroups[] = $scimGroup; + } + + return $scimGroups; + } + + public function getOneById(string $id): ?CoreGroup + { + if (isset($id) && !empty($id)) { + $mockGroup = $this->dataAccess::find($id); + + if (isset($mockGroup) && !empty($mockGroup)) { + $this->adapter->setGroup($mockGroup); + + $scimGroup = new CoreGroup(); + $scimGroup->setId($this->adapter->getId()); + $scimGroup->setDisplayName($this->adapter->getDisplayName()); + $scimGroup->setMembers($this->adapter->getMembers()); + + $scimGroupMeta = new Meta(); + $scimGroupMeta->setCreated($this->adapter->getCreatedAt()); + + $scimGroup->setMeta($scimGroupMeta); + + return $scimGroup; + } else { + return null; + } + } else { + return null; + } + } + + public function create($object): ?CoreGroup + { + if (isset($object) && !empty($object)) { + $scimGroup = new CoreGroup(); + $scimGroup->fromSCIM($object); + + $this->adapter->setGroup($this->dataAccess); + + $this->adapter->setId($scimGroup->getId()); + $this->adapter->setDisplayName($scimGroup->getDisplayName()); + $this->adapter->setMembers($scimGroup->getMembers()); + $this->adapter->setCreatedAt($scimGroup->getMeta()->getCreated()); + + $this->dataAccess = $this->adapter->getGroup(); + if ($this->dataAccess->save()) { + return $scimGroup; + } else { + return null; + } + } + } + + public function update(string $id, $object): ?CoreGroup + { + if (isset($id) && !empty($id)) { + $mockGroup = $this->dataAccess::find($id); + if (isset($mockGroup) && !empty($mockGroup)) { + $scimGroup = new CoreGroup(); + $scimGroup->fromSCIM($object); + + $this->adapter->setGroup($mockGroup); + + $scimGroup->setId($this->adapter->getId()); + + $this->adapter->setDisplayName($scimGroup->getDisplayName()); + $this->adapter->setMembers($scimGroup->getMembers()); + $this->adapter->setCreatedAt($scimGroup->getMeta()->getCreated()); + + $mockGroup = $this->adapter->getGroup(); + if ($mockGroup->save()) { + return $scimGroup; + } else { + return null; + } + } else { + return null; + } + } else { + return null; + } + } + + public function delete(string $id): bool + { + if (isset($id) && !empty($id)) { + $mockGroup = $this->dataAccess::find($id); + if (!isset($mockGroup) || empty($mockGroup)) { + return false; + } + $mockGroup->delete(); + return true; + } else { + return false; + } + } +} diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php new file mode 100644 index 0000000..7a11484 --- /dev/null +++ b/src/Repositories/Repository.php @@ -0,0 +1,23 @@ +container = $container; + } + + abstract public function getAll(): array; + abstract public function getOneById(string $id): ?object; + abstract public function create($object): ?object; + abstract public function update(string $id, $object): ?object; + abstract public function delete(string $id): bool; +} diff --git a/src/Repositories/Users/MockUsersRepository.php b/src/Repositories/Users/MockUsersRepository.php new file mode 100644 index 0000000..2e4bd5e --- /dev/null +++ b/src/Repositories/Users/MockUsersRepository.php @@ -0,0 +1,191 @@ +dataAccess = $this->container->get('UsersDataAccess'); + $this->adapter = $this->container->get('UsersAdapter'); + } + public function getAll(): array + { + // Read all mock users from the database + $mockUsers = $this->dataAccess::all(); + $scimUsers = []; + + // Transform each mock user to a SCIM user via the injected adapter + foreach ($mockUsers as $mockUser) { + // TODO: Possibly refactor the transformation logic between SCIM users and other users + // in a separate method or class, since it seems to be rather repetitive + $this->adapter->setUser($mockUser); + + $scimUser = new CoreUser(); + $scimUser->setId($this->adapter->getId()); + $scimUser->setUserName($this->adapter->getUserName()); + + $scimUserMeta = new Meta(); + $scimUserMeta->setCreated($this->adapter->getCreatedAt()); + + $scimUser->setMeta($scimUserMeta); + $scimUser->setActive($this->adapter->getActive()); + $scimUser->setExternalId($this->adapter->getExternalId()); + $scimUser->setProfileUrl($this->adapter->getProfileUrl()); + + $scimUsers[] = $scimUser; + } + + return $scimUsers; + } + + public function getOneByUserName(string $userName): ?CoreUser + { + if (isset($userName) && !empty($userName)) { + // Try to find the first user from the database with the supplied username + $mockUser = $this->dataAccess::where('userName', $userName)->first(); + + // If such a user exists, map it to a SCIM user and return the SCIM user + if (isset($mockUser) && !empty($mockUser)) { + $this->adapter->setUser($mockUser); + + $scimUser = new CoreUser(); + $scimUser->setId($this->adapter->getId()); + $scimUser->setUserName($this->adapter->getUserName()); + + $scimUserMeta = new Meta(); + $scimUserMeta->setCreated($this->adapter->getCreatedAt()); + + $scimUser->setMeta($scimUserMeta); + $scimUser->setActive($this->adapter->getActive()); + $scimUser->setExternalId($this->adapter->getExternalId()); + $scimUser->setProfileUrl($this->adapter->getProfileUrl()); + + return $scimUser; + } else { + return null; + } + } + } + + public function getOneById(string $id): ?CoreUser + { + if (isset($id) && !empty($id)) { + // Try to find a user from the database with the supplied ID + $mockUser = $this->dataAccess::find($id); + + // If there's such a user, transform it to a SCIM user and return the SCIM user + if (isset($mockUser) && !empty($mockUser)) { + $this->adapter->setUser($mockUser); + + $scimUser = new CoreUser(); + $scimUser->setId($this->adapter->getId()); + $scimUser->setUserName($this->adapter->getUserName()); + + $scimUserMeta = new Meta(); + $scimUserMeta->setCreated($this->adapter->getCreatedAt()); + + $scimUser->setMeta($scimUserMeta); + $scimUser->setActive($this->adapter->getActive()); + $scimUser->setExternalId($this->adapter->getExternalId()); + $scimUser->setProfileUrl($this->adapter->getProfileUrl()); + + return $scimUser; + } else { + return null; + } + } else { + return null; + } + } + + public function create($object): ?CoreUser + { + if (isset($object) && !empty($object)) { + // Transform the incoming JSON user object to a SCIM object + // Then transform the SCIM object to a mock user that can be stored in the database + $scimUser = new CoreUser(); + $scimUser->fromSCIM($object); + + // $this->dataAccess represents an instance of the MockUser ORM model + // that we use for user storage to and retrieval from SQLite + $this->adapter->setUser($this->dataAccess); + + $this->adapter->setId($scimUser->getId()); + $this->adapter->setUserName($scimUser->getUserName()); + $this->adapter->setCreatedAt($scimUser->getMeta()->getCreated()); + $this->adapter->setActive($scimUser->getActive()); + $this->adapter->setExternalId($scimUser->getExternalId()); + $this->adapter->setProfileUrl($scimUser->getProfileUrl()); + + // Obtain the transformed mock user from the adapter and try to save it to the database + $this->dataAccess = $this->adapter->getUser(); + if ($this->dataAccess->save()) { + return $scimUser; + } else { + return null; + } + } + } + + public function update(string $id, $object): ?CoreUser + { + if (isset($id) && !empty($id)) { + // Try to find the user with the supplied ID + $mockUser = $this->dataAccess::find($id); + if (isset($mockUser) && !empty($mockUser)) { + // Transform the received JSON user object to a SCIM object + $scimUser = new CoreUser(); + $scimUser->fromSCIM($object); + + // Set the adapter's internal user object to the found user from the database + $this->adapter->setUser($mockUser); + + // Set the SCIM user's ID to be the same as the ID of the found user from the database + // Otherwise, we might lose the ID if a new one is supplied in the request + $scimUser->setId($this->adapter->getId()); + + // Transform the SCIM object to a mock user via the adapter and replace + // any properties of the mock user with the new properties incoming via the SCIM object + $this->adapter->setUserName($scimUser->getUserName()); + $this->adapter->setCreatedAt($scimUser->getMeta()->getCreated()); + $this->adapter->setActive($scimUser->getActive()); + $this->adapter->setExternalId($scimUser->getExternalId()); + $this->adapter->setProfileUrl($scimUser->getProfileUrl()); + + // Obtain the updated mock user via the adapter and try to save it to the database + $mockUser = $this->adapter->getUser(); + if ($mockUser->save()) { + return $scimUser; + } else { + return null; + } + } else { + return null; + } + } else { + return null; + } + } + + public function delete(string $id): bool + { + if (isset($id) && !empty($id)) { + // Try to find the user to be deleted + $mockUser = $this->dataAccess::find($id); + if (!isset($mockUser) || empty($mockUser)) { + return false; + } + $mockUser->delete(); + return true; + } else { + return false; + } + } +} diff --git a/src/Repositories/Users/PfaUsersRepository.php b/src/Repositories/Users/PfaUsersRepository.php new file mode 100644 index 0000000..4c8cff7 --- /dev/null +++ b/src/Repositories/Users/PfaUsersRepository.php @@ -0,0 +1,78 @@ +dataAccess = $this->container->get('UsersDataAccess'); + $this->adapter = $this->container->get('UsersAdapter'); + $this->logger = $this->container->get(Logger::class); + } + + public function getAll(): array + { + $pfaUsers = $this->dataAccess->getAll(); + $scimUsers = []; + + foreach ($pfaUsers as $pfaUser) { + $scimUser = $this->adapter->getProvisioningUser($pfaUser); + $scimUsers[] = $scimUser; + } + + return $scimUsers; + } + + public function getOneById(string $id): ?ProvisioningUser + { + $pfaUser = $this->dataAccess->getOneById($id); + return $this->adapter->getProvisioningUser($pfaUser); + } + + public function create($object): ?ProvisioningUser + { + $scimUserToCreate = new ProvisioningUser(); + $scimUserToCreate->fromSCIM($object); + + $pfaUserToCreate = $this->adapter->getPfaUser($scimUserToCreate); + + $pfaUserCreated = $this->dataAccess->create($pfaUserToCreate); + + if (isset($pfaUserCreated)) { + return $this->adapter->getProvisioningUser($pfaUserCreated); + } + return null; + } + + public function update(string $id, $object): ?ProvisioningUser + { + $scimUserToUpdate = new ProvisioningUser(); + $scimUserToUpdate->fromSCIM($object); + + $pfaUserToUpdate = $this->adapter->getPfaUser($scimUserToUpdate); + + $pfaUserUpdated = $this->dataAccess->update($id, $pfaUserToUpdate); + + if (isset($pfaUserUpdated)) { + return $this->adapter->getProvisioningUser($pfaUserUpdated); + } + return null; + } + + public function delete(string $id): bool + { + return $this->dataAccess->delete($id); + } +} diff --git a/src/Util/Util.php b/src/Util/Util.php new file mode 100644 index 0000000..b5b0d0e --- /dev/null +++ b/src/Util/Util.php @@ -0,0 +1,163 @@ +getTimezone()->getName() === \DateTimeZone::UTC) { + return $dateTime->format('Y-m-d\Th:i:s\Z'); + } else { + return $dateTime->format('Y-m-d\TH:i:sP'); + } + } + + /** + * @param string $string + * @param \DateTimeZone $zone + * + * @return \DateTime + */ + public static function string2dateTime($string, \DateTimeZone $zone = null) + { + if (!$zone) { + $zone = new \DateTimeZone('UTC'); + } + + $dt = new \DateTime('now', $zone); + $dt->setTimestamp(self::string2timestamp($string)); + return $dt; + } + + /** + * @param $string + * + * @return int + */ + public static function string2timestamp($string) + { + $matches = array(); + if ( + !preg_match( + '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D', + $string, + $matches + ) + ) { + throw new \InvalidArgumentException('Invalid timestamp: ' . $string); + } + + $year = intval($matches[1]); + $month = intval($matches[2]); + $day = intval($matches[3]); + $hour = intval($matches[4]); + $minute = intval($matches[5]); + $second = intval($matches[6]); + + // Use gmmktime because the timestamp will always be given in UTC? + $ts = gmmktime($hour, $minute, $second, $month, $day, $year); + return $ts; + } + + public static function getUserNameFromFilter($filter) + { + $username = null; + if (preg_match('/userName eq \"([a-z0-9\_\.\-\@]*)\"/i', $filter, $matches) === 1) { + $username = $matches[1]; + } + return $username; + } + + public static function genUuid(): string + { + $uuid4 = \Ramsey\Uuid\Uuid::uuid4(); + return $uuid4->toString(); + } + + public static function buildDbDsn(): ?string + { + $config = self::getConfigFile(); + if (isset($config) && !empty($config)) { + if (isset($config['db']) && !empty($config['db'])) { + if ( + isset($config['db']['driver']) && !empty($config['db']['driver']) + && isset($config['db']['host']) && !empty($config['db']['host']) + && isset($config['db']['port']) && !empty($config['db']['port']) + && isset($config['db']['database']) && !empty($config['db']['database']) + ) { + return $config['db']['driver'] . ':host=' + . $config['db']['host'] . ';port=' + . $config['db']['port'] . ';dbname=' + . $config['db']['database']; + } + } + } + + // In case we can't build a DSN, just return null + // Note: make sure to check for null equality in the caller + return null; + } + + public static function getDomainFromEmail($email) + { + $parts = explode("@", $email); + if (count($parts) != 2) { + return null; + } + return $parts[1]; + } + + public static function getLocalPartFromEmail($email) + { + $parts = explode("@", $email); + if (count($parts) != 2) { + return null; + } + return $parts[0]; + } + + /** + * This function can (and should) be used for obtaining the config file of the scim-server-php + * It tries to fetch the custom-defined config file and return its contents + * If no custom config file exists, it resorts to the config.default.php file as a fallback + * + * Either way, it returns the config file's contents in the form of an associative array + */ + public static function getConfigFile() + { + $defaultConfigFilePath = dirname(__DIR__) . '/../config/config.default.php'; + $customConfigFilePath = dirname(__DIR__) . '/../config/config.php'; + + $config = []; + + // In case we don't have a custom config, we just rely on the default one + if (!file_exists($customConfigFilePath)) { + $config = require($defaultConfigFilePath); + } else { + $config = require($customConfigFilePath); + } + + return $config; + } +} diff --git a/src/eloquent.php b/src/eloquent.php new file mode 100644 index 0000000..41088d4 --- /dev/null +++ b/src/eloquent.php @@ -0,0 +1,15 @@ +addConnection($dbSettings); + $capsule->setAsGlobal(); + $capsule->bootEloquent(); +}; diff --git a/src/routes.php b/src/routes.php new file mode 100644 index 0000000..cb62e1c --- /dev/null +++ b/src/routes.php @@ -0,0 +1,54 @@ +get('/Users', ListUsersAction::class)->setName('users.list'); + $app->get('/Users/{id}', GetUserAction::class)->setName('users.get'); + $app->post('/Users', CreateUserAction::class)->setName('users.create'); + $app->put('/Users/{id}', UpdateUserAction::class)->setName('users.update'); + $app->delete('/Users/{id}', DeleteUserAction::class)->setName('users.delete'); + } + + // Group routes + if (in_array('Group', $supportedResourceTypes)) { + $app->get('/Groups', ListGroupsAction::class)->setName('groups.list'); + $app->get('/Groups/{id}', GetGroupAction::class)->setName('groups.get'); + $app->post('/Groups', CreateGroupAction::class)->setName('groups.create'); + $app->put('/Groups/{id}', UpdateGroupAction::class)->setName('groups.update'); + $app->delete('/Groups/{id}', DeleteGroupAction::class)->setName('groups.delete'); + } + + // ServiceProvider routes + $app->get('/ResourceTypes', ListResourceTypesAction::class)->setName('resourceTypes.list'); + $app->get('/Schemas', ListSchemasAction::class)->setName('schemas.list'); + $app->get( + '/ServiceProviderConfig', + ListServiceProviderConfigurationsAction::class + )->setName('serviceProviderConfigs.list'); + + // JWT + $app->get('/jwt', GenerateJWTAction::class)->setName('jwt.generate'); +}; diff --git a/test/integration/.gitignore b/test/integration/.gitignore new file mode 100644 index 0000000..b0e1636 --- /dev/null +++ b/test/integration/.gitignore @@ -0,0 +1,2 @@ +# Custom hosts files +hosts_* diff --git a/test/integration/ansible.cfg b/test/integration/ansible.cfg new file mode 100644 index 0000000..74a138f --- /dev/null +++ b/test/integration/ansible.cfg @@ -0,0 +1,4 @@ +[defaults] +inventory = hosts_local +interpreter_python = auto_silent +stdout_callback = yaml diff --git a/test/integration/hosts.sample b/test/integration/hosts.sample new file mode 100644 index 0000000..49d289e --- /dev/null +++ b/test/integration/hosts.sample @@ -0,0 +1,2 @@ +[scim_php_servers] +container_scim_php users_address=http:///Users user=testuser pass=testpass \ No newline at end of file diff --git a/test/integration/tests.yml b/test/integration/tests.yml new file mode 100644 index 0000000..f16ce3f --- /dev/null +++ b/test/integration/tests.yml @@ -0,0 +1,5 @@ +# Ansible integration tests +--- +- import_playbook: users.yml + tags: + users \ No newline at end of file diff --git a/test/integration/users.yml b/test/integration/users.yml new file mode 100644 index 0000000..116d161 --- /dev/null +++ b/test/integration/users.yml @@ -0,0 +1,51 @@ +# Tests /Users SCIM endpoint +--- +- hosts: all + ignore_errors: true + gather_facts: false + tasks: + - name: Create test folder + ansible.builtin.file: + dest: "/tmp/opf_ansible_output/{{ inventory_hostname }}" + state: directory + mode: '0755' + delegate_to: 127.0.0.1 + + - name: Get all users + uri: + url: "{{ users_address }}" + method: GET + return_content: true + validate_certs: false + delegate_to: 127.0.0.1 + register: call_response + + - name: Store reply under /tmp/ + copy: + content: "{{ call_response.content }}" + dest: "/tmp/opf_ansible_output/{{ inventory_hostname }}/\ + get_all_users.json" + delegate_to: 127.0.0.1 + +- hosts: all + ignore_errors: true + gather_facts: false + tasks: + - name: Create a user + uri: + body: '{"userName":"testuser","externalId":"testuserexternal","profileUrl":"https://www.example.com/testuser"}' + body_format: json + url: "{{ users_address }}" + method: POST + status_code: 201 # Expect 201 for a create user response + return_content: true + validate_certs: false + delegate_to: 127.0.0.1 + register: call_response + + - name: Store reply under /tmp/ + copy: + content: "{{ call_response.content }}" + dest: "/tmp/opf_ansible_output/{{ inventory_hostname }}/\ + create_user.json" + delegate_to: 127.0.0.1 diff --git a/test/phpunit.xml b/test/phpunit.xml new file mode 100644 index 0000000..8a4992e --- /dev/null +++ b/test/phpunit.xml @@ -0,0 +1,12 @@ + + + + unit + + + \ No newline at end of file diff --git a/test/postman/scim-env.postman_environment.json b/test/postman/scim-env.postman_environment.json new file mode 100644 index 0000000..17d0511 --- /dev/null +++ b/test/postman/scim-env.postman_environment.json @@ -0,0 +1,15 @@ +{ + "id": "7880a4e5-d9ce-42f8-8ed0-57f886616527", + "name": "scim-env", + "values": [ + { + "key": "url", + "value": "http://localhost:8888", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2022-04-14T14:54:36.929Z", + "_postman_exported_using": "Postman/9.15.2" +} \ No newline at end of file diff --git a/test/postman/scim-opf-pfa.postman_collection.json b/test/postman/scim-opf-pfa.postman_collection.json new file mode 100644 index 0000000..9f3a833 --- /dev/null +++ b/test/postman/scim-opf-pfa.postman_collection.json @@ -0,0 +1,597 @@ +{ + "info": { + "_postman_id": "2b79327d-70c4-425f-942f-6037f38d67e5", + "name": "PFA SCIM PHP Collection", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "JWT", + "item": [ + { + "name": "Get JWT", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "pm.environment.set(\"jwt_token\", response.Bearer)" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/jwt", + "host": [ + "{{url}}" + ], + "path": [ + "jwt" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "ResourceTypes", + "item": [ + { + "name": "Read all ResourceTypes", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json().Resources).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains exactly one entry\", () => {", + " pm.expect(pm.response.json().Resources.length).to.eql(1);", + "});", + "", + "pm.test(\"Response body contains ResourceType with id \\\"User\\\"\", () => {", + " pm.expect(pm.response.json().Resources[0].id).to.eql(\"User\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer: {{jwt_token}}", + "type": "default" + } + ], + "url": { + "raw": "{{url}}/ResourceTypes", + "host": [ + "{{url}}" + ], + "path": [ + "ResourceTypes" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Schemas", + "item": [ + { + "name": "Read all Schemas", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json().Resources).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains exactly four entries\", () => {", + " pm.expect(pm.response.json().Resources.length).to.eql(4);", + "});", + "", + "pm.test(\"Response body contains Schema with id \\\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\\\"\", () => {", + " pm.expect(pm.response.json().Resources[0].id).to.eql(\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\");", + "});", + "", + "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\\\"\", () => {", + " pm.expect(pm.response.json().Resources[1].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\");", + "});", + "", + "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:User\\\"\", () => {", + " pm.expect(pm.response.json().Resources[2].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:User\");", + "});", + "", + "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Schema\\\"\", () => {", + " pm.expect(pm.response.json().Resources[3].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Schema\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer: {{jwt_token}}", + "type": "default" + } + ], + "url": { + "raw": "{{url}}/Schemas", + "host": [ + "{{url}}" + ], + "path": [ + "Schemas" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "ServiceProviderConfigs", + "item": [ + { + "name": "Read all ServiceProviderConfigs", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains a ServiceProviderConfig with a correct schema\", () => {", + " pm.expect(pm.response.json().schemas).to.include(\"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer: {{jwt_token}}", + "type": "default" + } + ], + "url": { + "raw": "{{url}}/ServiceProviderConfig", + "host": [ + "{{url}}" + ], + "path": [ + "ServiceProviderConfig" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "Create a single user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 201\", () => {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains user with userName \\\"createdtestuser@test.org\\\"\", () => {", + " pm.expect(pm.response.json().userName).to.eql(\"createdtestuser@test.org\");", + "});", + "", + "pm.test(\"Response body contains a valid non-null user ID (the ID of the user which was created)\", () => {", + " pm.expect(pm.response.json().id).to.not.be.null;", + "});", + "", + "pm.test(\"Response body contains user with sizeQuota equal to 12345\", () => {", + " pm.expect(pm.response.json()[\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\"].sizeQuota).to.eql(12345);", + "});", + "", + "pm.collectionVariables.set(\"testUserId\", pm.response.json().id);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/scim+json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userName\": \"createdtestuser@test.org\",\n \"id\": \"createdtestuser@test.org\",\n \"active\": true,\n \"password\": \"somepass123\",\n \"displayName\": \"createdtestuser\",\n \"emails\": [{\"value\":\"createdtestuser@test.org\"}],\n \"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\": {\n \"sizeQuota\": 12345\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/Users", + "host": [ + "{{url}}" + ], + "path": [ + "Users" + ] + } + }, + "response": [] + }, + { + "name": "Read a single user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains the user ID of the user we want to read\", () => {", + " pm.expect(pm.response.json().id).to.eql(\"createdtestuser@test.org\");", + "});", + "", + "pm.test(\"Response body contains user with userName \\\"createdtestuser@test.org\\\"\", () => {", + " pm.expect(pm.response.json().userName).to.eql(\"createdtestuser@test.org\");", + "});", + "", + "pm.test(\"Response body contains user with sizeQuota equal to 12345\", () => {", + " pm.expect(pm.response.json()[\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\"].sizeQuota).to.eql(12345);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer: {{jwt_token}}", + "type": "default" + } + ], + "url": { + "raw": "{{url}}/Users/{{testUserId}}", + "host": [ + "{{url}}" + ], + "path": [ + "Users", + "{{testUserId}}" + ] + } + }, + "response": [] + }, + { + "name": "Read a non-existent user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 404\", () => {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test(\"Response body is empty\", () => {", + " pm.expect(pm.response.body).to.be.undefined;", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer: {{jwt_token}}", + "type": "default" + } + ], + "url": { + "raw": "{{url}}/Users/some-non-existent-user@test.org", + "host": [ + "{{url}}" + ], + "path": [ + "Users", + "some-non-existent-user@test.org" + ] + } + }, + "response": [] + }, + { + "name": "Read all users", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json().Resources).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains user with userName \\\"createdtestuser@test.org\\\"\", () => {", + " pm.expect(pm.response.json().Resources[0].userName).to.eql(\"createdtestuser@test.org\");", + "});", + "", + "pm.test(\"Response body contains user with sizeQuota equal to 12345\", () => {", + " pm.expect(pm.response.json().Resources[0][\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\"].sizeQuota).to.eql(12345);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer: {{jwt_token}}", + "type": "default" + } + ], + "url": { + "raw": "{{url}}/Users", + "host": [ + "{{url}}" + ], + "path": [ + "Users" + ] + } + }, + "response": [] + }, + { + "name": "Update a single user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains the user ID of the user we want to read\", () => {", + " pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));", + "});", + "", + "pm.test(\"Response body contains user with displayName \\\"updatedtestuser\\\"\", () => {", + " pm.expect(pm.response.json().displayName).to.eql(\"updatedtestuser\");", + "});", + "", + "pm.test(\"Response body contains user with sizeQuota equal to 123456789\", () => {", + " pm.expect(pm.response.json()[\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\"].sizeQuota).to.eql(123456789);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/scim+json", + "type": "default" + }, + { + "key": "Authorization", + "value": "Bearer: {{jwt_token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"createdtestuser@test.org\",\n \"displayName\": \"updatedtestuser\",\n \"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\": {\n \"sizeQuota\": 123456789\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/Users/{{testUserId}}", + "host": [ + "{{url}}" + ], + "path": [ + "Users", + "{{testUserId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete a single user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 204\", () => {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/scim+json", + "type": "default" + }, + { + "key": "Authorization", + "value": "Bearer: {{jwt_token}}", + "type": "default" + } + ], + "url": { + "raw": "{{url}}/Users/{{testUserId}}", + "host": [ + "{{url}}" + ], + "path": [ + "Users", + "{{testUserId}}" + ] + } + }, + "response": [] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt_token}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "testUserId", + "value": "" + }, + { + "key": "testGroupId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/test/postman/scim-opf.postman_collection.json b/test/postman/scim-opf.postman_collection.json new file mode 100644 index 0000000..0d2bcd9 --- /dev/null +++ b/test/postman/scim-opf.postman_collection.json @@ -0,0 +1,717 @@ +{ + "info": { + "_postman_id": "73043646-f766-4adc-96ee-05316cc59bdd", + "name": "SCIM PHP Collection", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Users", + "item": [ + { + "name": "Create a single user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 201\", () => {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {", + " pm.expect(pm.response.json().userName).to.eql(\"createdtestuser\");", + "});", + "", + "pm.test(\"Response body contains a valid non-null user ID (the ID of the user which was created)\", () => {", + " pm.expect(pm.response.json().id).to.not.be.null;", + "});", + "", + "pm.collectionVariables.set(\"testUserId\", pm.response.json().id);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/scim+json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userName\": \"createdtestuser\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/Users", + "host": [ + "{{url}}" + ], + "path": [ + "Users" + ] + } + }, + "response": [] + }, + { + "name": "Read a single user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains the user ID of the user we want to read\", () => {", + " pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));", + "});", + "", + "pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {", + " pm.expect(pm.response.json().userName).to.eql(\"createdtestuser\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/Users/{{testUserId}}", + "host": [ + "{{url}}" + ], + "path": [ + "Users", + "{{testUserId}}" + ] + } + }, + "response": [] + }, + { + "name": "Read all users", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json().Resources).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {", + " pm.expect(pm.response.json().Resources[0].userName).to.eql(\"createdtestuser\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/Users", + "host": [ + "{{url}}" + ], + "path": [ + "Users" + ] + } + }, + "response": [] + }, + { + "name": "Update a single user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains the user ID of the user we want to read\", () => {", + " pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));", + "});", + "", + "pm.test(\"Response body contains user with userName \\\"updatedtestuser\\\"\", () => {", + " pm.expect(pm.response.json().userName).to.eql(\"updatedtestuser\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/scim+json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userName\": \"updatedtestuser\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/Users/{{testUserId}}", + "host": [ + "{{url}}" + ], + "path": [ + "Users", + "{{testUserId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete a single user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 204\", () => {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/scim+json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userName\": \"updatedtestuser\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/Users/{{testUserId}}", + "host": [ + "{{url}}" + ], + "path": [ + "Users", + "{{testUserId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Groups", + "item": [ + { + "name": "Create a single group", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 201\", () => {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {", + " pm.expect(pm.response.json().displayName).to.eql(\"createdtestgroup\");", + "});", + "", + "pm.test(\"Response body contains a valid non-null group ID (the ID of the group which was created)\", () => {", + " pm.expect(pm.response.json().id).to.not.be.null;", + "});", + "", + "pm.collectionVariables.set(\"testGroupId\", pm.response.json().id);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/scim+json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"displayName\": \"createdtestgroup\",\n \"members\": []\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/Groups", + "host": [ + "{{url}}" + ], + "path": [ + "Groups" + ] + } + }, + "response": [] + }, + { + "name": "Read a single group", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains the group ID of the group we want to read\", () => {", + " pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testGroupId'));", + "});", + "", + "pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {", + " pm.expect(pm.response.json().displayName).to.eql(\"createdtestgroup\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/Groups/{{testGroupId}}", + "host": [ + "{{url}}" + ], + "path": [ + "Groups", + "{{testGroupId}}" + ] + } + }, + "response": [] + }, + { + "name": "Read all groups", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json().Resources).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {", + " pm.expect(pm.response.json().Resources[0].displayName).to.eql(\"createdtestgroup\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/Groups", + "host": [ + "{{url}}" + ], + "path": [ + "Groups" + ] + } + }, + "response": [] + }, + { + "name": "Update a single group", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 201\", () => {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains the group ID of the group we want to read\", () => {", + " pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testGroupId'));", + "});", + "", + "pm.test(\"Response body contains group with displayName \\\"updatedtestgroup\\\"\", () => {", + " pm.expect(pm.response.json().displayName).to.eql(\"updatedtestgroup\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"displayName\": \"updatedtestgroup\",\n \"members\": []\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/Groups/{{testGroupId}}", + "host": [ + "{{url}}" + ], + "path": [ + "Groups", + "{{testGroupId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete a single group", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{url}}/Groups/{{testGroupId}}", + "host": [ + "{{url}}" + ], + "path": [ + "Groups", + "{{testGroupId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "ResourceTypes", + "item": [ + { + "name": "Read all ResourceTypes", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json().Resources).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains exactly two entries\", () => {", + " pm.expect(pm.response.json().Resources.length).to.eql(2);", + "});", + "", + "pm.test(\"Response body contains ResourceType with id \\\"User\\\"\", () => {", + " pm.expect(pm.response.json().Resources[0].id).to.eql(\"User\");", + "});", + "", + "pm.test(\"Response body contains ResourceType with id \\\"Group\\\"\", () => {", + " pm.expect(pm.response.json().Resources[1].id).to.eql(\"Group\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/ResourceTypes", + "host": [ + "{{url}}" + ], + "path": [ + "ResourceTypes" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Schemas", + "item": [ + { + "name": "Read all Schemas", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json().Resources).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains exactly five entries\", () => {", + " pm.expect(pm.response.json().Resources.length).to.eql(5);", + "});", + "", + "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Group\\\"\", () => {", + " pm.expect(pm.response.json().Resources[0].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Group\");", + "});", + "", + "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\\\"\", () => {", + " pm.expect(pm.response.json().Resources[1].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\");", + "});", + "", + "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Schema\\\"\", () => {", + " pm.expect(pm.response.json().Resources[2].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Schema\");", + "});", + "", + "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:User\\\"\", () => {", + " pm.expect(pm.response.json().Resources[3].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:User\");", + "});", + "", + "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\\\"\", () => {", + " pm.expect(pm.response.json().Resources[4].id).to.eql(\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/Schemas", + "host": [ + "{{url}}" + ], + "path": [ + "Schemas" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "ServiceProviderConfigs", + "item": [ + { + "name": "Read all ServiceProviderConfigs", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", () => {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type header is application/scim+json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');", + "});", + "", + "pm.test(\"Response body is not empty\", () => {", + " pm.expect(pm.response.json()).to.not.be.empty;", + "});", + "", + "pm.test(\"Response body contains a ServiceProviderConfig with a correct schema\", () => {", + " pm.expect(pm.response.json().schemas).to.include(\"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{url}}/ServiceProviderConfig", + "host": [ + "{{url}}" + ], + "path": [ + "ServiceProviderConfig" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "testUserId", + "value": "" + }, + { + "key": "testGroupId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/test/resources/testGroup.json b/test/resources/testGroup.json new file mode 100644 index 0000000..5d83d9a --- /dev/null +++ b/test/resources/testGroup.json @@ -0,0 +1,4 @@ +{ + "displayName": "testGroup", + "members": ["12345678-9012-3456-7890-12345678"] +} \ No newline at end of file diff --git a/test/resources/testUser.json b/test/resources/testUser.json new file mode 100644 index 0000000..10c36b2 --- /dev/null +++ b/test/resources/testUser.json @@ -0,0 +1,5 @@ +{ + "userName": "testusercreate", + "externalId": "testusercreateexternal", + "profileUrl": "http://example.com/testusercreate" +} \ No newline at end of file diff --git a/test/unit/MockGroupsDataAccessTest.php b/test/unit/MockGroupsDataAccessTest.php new file mode 100644 index 0000000..4097cbf --- /dev/null +++ b/test/unit/MockGroupsDataAccessTest.php @@ -0,0 +1,87 @@ +database = new SQLite3(__DIR__ . '/../resources/test-scim-opf.sqlite'); + + $groupDbSql = "CREATE TABLE IF NOT EXISTS groups ( + id varchar(160) NOT NULL UNIQUE, + displayName varchar(160) NOT NULL DEFAULT '', + members TEXT NOT NULL DEFAULT '', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NULL + )"; + + $this->database->exec($groupDbSql); + + $createGroupSql = "INSERT INTO groups ( + id, + displayName, + members + ) VALUES ( + '12345678-9012-3456-7890-12345679', + 'testGroup', + '12345678-9012-3456-7890-12345678' + )"; + + $this->database->exec($createGroupSql); + + $this->dbSettings = [ + 'driver' => 'sqlite', + 'database' => __DIR__ . '/../resources/test-scim-opf.sqlite', + 'prefix' => '' + ]; + + $this->capsule = new Manager(); + $this->capsule->addConnection($this->dbSettings); + $this->capsule->setAsGlobal(); + $this->capsule->bootEloquent(); + + $this->mockGroupDataAccess = new MockGroupDataAccess(); + } + + public function tearDown(): void + { + $this->mockGroupDataAccess = null; + $this->capsule = null; + $this->dbSettings = null; + $this->database->exec("DROP TABLE groups"); + $this->database = null; + + unlink(__DIR__ . '/../resources/test-scim-opf.sqlite'); + } + + public function testReadAllGroups() + { + $this->assertNotEmpty($this->mockGroupDataAccess->all()); + } + + public function testCreateGroup() + { + $testGroupJson = json_decode(file_get_contents(__DIR__ . '/../resources/testGroup.json'), true); + $this->mockGroupDataAccess->fromSCIM($testGroupJson); + $groupCreateRes = $this->mockGroupDataAccess->save(); + $this->assertTrue($groupCreateRes); + } +} diff --git a/test/unit/MockUsersDataAccessTest.php b/test/unit/MockUsersDataAccessTest.php new file mode 100644 index 0000000..3f1c347 --- /dev/null +++ b/test/unit/MockUsersDataAccessTest.php @@ -0,0 +1,91 @@ +database = new SQLite3(__DIR__ . '/../resources/test-scim-opf.sqlite'); + + $userDbSql = "CREATE TABLE IF NOT EXISTS users ( + id varchar(160) NOT NULL UNIQUE, + userName varchar(160) NOT NULL, + active BOOLEAN NOT NULL DEFAULT 1, + externalId varchar(160) NULL, + profileUrl varchar(160) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NULL + )"; + + $this->database->exec($userDbSql); + + $createUserSql = "INSERT INTO users ( + id, + userName, + externalId, + profileUrl + ) VALUES ( + '12345678-9012-3456-7890-12345678', + 'testuser', + 'testuserexternal', + 'https://example.com/testuser' + )"; + + $this->database->exec($createUserSql); + + $this->dbSettings = [ + 'driver' => 'sqlite', + 'database' => __DIR__ . '/../resources/test-scim-opf.sqlite', + 'prefix' => '' + ]; + + $this->capsule = new Manager(); + $this->capsule->addConnection($this->dbSettings); + $this->capsule->setAsGlobal(); + $this->capsule->bootEloquent(); + + $this->mockUserDataAccess = new MockUserDataAccess(); + } + + public function tearDown(): void + { + $this->mockUserDataAccess = null; + $this->capsule = null; + $this->dbSettings = null; + $this->database->exec("DROP TABLE users"); + $this->database = null; + + unlink(__DIR__ . '/../resources/test-scim-opf.sqlite'); + } + + public function testReadAllUsers() + { + $this->assertNotEmpty($this->mockUserDataAccess->all()); + } + + public function testCreateUser() + { + $testUserJson = json_decode(file_get_contents(__DIR__ . '/../resources/testUser.json'), true); + $this->mockUserDataAccess->fromSCIM($testUserJson); + $userCreateRes = $this->mockUserDataAccess->save(); + $this->assertTrue($userCreateRes); + } +}