~fkooman/vpn-user-portal

401ef549af378d73c966a88c111a0e245c4556ba — François Kooman 2 months ago c07d2e2 + 9791072 irma
Merge branch 'v2' into irma
M CHANGES.md => CHANGES.md +33 -0
@@ 1,5 1,38 @@
# Changelog

## 2.3.14 (2021-08-26)
- **SECURITY**: implement proper input validation for QR code generation 
  (CVE-2021-41583)
- only enable QR code module when 2FA is enabled

## 2.3.13 (2021-08-02)
- support (non vendor-specific) RADIUS attribute for authorization

## 2.3.12 (2021-07-13)
- remove 
  [nightly session expiry](https://github.com/eduvpn/documentation/blob/v2/EXPIRE_AT_NIGHT.md)
  again as it was very broken
- no longer show "Danger Zone" when managing your own account
- allow hiding the "Permission(s)" on the user's "Account" page
- show profiles available to the user on the "Account" page
- fix margin for lists in tables

## 2.3.11 (2021-06-08)
- support expiring VPN session at night now for all scenarios where 
  `sessionExpiry` >= 1 day
- session expiry is now always (upper)bound to CA expiry
- implement a "Delete User" (for local accounts) and "Delete User Data" (for 
  accounts in external IdM)
- add Romanian (Romania) translation

## 2.3.10 (2021-05-03)
- implement support for 
  [nightly session expiry](https://github.com/eduvpn/documentation/blob/v2/EXPIRE_AT_NIGHT.md) 
  @ 04:00 according to server timezone when session expiry is >= 7 days
  
## 2.3.9 (2021-04-12)
- add Spanish (Latin America) translation

## 2.3.8 (2021-03-15)
- show portal version on "Info" page
- implement support for optional LDAP bind with credentials for initial search

M CONFIG_CHANGES.md => CONFIG_CHANGES.md +39 -0
@@ 8,6 8,45 @@ This will help upgrades to a future 3.x release. Configuration changes during
the 2.x life cycle are NOT required. Any existing configuration file will keep
working!

## 2.3.13

Support `permissionAttribute` configuration option for the RADIUS 
authentication backend. This allows you to specify an attribute to be used for
authorization. See 
[documentation](https://github.com/eduvpn/documentation/blob/v2/RADIUS.md).

## 2.3.12

remove `sessionExpireAtNight` as it is too complicated to implement correctly 
in eduVPN/Let's Connect! 2.x.

We have now the `showPermissions` option that takes a `bool` to show/hide the
"Permission(s)" on the user's "Account" page. The default is `true`.

## 2.3.11

We added the translation for Romanian (Romania). You can add it to 
`config.php` under `supportedLanguages` to enable it in your portal:

```
'ro-RO' => 'română'
```

## 2.3.10

The `sessionExpireAtNight` option (taking a boolean) has been added. Read the
[docs](https://github.com/eduvpn/documentation/blob/v2/EXPIRE_AT_NIGHT.md) 
on how to use it.

## 2.3.9

We added the translation for Spanish (Latin America). You can add it to 
`config.php` under `supportedLanguages` to enable it in your portal:

```
'es_LA' => 'español',
```

## 2.3.8

The `FormLdapAuthentication` section also takes `searchBindDn` and 

M VERSION => VERSION +1 -1
@@ 1,1 1,1 @@
2.3.8
2.3.14

M bin/foreign-key-list-fetcher.php => bin/foreign-key-list-fetcher.php +1 -2
@@ 23,8 23,7 @@ try {
            new CurlHttpClient(),
            'https://disco.eduvpn.org/v2/server_list.json',
            [
                'RWRtBSX1alxyGX+Xn3LuZnWUT0w//B6EmTJvgaAxBMYzlQeI+jdrO6KF', // fkooman@deic.dk
                'RWQ68Y5/b8DED0TJ41B1LE7yAvkmavZWjDwCBUuC+Z2pP9HaSawzpEDA', // jornane@uninett.no
                'RWRtBSX1alxyGX+Xn3LuZnWUT0w//B6EmTJvgaAxBMYzlQeI+jdrO6KF', // fkooman@deic.dk, kolla@uninett.no
                'RWQKqtqvd0R7rUDp0rWzbtYPA3towPWcLDCl7eY9pBMMI/ohCmrS0WiM', // RoSp
            ]
        );

M composer.json => composer.json +2 -1
@@ 68,7 68,8 @@
    "require-dev": {
        "ext-json": "*",
        "phpunit/phpunit": "^4.8.35|^5|^6|^7",
        "fkooman/saml-sp": "^0.5.2"
        "fkooman/saml-sp": "^1",
        "friendsofphp/php-cs-fixer": "^2"
    },
    "support": {
        "email": "eduvpn@surfnet.nl",

M composer.lock => composer.lock +2063 -94
@@ 4,7 4,7 @@
        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
        "This file is @generated automatically"
    ],
    "content-hash": "f3ac70eda9d73fc4c8c9a518bc2cf5f9",
    "content-hash": "2c0678ac384f134571d3ca874f7fe93b",
    "packages": [
        {
            "name": "fkooman/jwt",


@@ 248,7 248,7 @@
            "dist": {
                "type": "path",
                "url": "../vpn-lib-common",
                "reference": "82274aa605cd2c3aff51eac083fadcd8ea356653"
                "reference": "0d21048e9d0e95502c7125211869385a0c9f4582"
            },
            "require": {
                "ext-curl": "*",


@@ 371,16 371,16 @@
        },
        {
            "name": "paragonie/random_compat",
            "version": "v2.0.19",
            "version": "v2.0.20",
            "source": {
                "type": "git",
                "url": "https://github.com/paragonie/random_compat.git",
                "reference": "446fc9faa5c2a9ddf65eb7121c0af7e857295241"
                "reference": "0f1f60250fccffeaf5dda91eea1c018aed1adc2a"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/446fc9faa5c2a9ddf65eb7121c0af7e857295241",
                "reference": "446fc9faa5c2a9ddf65eb7121c0af7e857295241",
                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/0f1f60250fccffeaf5dda91eea1c018aed1adc2a",
                "reference": "0f1f60250fccffeaf5dda91eea1c018aed1adc2a",
                "shasum": ""
            },
            "require": {


@@ 421,20 421,20 @@
                "issues": "https://github.com/paragonie/random_compat/issues",
                "source": "https://github.com/paragonie/random_compat"
            },
            "time": "2020-10-15T10:06:57+00:00"
            "time": "2021-04-17T09:33:01+00:00"
        },
        {
            "name": "paragonie/sodium_compat",
            "version": "v1.14.0",
            "version": "v1.17.0",
            "source": {
                "type": "git",
                "url": "https://github.com/paragonie/sodium_compat.git",
                "reference": "a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3"
                "reference": "c59cac21abbcc0df06a3dd18076450ea4797b321"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3",
                "reference": "a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3",
                "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/c59cac21abbcc0df06a3dd18076450ea4797b321",
                "reference": "c59cac21abbcc0df06a3dd18076450ea4797b321",
                "shasum": ""
            },
            "require": {


@@ 505,22 505,22 @@
            ],
            "support": {
                "issues": "https://github.com/paragonie/sodium_compat/issues",
                "source": "https://github.com/paragonie/sodium_compat/tree/v1.14.0"
                "source": "https://github.com/paragonie/sodium_compat/tree/v1.17.0"
            },
            "time": "2020-12-03T16:26:19+00:00"
            "time": "2021-08-10T02:43:50+00:00"
        },
        {
            "name": "psr/log",
            "version": "1.1.3",
            "version": "1.1.4",
            "source": {
                "type": "git",
                "url": "https://github.com/php-fig/log.git",
                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
                "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
                "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
                "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
                "shasum": ""
            },
            "require": {


@@ 544,7 544,7 @@
            "authors": [
                {
                    "name": "PHP-FIG",
                    "homepage": "http://www.php-fig.org/"
                    "homepage": "https://www.php-fig.org/"
                }
            ],
            "description": "Common interface for logging libraries",


@@ 555,9 555,9 @@
                "psr-3"
            ],
            "support": {
                "source": "https://github.com/php-fig/log/tree/1.1.3"
                "source": "https://github.com/php-fig/log/tree/1.1.4"
            },
            "time": "2020-03-23T09:12:05+00:00"
            "time": "2021-05-03T11:20:27+00:00"
        },
        {
            "name": "symfony/polyfill-php56",


@@ 630,6 630,223 @@
    ],
    "packages-dev": [
        {
            "name": "composer/semver",
            "version": "3.2.5",
            "source": {
                "type": "git",
                "url": "https://github.com/composer/semver.git",
                "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/composer/semver/zipball/31f3ea725711245195f62e54ffa402d8ef2fdba9",
                "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9",
                "shasum": ""
            },
            "require": {
                "php": "^5.3.2 || ^7.0 || ^8.0"
            },
            "require-dev": {
                "phpstan/phpstan": "^0.12.54",
                "symfony/phpunit-bridge": "^4.2 || ^5"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-main": "3.x-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "Composer\\Semver\\": "src"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Nils Adermann",
                    "email": "naderman@naderman.de",
                    "homepage": "http://www.naderman.de"
                },
                {
                    "name": "Jordi Boggiano",
                    "email": "j.boggiano@seld.be",
                    "homepage": "http://seld.be"
                },
                {
                    "name": "Rob Bast",
                    "email": "rob.bast@gmail.com",
                    "homepage": "http://robbast.nl"
                }
            ],
            "description": "Semver library that offers utilities, version constraint parsing and validation.",
            "keywords": [
                "semantic",
                "semver",
                "validation",
                "versioning"
            ],
            "support": {
                "irc": "irc://irc.freenode.org/composer",
                "issues": "https://github.com/composer/semver/issues",
                "source": "https://github.com/composer/semver/tree/3.2.5"
            },
            "funding": [
                {
                    "url": "https://packagist.com",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/composer",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
                    "type": "tidelift"
                }
            ],
            "time": "2021-05-24T12:41:47+00:00"
        },
        {
            "name": "composer/xdebug-handler",
            "version": "2.0.2",
            "source": {
                "type": "git",
                "url": "https://github.com/composer/xdebug-handler.git",
                "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/84674dd3a7575ba617f5a76d7e9e29a7d3891339",
                "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339",
                "shasum": ""
            },
            "require": {
                "php": "^5.3.2 || ^7.0 || ^8.0",
                "psr/log": "^1 || ^2 || ^3"
            },
            "require-dev": {
                "phpstan/phpstan": "^0.12.55",
                "symfony/phpunit-bridge": "^4.2 || ^5"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Composer\\XdebugHandler\\": "src"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "John Stevenson",
                    "email": "john-stevenson@blueyonder.co.uk"
                }
            ],
            "description": "Restarts a process without Xdebug.",
            "keywords": [
                "Xdebug",
                "performance"
            ],
            "support": {
                "irc": "irc://irc.freenode.org/composer",
                "issues": "https://github.com/composer/xdebug-handler/issues",
                "source": "https://github.com/composer/xdebug-handler/tree/2.0.2"
            },
            "funding": [
                {
                    "url": "https://packagist.com",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/composer",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
                    "type": "tidelift"
                }
            ],
            "time": "2021-07-31T17:03:58+00:00"
        },
        {
            "name": "doctrine/annotations",
            "version": "1.13.2",
            "source": {
                "type": "git",
                "url": "https://github.com/doctrine/annotations.git",
                "reference": "5b668aef16090008790395c02c893b1ba13f7e08"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08",
                "reference": "5b668aef16090008790395c02c893b1ba13f7e08",
                "shasum": ""
            },
            "require": {
                "doctrine/lexer": "1.*",
                "ext-tokenizer": "*",
                "php": "^7.1 || ^8.0",
                "psr/cache": "^1 || ^2 || ^3"
            },
            "require-dev": {
                "doctrine/cache": "^1.11 || ^2.0",
                "doctrine/coding-standard": "^6.0 || ^8.1",
                "phpstan/phpstan": "^0.12.20",
                "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5",
                "symfony/cache": "^4.4 || ^5.2"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
                }
            },
            "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": "Docblock Annotations Parser",
            "homepage": "https://www.doctrine-project.org/projects/annotations.html",
            "keywords": [
                "annotations",
                "docblock",
                "parser"
            ],
            "support": {
                "issues": "https://github.com/doctrine/annotations/issues",
                "source": "https://github.com/doctrine/annotations/tree/1.13.2"
            },
            "time": "2021-08-05T19:00:23+00:00"
        },
        {
            "name": "doctrine/instantiator",
            "version": "1.4.0",
            "source": {


@@ 699,12 916,92 @@
            "time": "2020-11-10T18:47:58+00:00"
        },
        {
            "name": "doctrine/lexer",
            "version": "1.2.1",
            "source": {
                "type": "git",
                "url": "https://github.com/doctrine/lexer.git",
                "reference": "e864bbf5904cb8f5bb334f99209b48018522f042"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042",
                "reference": "e864bbf5904cb8f5bb334f99209b48018522f042",
                "shasum": ""
            },
            "require": {
                "php": "^7.2 || ^8.0"
            },
            "require-dev": {
                "doctrine/coding-standard": "^6.0",
                "phpstan/phpstan": "^0.11.8",
                "phpunit/phpunit": "^8.2"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.2.x-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
                }
            },
            "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": "Johannes Schmitt",
                    "email": "schmittjoh@gmail.com"
                }
            ],
            "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
            "homepage": "https://www.doctrine-project.org/projects/lexer.html",
            "keywords": [
                "annotations",
                "docblock",
                "lexer",
                "parser",
                "php"
            ],
            "support": {
                "issues": "https://github.com/doctrine/lexer/issues",
                "source": "https://github.com/doctrine/lexer/tree/1.2.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%2Flexer",
                    "type": "tidelift"
                }
            ],
            "time": "2020-05-25T17:44:05+00:00"
        },
        {
            "name": "fkooman/saml-sp",
            "version": "0.5.9",
            "version": "1.0.3",
            "source": {
                "type": "git",
                "url": "https://git.sr.ht/~fkooman/php-saml-sp",
                "reference": "83693ae0a631ba04440c13b1b8bc22007e3f81e3"
                "reference": "31c7c0b9d4a96e5870a1399a9a3e99ac8aa452d8"
            },
            "require": {
                "ext-curl": "*",


@@ 749,9 1046,118 @@
            "description": "Secure SAML Service Provider",
            "support": {
                "email": "fkooman@tuxed.net",
                "source": "https://git.tuxed.net/fkooman/php-saml-sp"
                "source": "https://git.sr.ht/~fkooman/php-saml-sp"
            },
            "time": "2021-04-23T11:46:22+00:00"
        },
        {
            "name": "friendsofphp/php-cs-fixer",
            "version": "v2.19.2",
            "source": {
                "type": "git",
                "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
                "reference": "d5c737c2e18ba502b75b44832b31fe627f82e307"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/d5c737c2e18ba502b75b44832b31fe627f82e307",
                "reference": "d5c737c2e18ba502b75b44832b31fe627f82e307",
                "shasum": ""
            },
            "require": {
                "composer/semver": "^1.4 || ^2.0 || ^3.0",
                "composer/xdebug-handler": "^1.2 || ^2.0",
                "doctrine/annotations": "^1.2",
                "ext-json": "*",
                "ext-tokenizer": "*",
                "php": "^5.6 || ^7.0 || ^8.0",
                "php-cs-fixer/diff": "^1.3",
                "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0",
                "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0",
                "symfony/filesystem": "^3.0 || ^4.0 || ^5.0",
                "symfony/finder": "^3.0 || ^4.0 || ^5.0",
                "symfony/options-resolver": "^3.0 || ^4.0 || ^5.0",
                "symfony/polyfill-php70": "^1.0",
                "symfony/polyfill-php72": "^1.4",
                "symfony/process": "^3.0 || ^4.0 || ^5.0",
                "symfony/stopwatch": "^3.0 || ^4.0 || ^5.0"
            },
            "require-dev": {
                "justinrainbow/json-schema": "^5.0",
                "keradus/cli-executor": "^1.4",
                "mikey179/vfsstream": "^1.6",
                "php-coveralls/php-coveralls": "^2.4.2",
                "php-cs-fixer/accessible-object": "^1.0",
                "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2",
                "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1",
                "phpspec/prophecy-phpunit": "^1.1 || ^2.0",
                "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.13 || ^9.5",
                "phpunitgoodpractices/polyfill": "^1.5",
                "phpunitgoodpractices/traits": "^1.9.1",
                "sanmai/phpunit-legacy-adapter": "^6.4 || ^8.2.1",
                "symfony/phpunit-bridge": "^5.2.1",
                "symfony/yaml": "^3.0 || ^4.0 || ^5.0"
            },
            "suggest": {
                "ext-dom": "For handling output formats in XML",
                "ext-mbstring": "For handling non-UTF8 characters.",
                "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.",
                "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.",
                "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible."
            },
            "bin": [
                "php-cs-fixer"
            ],
            "type": "application",
            "extra": {
                "branch-alias": {
                    "dev-master": "2.19-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "PhpCsFixer\\": "src/"
                },
                "classmap": [
                    "tests/Test/AbstractFixerTestCase.php",
                    "tests/Test/AbstractIntegrationCaseFactory.php",
                    "tests/Test/AbstractIntegrationTestCase.php",
                    "tests/Test/Assert/AssertTokensTrait.php",
                    "tests/Test/IntegrationCase.php",
                    "tests/Test/IntegrationCaseFactory.php",
                    "tests/Test/IntegrationCaseFactoryInterface.php",
                    "tests/Test/InternalIntegrationCaseFactory.php",
                    "tests/Test/IsIdenticalConstraint.php",
                    "tests/Test/TokensWithObservedTransformers.php",
                    "tests/TestCase.php"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Fabien Potencier",
                    "email": "fabien@symfony.com"
                },
                {
                    "name": "Dariusz Rumiński",
                    "email": "dariusz.ruminski@gmail.com"
                }
            ],
            "description": "A tool to automatically fix PHP code style",
            "support": {
                "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
                "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.19.2"
            },
            "time": "2020-10-12T08:24:51+00:00"
            "funding": [
                {
                    "url": "https://github.com/keradus",
                    "type": "github"
                }
            ],
            "time": "2021-08-18T19:55:46+00:00"
        },
        {
            "name": "myclabs/deep-copy",


@@ 922,41 1328,96 @@
            "time": "2018-07-08T19:19:57+00:00"
        },
        {
            "name": "phpdocumentor/reflection-common",
            "version": "2.2.0",
            "name": "php-cs-fixer/diff",
            "version": "v1.3.1",
            "source": {
                "type": "git",
                "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
                "url": "https://github.com/PHP-CS-Fixer/diff.git",
                "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
                "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/dbd31aeb251639ac0b9e7e29405c1441907f5759",
                "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759",
                "shasum": ""
            },
            "require": {
                "php": "^7.2 || ^8.0"
                "php": "^5.6 || ^7.0 || ^8.0"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-2.x": "2.x-dev"
                }
            "require-dev": {
                "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0",
                "symfony/process": "^3.3"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "phpDocumentor\\Reflection\\": "src/"
                }
                "classmap": [
                    "src/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
                "BSD-3-Clause"
            ],
            "authors": [
                {
                    "name": "Jaap van Otterdijk",
                    "email": "opensource@ijaap.nl"
                    "name": "Sebastian Bergmann",
                    "email": "sebastian@phpunit.de"
                },
                {
                    "name": "Kore Nordmann",
                    "email": "mail@kore-nordmann.de"
                },
                {
                    "name": "SpacePossum"
                }
            ],
            "description": "sebastian/diff v2 backport support for PHP5.6",
            "homepage": "https://github.com/PHP-CS-Fixer",
            "keywords": [
                "diff"
            ],
            "support": {
                "issues": "https://github.com/PHP-CS-Fixer/diff/issues",
                "source": "https://github.com/PHP-CS-Fixer/diff/tree/v1.3.1"
            },
            "time": "2020-10-14T08:39: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",


@@ 1081,16 1542,16 @@
        },
        {
            "name": "phpspec/prophecy",
            "version": "1.12.2",
            "version": "1.13.0",
            "source": {
                "type": "git",
                "url": "https://github.com/phpspec/prophecy.git",
                "reference": "245710e971a030f42e08f4912863805570f23d39"
                "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/245710e971a030f42e08f4912863805570f23d39",
                "reference": "245710e971a030f42e08f4912863805570f23d39",
                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea",
                "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea",
                "shasum": ""
            },
            "require": {


@@ 1142,9 1603,9 @@
            ],
            "support": {
                "issues": "https://github.com/phpspec/prophecy/issues",
                "source": "https://github.com/phpspec/prophecy/tree/1.12.2"
                "source": "https://github.com/phpspec/prophecy/tree/1.13.0"
            },
            "time": "2020-12-19T10:15:11+00:00"
            "time": "2021-03-17T13:42:18+00:00"
        },
        {
            "name": "phpunit/php-code-coverage",


@@ 1215,16 1676,16 @@
        },
        {
            "name": "phpunit/php-file-iterator",
            "version": "2.0.3",
            "version": "2.0.4",
            "source": {
                "type": "git",
                "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
                "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357"
                "reference": "28af674ff175d0768a5a978e6de83f697d4a7f05"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357",
                "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357",
                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/28af674ff175d0768a5a978e6de83f697d4a7f05",
                "reference": "28af674ff175d0768a5a978e6de83f697d4a7f05",
                "shasum": ""
            },
            "require": {


@@ 1263,7 1724,7 @@
            ],
            "support": {
                "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3"
                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.4"
            },
            "funding": [
                {


@@ 1271,7 1732,7 @@
                    "type": "github"
                }
            ],
            "time": "2020-11-30T08:25:21+00:00"
            "time": "2021-07-19T06:46:01+00:00"
        },
        {
            "name": "phpunit/php-text-template",


@@ 1379,16 1840,16 @@
        },
        {
            "name": "phpunit/php-token-stream",
            "version": "3.1.2",
            "version": "3.1.3",
            "source": {
                "type": "git",
                "url": "https://github.com/sebastianbergmann/php-token-stream.git",
                "reference": "472b687829041c24b25f475e14c2f38a09edf1c2"
                "reference": "9c1da83261628cb24b6a6df371b6e312b3954768"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2",
                "reference": "472b687829041c24b25f475e14c2f38a09edf1c2",
                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768",
                "reference": "9c1da83261628cb24b6a6df371b6e312b3954768",
                "shasum": ""
            },
            "require": {


@@ 1426,7 1887,7 @@
            ],
            "support": {
                "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
                "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.2"
                "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3"
            },
            "funding": [
                {


@@ 1435,7 1896,7 @@
                }
            ],
            "abandoned": true,
            "time": "2020-11-30T08:38:46+00:00"
            "time": "2021-07-26T12:15:06+00:00"
        },
        {
            "name": "phpunit/phpunit",


@@ 1526,6 1987,153 @@
            "time": "2020-01-08T08:45:45+00:00"
        },
        {
            "name": "psr/cache",
            "version": "1.0.1",
            "source": {
                "type": "git",
                "url": "https://github.com/php-fig/cache.git",
                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
                "shasum": ""
            },
            "require": {
                "php": ">=5.3.0"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.0.x-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "Psr\\Cache\\": "src/"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "PHP-FIG",
                    "homepage": "http://www.php-fig.org/"
                }
            ],
            "description": "Common interface for caching libraries",
            "keywords": [
                "cache",
                "psr",
                "psr-6"
            ],
            "support": {
                "source": "https://github.com/php-fig/cache/tree/master"
            },
            "time": "2016-08-06T20:24:11+00:00"
        },
        {
            "name": "psr/container",
            "version": "1.1.1",
            "source": {
                "type": "git",
                "url": "https://github.com/php-fig/container.git",
                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.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.1"
            },
            "time": "2021-03-05T17:36:06+00:00"
        },
        {
            "name": "psr/event-dispatcher",
            "version": "1.0.0",
            "source": {
                "type": "git",
                "url": "https://github.com/php-fig/event-dispatcher.git",
                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.0"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.0.x-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "Psr\\EventDispatcher\\": "src/"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "PHP-FIG",
                    "homepage": "http://www.php-fig.org/"
                }
            ],
            "description": "Standard interfaces for event handling.",
            "keywords": [
                "events",
                "psr",
                "psr-14"
            ],
            "support": {
                "issues": "https://github.com/php-fig/event-dispatcher/issues",
                "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
            },
            "time": "2019-01-08T18:20:26+00:00"
        },
        {
            "name": "sebastian/code-unit-reverse-lookup",
            "version": "1.0.2",
            "source": {


@@ 2190,41 2798,134 @@
            "time": "2016-10-03T07:35:21+00:00"
        },
        {
            "name": "symfony/polyfill-ctype",
            "version": "v1.22.1",
            "name": "symfony/console",
            "version": "v5.3.7",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-ctype.git",
                "reference": "c6c942b1ac76c82448322025e084cadc56048b4e"
                "url": "https://github.com/symfony/console.git",
                "reference": "8b1008344647462ae6ec57559da166c2bfa5e16a"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e",
                "reference": "c6c942b1ac76c82448322025e084cadc56048b4e",
                "url": "https://api.github.com/repos/symfony/console/zipball/8b1008344647462ae6ec57559da166c2bfa5e16a",
                "reference": "8b1008344647462ae6ec57559da166c2bfa5e16a",
                "shasum": ""
            },
            "require": {
                "php": ">=7.1"
                "php": ">=7.2.5",
                "symfony/deprecation-contracts": "^2.1",
                "symfony/polyfill-mbstring": "~1.0",
                "symfony/polyfill-php73": "^1.8",
                "symfony/polyfill-php80": "^1.16",
                "symfony/service-contracts": "^1.1|^2",
                "symfony/string": "^5.1"
            },
            "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",
                "symfony/dependency-injection": "^4.4|^5.0",
                "symfony/event-dispatcher": "^4.4|^5.0",
                "symfony/lock": "^4.4|^5.0",
                "symfony/process": "^4.4|^5.0",
                "symfony/var-dumper": "^4.4|^5.0"
            },
            "suggest": {
                "ext-ctype": "For best performance"
                "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.3.7"
            },
            "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-25T20:02:16+00:00"
        },
        {
            "name": "symfony/deprecation-contracts",
            "version": "v2.4.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/deprecation-contracts.git",
                "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627",
                "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627",
                "shasum": ""
            },
            "require": {
                "php": ">=7.1"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-main": "1.22-dev"
                    "dev-main": "2.4-dev"
                },
                "thanks": {
                    "name": "symfony/polyfill",
                    "url": "https://github.com/symfony/polyfill"
                    "name": "symfony/contracts",
                    "url": "https://github.com/symfony/contracts"
                }
            },
            "autoload": {
                "psr-4": {
                    "Symfony\\Polyfill\\Ctype\\": ""
                },
                "files": [
                    "bootstrap.php"
                    "function.php"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",


@@ 2233,24 2934,18 @@
            ],
            "authors": [
                {
                    "name": "Gert de Pagter",
                    "email": "BackEndTea@gmail.com"
                    "name": "Nicolas Grekas",
                    "email": "p@tchwork.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Symfony polyfill for ctype functions",
            "description": "A generic function and convention to trigger deprecation notices",
            "homepage": "https://symfony.com",
            "keywords": [
                "compatibility",
                "ctype",
                "polyfill",
                "portable"
            ],
            "support": {
                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1"
                "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0"
            },
            "funding": [
                {


@@ 2266,20 2961,1294 @@
                    "type": "tidelift"
                }
            ],
            "time": "2021-01-07T16:49:33+00:00"
            "time": "2021-03-23T23:28:01+00:00"
        },
        {
            "name": "theseer/tokenizer",
            "version": "1.2.0",
            "name": "symfony/event-dispatcher",
            "version": "v5.3.7",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/event-dispatcher.git",
                "reference": "ce7b20d69c66a20939d8952b617506a44d102130"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ce7b20d69c66a20939d8952b617506a44d102130",
                "reference": "ce7b20d69c66a20939d8952b617506a44d102130",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.5",
                "symfony/deprecation-contracts": "^2.1",
                "symfony/event-dispatcher-contracts": "^2",
                "symfony/polyfill-php80": "^1.16"
            },
            "conflict": {
                "symfony/dependency-injection": "<4.4"
            },
            "provide": {
                "psr/event-dispatcher-implementation": "1.0",
                "symfony/event-dispatcher-implementation": "2.0"
            },
            "require-dev": {
                "psr/log": "^1|^2|^3",
                "symfony/config": "^4.4|^5.0",
                "symfony/dependency-injection": "^4.4|^5.0",
                "symfony/error-handler": "^4.4|^5.0",
                "symfony/expression-language": "^4.4|^5.0",
                "symfony/http-foundation": "^4.4|^5.0",
                "symfony/service-contracts": "^1.1|^2",
                "symfony/stopwatch": "^4.4|^5.0"
            },
            "suggest": {
                "symfony/dependency-injection": "",
                "symfony/http-kernel": ""
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\EventDispatcher\\": ""
                },
                "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 that allow your application components to communicate with each other by dispatching events and listening to them",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/event-dispatcher/tree/v5.3.7"
            },
            "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-04T21:20:46+00:00"
        },
        {
            "name": "symfony/event-dispatcher-contracts",
            "version": "v2.4.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
                "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/69fee1ad2332a7cbab3aca13591953da9cdb7a11",
                "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.5",
                "psr/event-dispatcher": "^1"
            },
            "suggest": {
                "symfony/event-dispatcher-implementation": ""
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-main": "2.4-dev"
                },
                "thanks": {
                    "name": "symfony/contracts",
                    "url": "https://github.com/symfony/contracts"
                }
            },
            "autoload": {
                "psr-4": {
                    "Symfony\\Contracts\\EventDispatcher\\": ""
                }
            },
            "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 dispatching event",
            "homepage": "https://symfony.com",
            "keywords": [
                "abstractions",
                "contracts",
                "decoupling",
                "interfaces",
                "interoperability",
                "standards"
            ],
            "support": {
                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.4.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-03-23T23:28:01+00:00"
        },
        {
            "name": "symfony/filesystem",
            "version": "v5.3.4",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/filesystem.git",
                "reference": "343f4fe324383ca46792cae728a3b6e2f708fb32"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/filesystem/zipball/343f4fe324383ca46792cae728a3b6e2f708fb32",
                "reference": "343f4fe324383ca46792cae728a3b6e2f708fb32",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.5",
                "symfony/polyfill-ctype": "~1.8",
                "symfony/polyfill-php80": "^1.16"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\Filesystem\\": ""
                },
                "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 basic utilities for the filesystem",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/filesystem/tree/v5.3.4"
            },
            "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-21T12:40:44+00:00"
        },
        {
            "name": "symfony/finder",
            "version": "v5.3.7",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/finder.git",
                "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/finder/zipball/a10000ada1e600d109a6c7632e9ac42e8bf2fb93",
                "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.5",
                "symfony/polyfill-php80": "^1.16"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\Finder\\": ""
                },
                "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": "Finds files and directories via an intuitive fluent interface",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/finder/tree/v5.3.7"
            },
            "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-04T21:20:46+00:00"
        },
        {
            "name": "symfony/options-resolver",
            "version": "v5.3.7",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/options-resolver.git",
                "reference": "4b78e55b179003a42523a362cc0e8327f7a69b5e"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/options-resolver/zipball/4b78e55b179003a42523a362cc0e8327f7a69b5e",
                "reference": "4b78e55b179003a42523a362cc0e8327f7a69b5e",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.5",
                "symfony/deprecation-contracts": "^2.1",
                "symfony/polyfill-php73": "~1.0",
                "symfony/polyfill-php80": "^1.16"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\OptionsResolver\\": ""
                },
                "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 an improved replacement for the array_replace PHP function",
            "homepage": "https://symfony.com",
            "keywords": [
                "config",
                "configuration",
                "options"
            ],
            "support": {
                "source": "https://github.com/symfony/options-resolver/tree/v5.3.7"
            },
            "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-04T21:20:46+00:00"
        },
        {
            "name": "symfony/polyfill-ctype",
            "version": "v1.23.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-ctype.git",
                "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
                "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
                "shasum": ""
            },
            "require": {
                "php": ">=7.1"
            },
            "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": {
                "psr-4": {
                    "Symfony\\Polyfill\\Ctype\\": ""
                },
                "files": [
                    "bootstrap.php"
                ]
            },
            "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.23.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-intl-grapheme",
            "version": "v1.23.1",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
                "reference": "16880ba9c5ebe3642d1995ab866db29270b36535"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/16880ba9c5ebe3642d1995ab866db29270b36535",
                "reference": "16880ba9c5ebe3642d1995ab866db29270b36535",
                "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": {
                "psr-4": {
                    "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
                },
                "files": [
                    "bootstrap.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": "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.23.1"
            },
            "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-05-27T12:26:48+00:00"
        },
        {
            "name": "symfony/polyfill-intl-normalizer",
            "version": "v1.23.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": {
                "psr-4": {
                    "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
                },
                "files": [
                    "bootstrap.php"
                ],
                "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.23.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.23.1",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-mbstring.git",
                "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
                "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
                "shasum": ""
            },
            "require": {
                "php": ">=7.1"
            },
            "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": {
                "psr-4": {
                    "Symfony\\Polyfill\\Mbstring\\": ""
                },
                "files": [
                    "bootstrap.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": "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.23.1"
            },
            "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-05-27T12:26:48+00:00"
        },
        {
            "name": "symfony/polyfill-php70",
            "version": "v1.20.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-php70.git",
                "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/5f03a781d984aae42cebd18e7912fa80f02ee644",
                "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644",
                "shasum": ""
            },
            "require": {
                "php": ">=7.1"
            },
            "type": "metapackage",
            "extra": {
                "branch-alias": {
                    "dev-main": "1.20-dev"
                },
                "thanks": {
                    "name": "symfony/polyfill",
                    "url": "https://github.com/symfony/polyfill"
                }
            },
            "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.0+ features to lower PHP versions",
            "homepage": "https://symfony.com",
            "keywords": [
                "compatibility",
                "polyfill",
                "portable",
                "shim"
            ],
            "support": {
                "source": "https://github.com/symfony/polyfill-php70/tree/v1.20.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": "2020-10-23T14:02:19+00:00"
        },
        {
            "name": "symfony/polyfill-php72",
            "version": "v1.23.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-php72.git",
                "reference": "9a142215a36a3888e30d0a9eeea9766764e96976"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976",
                "reference": "9a142215a36a3888e30d0a9eeea9766764e96976",
                "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": {
                "psr-4": {
                    "Symfony\\Polyfill\\Php72\\": ""
                },
                "files": [
                    "bootstrap.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": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
            "homepage": "https://symfony.com",
            "keywords": [
                "compatibility",
                "polyfill",
                "portable",
                "shim"
            ],
            "support": {
                "source": "https://github.com/symfony/polyfill-php72/tree/v1.23.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-05-27T09:17:38+00:00"
        },
        {
            "name": "symfony/polyfill-php73",
            "version": "v1.23.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-php73.git",
                "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010",
                "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010",
                "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": {
                "psr-4": {
                    "Symfony\\Polyfill\\Php73\\": ""
                },
                "files": [
                    "bootstrap.php"
                ],
                "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.23.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-php80",
            "version": "v1.23.1",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-php80.git",
                "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
                "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
                "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": {
                "psr-4": {
                    "Symfony\\Polyfill\\Php80\\": ""
                },
                "files": [
                    "bootstrap.php"
                ],
                "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.23.1"
            },
            "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-28T13:41:28+00:00"
        },
        {
            "name": "symfony/process",
            "version": "v5.3.7",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/process.git",
                "reference": "38f26c7d6ed535217ea393e05634cb0b244a1967"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/process/zipball/38f26c7d6ed535217ea393e05634cb0b244a1967",
                "reference": "38f26c7d6ed535217ea393e05634cb0b244a1967",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.5",
                "symfony/polyfill-php80": "^1.16"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\Process\\": ""
                },
                "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": "Executes commands in sub-processes",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/process/tree/v5.3.7"
            },
            "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-04T21:20:46+00:00"
        },
        {
            "name": "symfony/service-contracts",
            "version": "v2.4.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/service-contracts.git",
                "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
                "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.5",
                "psr/container": "^1.1"
            },
            "suggest": {
                "symfony/service-implementation": ""
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-main": "2.4-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.4.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-04-01T10:43:52+00:00"
        },
        {
            "name": "symfony/stopwatch",
            "version": "v5.3.4",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/stopwatch.git",
                "reference": "b24c6a92c6db316fee69e38c80591e080e41536c"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b24c6a92c6db316fee69e38c80591e080e41536c",
                "reference": "b24c6a92c6db316fee69e38c80591e080e41536c",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.5",
                "symfony/service-contracts": "^1.0|^2"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\Stopwatch\\": ""
                },
                "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 a way to profile code",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/stopwatch/tree/v5.3.4"
            },
            "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-10T08:58:57+00:00"
        },
        {
            "name": "symfony/string",
            "version": "v5.3.7",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/string.git",
                "reference": "8d224396e28d30f81969f083a58763b8b9ceb0a5"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/string/zipball/8d224396e28d30f81969f083a58763b8b9ceb0a5",
                "reference": "8d224396e28d30f81969f083a58763b8b9ceb0a5",
                "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"
            },
            "require-dev": {
                "symfony/error-handler": "^4.4|^5.0",
                "symfony/http-client": "^4.4|^5.0",
                "symfony/translation-contracts": "^1.1|^2",
                "symfony/var-exporter": "^4.4|^5.0"
            },
            "type": "library",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\String\\": ""
                },
                "files": [
                    "Resources/functions.php"
                ],
                "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.3.7"
            },
            "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-26T08:00:08+00:00"
        },
        {
            "name": "theseer/tokenizer",
            "version": "1.2.1",
            "source": {
                "type": "git",
                "url": "https://github.com/theseer/tokenizer.git",
                "reference": "75a63c33a8577608444246075ea0af0d052e452a"
                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
                "reference": "75a63c33a8577608444246075ea0af0d052e452a",
                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
                "shasum": ""
            },
            "require": {


@@ 2308,7 4277,7 @@
            "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/master"
                "source": "https://github.com/theseer/tokenizer/tree/1.2.1"
            },
            "funding": [
                {


@@ 2316,7 4285,7 @@
                    "type": "github"
                }
            ],
            "time": "2020-07-12T23:59:07+00:00"
            "time": "2021-07-28T10:34:58+00:00"
        },
        {
            "name": "webmozart/assert",


@@ 2397,5 4366,5 @@
    "platform-dev": {
        "ext-json": "*"
    },
    "plugin-api-version": "2.0.0"
    "plugin-api-version": "2.1.0"
}

M locale/CREDITS.md => locale/CREDITS.md +2 -0
@@ 10,3 10,5 @@
* `et_EE`: Anne Märdimäe (EENet of HITSA)
* `de_DE`: Fred-Oliver Jury (University of applied Sciences Osnabrück), René-Maximilian Malsky (Universität Osnabrück)
* `pt_PT`: Raul Ferreira, Marco Teixeira (Universidade do Minho)
* `es_LA`: Carlos Pedreros (Universidad de La Serena, Chile)
* `ro_RO`: Vlăduț Butnaru (Gheorghe Asachi Technical University of Iaşi)

M locale/ar_MA.php => locale/ar_MA.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => 'الاتصالات النشطة',
    //'ACL Permission List' => '',
    'Account' => 'حساب',


@@ 39,11 41,14 @@ return [
    //'DNS Search Domain(s)' => '',
    //'DNS Server(s)' => '',
    //'DNS Suffix' => '',
    //'Danger Zone' => '',
    'Date' => 'التاريخ',
    'Date/Time' => 'التاريخ/الوقت',
    //'Default Gateway' => '',
    'Delete' => 'حذف',
    'Delete TOTP Secret' => 'حذف سر كلمة المرور وحيدة الاستخدام المؤقتة',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'تفاصيل',
    'Disable User' => 'تعطيل المستخدم',
    'Disabled' => 'معطل',

M locale/da_DK.php => locale/da_DK.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => 'Aktive forbindelser',
    //'ACL Permission List' => '',
    'Account' => 'Konto',


@@ 39,11 41,14 @@ return [
    //'DNS Search Domain(s)' => '',
    //'DNS Server(s)' => '',
    //'DNS Suffix' => '',
    //'Danger Zone' => '',
    'Date' => 'Dato',
    'Date/Time' => 'Dato/klokkeslæt',
    //'Default Gateway' => '',
    'Delete' => 'Slet',
    'Delete TOTP Secret' => 'Slet TOTP-hemmelighed',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'Detaljer',
    'Disable User' => 'Deaktiver bruger',
    'Disabled' => 'Deaktiveret',

M locale/de_DE.php => locale/de_DE.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => 'Aktive Verbindungen',
    //'ACL Permission List' => '',
    'Account' => 'Benutzerkonto',


@@ 39,11 41,14 @@ return [
    //'DNS Search Domain(s)' => '',
    //'DNS Server(s)' => '',
    //'DNS Suffix' => '',
    //'Danger Zone' => '',
    'Date' => 'Datum',
    'Date/Time' => 'Datum/Zeit',
    //'Default Gateway' => '',
    'Delete' => 'Löschen',
    'Delete TOTP Secret' => 'Lösche TOTP-Geheimnis',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'Details...',
    'Disable User' => 'Deaktiviere Benutzer',
    'Disabled' => 'Deaktiviert',

M locale/empty.php => locale/empty.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    '"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    '"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => '',
    'ACL Permission List' => '',
    'Account' => '',


@@ 39,11 41,14 @@ return [
    'DNS Search Domain(s)' => '',
    'DNS Server(s)' => '',
    'DNS Suffix' => '',
    'Danger Zone' => '',
    'Date' => '',
    'Date/Time' => '',
    'Default Gateway' => '',
    'Delete' => '',
    'Delete TOTP Secret' => '',
    'Delete User' => '',
    'Delete User Data' => '',
    'Details...' => '',
    'Disable User' => '',
    'Disabled' => '',

A locale/es_LA.php => locale/es_LA.php +163 -0
@@ 0,0 1,163 @@
<?php

/*
 * eduVPN - End-user friendly VPN.
 *
 * Copyright: 2016-2019, The Commons Conservancy eduVPN Programme
 * SPDX-License-Identifier: AGPL-3.0+
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => ' Número de Conexiones Activas',
    'ACL Permission List' => 'Lista de Permisos ACL',
    'Account' => 'Cuenta',
    'Active' => 'Activo',
    'An application attempts to establish a VPN connection.' => 'Una aplicación intenta establecer una conexión VPN.',
    'An error occurred.' => 'Ocurrió un error.',
    'Application Usage' => 'Uso de la Aplicación',
    'Approve' => 'Aprobar',
    'Approve Application' => 'Aprobar Aplicación',
    'Authorized' => 'Autorizado',
    'Authorized Applications' => 'Aplicaciones Autorizadas',
    'Block LAN' => 'Bloquear LAN',
    'CA' => 'CA',
    'Certificates' => 'Certificados',
    'Change Password' => 'Cambiar Contraseña',
    'Client Lost' => 'Cliente Perdido',
    'Client-to-client' => 'Cliente-a-cliente',
    'Configurations' => 'Configuraciones',
    'Confirm' => 'Confirmar',
    'Connected' => 'Conectado',
    'Connections' => 'Conexiones',
    'Contact support if you lost your TOTP.' => 'Comuníquese con el soporte técnico si perdió su TOTP.',
    'Create' => 'Crear',
    'Create and Download' => 'Crear y Descargar',
    'Created' => 'Creado',
    'Current Password' => 'Contraseña Actual',
    'Current Password not correct!' => '¡Contraseña no es correcta!',
    'DNS Domain' => 'Dominio DNS',
    'DNS Search Domain(s)' => 'Dominio(s) de Búsqueda DNS',
    'DNS Server(s)' => 'Servidor(es) DNS',
    'DNS Suffix' => 'Sufijo DNS',
    //'Danger Zone' => '',
    'Date' => 'Fecha',
    'Date/Time' => 'Fecha/Hora',
    'Default Gateway' => 'Gateway por Defecto',
    'Delete' => 'Borrar',
    'Delete TOTP Secret' => 'Borrar Secret TOTP',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'Detalles...',
    'Disable User' => 'Usuario Deshabilitado',
    'Disabled' => 'Deshabilitado',
    'Disconnected' => 'Desconectado',
    'Distribution of unique users over the VPN applications.' => 'Distribución de usuarios únicos sobre las aplicaciones VPN.',
    'Documentation' => 'Documentación',
    'Enable ACL' => 'Habilitar ACL',
    'Enable Log' => 'Habilitar Log',
    'Enable User' => 'Habilitar Usuario',
    'Enroll' => 'Registrar',
    'Error' => 'Error',
    'Events' => 'Eventos',
    'Existing' => 'Existente',
    'Expires' => 'Expira',
    'Find the user identifier that used an IPv4 or IPv6 address at a particular point in time.' => 'Busque el identificador de usuario que utilizó una dirección IPv4 o IPv6 en un momento determinado.',
    'Here you can change the password for your account.' => 'Aquí puede cambiar la contraseña de su cuenta.',
    'Here you can enroll for two-factor authentication (2FA) using a Time-based One-time Password (TOTP).' => 'Aquí puede registrar para la autenticación de dos factores (2FA) utilizando una contraseña de un solo uso basada en el tiempo (TOTP).',
    'Hide Profile' => 'Esconder perfil',
    'Highest (Maximum) # Concurrent Connections' => 'Número más alto (máximo) de conexiones simultáneas',
    'Home' => 'Inicio',
    'Hostname' => 'Hostname',
    'IP Address' => 'Dirección IP',
    'IPs' => 'IP’s',
    'IPv4 Prefix' => 'Prefijo IPv4',
    'IPv6 Prefix' => 'Prefijo IPv6',
    'In order to continue, you must first enroll for Two-factor authentication!' => '¡Para continuar, primero debe inscribir la autenticación de dos factores!',
    'Info' => 'Info',
    'Key Type' => 'Key Type',
    'Legacy' => 'Legacy',
    'Listen IP' => 'Listen IP',
    'Log' => 'Log',
    'Management IP' => 'IP de Administración',
    'Managing user <code>%userId%</code>.' => 'Administrando usuario <code>%userId%</code>.',
    'Manual Configuration Download' => 'Descarga de configuración manual',
    'Manually create and download an OpenVPN configuration file for use in your OpenVPN client.' => 'Cree y descargue manualmente un archivo de configuración de OpenVPN para usar en su cliente OpenVPN.',
    'Message' => 'Mensaje',
    'Message of the Day' => 'Mensaje del Día',
    'Messages' => 'Mensajes',
    'N/A' => 'N/A',
    'Name' => 'Nombre',
    'New Password' => ' Nueva Contraseña',
    'New Password (confirm)' => 'Nueva Contraseña (confirmar)',
    'New Password and New Password (confirm) MUST match!' => '¡La Nueva Contraseña y la Nueva Contraseña (confirmar) DEBEN coincidir!',
    'No' => 'No',
    'No VPN profiles are available for your account.' => 'No hay perfiles VPN disponibles para su cuenta.',
    'No authorized applications yet.' => 'Aún no hay aplicaciones autorizadas.',
    'No clients connected.' => 'No hay clientes conectados.',
    'No connections yet.' => 'Aún no hay conexiones.',
    'No events yet.' => 'Aún no hay eventos.',
    'No user account(s) to show.' => 'No hay cuenta(s) de usuario para mostrar.',
    'Number of unique users of the VPN service over the last month.' => 'Número de usuarios únicos del servicio VPN durante el último mes.',
    'OTP' => 'OTP',
    'Offered Protocols/Ports' => 'Protocolos/Puertos Ofrecidos',
    'Only approve this when you are trying to establish a VPN connection with this application!' => '¡Apruebe esto solo cuando intente establecer una conexión VPN con esta aplicación!',
    'Password' => 'Contraseña',
    'Permission(s)' => 'Permiso(s)',
    'Please provide your TOTP.' => 'Proporcione su TOTP.',
    'Please sign in with your username and password.' => 'Inicie sesión con su nombre de usuario y contraseña.',
    'Profile' => 'Perfil',
    'Profile Number' => 'Número Perfil',
    'Profile Usage' => 'Uso de Perfil',
    'Profiles' => 'Perfiles',
    'Protocols/Ports' => 'Protocolo/Puertos',
    'QR' => 'QR',
    'Results' => 'Resultados',
    'Revoke' => 'Revocar',
    'Route(s)' => 'Ruta(s)',
    'Scan the QR code using your 2FA application, or import the secret manually. Confirm the successful configuration by entering a 6 digit OTP generated by the application below.' => 'Escanee el código QR con su aplicación 2FA o importe el secreto manualmente. Confirme la configuración exitosa ingresando una OTP de 6 dígitos generada por la aplicación a continuación.',
    'Search' => 'Buscar',
    'Secret' => 'Secreto',
    'See the <a href="documentation#2fa">documentation</a> for more information on 2FA and a list of applications to use.' => 'Consulte la <a href="documentation#2fa">documentación</a> para obtener más información sobre 2FA y una lista de aplicaciones que puede utilizar.',
    'Select a profile and choose a name, e.g. "Phone".' => 'Seleccione un perfil y elija un nombre, p. ej. "Teléfono".',
    'Server' => 'Servidor',
    'Set' => 'Definir',
    'Sign In' => 'Ingresar',
    'Sign Out' => 'Salir',
    'Stats' => 'Estadísticas',
    'Summary' => 'Resumen',
    'TLS >= 1.3' => 'TLS >= 1.3',
    'TOTP' => 'TOTP',
    'The <em>Date/Time</em> field accepts dates of the format <code>Y-m-d H:i:s</code>, e.g. <code>2019-01-01 08:00:00</code>.' => 'El campo <em>Fecha/Hora</em> acepta fechas con el formato <code>Y-m-d H:i:s</code>, p. ej. <code>2019-01-01 08:00:00</code>.',
    'The OTP key you entered does not match the expected value for this OTP secret.' => 'La clave de OTP que ingresó no coincide con el valor esperado para este secreto de OTP.',
    'The TOTP you provided is incorrect.' => 'El TOTP que proporcionó es incorrecto.',
    'The credentials you provided were not correct.' => 'Las credenciales que proporcionó no eran correctas.',
    'The list of applications you authorized to create a VPN connection.' => 'La lista de aplicaciones que autorizó para crear una conexión VPN.',
    'The most recent events related to this account.' => 'Los eventos más recientes relacionados con esta cuenta.',
    'The most recent, concluded, VPN connections with this account.' => 'Las conexiones VPN más recientes y concluidas con esta cuenta.',
    'There are no results matching your criteria.' => 'No hay resultados que coincidan con sus criterios.',
    'This message will be shown on the "Home" screen.' => 'Este mensaje se mostrará en la pantalla "Inicio".',
    'This user does not have any active certificates.' => 'Este usuario no tiene ningún certificado activo.',
    'To prevent malicious applications from secretly establishing a VPN connection on your behalf, you have to explicitly approve this application first.' => 'Para evitar que aplicaciones maliciosas establezcan en secreto una conexión VPN en su nombre, primero debe aprobar explícitamente esta aplicación.',
    'Total # Unique Users' => 'Número Total de Usuarios Únicos',
    'Total Traffic' => 'Tráfico Total',
    'Two-factor Authentication' => 'Autenticación de dos factores',
    'Two-factor Enrollment' => 'Registro de dos factores',
    'Two-factor authentication (2FA) is disabled by the administrator.' => 'La autenticación de dos factores (2FA) está deshabilitada por el administrador.',
    'Unregistered Client' => 'Cliente sin Registar',
    'User ID' => 'ID de Usuario',
    'Username' => 'Nombre de Usuario',
    'Users' => 'Usuarios',
    'VPN Portal' => 'Portal VPN',
    'VPN traffic over the last month.' => 'Tráfico VPN durante el último mes.',
    'Verify' => 'Verificar',
    'Version' => 'Versión',
    'Welcome to this VPN service!' => '¡Bienvenido a este servicio VPN!',
    'Why is this necessary?' => '¿Por qué es esto necesario?',
    'Yes' => 'Si',
    'You are already enrolled for Two-factor authentication (2FA).' => 'Ya está registrado para la autenticación de dos factores (2FA).',
    'You cannot manage your own user account.' => 'No puede administrar su propia cuenta de usuario.',
    'Your ID is <code>%_two_factor_user_id%</code>.' => 'Su ID es <code>%_two_factor_user_id%</code>.',
    'Your new configuration will expire on %expiryDate%. Come back here to obtain a new configuration after expiry!' => 'Su nueva configuración caducará el %expiryDate%. ¡Vuelva aquí para obtener una nueva configuración después del vencimiento!',
];

M locale/et_EE.php => locale/et_EE.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    //'#Active Connections' => '',
    //'ACL Permission List' => '',
    'Account' => 'Konto',


@@ 39,11 41,14 @@ return [
    //'DNS Search Domain(s)' => '',
    //'DNS Server(s)' => '',
    //'DNS Suffix' => '',
    //'Danger Zone' => '',
    'Date' => 'Kuupäev',
    'Date/Time' => 'Kuupäev/kellaaeg',
    //'Default Gateway' => '',
    'Delete' => 'Kustuta',
    'Delete TOTP Secret' => 'Kustuta ajapõhise ühekordse parooli (TOTP) saladus',
    //'Delete User' => '',
    //'Delete User Data' => '',
    //'Details...' => '',
    'Disable User' => 'Desaktiveeri kasutaja',
    'Disabled' => 'Desaktiveeritud',

M locale/fr_FR.php => locale/fr_FR.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => 'Nombre de connexions actives',
    //'ACL Permission List' => '',
    'Account' => 'Compte',


@@ 39,11 41,14 @@ return [
    //'DNS Search Domain(s)' => '',
    //'DNS Server(s)' => '',
    //'DNS Suffix' => '',
    //'Danger Zone' => '',
    'Date' => 'Date',
    'Date/Time' => 'Date/Heure',
    //'Default Gateway' => '',
    'Delete' => 'Supprimer',
    'Delete TOTP Secret' => 'Supprimer le secret TOTP',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'Détails…',
    'Disable User' => 'Désactiver l\'utilisateur',
    'Disabled' => 'Désactivé',

M locale/nb_NO.php => locale/nb_NO.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => '#Aktive tilkoblinger',
    //'ACL Permission List' => '',
    'Account' => 'Konto',


@@ 39,11 41,14 @@ return [
    //'DNS Search Domain(s)' => '',
    //'DNS Server(s)' => '',
    //'DNS Suffix' => '',
    //'Danger Zone' => '',
    'Date' => 'Dato',
    'Date/Time' => 'Dato/Tid',
    //'Default Gateway' => '',
    'Delete' => 'Slett',
    'Delete TOTP Secret' => 'Slett TOTP Hemmelighet',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'Detaljer…',
    'Disable User' => 'Sperr Bruker',
    'Disabled' => 'Sperret',

M locale/nl_NL.php => locale/nl_NL.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    '"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '"Verwijder gebruikersdata" verwijdert enkel de data van het gebruikersaccount, maar logt de gebruiker NIET uit als ze momenteel zijn ingelogd, en voorkomt ook niet dat de gebruiker opnieuw kan inloggen!',
    '"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '"Verwijder gebruiker" verwijdert enkel het account en bijhorende data, maar logt de gebruiker NIET uit als deze momenteel is ingelogd!',
    '#Active Connections' => '#Actieve verbindingen',
    'ACL Permission List' => 'ACL-lijst',
    'Account' => 'Account',


@@ 39,11 41,14 @@ return [
    'DNS Search Domain(s)' => 'DNS-zoekdomein(en)',
    'DNS Server(s)' => 'DNS-server(s)',
    'DNS Suffix' => 'DNS-suffix',
    'Danger Zone' => 'Gevarenzone',
    'Date' => 'Datum',
    'Date/Time' => 'Datum/Tijd',
    'Default Gateway' => 'Default Gateway',
    'Delete' => 'Verwijderen',
    'Delete TOTP Secret' => 'Verwijder TOTP-geheim',
    'Delete User' => 'Verwijder gebruiker',
    'Delete User Data' => 'Verwijder gebruikersdata',
    'Details...' => 'Details...',
    'Disable User' => 'Blokkeer account',
    'Disabled' => 'Uitgeschakeld',

M locale/pl_PL.php => locale/pl_PL.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => 'Liczba aktywnych połączeń',
    //'ACL Permission List' => '',
    'Account' => 'Konto',


@@ 39,11 41,14 @@ return [
    //'DNS Search Domain(s)' => '',
    //'DNS Server(s)' => '',
    //'DNS Suffix' => '',
    //'Danger Zone' => '',
    'Date' => 'Data',
    'Date/Time' => 'Data/Czas',
    //'Default Gateway' => '',
    'Delete' => 'Skasuj',
    'Delete TOTP Secret' => 'Skasuj sekrety TOTP',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'Szczegóły...',
    'Disable User' => 'Zablokuj użytkownika',
    'Disabled' => 'Zablokowany',

M locale/pt_PT.php => locale/pt_PT.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => '# Ligações ativas',
    //'ACL Permission List' => '',
    'Account' => 'Conta',


@@ 39,11 41,14 @@ return [
    //'DNS Search Domain(s)' => '',
    //'DNS Server(s)' => '',
    //'DNS Suffix' => '',
    //'Danger Zone' => '',
    'Date' => 'Data',
    'Date/Time' => 'Data/Hora',
    //'Default Gateway' => '',
    'Delete' => 'Apagar',
    'Delete TOTP Secret' => 'Apagar o segredo TOTP',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'Detalhes...',
    'Disable User' => 'Desativar utilizador',
    'Disabled' => 'Desativado',

A locale/ro_RO.php => locale/ro_RO.php +163 -0
@@ 0,0 1,163 @@
<?php

/*
 * eduVPN - End-user friendly VPN.
 *
 * Copyright: 2016-2019, The Commons Conservancy eduVPN Programme
 * SPDX-License-Identifier: AGPL-3.0+
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => 'Numărul de conexiuni active',
    'ACL Permission List' => 'Lista de permisiuni (ACL)',
    'Account' => 'Cont',
    'Active' => 'Activ',
    'An application attempts to establish a VPN connection.' => 'O aplicație încearcă să stabilească o conexiune VPN.',
    'An error occurred.' => 'A apărut o eroare.',
    'Application Usage' => 'Aplicații utilizate',
    'Approve' => 'Permite',
    'Approve Application' => 'Aprobă aplicația',
    'Authorized' => 'Autorizat',
    'Authorized Applications' => 'Aplicații autorizate',
    'Block LAN' => 'Blocare LAN',
    'CA' => 'CA',
    'Certificates' => 'Certificate',
    'Change Password' => 'Schimbă parola',
    'Client Lost' => 'Client pierdut',
    'Client-to-client' => 'Client-la-client',
    'Configurations' => 'Configurare',
    'Confirm' => 'Confirmă',
    'Connected' => 'Conectat',
    'Connections' => 'Conexiuni',
    'Contact support if you lost your TOTP.' => 'Contactează echipa de asistență dacă ai pierdut codul unic (TOTP).',
    'Create' => 'Creează',
    'Create and Download' => 'Creează si descarcă',
    'Created' => 'Creat',
    'Current Password' => 'Parolă curentă',
    'Current Password not correct!' => 'Parola curentă nu este corectă!',
    'DNS Domain' => 'Domeniu DNS',
    'DNS Search Domain(s)' => 'Domeniu de cautare DNS',
    'DNS Server(s)' => 'Server DNS',
    'DNS Suffix' => 'Sufix DNS',
    //'Danger Zone' => '',
    'Date' => 'Dată',
    'Date/Time' => 'Dată/Oră',
    'Default Gateway' => 'Gateway implicit',
    'Delete' => 'Șterge',
    'Delete TOTP Secret' => 'Șterge codul TOTP.',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'Detalii...',
    'Disable User' => 'Dezactivează utilizator',
    'Disabled' => 'Dezactivat',
    'Disconnected' => 'Deconectat',
    'Distribution of unique users over the VPN applications.' => 'Distribuirea utilizatorilor unici pe aplicațiile VPN.',
    'Documentation' => 'Documentație',
    'Enable ACL' => 'Liste de permisiuni (ACL) active',
    'Enable Log' => 'Jurnal activ',
    'Enable User' => 'Activează utilizator',
    'Enroll' => 'Înscriere',
    'Error' => 'Eroare',
    'Events' => 'Evenimente',
    'Existing' => 'Existente',
    'Expires' => 'Expiră',
    'Find the user identifier that used an IPv4 or IPv6 address at a particular point in time.' => 'Găsește utilizatorul care a folosit o adresă IPv4 sau IPv6 la un moment dat.',
    'Here you can change the password for your account.' => 'Aici poți schimba parola contului tău.',
    'Here you can enroll for two-factor authentication (2FA) using a Time-based One-time Password (TOTP).' => 'Aici poți activa autentificarea în doi pași (2FA) folosind un cod unic (TOTP).',
    'Hide Profile' => 'Ascunde profilul',
    'Highest (Maximum) # Concurrent Connections' => 'Numărul maxim de conexiuni simultane',
    'Home' => 'Acasă',
    'Hostname' => 'Hostname',
    'IP Address' => 'Adresa IP',
    'IPs' => 'IPs',
    'IPv4 Prefix' => 'Prefix IPv4',
    'IPv6 Prefix' => 'Prefix IPv6',
    'In order to continue, you must first enroll for Two-factor authentication!' => 'Pentru a continua, trebuie să activezi autentificarea în doi pași.',
    'Info' => 'Informații',
    'Key Type' => 'Tip cheie',
    'Legacy' => 'Legacy',
    'Listen IP' => 'Listen IP',
    'Log' => 'Jurnal',
    'Management IP' => 'Adresa IP de administrare',
    'Managing user <code>%userId%</code>.' => 'Gestionezi utilizatorul: <code>%userId%</code>.',
    'Manual Configuration Download' => 'Descarcă setări manuale',
    'Manually create and download an OpenVPN configuration file for use in your OpenVPN client.' => 'Creează și descarcă manual un fișier de configurare pentru a fi folosit în clientul tău de OpenVPN.',
    'Message' => 'Mesaj',
    'Message of the Day' => 'Mesajul zilei',
    'Messages' => 'Mesaje',
    'N/A' => 'Indisponibil',
    'Name' => 'Nume',
    'New Password' => 'Parolă nouă',
    'New Password (confirm)' => 'Confirmă parola',
    'New Password and New Password (confirm) MUST match!' => 'Parola din câmpul "Parolă nouă" și cea din câmpul "Confirmă parola" trebuie să conincidă!',
    'No' => 'Nu',
    'No VPN profiles are available for your account.' => 'Nu există profiluri VPN disponibile pentru contul tău.',
    'No authorized applications yet.' => 'Nici o aplicație autorizată momentan.',
    'No clients connected.' => 'Niciun client conectat.',
    'No connections yet.' => 'Nicio conexiune momentan.',
    'No events yet.' => 'Niciun eveniment momentan.',
    'No user account(s) to show.' => 'Niciun utilizator de afișat.',
    'Number of unique users of the VPN service over the last month.' => 'Numărul de utilizatori unici ai serviciului VPN în ultima lună.',
    'OTP' => 'OTP',
    'Offered Protocols/Ports' => 'Protocoale/Porturi oferite',
    'Only approve this when you are trying to establish a VPN connection with this application!' => 'Aprobă acest lucru numai atunci când încerci să stabilești o conexiune VPN cu această aplicație!',
    'Password' => 'Parolă',
    'Permission(s)' => 'Permisiuni',
    'Please provide your TOTP.' => 'Te rog să introduci codul unic (TOTP)',
    'Please sign in with your username and password.' => 'Conectează-te cu numele și parola ta de utilizator.',
    'Profile' => 'Profil',
    'Profile Number' => 'Număr profil',
    'Profile Usage' => 'Utilizare profil',
    'Profiles' => 'Profiluri',
    'Protocols/Ports' => 'Protocoale/Porturi',
    'QR' => 'QR',
    'Results' => 'Rezultate',
    'Revoke' => 'Revocă',
    'Route(s)' => 'Rute',
    'Scan the QR code using your 2FA application, or import the secret manually. Confirm the successful configuration by entering a 6 digit OTP generated by the application below.' => 'Scanează codul QR folosind aplicația ta 2FA sau adaugă codul manual. Confirmă configurarea introducând codul de 6 cifre generat de aplicația ta.',
    'Search' => 'Caută',
    'Secret' => 'Secret',
    'See the <a href="documentation#2fa">documentation</a> for more information on 2FA and a list of applications to use.' => 'Consultă <a href="documentation#2fa">documentația</a> pentru mai multe informații despre 2FA și pentru o listă cu aplicații de utilizat.',
    'Select a profile and choose a name, e.g. "Phone".' => 'Selectează un profil și alege un nume, de ex: "Telefon',
    'Server' => 'Server',
    'Set' => 'Aplică',
    'Sign In' => 'Conectare',
    'Sign Out' => 'Deconectare',
    'Stats' => 'Statistici',
    'Summary' => 'Rezumat',
    'TLS >= 1.3' => 'TLS >= 1.3',
    'TOTP' => 'TOTP',
    'The <em>Date/Time</em> field accepts dates of the format <code>Y-m-d H:i:s</code>, e.g. <code>2019-01-01 08:00:00</code>.' => 'Câmpul <em>Dată/Oră</em> acceptă date în formatul <code>Y-m-d H:i:s</code>, de exemplu: <code>2019-12-23 08:00:00</code>',
    'The OTP key you entered does not match the expected value for this OTP secret.' => 'Codul OTP pe care l-ai introdus nu se potrivește cu valoarea așteptată pentru codul de mai sus (Secret).',
    'The TOTP you provided is incorrect.' => 'Codul unic (TOTP) pe care l-ai introdus este incorect.',
    'The credentials you provided were not correct.' => 'Numele de utilizator și/sau parola pe care le-ai introdus nu sunt corecte.',
    'The list of applications you authorized to create a VPN connection.' => 'Lista aplicațiilor pe care le-ai autorizat să creeze o conexiune VPN.',
    'The most recent events related to this account.' => 'Cele mai recente evenimente asociate acestui cont.',
    'The most recent, concluded, VPN connections with this account.' => 'Cele mai recente conexiuni VPN finalizate asociate acestui cont.',
    'There are no results matching your criteria.' => 'Nu există rezultate care să corespundă criteriilor tale.',
    'This message will be shown on the "Home" screen.' => 'Acest mesaj va fi afișat pe pagina "Acasă".',
    'This user does not have any active certificates.' => 'Acest utilizator nu are niciun certificat activ.',
    'To prevent malicious applications from secretly establishing a VPN connection on your behalf, you have to explicitly approve this application first.' => 'Pentru a preveni aplicațiile rău intenționate să stabilească în secret o conexiune VPN în numele tău, trebuie să aprobi în mod explicit această aplicație.',
    'Total # Unique Users' => 'Numărul total de utiliaztori unici.',
    'Total Traffic' => 'Trafic total',
    'Two-factor Authentication' => 'Autentificare în doi pași',
    'Two-factor Enrollment' => 'Activare autentificare în doi pași',
    'Two-factor authentication (2FA) is disabled by the administrator.' => 'Autentificarea în doi pași este dezactivată de către administrator.',
    'Unregistered Client' => 'Client neînregistrat',
    'User ID' => 'Cod de identificare (ID) utilizator',
    'Username' => 'Nume de utilizator',
    'Users' => 'Utilizatori',
    'VPN Portal' => 'Portalul VPN',
    'VPN traffic over the last month.' => 'Trafic VPN în ultima lună.',
    'Verify' => 'Verifică',
    'Version' => 'Versiune',
    'Welcome to this VPN service!' => 'Bine ai venit la acest serviciu de VPN!',
    'Why is this necessary?' => 'De ce este nevoie de asta?',
    'Yes' => 'Da',
    'You are already enrolled for Two-factor authentication (2FA).' => 'Autentificarea în doi pași este deja activă.',
    'You cannot manage your own user account.' => 'Nu poți să-ți gestionezi propriul cont.',
    'Your ID is <code>%_two_factor_user_id%</code>.' => 'Contul tău este <code>%_two_factor_user_id%</code>.',
    'Your new configuration will expire on %expiryDate%. Come back here to obtain a new configuration after expiry!' => 'Noua configurație va expira la data de %expiryDate%. După aceasta, poți reveni aici pentru a obține o nouă configurație.',
];

M locale/uk_UA.php => locale/uk_UA.php +5 -0
@@ 8,6 8,8 @@
 */

return [
    //'"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!' => '',
    //'"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!' => '',
    '#Active Connections' => 'Активні з’єднання.',
    //'ACL Permission List' => '',
    'Account' => 'Акаунт',


@@ 39,11 41,14 @@ return [
    //'DNS Search Domain(s)' => '',
    //'DNS Server(s)' => '',
    //'DNS Suffix' => '',
    //'Danger Zone' => '',
    'Date' => 'Дата',
    'Date/Time' => 'Дата/Час',
    //'Default Gateway' => '',
    'Delete' => 'Видалити',
    'Delete TOTP Secret' => 'Видалити секретний TOTP',
    //'Delete User' => '',
    //'Delete User Data' => '',
    'Details...' => 'Деталі...',
    'Disable User' => 'Деактивувати користувача',
    'Disabled' => 'Деактивовано',

M src/AdminPortalModule.php => src/AdminPortalModule.php +34 -12
@@ 36,14 36,21 @@ class AdminPortalModule implements ServiceModuleInterface
    /** @var \LC\Common\HttpClient\ServerClient */
    private $serverClient;

    /** @var string */
    private $authMethod;

    /** @var \DateTime */
    private $dateTime;

    public function __construct(TplInterface $tpl, Storage $storage, ServerClient $serverClient)
    /**
     * @param string $authMethod
     */
    public function __construct(TplInterface $tpl, Storage $storage, ServerClient $serverClient, $authMethod)
    {
        $this->tpl = $tpl;
        $this->storage = $storage;
        $this->serverClient = $serverClient;
        $this->authMethod = $authMethod;
        $this->dateTime = new DateTime();
    }



@@ 138,8 145,7 @@ class AdminPortalModule implements ServiceModuleInterface
                /** @var \LC\Common\Http\UserInfo */
                $userInfo = $hookData['auth'];
                $adminUserId = $userInfo->getUserId();
                $userId = $request->requireQueryParameter('user_id');
                InputValidation::userId($userId);
                $userId = InputValidation::userId($request->requireQueryParameter('user_id'));

                $clientCertificateList = $this->serverClient->getRequireArray('client_certificate_list', ['user_id' => $userId]);
                $userMessages = $this->serverClient->getRequireArray('user_messages', ['user_id' => $userId]);


@@ 182,8 188,7 @@ class AdminPortalModule implements ServiceModuleInterface
                /** @var \LC\Common\Http\UserInfo */
                $userInfo = $hookData['auth'];
                $adminUserId = $userInfo->getUserId();
                $userId = $request->requirePostParameter('user_id');
                InputValidation::userId($userId);
                $userId = InputValidation::userId($request->requirePostParameter('user_id'));

                // if the current user being managed is the account itself,
                // do not allow this. We don't want admins allow to disable


@@ 223,6 228,27 @@ class AdminPortalModule implements ServiceModuleInterface
                        $this->serverClient->post('enable_user', ['user_id' => $userId]);
                        break;

                    case 'deleteUser':
                        // delete OAuth authorizations
                        $this->storage->deleteAuthorizationsOfUserId($userId);
                        // delete all certificates of user and associated data
                        $this->serverClient->post('delete_user', ['user_id' => $userId]);

                        if ('FormPdoAuthentication' === $this->authMethod) {
                            // also delete the user account when the user is local
                            $this->storage->deleteUser($userId);
                        }

                        // get active connections for this user
                        $connectionList = $this->serverClient->getRequireArray('client_connections', ['user_id' => $userId]);
                        // kill all active connections for this user
                        foreach ($connectionList as $profileId => $clientConnectionList) {
                            foreach ($clientConnectionList as $clientInfo) {
                                $this->serverClient->post('kill_client', ['common_name' => $clientInfo['common_name']]);
                            }
                        }

                        return new RedirectResponse($request->getRootUri().'users');
                    case 'deleteTotpSecret':
                        $this->serverClient->post('delete_totp_secret', ['user_id' => $userId]);
                        break;


@@ 231,9 257,7 @@ class AdminPortalModule implements ServiceModuleInterface
                        throw new HttpException('unsupported "user_action"', 400);
                }

                $returnUrl = sprintf('%susers', $request->getRootUri());

                return new RedirectResponse($returnUrl);
                return new RedirectResponse($request->getRootUri().'user?user_id='.$userId);
            }
        );



@@ 373,9 397,7 @@ class AdminPortalModule implements ServiceModuleInterface
                }
                // convert it to UTC as our server logs are all in UTC
                $dateTime->setTimeZone(new DateTimeZone('UTC'));

                $ipAddress = $request->requirePostParameter('ip_address');
                InputValidation::ipAddress($ipAddress);
                $ipAddress = InputValidation::ipAddress($request->requirePostParameter('ip_address'));

                return new HtmlResponse(
                    $this->tpl->render(


@@ 556,6 578,6 @@ class AdminPortalModule implements ServiceModuleInterface
     */
    private static function getCoordinates($f)
    {
        return [cos(2 * M_PI * $f), sin(2 * M_PI * $f)];
        return [cos(2 * \M_PI * $f), sin(2 * \M_PI * $f)];
    }
}

A src/Expiry.php => src/Expiry.php +36 -0
@@ 0,0 1,36 @@
<?php

/*
 * eduVPN - End-user friendly VPN.
 *
 * Copyright: 2016-2019, The Commons Conservancy eduVPN Programme
 * SPDX-License-Identifier: AGPL-3.0+
 */

namespace LC\Portal;

use DateInterval;
use DateTime;

class Expiry
{
    /**
     * @return \DateInterval
     */
    public static function doNotOutliveCa(DateTime $caExpiresAt, DateInterval $sessionExpiry, DateTime $dateTime = null)
    {
        if (null === $dateTime) {
            $dateTime = new DateTime();
        }
        $expiresAt = clone $dateTime;
        $expiresAt->add($sessionExpiry);

        if ($expiresAt < $caExpiresAt) {
            // we do not outlive the CA, great!
            return $sessionExpiry;
        }

        // return the interval between "now" and the moment the CA expires
        return $dateTime->diff($caExpiresAt);
    }
}

M src/FormRadiusAuthentication.php => src/FormRadiusAuthentication.php +1 -1
@@ 21,7 21,7 @@ class FormRadiusAuthentication extends FormAuthentication
    public function __construct(Config $config, SessionInterface $session, TplInterface $tpl, LoggerInterface $logger)
    {
        $serverList = $config->requireArray('serverList');
        $userAuth = new RadiusAuth($logger, $serverList);
        $userAuth = new RadiusAuth($logger, $serverList, $config->optionalInt('permissionAttribute'));
        if (null !== $addRealm = $config->optionalString('addRealm')) {
            $userAuth->setRealm($addRealm);
        }

M src/PasswdModule.php => src/PasswdModule.php +3 -0
@@ 65,6 65,9 @@ class PasswdModule implements ServiceModuleInterface
                /** @var \LC\Common\Http\UserInfo */
                $userInfo = $hookData['auth'];

                // we do not validate the input here because we validate it
                // strictly... we do not want to limit the current password to
                // the "new password" restrictions
                $userPass = $request->requirePostParameter('userPass');
                $newUserPass = InputValidation::userPass($request->requirePostParameter('newUserPass'));
                $newUserPassConfirm = InputValidation::userPass($request->requirePostParameter('newUserPassConfirm'));

M src/QrModule.php => src/QrModule.php +33 -4
@@ 9,10 9,12 @@

namespace LC\Portal;

use LC\Common\Http\InputValidation;
use LC\Common\Http\Request;
use LC\Common\Http\Response;
use LC\Common\Http\Service;
use LC\Common\Http\ServiceModuleInterface;
use LC\Common\Http\UserInfo;

class QrModule implements ServiceModuleInterface
{


@@ 24,15 26,16 @@ class QrModule implements ServiceModuleInterface
    public function init(Service $service)
    {
        $service->get(
            '/qr',
            '/qr/totp',
            /**
             * @return \LC\Common\Http\Response
             */
            function (Request $request, array $hookData) {
                $qrText = $request->requireQueryParameter('qr_text');

                /** @var \LC\Common\Http\UserInfo */
                $userInfo = $hookData['auth'];
                $totpSecret = InputValidation::totpSecret($request->requireQueryParameter('secret'));
                $response = new Response(200, 'image/png');
                $response->setBody(self::generate($qrText));
                $response->setBody(self::generate(self::getOtpAuthUrl($request, $userInfo, $totpSecret)));

                return $response;
            }


@@ 40,6 43,32 @@ class QrModule implements ServiceModuleInterface
    }

    /**
     * @param string $labelStr
     *
     * @return string
     */
    private static function labelEncode($labelStr)
    {
        return rawurlencode(str_replace(':', '_', $labelStr));
    }

    /**
     * @param string $totpSecret
     *
     * @return string
     */
    private static function getOtpAuthUrl(Request $request, UserInfo $userInfo, $totpSecret)
    {
        return sprintf(
            'otpauth://totp/%s:%s?secret=%s&issuer=%s',
            self::labelEncode($request->getServerName()),
            self::labelEncode($userInfo->getUserId()),
            $totpSecret,
            self::labelEncode($request->getServerName())
        );
    }

    /**
     * @param string $qrText
     *
     * @return string

M src/Storage.php => src/Storage.php +38 -2
@@ 104,7 104,7 @@ class Storage implements CredentialValidatorInterface, StorageInterface
                (:user_id, :password_hash, :created_at)'
        );

        $passwordHash = password_hash($userPass, PASSWORD_DEFAULT);
        $passwordHash = password_hash($userPass, \PASSWORD_DEFAULT);
        $stmt->bindValue(':user_id', $userId, PDO::PARAM_STR);
        $stmt->bindValue(':password_hash', $passwordHash, PDO::PARAM_STR);
        $stmt->bindValue(':created_at', $this->dateTime->format(DateTime::ATOM), PDO::PARAM_STR);


@@ 112,6 112,24 @@ class Storage implements CredentialValidatorInterface, StorageInterface
    }

    /**
     * @param string $userId
     *
     * @return void
     */
    public function deleteUser($userId)
    {
        $stmt = $this->db->prepare(
            'DELETE FROM
                users
             WHERE
                user_id = :user_id'
        );

        $stmt->bindValue(':user_id', $userId, PDO::PARAM_STR);
        $stmt->execute();
    }

    /**
     * @param string $authUser
     *
     * @return bool


@@ 149,7 167,7 @@ class Storage implements CredentialValidatorInterface, StorageInterface
                user_id = :user_id'
        );

        $passwordHash = password_hash($newUserPass, PASSWORD_DEFAULT);
        $passwordHash = password_hash($newUserPass, \PASSWORD_DEFAULT);
        $stmt->bindValue(':user_id', $userId, PDO::PARAM_STR);
        $stmt->bindValue(':password_hash', $passwordHash, PDO::PARAM_STR);
        $stmt->execute();


@@ 267,6 285,24 @@ class Storage implements CredentialValidatorInterface, StorageInterface
    }

    /**
     * @param string $userId
     *
     * @return void
     */
    public function deleteAuthorizationsOfUserId($userId)
    {
        $stmt = $this->db->prepare(
            'DELETE FROM
                authorizations
             WHERE
                user_id = :user_id'
        );

        $stmt->bindValue(':user_id', $userId, PDO::PARAM_STR);
        $stmt->execute();
    }

    /**
     * @return void
     */
    public function init()

M src/TwoFactorEnrollModule.php => src/TwoFactorEnrollModule.php +0 -29
@@ 16,7 16,6 @@ use LC\Common\Http\Request;
use LC\Common\Http\Service;
use LC\Common\Http\ServiceModuleInterface;
use LC\Common\Http\SessionInterface;
use LC\Common\Http\UserInfo;
use LC\Common\HttpClient\Exception\ApiException;
use LC\Common\HttpClient\ServerClient;
use LC\Common\TplInterface;


@@ 71,7 70,6 @@ class TwoFactorEnrollModule implements ServiceModuleInterface
                            'twoFactorMethods' => $this->twoFactorMethods,
                            'hasTotpSecret' => $hasTotpSecret,
                            'totpSecret' => $totpSecret,
                            'otpAuthUrl' => self::getOtpAuthUrl($request, $userInfo, $totpSecret),
                        ]
                    )
                );


@@ 107,7 105,6 @@ class TwoFactorEnrollModule implements ServiceModuleInterface
                                'hasTotpSecret' => $hasTotpSecret,
                                'totpSecret' => $totpSecret,
                                'error_code' => 'invalid_otp_code',
                                'otpAuthUrl' => self::getOtpAuthUrl($request, $userInfo, $totpSecret),
                            ]
                        )
                    );


@@ 127,30 124,4 @@ class TwoFactorEnrollModule implements ServiceModuleInterface
            }
        );
    }

    /**
     * @param string $labelStr
     *
     * @return string
     */
    private static function labelEncode($labelStr)
    {
        return rawurlencode(str_replace(':', '_', $labelStr));
    }

    /**
     * @param string $totpSecret
     *
     * @return string
     */
    private static function getOtpAuthUrl(Request $request, UserInfo $userInfo, $totpSecret)
    {
        return sprintf(
            'otpauth://totp/%s:%s?secret=%s&issuer=%s',
            self::labelEncode($request->getServerName()),
            self::labelEncode($userInfo->getUserId()),
            $totpSecret,
            self::labelEncode($request->getServerName())
        );
    }
}

M src/UpdateSessionInfoHook.php => src/UpdateSessionInfoHook.php +2 -1
@@ 11,6 11,7 @@ namespace LC\Portal;

use DateInterval;
use DateTime;
use DateTimeZone;
use LC\Common\Http\BeforeHookInterface;
use LC\Common\Http\Exception\HttpException;
use LC\Common\Http\Request;


@@ 40,7 41,7 @@ class UpdateSessionInfoHook implements BeforeHookInterface
    {
        $this->session = $session;
        $this->serverClient = $serverClient;
        $this->dateTime = new DateTime();
        $this->dateTime = new DateTime('now', new DateTimeZone('UTC'));
        $this->sessionExpiry = $sessionExpiry;
    }


M src/VpnPortalModule.php => src/VpnPortalModule.php +9 -2
@@ 49,7 49,10 @@ class VpnPortalModule implements ServiceModuleInterface
    /** @var \DateTime */
    private $dateTime;

    public function __construct(Config $config, TplInterface $tpl, ServerClient $serverClient, SessionInterface $session, Storage $storage, ClientDbInterface $clientDb)
    /** @var \DateInterval */
    private $sessionExpiry;

    public function __construct(Config $config, TplInterface $tpl, ServerClient $serverClient, SessionInterface $session, Storage $storage, ClientDbInterface $clientDb, DateInterval $sessionExpiry)
    {
        $this->config = $config;
        $this->tpl = $tpl;


@@ 57,6 60,7 @@ class VpnPortalModule implements ServiceModuleInterface
        $this->session = $session;
        $this->storage = $storage;
        $this->clientDb = $clientDb;
        $this->sessionExpiry = $sessionExpiry;
        $this->dateTime = new DateTime();
    }



@@ 139,7 143,7 @@ class VpnPortalModule implements ServiceModuleInterface
                    $this->tpl->render(
                        'vpnPortalConfigurations',
                        [
                            'expiryDate' => $this->getExpiryDate(new DateInterval($this->config->requireString('sessionExpiry', 'P90D'))),
                            'expiryDate' => $this->getExpiryDate($this->sessionExpiry),
                            'profileList' => $visibleProfileList,
                            'userCertificateList' => $showAll ? $userCertificateList : $manualCertificateList,
                        ]


@@ 220,6 224,7 @@ class VpnPortalModule implements ServiceModuleInterface

                // get the fancy profile name
                $profileList = $this->serverClient->getRequireArray('profile_list');
                $visibleProfileList = self::getProfileList($profileList, $userPermissions);

                $idNameMapping = [];
                foreach ($profileList as $profileId => $profileData) {


@@ 232,12 237,14 @@ class VpnPortalModule implements ServiceModuleInterface
                        [
                            'hasTotpSecret' => $hasTotpSecret,
                            'userInfo' => $userInfo,
                            'showPermissions' => $this->config->requireBool('showPermissions', true),
                            'userPermissions' => $userPermissions,
                            'authorizedClients' => $authorizedClients,
                            'twoFactorMethods' => $this->config->requireArray('twoFactorMethods', ['totp']),
                            'userMessages' => $userMessages,
                            'userConnectionLogEntries' => $userConnectionLogEntries,
                            'idNameMapping' => $idNameMapping,
                            'visibleProfileList' => $visibleProfileList,
                        ]
                    )
                );

A tests/ExpiryTest.php => tests/ExpiryTest.php +36 -0
@@ 0,0 1,36 @@
<?php

/*
 * eduVPN - End-user friendly VPN.
 *
 * Copyright: 2016-2019, The Commons Conservancy eduVPN Programme
 * SPDX-License-Identifier: AGPL-3.0+
 */

namespace LC\Portal\Tests;

use DateInterval;
use DateTime;
use LC\Portal\Expiry;
use PHPUnit\Framework\TestCase;

class ExpiryTest extends TestCase
{
    /**
     * @return void
     */
    public function testDoNotOutliveCa()
    {
        $dataSet = [
            // sessionExpiresAt, caExpiresAt, sessionExpiry, currentDate
            ['2025-01-01T03:00:00+02:00', '2025-01-01T01:00:00+00:00', 'P10Y', '2021-05-11T12:22:26+02:00'],  // sessionExpiry outlives CA
            ['2021-08-09T10:50:00+02:00', '2025-01-01T09:00:00+02:00', 'P90D', '2021-05-11T10:50:00+02:00'], // sessionExpiry does not outlive CA
        ];

        foreach ($dataSet as $dataPoint) {
            $dateTime = new DateTime($dataPoint[3]);
            $expiryInterval = Expiry::doNotOutliveCa(new DateTime($dataPoint[1]), new DateInterval($dataPoint[2]), $dateTime);
            $this->assertSame($dataPoint[0], $dateTime->add($expiryInterval)->format(DateTime::ATOM));
        }
    }
}

M tests/VpnPortalModuleTest.php => tests/VpnPortalModuleTest.php +8 -1
@@ 43,7 43,8 @@ class VpnPortalModuleTest extends TestCase
            $serverClient,
            new TestSession(),
            $storage,
            new ClientFetcher(new Config(['Api' => []]))
            new ClientFetcher(new Config(['Api' => []])),
            new DateInterval('P90D')
        );
        $vpnPortalModule->setDateTime(new DateTime('2019-01-01'));
        $this->service = new Service();


@@ 97,6 98,7 @@ class VpnPortalModuleTest extends TestCase
                'vpnPortalAccount' => [
                    'hasTotpSecret' => false,
                    'userInfo' => [],
                    'showPermissions' => true,
                    'userPermissions' => [],
                    'authorizedClients' => [],
                    'twoFactorMethods' => [


@@ 107,6 109,11 @@ class VpnPortalModuleTest extends TestCase
                    'idNameMapping' => [
                        'internet' => 'Internet Access',
                    ],
                    'visibleProfileList' => [
                        'internet' => [
                            'displayName' => 'Internet Access',
                        ],
                    ],
                ],
            ],
            $this->makeRequest('GET', '/account')

M views/vpnAdminUserConfigList.php => views/vpnAdminUserConfigList.php +22 -6
@@ 4,14 4,12 @@
        <?=$this->t('Managing user <code>%userId%</code>.'); ?>
    </p>

    <?php if ($isSelf): ?>
        <p class="warning"><?=$this->t('You cannot manage your own user account.'); ?></p>
    <?php endif; ?>

<?php if ($isSelf): ?>
    <p class="warning"><?=$this->t('You cannot manage your own user account.'); ?></p>
<?php else: ?>
    <form class="frm" method="post" action="<?=$this->e($requestRoot); ?>user">
        <fieldset>
            <input type="hidden" name="user_id" value="<?=$this->e($userId); ?>">
            <?php if (!$isSelf): ?>
                <?php if ($isDisabled): ?>
                    <button name="user_action" value="enableUser"><?=$this->t('Enable User'); ?></button>
                <?php else: ?>


@@ 20,9 18,27 @@
                <?php if ($hasTotpSecret): ?>
                    <button class="warning" name="user_action" value="deleteTotpSecret"><?=$this->t('Delete TOTP Secret'); ?></button>
                <?php endif; ?>
            <?php endif; ?>
            <details>
                <summary><?=$this->t('Danger Zone'); ?></summary>
<?php if ('FormPdoAuthentication' === $authMethod): ?>
                    <button class="error" name="user_action" value="deleteUser"><?=$this->t('Delete User'); ?></button>
                    <p>
                        <small>⚠️
<?=$this->t('"Delete User" will only delete the account and associated data of the user, but NOT log the user out if they are currently logged in!'); ?>
                        </small>
                    </p>
<?php else: ?>
                    <button class="error" name="user_action" value="deleteUser"><?=$this->t('Delete User Data'); ?></button>
                    <p>
                        <small>⚠️
<?=$this->t('"Delete User Data" will only delete the account data of the user, but NOT log them out if they are currently logged in, nor prevent the user from logging in again!'); ?>
                        </small>
                    </p>
<?php endif; ?>
            </details>
        </fieldset>
    </form>
<?php endif; ?>

    <h2><?=$this->t('Certificates'); ?></h2>


M views/vpnPortalAccount.php => views/vpnPortalAccount.php +17 -1
@@ 13,7 13,7 @@
            </tr>
        <?php endif; ?>

        <?php if (0 !== count($userPermissions)): ?>
        <?php if ($showPermissions && 0 !== count($userPermissions)): ?>
        <tr>
            <th><?=$this->t('Permission(s)'); ?></th>
            <td>


@@ 25,6 25,22 @@
            </td>
        </tr>
        <?php endif; ?>
        <tr>
            <th><?=$this->t('Profiles'); ?></th>
            <td>
<?php if (0 === count($visibleProfileList)): ?>
                <em>
                    <?=$this->t('No VPN profiles are available for your account.'); ?>
                </em>
<?php else: ?>
                <ul>
                    <?php foreach ($visibleProfileList as $profileId => $profileInfo): ?>
                        <li><?=$this->e($profileInfo['displayName']); ?></li>
                    <?php endforeach; ?>
                </ul>
<?php endif; ?>
            </td>
        </tr>
    </table>

    <details>

M views/vpnPortalEnrollTwoFactor.php => views/vpnPortalEnrollTwoFactor.php +1 -1
@@ 28,7 28,7 @@
                    <dt><?=$this->t('Secret'); ?></dt>
                    <dd><code><?=$this->e($totpSecret); ?></code></dd>
                    <dt><?=$this->t('QR'); ?></dt>
                    <dd><img alt="<?=$this->t('QR'); ?>" src="qr?qr_text=<?=$this->e($otpAuthUrl); ?>"></dd>
                    <dd><img alt="<?=$this->t('QR'); ?>" src="qr/totp?secret=<?=$this->e($totpSecret); ?>"></dd>
                </dl>

                <p>

M web/api.php => web/api.php +8 -2
@@ 21,6 21,7 @@ use LC\Common\HttpClient\ServerClient;
use LC\Common\Logger;
use LC\Portal\BearerAuthenticationHook;
use LC\Portal\ClientFetcher;
use LC\Portal\Expiry;
use LC\Portal\OAuth\BearerValidator;
use LC\Portal\Storage;
use LC\Portal\VpnApiModule;


@@ 42,10 43,15 @@ try {
        $config->requireString('apiUri')
    );

    $sessionExpiry = new DateInterval($config->requireString('sessionExpiry', 'P90D'));
    $caInfo = $serverClient->getRequireArray('ca_info');
    $caExpiresAt = new DateTime($caInfo['valid_to']);
    $sessionExpiry = Expiry::doNotOutliveCa($caExpiresAt, $sessionExpiry);

    $storage = new Storage(
        new PDO(sprintf('sqlite://%s/db.sqlite', $dataDir)),
        sprintf('%s/schema', $baseDir),
        new DateInterval($config->requireString('sessionExpiry', 'P90D'))
        $sessionExpiry
    );
    $storage->update();



@@ 83,7 89,7 @@ try {
    $vpnApiModule = new VpnApiModule(
        $config,
        $serverClient,
        new DateInterval($config->requireString('sessionExpiry', 'P90D'))
        $sessionExpiry
    );
    $service->addModule($vpnApiModule);
    $service->run($request)->send();

M web/css/screen.css => web/css/screen.css +5 -4
@@ 260,16 260,17 @@ table.tbl td, table.tbl th {

table.tbl ul {
  padding: 0 0 0 1em;
  margin: 0;
}

table.tbl dl, table.tbl dt {
    margin: 0;
    padding: 0;
  margin: 0;
  padding: 0;
}

table.tbl dd {
    margin-top: 0.5em;
    margin-bottom: 0.5em;
  margin-top: 0.5em;
  margin-bottom: 0.5em;
}

table.tbl form.frm {

M web/index.php => web/index.php +22 -16
@@ 36,6 36,7 @@ use LC\Portal\AdminPortalModule;
use LC\Portal\ClientCertAuthentication;
use LC\Portal\ClientFetcher;
use LC\Portal\DisabledUserHook;
use LC\Portal\Expiry;
use LC\Portal\FormLdapAuthentication;
use LC\Portal\FormPdoAuthentication;
use LC\Portal\FormRadiusAuthentication;


@@ 79,15 80,23 @@ try {
        $localeDirs[] = sprintf('%s/config/locale/%s', $baseDir, $styleName);
    }

    $sessionExpiry = $config->requireString('sessionExpiry', 'P90D');
    $serverClient = new ServerClient(
        new CurlHttpClient($config->requireString('apiUser'), $config->requireString('apiPass')),
        $config->requireString('apiUri')
    );

    // we always want browser session to expiry after PT8H hours, *EXCEPT* when
    // the configured "sessionExpiry" is < PT8H, then we want to follow that
    $sessionExpiry = new DateInterval($config->requireString('sessionExpiry', 'P90D'));
    $caInfo = $serverClient->getRequireArray('ca_info');
    $caExpiresAt = new DateTime($caInfo['valid_to']);
    $sessionExpiry = Expiry::doNotOutliveCa($caExpiresAt, $sessionExpiry);

    // we always want browser session to expiry after 30 minutes, *EXCEPT* when
    // the configured "sessionExpiry" is < PT30M, then we want to follow that
    // setting...
    $sessionOptions = SessionOptions::init();
    $dateTime = new DateTime();
    if (date_add(clone $dateTime, new DateInterval('PT30M')) > date_add(clone $dateTime, new DateInterval($sessionExpiry))) {
        $sessionOptions = SessionOptions::init()->withExpiresIn(new DateInterval($sessionExpiry));
    if (date_add(clone $dateTime, new DateInterval('PT30M')) > date_add(clone $dateTime, $sessionExpiry)) {
        $sessionOptions = SessionOptions::init()->withExpiresIn($sessionExpiry);
    }

    $secureCookie = $config->requireBool('secureCookie', true);


@@ 130,17 139,13 @@ try {
        'portalVersion' => trim(FileIO::readFile(sprintf('%s/VERSION', $baseDir))),
        'isAdmin' => false,
        'useRtl' => 0 === strpos($uiLang, 'ar_') || 0 === strpos($uiLang, 'fa_') || 0 === strpos($uiLang, 'he_'),
        'authMethod' => $authMethod,
    ];
    if ('ClientCertAuthentication' === $authMethod) {
        $templateDefaults['_show_logout_button'] = false;
    }
    $tpl->addDefault($templateDefaults);

    $serverClient = new ServerClient(
        new CurlHttpClient($config->requireString('apiUser'), $config->requireString('apiPass')),
        $config->requireString('apiUri')
    );

    $service = new Service($tpl);
    $service->addBeforeHook('csrf_protection', new CsrfProtectionHook());
    $service->addBeforeHook('language_switcher', new LanguageSwitcherHook(array_keys($supportedLanguages), $seCookie));


@@ 169,7 174,7 @@ try {
    $storage = new Storage(
        new PDO(sprintf('sqlite://%s/db.sqlite', $dataDir)),
        sprintf('%s/schema', $baseDir),
        new DateInterval($sessionExpiry)
        $sessionExpiry
    );
    $storage->update();



@@ 296,14 301,13 @@ try {
    }

    $service->addBeforeHook('disabled_user', new DisabledUserHook($serverClient));
    $service->addBeforeHook('update_session_info', new UpdateSessionInfoHook($seSession, $serverClient, new DateInterval($sessionExpiry)));

    $service->addModule(new QrModule());
    $service->addBeforeHook('update_session_info', new UpdateSessionInfoHook($seSession, $serverClient, $sessionExpiry));

    // two factor module
    if (0 !== count($twoFactorMethods)) {
        $twoFactorModule = new TwoFactorModule($serverClient, $seSession, $tpl);
        $service->addModule($twoFactorModule);
        $service->addModule(new QrModule());
    }

    // isAdmin


@@ 325,14 329,16 @@ try {
        $serverClient,
        $seSession,
        $storage,
        $clientFetcher
        $clientFetcher,
        $sessionExpiry
    );
    $service->addModule($vpnPortalModule);

    $adminPortalModule = new AdminPortalModule(
        $tpl,
        $storage,
        $serverClient
        $serverClient,
        $authMethod
    );
    $service->addModule($adminPortalModule);


M web/oauth.php => web/oauth.php +14 -1
@@ 17,8 17,11 @@ use LC\Common\FileIO;
use LC\Common\Http\JsonResponse;
use LC\Common\Http\Request;
use LC\Common\Http\Service;
use LC\Common\HttpClient\CurlHttpClient;
use LC\Common\HttpClient\ServerClient;
use LC\Common\Logger;
use LC\Portal\ClientFetcher;
use LC\Portal\Expiry;
use LC\Portal\OAuth\PublicSigner;
use LC\Portal\OAuthTokenModule;
use LC\Portal\Storage;


@@ 34,11 37,21 @@ try {
    $config = Config::fromFile(sprintf('%s/config/config.php', $baseDir));
    $service = new Service();

    $serverClient = new ServerClient(
        new CurlHttpClient($config->requireString('apiUser'), $config->requireString('apiPass')),
        $config->requireString('apiUri')
    );

    $sessionExpiry = new DateInterval($config->requireString('sessionExpiry', 'P90D'));
    $caInfo = $serverClient->getRequireArray('ca_info');
    $caExpiresAt = new DateTime($caInfo['valid_to']);
    $sessionExpiry = Expiry::doNotOutliveCa($caExpiresAt, $sessionExpiry);

    // OAuth tokens
    $storage = new Storage(
        new PDO(sprintf('sqlite://%s/db.sqlite', $dataDir)),
        sprintf('%s/schema', $baseDir),
        new DateInterval($config->requireString('sessionExpiry', 'P90D'))
        $sessionExpiry
    );
    $storage->update();