diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 000000000..8d43f11a3
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,99 @@
+COmanage Registry
+
+Copyright (C) 2010-2020
+University Corporation for Advanced Internet Development, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this software except in compliance with the License.
+You may obtain a copy of the License at:
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+---------------------------------------------------------------------------
+
+This material is based upon work supported by the National Science
+Foundation under Grant No. OCI-0721896, OCI-0330626, OCI-1032468,
+and ACI-1547268. Any opinions, findings and conclusions or recommendations
+expressed in this material are those of the author(s) and do not
+necessarily reflect the views of the National Science Foundation (NSF).
+
+---------------------------------------------------------------------------
+
+Contributions with original copyright held by, and copyright license granted
+to the University Corporation for Advanced Internet Development, Inc. as per
+the Contributor License Agreement by,
+
+ GRNET SA
+ https://grnet.gr
+
+ Modern Language Association
+ https://www.mla.org
+
+ Spherical Cow Group
+ https://sphericalcowgroup.com
+
+ SURFnet BV
+ https://www.surf.nl
+
+This project uses the following third party utilities, see the appropriate
+files and utilities for further information:
+
+ CakePHP (lib/Cake)
+ MIT License
+ http://cakephp.org
+
+ ADOdb (app/Vendor/adodb5)
+ BSD 3-Clause License
+ http://adodb.org
+
+ Guzzle (app/AvailablePlugin/GithubProvisioner/Vendor/guzzle)
+ MIT License
+ https://github.com/guzzle/guzzle
+
+ jQuery (app/webroot/js/jquery)
+ MIT License
+ http://jquery.com
+
+ jQuery UI (app/webroot/js/jquery/jquery-ui-*)
+ MIT License
+ http://jquery.com
+
+ jsTimezoneDetect (app/webroot/js/jstimezonedetect)
+ MIT License
+ https://bitbucket.org/pellepim/jstimezonedetect
+
+ Magnific Popup (app/webroot/js/jquery/magnificpopup)
+ MIT License
+ http://dimsemenov.com/plugins/magnific-popup
+
+ Material Design Light (app/webroot/js/mdl)
+ Apache 2.0
+ https://getmdl.io
+
+ noty (app/webroot/js/jquery/noty)
+ MIT License
+ http://ned.im/noty
+
+ PHP GitHub API 2.0 (app/AvailablePlugin/GithubProvisioner/Vendor/guzzle/guzzle)
+ MIT License
+ https://github.com/KnpLabs/php-github-api
+
+ Shibboleth Embedded Discovery Service (app/webroot/js/eds)
+ Apache 2.0
+ https://shibboleth.net/products/embedded-discovery-service.html
+
+ spin.js (app/webroot/js/jquery/spin*)
+ MIT License
+ http://spin.js.org
+
+ Superfish (app/webroot/js/superfish)
+ MIT License
+ https://superfish.joelbirch.co
+
+---------------------------------------------------------------------------
diff --git a/app/.editorconfig b/app/.editorconfig
new file mode 100644
index 000000000..a7b4d46fe
--- /dev/null
+++ b/app/.editorconfig
@@ -0,0 +1,23 @@
+; This file is for unifying the coding style for different editors and IDEs.
+; More information at https://editorconfig.org
+
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.bat]
+end_of_line = crlf
+
+[*.yml]
+indent_size = 2
+
+[*.twig]
+insert_final_newline = false
+
+[Makefile]
+indent_style = tab
diff --git a/app/.gitattributes b/app/.gitattributes
new file mode 100644
index 000000000..9963dddc5
--- /dev/null
+++ b/app/.gitattributes
@@ -0,0 +1,34 @@
+# Define the line ending behavior of the different file extensions
+# Set default behavior, in case users don't have core.autocrlf set.
+* text text=auto eol=lf
+
+# Declare files that will always have CRLF line endings on checkout.
+*.bat eol=crlf
+
+# Declare files that will always have LF line endings on checkout.
+*.pem eol=lf
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.webp binary
+*.ico binary
+*.mo binary
+*.pdf binary
+*.xls binary
+*.xlsx binary
+*.phar binary
+*.woff binary
+*.woff2 binary
+*.ttf binary
+*.otf binary
+*.eot binary
+*.gz binary
+*.bz2 binary
+*.7z binary
+*.zip binary
+*.webm binary
+*.mp4 binary
+*.ogv binary
diff --git a/app/.github/ISSUE_TEMPLATE.md b/app/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..660b5399d
--- /dev/null
+++ b/app/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,23 @@
+This is a (multiple allowed):
+
+* [x] bug
+* [ ] enhancement
+* [ ] feature-discussion (RFC)
+
+* CakePHP Application Skeleton Version: EXACT RELEASE VERSION OR COMMIT HASH, HERE.
+* Platform and Target: YOUR WEB-SERVER, DATABASE AND OTHER RELEVANT INFO AND HOW THE REQUEST IS BEING MADE, HERE.
+
+### What you did
+EXPLAIN WHAT YOU DID, PREFERABLY WITH CODE EXAMPLES, HERE.
+
+### What happened
+EXPLAIN WHAT IS ACTUALLY HAPPENING, HERE.
+
+### What you expected to happen
+EXPLAIN WHAT IS TO BE EXPECTED, HERE.
+
+P.S. Remember, an issue is not the place to ask questions. You can use [Stack Overflow](https://stackoverflow.com/questions/tagged/cakephp)
+for that or join the #cakephp channel on irc.freenode.net, where we will be more
+than happy to help answer your questions.
+
+Before you open an issue, please check if a similar issue already exists or has been closed before.
\ No newline at end of file
diff --git a/app/.github/PULL_REQUEST_TEMPLATE.md b/app/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..aae4cbd16
--- /dev/null
+++ b/app/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,14 @@
+
diff --git a/app/.github/workflows/ci.yml b/app/.github/workflows/ci.yml
new file mode 100644
index 000000000..5f54d2705
--- /dev/null
+++ b/app/.github/workflows/ci.yml
@@ -0,0 +1,82 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ - '4.next'
+ pull_request:
+ branches:
+ - '*'
+
+jobs:
+ testsuite:
+ runs-on: ubuntu-18.04
+ strategy:
+ fail-fast: false
+ matrix:
+ php-version: ['7.2', '7.4', '8.0']
+ name: PHP ${{ matrix.php-version }}
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-version }}
+ extensions: mbstring, intl, pdo_sqlite, pdo_mysql
+ coverage: none
+
+ - name: Composer install
+ run: |
+ if [[ ${{ matrix.php-version }} == '8.0' ]]; then
+ composer install --ignore-platform-reqs
+ else
+ composer install
+ fi
+ composer run-script post-install-cmd --no-interaction
+
+ - name: Run PHPUnit
+ run: |
+ vendor/bin/phpunit
+
+ coding-standard:
+ name: Coding Standard
+ runs-on: ubuntu-18.04
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '7.2'
+ extensions: mbstring, intl
+ coverage: none
+
+ - name: Composer install
+ run: composer install
+
+ - name: Run PHP CodeSniffer
+ run: composer cs-check
+
+ static-analysis:
+ name: Static Analysis
+ runs-on: ubuntu-18.04
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '7.2'
+ extensions: mbstring, intl
+ coverage: none
+
+ - name: Composer install
+ run: composer require --dev phpstan/phpstan:^0.12
+
+ - name: Run phpstan
+ run: vendor/bin/phpstan.phar analyse
diff --git a/app/.github/workflows/stale.yml b/app/.github/workflows/stale.yml
new file mode 100644
index 000000000..9192ec78f
--- /dev/null
+++ b/app/.github/workflows/stale.yml
@@ -0,0 +1,23 @@
+name: Mark stale issues and pull requests
+
+on:
+ schedule:
+ - cron: "0 0 * * *"
+
+jobs:
+ stale:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/stale@v1
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ stale-issue-message: 'This issue is stale because it has been open for 120 days with no activity. Remove the `stale` label or comment or this will be closed in 15 days'
+ stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove the `stale` label or comment on this issue, or it will be closed in 15 days'
+ stale-issue-label: 'stale'
+ stale-pr-label: 'stale'
+ days-before-stale: 120
+ days-before-close: 15
+ exempt-issue-label: 'pinned'
+ exempt-pr-label: 'pinned'
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 000000000..a20bb1f33
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,43 @@
+# CakePHP specific files #
+##########################
+/config/app_local.php
+/config/.env
+/logs/*
+/tmp/*
+/vendor/*
+
+# OS generated files #
+######################
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+Icon?
+ehthumbs.db
+Thumbs.db
+.directory
+
+# Tool specific files #
+#######################
+# PHPUnit
+.phpunit.result.cache
+# vim
+*~
+*.swp
+*.swo
+# sublime text & textmate
+*.sublime-*
+*.stTheme.cache
+*.tmlanguage.cache
+*.tmPreferences.cache
+# Eclipse
+.settings/*
+# JetBrains, aka PHPStorm, IntelliJ IDEA
+.idea/*
+# NetBeans
+nbproject/*
+# Visual Studio Code
+.vscode
+# Sass preprocessor
+.sass-cache/
diff --git a/app/.htaccess b/app/.htaccess
new file mode 100644
index 000000000..54b08e82e
--- /dev/null
+++ b/app/.htaccess
@@ -0,0 +1,12 @@
+# Uncomment the following to prevent the httpoxy vulnerability
+# See: https://httpoxy.org/
+#
+# RequestHeader unset Proxy
+#
+
+
+ RewriteEngine on
+ RewriteRule ^(\.well-known/.*)$ $1 [L]
+ RewriteRule ^$ webroot/ [L]
+ RewriteRule (.*) webroot/$1 [L]
+
diff --git a/app/bin/cake b/app/bin/cake
new file mode 100755
index 000000000..4b696c883
--- /dev/null
+++ b/app/bin/cake
@@ -0,0 +1,75 @@
+#!/usr/bin/env sh
+################################################################################
+#
+# Cake is a shell script for invoking CakePHP shell commands
+#
+# CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+# Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+#
+# Licensed under The MIT License
+# For full copyright and license information, please see the LICENSE.txt
+# Redistributions of files must retain the above copyright notice.
+#
+# @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+# @link https://cakephp.org CakePHP(tm) Project
+# @since 1.2.0
+# @license https://opensource.org/licenses/mit-license.php MIT License
+#
+################################################################################
+
+# Canonicalize by following every symlink of the given name recursively
+canonicalize() {
+ NAME="$1"
+ if [ -f "$NAME" ]
+ then
+ DIR=$(dirname -- "$NAME")
+ NAME=$(cd -P "$DIR" > /dev/null && pwd -P)/$(basename -- "$NAME")
+ fi
+ while [ -h "$NAME" ]; do
+ DIR=$(dirname -- "$NAME")
+ SYM=$(readlink "$NAME")
+ NAME=$(cd "$DIR" > /dev/null && cd "$(dirname -- "$SYM")" > /dev/null && pwd)/$(basename -- "$SYM")
+ done
+ echo "$NAME"
+}
+
+# Find a CLI version of PHP
+findCliPhp() {
+ for TESTEXEC in php php-cli /usr/local/bin/php
+ do
+ SAPI=$(echo "= PHP_SAPI ?>" | $TESTEXEC 2>/dev/null)
+ if [ "$SAPI" = "cli" ]
+ then
+ echo $TESTEXEC
+ return
+ fi
+ done
+ echo "Failed to find a CLI version of PHP; falling back to system standard php executable" >&2
+ echo "php";
+}
+
+# If current path is a symlink, resolve to real path
+realname="$0"
+if [ -L "$realname" ]
+then
+ realname=$(readlink -f "$0")
+fi
+
+CONSOLE=$(dirname -- "$(canonicalize "$realname")")
+APP=$(dirname "$CONSOLE")
+
+# If your CLI PHP is somewhere that this doesn't find, you can define a PHP environment
+# variable with the correct path in it.
+if [ -z "$PHP" ]
+then
+ PHP=$(findCliPhp)
+fi
+
+if [ "$(basename "$realname")" != 'cake' ]
+then
+ exec "$PHP" "$CONSOLE"/cake.php "$(basename "$realname")" "$@"
+else
+ exec "$PHP" "$CONSOLE"/cake.php "$@"
+fi
+
+exit
diff --git a/app/bin/cake.bat b/app/bin/cake.bat
new file mode 100644
index 000000000..ad1378229
--- /dev/null
+++ b/app/bin/cake.bat
@@ -0,0 +1,27 @@
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::
+:: Cake is a Windows batch script for invoking CakePHP shell commands
+::
+:: CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+:: Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+::
+:: Licensed under The MIT License
+:: Redistributions of files must retain the above copyright notice.
+::
+:: @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+:: @link https://cakephp.org CakePHP(tm) Project
+:: @since 2.0.0
+:: @license https://opensource.org/licenses/mit-license.php MIT License
+::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+@echo off
+
+SET app=%0
+SET lib=%~dp0
+
+php "%lib%cake.php" %*
+
+echo.
+
+exit /B %ERRORLEVEL%
diff --git a/app/bin/cake.php b/app/bin/cake.php
new file mode 100644
index 000000000..320ee3643
--- /dev/null
+++ b/app/bin/cake.php
@@ -0,0 +1,12 @@
+#!/usr/bin/php -q
+run($argv));
diff --git a/app/composer.json b/app/composer.json
new file mode 100644
index 000000000..3b26b1d97
--- /dev/null
+++ b/app/composer.json
@@ -0,0 +1,54 @@
+{
+ "name": "cakephp/app",
+ "description": "CakePHP skeleton app",
+ "homepage": "https://cakephp.org",
+ "type": "project",
+ "license": "MIT",
+ "require": {
+ "php": ">=7.2",
+ "cakephp/cakephp": "~4.2.0",
+ "cakephp/migrations": "^3.0",
+ "cakephp/plugin-installer": "^1.3",
+ "mobiledetect/mobiledetectlib": "^2.8"
+ },
+ "require-dev": {
+ "cakephp/bake": "^2.3",
+ "cakephp/cakephp-codesniffer": "~4.2.0",
+ "cakephp/debug_kit": "^4.4",
+ "josegonzalez/dotenv": "^3.2",
+ "phpunit/phpunit": "~8.5.0 || ^9.3",
+ "psy/psysh": "@stable"
+ },
+ "suggest": {
+ "markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.",
+ "dereuromark/cakephp-ide-helper": "After baking your code, this keeps your annotations in sync with the code evolving from there on for maximum IDE and PHPStan/Psalm compatibility.",
+ "phpstan/phpstan": "PHPStan focuses on finding errors in your code without actually running it. It catches whole classes of bugs even before you write tests for the code."
+ },
+ "autoload": {
+ "psr-4": {
+ "App\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "App\\Test\\": "tests/",
+ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
+ }
+ },
+ "scripts": {
+ "post-install-cmd": "App\\Console\\Installer::postInstall",
+ "post-create-project-cmd": "App\\Console\\Installer::postInstall",
+ "check": [
+ "@test",
+ "@cs-check"
+ ],
+ "cs-check": "phpcs --colors -p src/ tests/",
+ "cs-fix": "phpcbf --colors -p src/ tests/",
+ "stan": "phpstan analyse",
+ "test": "phpunit --colors=always"
+ },
+ "prefer-stable": true,
+ "config": {
+ "sort-packages": true
+ }
+}
diff --git a/app/composer.lock b/app/composer.lock
new file mode 100644
index 000000000..0b88da31b
--- /dev/null
+++ b/app/composer.lock
@@ -0,0 +1,6115 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "a05294b95dd0299877cdad34989c5834",
+ "packages": [
+ {
+ "name": "cakephp/cakephp",
+ "version": "4.2.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/cakephp.git",
+ "reference": "bd2d476e0b14b21c6103eca789667a016ec14458"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/cakephp/zipball/bd2d476e0b14b21c6103eca789667a016ec14458",
+ "reference": "bd2d476e0b14b21c6103eca789667a016ec14458",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/chronos": "^2.0",
+ "composer/ca-bundle": "^1.2",
+ "ext-intl": "*",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "laminas/laminas-diactoros": "^2.2.2",
+ "laminas/laminas-httphandlerrunner": "^1.1",
+ "league/container": "^3.2",
+ "php": ">=7.2.0",
+ "psr/http-client": "^1.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0",
+ "psr/log": "^1.0.0",
+ "psr/simple-cache": "^1.0.0"
+ },
+ "replace": {
+ "cakephp/cache": "self.version",
+ "cakephp/collection": "self.version",
+ "cakephp/console": "self.version",
+ "cakephp/core": "self.version",
+ "cakephp/database": "self.version",
+ "cakephp/datasource": "self.version",
+ "cakephp/event": "self.version",
+ "cakephp/filesystem": "self.version",
+ "cakephp/form": "self.version",
+ "cakephp/http": "self.version",
+ "cakephp/i18n": "self.version",
+ "cakephp/log": "self.version",
+ "cakephp/orm": "self.version",
+ "cakephp/utility": "self.version",
+ "cakephp/validation": "self.version"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^4.0",
+ "mikey179/vfsstream": "^1.6",
+ "paragonie/csp-builder": "^2.3",
+ "phpunit/phpunit": "^8.5 || ^9.3"
+ },
+ "suggest": {
+ "ext-curl": "To enable more efficient network calls in Http\\Client.",
+ "ext-openssl": "To use Security::encrypt() or have secure CSRF token generation.",
+ "lib-ICU": "The intl PHP library, to use Text::transliterate() or Text::slug()",
+ "paragonie/csp-builder": "CSP builder, to use the CSP Middleware"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Cake\\": "src/"
+ },
+ "files": [
+ "src/Core/functions.php",
+ "src/Collection/functions.php",
+ "src/I18n/functions.php",
+ "src/Routing/functions.php",
+ "src/Utility/bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/cakephp/graphs/contributors"
+ }
+ ],
+ "description": "The CakePHP framework",
+ "homepage": "https://cakephp.org",
+ "keywords": [
+ "conventions over configuration",
+ "dry",
+ "form",
+ "framework",
+ "mvc",
+ "orm",
+ "psr-7",
+ "rapid-development",
+ "validation"
+ ],
+ "support": {
+ "forum": "https://stackoverflow.com/tags/cakephp",
+ "irc": "irc://irc.freenode.org/cakephp",
+ "issues": "https://github.com/cakephp/cakephp/issues",
+ "source": "https://github.com/cakephp/cakephp"
+ },
+ "time": "2021-06-16T21:23:42+00:00"
+ },
+ {
+ "name": "cakephp/chronos",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/chronos.git",
+ "reference": "556e14da67307ffc2e68beeb7df0694dc8d1207d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/chronos/zipball/556e14da67307ffc2e68beeb7df0694dc8d1207d",
+ "reference": "556e14da67307ffc2e68beeb7df0694dc8d1207d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^4.5",
+ "phpunit/phpunit": "^8.0 || ^9.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Cake\\Chronos\\": "src/"
+ },
+ "files": [
+ "src/carbon_compat.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Brian Nesbitt",
+ "email": "brian@nesbot.com",
+ "homepage": "http://nesbot.com"
+ },
+ {
+ "name": "The CakePHP Team",
+ "homepage": "http://cakephp.org"
+ }
+ ],
+ "description": "A simple API extension for DateTime.",
+ "homepage": "http://cakephp.org",
+ "keywords": [
+ "date",
+ "datetime",
+ "time"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/cakephp",
+ "issues": "https://github.com/cakephp/chronos/issues",
+ "source": "https://github.com/cakephp/chronos"
+ },
+ "time": "2021-06-17T13:49:10+00:00"
+ },
+ {
+ "name": "cakephp/migrations",
+ "version": "3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/migrations.git",
+ "reference": "d22737c31243db89774abfe6a077d254c0eae75a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/migrations/zipball/d22737c31243db89774abfe6a077d254c0eae75a",
+ "reference": "d22737c31243db89774abfe6a077d254c0eae75a",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/cache": "^4.0.5",
+ "cakephp/orm": "^4.0.5",
+ "php": ">=7.2.0",
+ "robmorgan/phinx": "^0.12"
+ },
+ "require-dev": {
+ "cakephp/bake": "^2.1.0",
+ "cakephp/cakephp": "^4.0.5",
+ "cakephp/cakephp-codesniffer": "~4.1.0",
+ "phpunit/phpunit": "~8.5.0"
+ },
+ "suggest": {
+ "cakephp/bake": "If you want to generate migrations.",
+ "dereuromark/cakephp-ide-helper": "If you want to have IDE suggest/autocomplete when creating migrations."
+ },
+ "type": "cakephp-plugin",
+ "autoload": {
+ "psr-4": {
+ "Migrations\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/migrations/graphs/contributors"
+ }
+ ],
+ "description": "Database Migration plugin for CakePHP based on Phinx",
+ "homepage": "https://github.com/cakephp/migrations",
+ "keywords": [
+ "cakephp",
+ "migrations"
+ ],
+ "support": {
+ "forum": "https://stackoverflow.com/tags/cakephp",
+ "irc": "irc://irc.freenode.org/cakephp",
+ "issues": "https://github.com/cakephp/migrations/issues",
+ "source": "https://github.com/cakephp/migrations"
+ },
+ "time": "2021-05-20T13:57:37+00:00"
+ },
+ {
+ "name": "cakephp/plugin-installer",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/plugin-installer.git",
+ "reference": "e27027aa2d3d8ab64452c6817629558685a064cb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/plugin-installer/zipball/e27027aa2d3d8ab64452c6817629558685a064cb",
+ "reference": "e27027aa2d3d8ab64452c6817629558685a064cb",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0 || ^2.0",
+ "php": ">=5.6.0"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^3.3",
+ "composer/composer": "^2.0",
+ "phpunit/phpunit": "^5.7 || ^6.5 || ^8.5 || ^9.3"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Cake\\Composer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Composer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://cakephp.org"
+ }
+ ],
+ "description": "A composer installer for CakePHP 3.0+ plugins.",
+ "support": {
+ "issues": "https://github.com/cakephp/plugin-installer/issues",
+ "source": "https://github.com/cakephp/plugin-installer/tree/1.3.1"
+ },
+ "time": "2020-10-29T04:00:42+00:00"
+ },
+ {
+ "name": "composer/ca-bundle",
+ "version": "1.2.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/ca-bundle.git",
+ "reference": "9fdb22c2e97a614657716178093cd1da90a64aa8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/9fdb22c2e97a614657716178093cd1da90a64aa8",
+ "reference": "9fdb22c2e97a614657716178093cd1da90a64aa8",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "ext-pcre": "*",
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.55",
+ "psr/log": "^1.0",
+ "symfony/phpunit-bridge": "^4.2 || ^5",
+ "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\CaBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
+ "keywords": [
+ "cabundle",
+ "cacert",
+ "certificate",
+ "ssl",
+ "tls"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/ca-bundle/issues",
+ "source": "https://github.com/composer/ca-bundle/tree/1.2.10"
+ },
+ "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-06-07T13:58:28+00:00"
+ },
+ {
+ "name": "laminas/laminas-diactoros",
+ "version": "2.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-diactoros.git",
+ "reference": "7d2034110ae18afe05050b796a3ee4b3fe177876"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/7d2034110ae18afe05050b796a3ee4b3fe177876",
+ "reference": "7d2034110ae18afe05050b796a3ee4b3fe177876",
+ "shasum": ""
+ },
+ "require": {
+ "laminas/laminas-zendframework-bridge": "^1.0",
+ "php": "^7.3 || ~8.0.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0"
+ },
+ "conflict": {
+ "phpspec/prophecy": "<1.9.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "replace": {
+ "zendframework/zend-diactoros": "^2.2.1"
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "ext-dom": "*",
+ "ext-gd": "*",
+ "ext-libxml": "*",
+ "http-interop/http-factory-tests": "^0.8.0",
+ "laminas/laminas-coding-standard": "~1.0.0",
+ "php-http/psr7-integration-tests": "^1.1",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.1",
+ "psalm/plugin-phpunit": "^0.14.0",
+ "vimeo/psalm": "^4.3"
+ },
+ "type": "library",
+ "extra": {
+ "laminas": {
+ "config-provider": "Laminas\\Diactoros\\ConfigProvider",
+ "module": "Laminas\\Diactoros"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions/create_uploaded_file.php",
+ "src/functions/marshal_headers_from_sapi.php",
+ "src/functions/marshal_method_from_sapi.php",
+ "src/functions/marshal_protocol_version_from_sapi.php",
+ "src/functions/marshal_uri_from_sapi.php",
+ "src/functions/normalize_server.php",
+ "src/functions/normalize_uploaded_files.php",
+ "src/functions/parse_cookie_header.php",
+ "src/functions/create_uploaded_file.legacy.php",
+ "src/functions/marshal_headers_from_sapi.legacy.php",
+ "src/functions/marshal_method_from_sapi.legacy.php",
+ "src/functions/marshal_protocol_version_from_sapi.legacy.php",
+ "src/functions/marshal_uri_from_sapi.legacy.php",
+ "src/functions/normalize_server.legacy.php",
+ "src/functions/normalize_uploaded_files.legacy.php",
+ "src/functions/parse_cookie_header.legacy.php"
+ ],
+ "psr-4": {
+ "Laminas\\Diactoros\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "PSR HTTP Message implementations",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "http",
+ "laminas",
+ "psr",
+ "psr-17",
+ "psr-7"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "docs": "https://docs.laminas.dev/laminas-diactoros/",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-diactoros/issues",
+ "rss": "https://github.com/laminas/laminas-diactoros/releases.atom",
+ "source": "https://github.com/laminas/laminas-diactoros"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2021-05-18T14:41:54+00:00"
+ },
+ {
+ "name": "laminas/laminas-httphandlerrunner",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-httphandlerrunner.git",
+ "reference": "6a2dd33e4166469ade07ad1283b45924383b224b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/6a2dd33e4166469ade07ad1283b45924383b224b",
+ "reference": "6a2dd33e4166469ade07ad1283b45924383b224b",
+ "shasum": ""
+ },
+ "require": {
+ "laminas/laminas-zendframework-bridge": "^1.0",
+ "php": "^7.3 || ~8.0.0",
+ "psr/http-message": "^1.0",
+ "psr/http-message-implementation": "^1.0",
+ "psr/http-server-handler": "^1.0"
+ },
+ "replace": {
+ "zendframework/zend-httphandlerrunner": "^1.1.0"
+ },
+ "require-dev": {
+ "laminas/laminas-coding-standard": "~1.0.0",
+ "laminas/laminas-diactoros": "^2.1.1",
+ "phpunit/phpunit": "^9.3",
+ "psalm/plugin-phpunit": "^0.15.1",
+ "vimeo/psalm": "^4.6"
+ },
+ "type": "library",
+ "extra": {
+ "laminas": {
+ "config-provider": "Laminas\\HttpHandlerRunner\\ConfigProvider"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laminas\\HttpHandlerRunner\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Execute PSR-15 RequestHandlerInterface instances and emit responses they generate.",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "components",
+ "laminas",
+ "mezzio",
+ "psr-15",
+ "psr-7"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "docs": "https://docs.laminas.dev/laminas-httphandlerrunner/",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-httphandlerrunner/issues",
+ "rss": "https://github.com/laminas/laminas-httphandlerrunner/releases.atom",
+ "source": "https://github.com/laminas/laminas-httphandlerrunner"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2021-04-08T13:52:56+00:00"
+ },
+ {
+ "name": "laminas/laminas-zendframework-bridge",
+ "version": "1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-zendframework-bridge.git",
+ "reference": "13af2502d9bb6f7d33be2de4b51fb68c6cdb476e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/13af2502d9bb6f7d33be2de4b51fb68c6cdb476e",
+ "reference": "13af2502d9bb6f7d33be2de4b51fb68c6cdb476e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3",
+ "psalm/plugin-phpunit": "^0.15.1",
+ "squizlabs/php_codesniffer": "^3.5",
+ "vimeo/psalm": "^4.6"
+ },
+ "type": "library",
+ "extra": {
+ "laminas": {
+ "module": "Laminas\\ZendFrameworkBridge"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/autoload.php"
+ ],
+ "psr-4": {
+ "Laminas\\ZendFrameworkBridge\\": "src//"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Alias legacy ZF class names to Laminas Project equivalents.",
+ "keywords": [
+ "ZendFramework",
+ "autoloading",
+ "laminas",
+ "zf"
+ ],
+ "support": {
+ "forum": "https://discourse.laminas.dev/",
+ "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues",
+ "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom",
+ "source": "https://github.com/laminas/laminas-zendframework-bridge"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2021-06-24T12:49:22+00:00"
+ },
+ {
+ "name": "league/container",
+ "version": "3.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/container.git",
+ "reference": "84ecbc2dbecc31bd23faf759a0e329ee49abddbd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/container/zipball/84ecbc2dbecc31bd23faf759a0e329ee49abddbd",
+ "reference": "84ecbc2dbecc31bd23faf759a0e329ee49abddbd",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/container": "^1.0.0"
+ },
+ "provide": {
+ "psr/container-implementation": "^1.0"
+ },
+ "replace": {
+ "orno/di": "~2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.0 || ^7.0",
+ "roave/security-advisories": "dev-latest",
+ "scrutinizer/ocular": "^1.8",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev",
+ "dev-3.x": "3.x-dev",
+ "dev-2.x": "2.x-dev",
+ "dev-1.x": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Container\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Phil Bennett",
+ "email": "philipobenito@gmail.com",
+ "homepage": "http://www.philipobenito.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "A fast and intuitive dependency injection container.",
+ "homepage": "https://github.com/thephpleague/container",
+ "keywords": [
+ "container",
+ "dependency",
+ "di",
+ "injection",
+ "league",
+ "provider",
+ "service"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/container/issues",
+ "source": "https://github.com/thephpleague/container/tree/3.4.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/philipobenito",
+ "type": "github"
+ }
+ ],
+ "time": "2021-07-09T08:23:52+00:00"
+ },
+ {
+ "name": "mobiledetect/mobiledetectlib",
+ "version": "2.8.37",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/serbanghita/Mobile-Detect.git",
+ "reference": "9841e3c46f5bd0739b53aed8ac677fa712943df7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/9841e3c46f5bd0739b53aed8ac677fa712943df7",
+ "reference": "9841e3c46f5bd0739b53aed8ac677fa712943df7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.0.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8.35||~5.7"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "Mobile_Detect.php"
+ ],
+ "psr-0": {
+ "Detection": "namespaced/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Serban Ghita",
+ "email": "serbanghita@gmail.com",
+ "homepage": "http://mobiledetect.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.",
+ "homepage": "https://github.com/serbanghita/Mobile-Detect",
+ "keywords": [
+ "detect mobile devices",
+ "mobile",
+ "mobile detect",
+ "mobile detector",
+ "php mobile detect"
+ ],
+ "support": {
+ "issues": "https://github.com/serbanghita/Mobile-Detect/issues",
+ "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.37"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/serbanghita",
+ "type": "github"
+ }
+ ],
+ "time": "2021-02-19T21:22:57+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/http-client",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+ "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client/tree/master"
+ },
+ "time": "2020-06-29T06:28:15+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
+ "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0.0",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory/tree/master"
+ },
+ "time": "2019-04-30T12:38:16+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/master"
+ },
+ "time": "2016-08-06T14:39:51+00:00"
+ },
+ {
+ "name": "psr/http-server-handler",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-handler.git",
+ "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
+ "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side request handler",
+ "keywords": [
+ "handler",
+ "http",
+ "http-interop",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response",
+ "server"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/http-server-handler/issues",
+ "source": "https://github.com/php-fig/http-server-handler/tree/master"
+ },
+ "time": "2018-10-30T16:46:14+00:00"
+ },
+ {
+ "name": "psr/http-server-middleware",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-middleware.git",
+ "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5",
+ "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0",
+ "psr/http-server-handler": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side middleware",
+ "keywords": [
+ "http",
+ "http-interop",
+ "middleware",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/http-server-middleware/issues",
+ "source": "https://github.com/php-fig/http-server-middleware/tree/master"
+ },
+ "time": "2018-10-30T17:12:04+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
+ "time": "2021-05-03T11:20:27+00:00"
+ },
+ {
+ "name": "psr/simple-cache",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/simple-cache.git",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\SimpleCache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for simple caching",
+ "keywords": [
+ "cache",
+ "caching",
+ "psr",
+ "psr-16",
+ "simple-cache"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/master"
+ },
+ "time": "2017-10-23T01:57:42+00:00"
+ },
+ {
+ "name": "robmorgan/phinx",
+ "version": "0.12.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/phinx.git",
+ "reference": "bdd8f337fcdf24c20d0b708664a85ca9b8d5dbe2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/phinx/zipball/bdd8f337fcdf24c20d0b708664a85ca9b8d5dbe2",
+ "reference": "bdd8f337fcdf24c20d0b708664a85ca9b8d5dbe2",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/database": "^4.0",
+ "php": ">=7.2",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/config": "^3.4|^4.0|^5.0",
+ "symfony/console": "^3.4|^4.0|^5.0"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^3.0",
+ "ext-json": "*",
+ "ext-pdo": "*",
+ "phpunit/phpunit": "^8.5|^9.3",
+ "sebastian/comparator": ">=1.2.3",
+ "symfony/yaml": "^3.4|^4.0|^5.0"
+ },
+ "suggest": {
+ "ext-json": "Install if using JSON configuration format",
+ "ext-pdo": "PDO extension is needed",
+ "symfony/yaml": "Install if using YAML configuration format"
+ },
+ "bin": [
+ "bin/phinx"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phinx\\": "src/Phinx/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Rob Morgan",
+ "email": "robbym@gmail.com",
+ "homepage": "https://robmorgan.id.au",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Woody Gilk",
+ "email": "woody.gilk@gmail.com",
+ "homepage": "https://shadowhand.me",
+ "role": "Developer"
+ },
+ {
+ "name": "Richard Quadling",
+ "email": "rquadling@gmail.com",
+ "role": "Developer"
+ },
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/phinx/graphs/contributors",
+ "role": "Developer"
+ }
+ ],
+ "description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
+ "homepage": "https://phinx.org",
+ "keywords": [
+ "database",
+ "database migrations",
+ "db",
+ "migrations",
+ "phinx"
+ ],
+ "support": {
+ "issues": "https://github.com/cakephp/phinx/issues",
+ "source": "https://github.com/cakephp/phinx/tree/0.12.7"
+ },
+ "time": "2021-04-16T14:27:37+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v5.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "a69e0c55528b47df88d3c4067ddedf32d485d662"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/a69e0c55528b47df88d3c4067ddedf32d485d662",
+ "reference": "a69e0c55528b47df88d3c4067ddedf32d485d662",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1",
+ "symfony/filesystem": "^4.4|^5.0",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-php80": "^1.15",
+ "symfony/polyfill-php81": "^1.22"
+ },
+ "conflict": {
+ "symfony/finder": "<4.4"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "^4.4|^5.0",
+ "symfony/finder": "^4.4|^5.0",
+ "symfony/messenger": "^4.4|^5.0",
+ "symfony/service-contracts": "^1.1|^2",
+ "symfony/yaml": "^4.4|^5.0"
+ },
+ "suggest": {
+ "symfony/yaml": "To use the yaml reference dumper"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Config\\": ""
+ },
+ "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": "Helps you find, load, combine, autofill and validate configuration values of any kind",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/config/tree/v5.3.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-06-24T08:13:00+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v5.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/649730483885ff2ca99ca0560ef0e5f6b03f2ac1",
+ "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php73": "^1.8",
+ "symfony/polyfill-php80": "^1.15",
+ "symfony/service-contracts": "^1.1|^2",
+ "symfony/string": "^5.1"
+ },
+ "conflict": {
+ "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"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "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": {
+ "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.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-06-12T09:42:48+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": "2.4-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.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.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "19b71c8f313b411172dd5f470fd61f24466d79a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/19b71c8f313b411172dd5f470fd61f24466d79a9",
+ "reference": "19b71c8f313b411172dd5f470fd61f24466d79a9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "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.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-06-30T07:27:52+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.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "24b72c6baa32c746a4d0840147c9715e42bb68ab"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/24b72c6baa32c746a4d0840147c9715e42bb68ab",
+ "reference": "24b72c6baa32c746a4d0840147c9715e42bb68ab",
+ "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.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-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.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
+ "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
+ "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.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:27:20+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.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/eca0bf41ed421bed1b57c4958bab16aa86b757d0",
+ "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0",
+ "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.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-php81",
+ "version": "v1.23.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php81.git",
+ "reference": "e66119f3de95efc359483f810c4c3e6436279436"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/e66119f3de95efc359483f810c4c3e6436279436",
+ "reference": "e66119f3de95efc359483f810c4c3e6436279436",
+ "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\\Php81\\": ""
+ },
+ "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 8.1+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.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-21T13:25:03+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/string",
+ "version": "v5.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1",
+ "reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1",
+ "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.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-06-27T11:44:38+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "brick/varexporter",
+ "version": "0.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/brick/varexporter.git",
+ "reference": "05241f28dfcba2b51b11e2d750e296316ebbe518"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/brick/varexporter/zipball/05241f28dfcba2b51b11e2d750e296316ebbe518",
+ "reference": "05241f28dfcba2b51b11e2d750e296316ebbe518",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.0",
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.2",
+ "phpunit/phpunit": "^8.5 || ^9.0",
+ "vimeo/psalm": "4.4.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Brick\\VarExporter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()",
+ "keywords": [
+ "var_export"
+ ],
+ "support": {
+ "issues": "https://github.com/brick/varexporter/issues",
+ "source": "https://github.com/brick/varexporter/tree/0.3.5"
+ },
+ "time": "2021-02-10T13:53:07+00:00"
+ },
+ {
+ "name": "cakephp/bake",
+ "version": "2.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/bake.git",
+ "reference": "5cec940065af0846dd58b6dcfd6cb4f8d321ccce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/bake/zipball/5cec940065af0846dd58b6dcfd6cb4f8d321ccce",
+ "reference": "5cec940065af0846dd58b6dcfd6cb4f8d321ccce",
+ "shasum": ""
+ },
+ "require": {
+ "brick/varexporter": "^0.3.5",
+ "cakephp/cakephp": "^4.1",
+ "cakephp/twig-view": "^1.0.2",
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^4.0",
+ "cakephp/debug_kit": "^4.1",
+ "cakephp/plugin-installer": "^1.3",
+ "phpunit/phpunit": "~8.5.0"
+ },
+ "type": "cakephp-plugin",
+ "autoload": {
+ "psr-4": {
+ "Bake\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/bake/graphs/contributors"
+ }
+ ],
+ "description": "Bake plugin for CakePHP",
+ "homepage": "https://github.com/cakephp/bake",
+ "keywords": [
+ "bake",
+ "cakephp"
+ ],
+ "support": {
+ "forum": "https://stackoverflow.com/tags/cakephp",
+ "irc": "irc://irc.freenode.org/cakephp",
+ "issues": "https://github.com/cakephp/bake/issues",
+ "source": "https://github.com/cakephp/bake"
+ },
+ "time": "2021-04-30T13:52:48+00:00"
+ },
+ {
+ "name": "cakephp/cakephp-codesniffer",
+ "version": "4.2.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/cakephp-codesniffer.git",
+ "reference": "c5bb1faeebf09cd4a3604bdb0c84f7bc92dc5475"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/cakephp-codesniffer/zipball/c5bb1faeebf09cd4a3604bdb0c84f7bc92dc5475",
+ "reference": "c5bb1faeebf09cd4a3604bdb0c84f7bc92dc5475",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "slevomat/coding-standard": "^6.3.6",
+ "squizlabs/php_codesniffer": "~3.5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.1"
+ },
+ "type": "phpcodesniffer-standard",
+ "autoload": {
+ "psr-4": {
+ "CakePHP\\": "CakePHP/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/cakephp-codesniffer/graphs/contributors"
+ }
+ ],
+ "description": "CakePHP CodeSniffer Standards",
+ "homepage": "https://cakephp.org",
+ "keywords": [
+ "codesniffer",
+ "framework"
+ ],
+ "support": {
+ "forum": "https://stackoverflow.com/tags/cakephp",
+ "irc": "irc://irc.freenode.org/cakephp",
+ "issues": "https://github.com/cakephp/cakephp-codesniffer/issues",
+ "source": "https://github.com/cakephp/cakephp-codesniffer"
+ },
+ "time": "2020-12-03T20:39:38+00:00"
+ },
+ {
+ "name": "cakephp/debug_kit",
+ "version": "4.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/debug_kit.git",
+ "reference": "d8a5552096b09a1cdf192b454ec06dc0157a3515"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/debug_kit/zipball/d8a5552096b09a1cdf192b454ec06dc0157a3515",
+ "reference": "d8a5552096b09a1cdf192b454ec06dc0157a3515",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/cakephp": "^4.2.0",
+ "cakephp/chronos": "^2.0",
+ "composer/composer": "^1.3 | ^2.0",
+ "jdorn/sql-formatter": "^1.2",
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "cakephp/authorization": "^2.0",
+ "cakephp/cakephp-codesniffer": "^4.0",
+ "phpunit/phpunit": "~8.5.0 | ^9.3"
+ },
+ "suggest": {
+ "ext-pdo_sqlite": "DebugKit needs to store panel data in a database. SQLite is simple and easy to use."
+ },
+ "type": "cakephp-plugin",
+ "autoload": {
+ "psr-4": {
+ "DebugKit\\": "src/",
+ "DebugKit\\Test\\Fixture\\": "tests/Fixture/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Story",
+ "homepage": "https://mark-story.com",
+ "role": "Author"
+ },
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/debug_kit/graphs/contributors"
+ }
+ ],
+ "description": "CakePHP Debug Kit",
+ "homepage": "https://github.com/cakephp/debug_kit",
+ "keywords": [
+ "cakephp",
+ "debug",
+ "kit"
+ ],
+ "support": {
+ "forum": "http://stackoverflow.com/tags/cakephp",
+ "irc": "irc://irc.freenode.org/cakephp",
+ "issues": "https://github.com/cakephp/debug_kit/issues",
+ "source": "https://github.com/cakephp/debug_kit"
+ },
+ "time": "2021-05-09T01:21:26+00:00"
+ },
+ {
+ "name": "cakephp/twig-view",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/twig-view.git",
+ "reference": "668dd6aee43dd616b1e83cb9ba166f094c10fbce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/twig-view/zipball/668dd6aee43dd616b1e83cb9ba166f094c10fbce",
+ "reference": "668dd6aee43dd616b1e83cb9ba166f094c10fbce",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/cakephp": "^4.0",
+ "jasny/twig-extensions": "^1.3",
+ "php": ">=7.2",
+ "twig/markdown-extra": "^3.0",
+ "twig/twig": "^3.0"
+ },
+ "conflict": {
+ "wyrihaximus/twig-view": "*"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^4.0",
+ "cakephp/debug_kit": "^4.0",
+ "cakephp/plugin-installer": "^1.3",
+ "michelf/php-markdown": "^1.9",
+ "mikey179/vfsstream": "^1.6",
+ "phpunit/phpunit": "^8.5 || ^9.3"
+ },
+ "type": "cakephp-plugin",
+ "autoload": {
+ "psr-4": {
+ "Cake\\TwigView\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/cakephp/graphs/contributors"
+ }
+ ],
+ "description": "Twig powered View for CakePHP",
+ "keywords": [
+ "cakephp",
+ "template",
+ "twig",
+ "view"
+ ],
+ "support": {
+ "forum": "https://stackoverflow.com/tags/cakephp",
+ "irc": "irc://irc.freenode.org/cakephp",
+ "issues": "https://github.com/cakephp/twig-view/issues",
+ "source": "https://github.com/cakephp/twig-view"
+ },
+ "time": "2020-12-13T19:57:31+00:00"
+ },
+ {
+ "name": "composer/composer",
+ "version": "2.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/composer.git",
+ "reference": "fc5c4573aafce3a018eb7f1f8f91cea423970f2e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/composer/zipball/fc5c4573aafce3a018eb7f1f8f91cea423970f2e",
+ "reference": "fc5c4573aafce3a018eb7f1f8f91cea423970f2e",
+ "shasum": ""
+ },
+ "require": {
+ "composer/ca-bundle": "^1.0",
+ "composer/metadata-minifier": "^1.0",
+ "composer/semver": "^3.0",
+ "composer/spdx-licenses": "^1.2",
+ "composer/xdebug-handler": "^2.0",
+ "justinrainbow/json-schema": "^5.2.10",
+ "php": "^5.3.2 || ^7.0 || ^8.0",
+ "psr/log": "^1.0",
+ "react/promise": "^1.2 || ^2.7",
+ "seld/jsonlint": "^1.4",
+ "seld/phar-utils": "^1.0",
+ "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
+ "symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
+ "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
+ "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0"
+ },
+ "require-dev": {
+ "phpspec/prophecy": "^1.10",
+ "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0"
+ },
+ "suggest": {
+ "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
+ "ext-zip": "Enabling the zip extension allows you to unzip archives",
+ "ext-zlib": "Allow gzip compression of HTTP requests"
+ },
+ "bin": [
+ "bin/composer"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\": "src/Composer"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "https://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "https://seld.be"
+ }
+ ],
+ "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
+ "homepage": "https://getcomposer.org/",
+ "keywords": [
+ "autoload",
+ "dependency",
+ "package"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/composer/issues",
+ "source": "https://github.com/composer/composer/tree/2.1.3"
+ },
+ "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-06-09T14:31:20+00:00"
+ },
+ {
+ "name": "composer/metadata-minifier",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/metadata-minifier.git",
+ "reference": "c549d23829536f0d0e984aaabbf02af91f443207"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207",
+ "reference": "c549d23829536f0d0e984aaabbf02af91f443207",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2",
+ "phpstan/phpstan": "^0.12.55",
+ "symfony/phpunit-bridge": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\MetadataMinifier\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Small utility library that handles metadata minification and expansion.",
+ "keywords": [
+ "composer",
+ "compression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/metadata-minifier/issues",
+ "source": "https://github.com/composer/metadata-minifier/tree/1.0.0"
+ },
+ "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-04-07T13:37:33+00:00"
+ },
+ {
+ "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/spdx-licenses",
+ "version": "1.5.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/spdx-licenses.git",
+ "reference": "de30328a7af8680efdc03e396aad24befd513200"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/de30328a7af8680efdc03e396aad24befd513200",
+ "reference": "de30328a7af8680efdc03e396aad24befd513200",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Spdx\\": "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": "SPDX licenses list and validation library.",
+ "keywords": [
+ "license",
+ "spdx",
+ "validator"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/spdx-licenses/issues",
+ "source": "https://github.com/composer/spdx-licenses/tree/1.5.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": "2020-12-03T16:04:16+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/964adcdd3a28bf9ed5d9ac6450064e0d71ed7496",
+ "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0",
+ "psr/log": "^1.0"
+ },
+ "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.1"
+ },
+ "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-05T19:37:51+00:00"
+ },
+ {
+ "name": "dealerdirect/phpcodesniffer-composer-installer",
+ "version": "v0.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git",
+ "reference": "fe390591e0241955f22eb9ba327d137e501c771c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/fe390591e0241955f22eb9ba327d137e501c771c",
+ "reference": "fe390591e0241955f22eb9ba327d137e501c771c",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0 || ^2.0",
+ "php": ">=5.3",
+ "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0"
+ },
+ "require-dev": {
+ "composer/composer": "*",
+ "phpcompatibility/php-compatibility": "^9.0",
+ "sensiolabs/security-checker": "^4.1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Franck Nijhof",
+ "email": "franck.nijhof@dealerdirect.com",
+ "homepage": "http://www.frenck.nl",
+ "role": "Developer / IT Manager"
+ }
+ ],
+ "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
+ "homepage": "http://www.dealerdirect.com",
+ "keywords": [
+ "PHPCodeSniffer",
+ "PHP_CodeSniffer",
+ "code quality",
+ "codesniffer",
+ "composer",
+ "installer",
+ "phpcs",
+ "plugin",
+ "qa",
+ "quality",
+ "standard",
+ "standards",
+ "style guide",
+ "stylecheck",
+ "tests"
+ ],
+ "support": {
+ "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues",
+ "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer"
+ },
+ "time": "2020-12-07T18:04:37+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b",
+ "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^8.0",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.13 || 1.0.0-alpha2",
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-11-10T18:47:58+00:00"
+ },
+ {
+ "name": "jasny/twig-extensions",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jasny/twig-extensions.git",
+ "reference": "a694eb02f6fc14ff8e2fceb8b80882c0c926102b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jasny/twig-extensions/zipball/a694eb02f6fc14ff8e2fceb8b80882c0c926102b",
+ "reference": "a694eb02f6fc14ff8e2fceb8b80882c0c926102b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0.0",
+ "twig/twig": "^2.0 | ^3.0"
+ },
+ "require-dev": {
+ "ext-intl": "*",
+ "ext-pcre": "*",
+ "jasny/php-code-quality": "^2.5",
+ "php": ">=7.2.0"
+ },
+ "suggest": {
+ "ext-intl": "Required for the use of the LocalDate Twig extension",
+ "ext-pcre": "Required for the use of the PCRE Twig extension"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Jasny\\Twig\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Arnold Daniels",
+ "email": "arnold@jasny.net",
+ "homepage": "http://www.jasny.net"
+ }
+ ],
+ "description": "A set of useful Twig filters",
+ "homepage": "http://github.com/jasny/twig-extensions#README",
+ "keywords": [
+ "PCRE",
+ "array",
+ "date",
+ "datetime",
+ "preg",
+ "regex",
+ "templating",
+ "text",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/jasny/twig-extensions/issues",
+ "source": "https://github.com/jasny/twig-extensions"
+ },
+ "time": "2019-12-10T16:04:23+00:00"
+ },
+ {
+ "name": "jdorn/sql-formatter",
+ "version": "v1.2.17",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jdorn/sql-formatter.git",
+ "reference": "64990d96e0959dff8e059dfcdc1af130728d92bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jdorn/sql-formatter/zipball/64990d96e0959dff8e059dfcdc1af130728d92bc",
+ "reference": "64990d96e0959dff8e059dfcdc1af130728d92bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.2.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "3.7.*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "lib"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeremy Dorn",
+ "email": "jeremy@jeremydorn.com",
+ "homepage": "http://jeremydorn.com/"
+ }
+ ],
+ "description": "a PHP SQL highlighting library",
+ "homepage": "https://github.com/jdorn/sql-formatter/",
+ "keywords": [
+ "highlight",
+ "sql"
+ ],
+ "support": {
+ "issues": "https://github.com/jdorn/sql-formatter/issues",
+ "source": "https://github.com/jdorn/sql-formatter/tree/master"
+ },
+ "time": "2014-01-12T16:20:24+00:00"
+ },
+ {
+ "name": "josegonzalez/dotenv",
+ "version": "3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/josegonzalez/php-dotenv.git",
+ "reference": "f19174d9d7213a6c20e8e5e268aa7dd042d821ca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/josegonzalez/php-dotenv/zipball/f19174d9d7213a6c20e8e5e268aa7dd042d821ca",
+ "reference": "f19174d9d7213a6c20e8e5e268aa7dd042d821ca",
+ "shasum": ""
+ },
+ "require": {
+ "m1/env": "2.*",
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "php-mock/php-mock-phpunit": "^1.1",
+ "satooshi/php-coveralls": "1.*",
+ "squizlabs/php_codesniffer": "2.*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "josegonzalez\\Dotenv": [
+ "src",
+ "tests"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jose Diaz-Gonzalez",
+ "email": "dotenv@josegonzalez.com",
+ "homepage": "http://josediazgonzalez.com",
+ "role": "Maintainer"
+ }
+ ],
+ "description": "dotenv file parsing for PHP",
+ "homepage": "https://github.com/josegonzalez/php-dotenv",
+ "keywords": [
+ "configuration",
+ "dotenv",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/josegonzalez/php-dotenv/issues",
+ "source": "https://github.com/josegonzalez/php-dotenv/tree/master"
+ },
+ "time": "2017-09-19T15:49:58+00:00"
+ },
+ {
+ "name": "justinrainbow/json-schema",
+ "version": "5.2.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/justinrainbow/json-schema.git",
+ "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
+ "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
+ "json-schema/json-schema-test-suite": "1.2.0",
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "bin": [
+ "bin/validate-json"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "JsonSchema\\": "src/JsonSchema/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bruno Prieto Reis",
+ "email": "bruno.p.reis@gmail.com"
+ },
+ {
+ "name": "Justin Rainbow",
+ "email": "justin.rainbow@gmail.com"
+ },
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch"
+ },
+ {
+ "name": "Robert Schönthal",
+ "email": "seroscho@googlemail.com"
+ }
+ ],
+ "description": "A library to validate a json schema.",
+ "homepage": "https://github.com/justinrainbow/json-schema",
+ "keywords": [
+ "json",
+ "schema"
+ ],
+ "support": {
+ "issues": "https://github.com/justinrainbow/json-schema/issues",
+ "source": "https://github.com/justinrainbow/json-schema/tree/5.2.10"
+ },
+ "time": "2020-05-27T16:41:55+00:00"
+ },
+ {
+ "name": "m1/env",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/m1/Env.git",
+ "reference": "5c296e3e13450a207e12b343f3af1d7ab569f6f3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/m1/Env/zipball/5c296e3e13450a207e12b343f3af1d7ab569f6f3",
+ "reference": "5c296e3e13450a207e12b343f3af1d7ab569f6f3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*",
+ "scrutinizer/ocular": "~1.1",
+ "squizlabs/php_codesniffer": "^2.3"
+ },
+ "suggest": {
+ "josegonzalez/dotenv": "For loading of .env",
+ "m1/vars": "For loading of configs"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "M1\\Env\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Miles Croxford",
+ "email": "hello@milescroxford.com",
+ "homepage": "http://milescroxford.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "Env is a lightweight library bringing .env file parser compatibility to PHP. In short - it enables you to read .env files with PHP.",
+ "homepage": "https://github.com/m1/Env",
+ "keywords": [
+ ".env",
+ "config",
+ "dotenv",
+ "env",
+ "loader",
+ "m1",
+ "parser",
+ "support"
+ ],
+ "support": {
+ "issues": "https://github.com/m1/Env/issues",
+ "source": "https://github.com/m1/Env/tree/2.2.0"
+ },
+ "time": "2020-02-19T09:02:13+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.10.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220",
+ "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "replace": {
+ "myclabs/deep-copy": "self.version"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.0",
+ "doctrine/common": "^2.6",
+ "phpunit/phpunit": "^7.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ },
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-11-13T09:40:50+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v4.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/fe14cf3672a149364fb66dfe11bf6549af899f94",
+ "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.11.0"
+ },
+ "time": "2021-07-03T13:36:55+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+ "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/master"
+ },
+ "time": "2020-06-27T14:33:11+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "bae7c545bef187884426f042434e561ab1ddb182"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182",
+ "reference": "bae7c545bef187884426f042434e561ab1ddb182",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.1.0"
+ },
+ "time": "2021-02-23T14:00:09+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-2.x": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+ },
+ "time": "2020-06-27T09:03:43+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "5.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556",
+ "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556",
+ "shasum": ""
+ },
+ "require": {
+ "ext-filter": "*",
+ "php": "^7.2 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.2",
+ "phpdocumentor/type-resolver": "^1.3",
+ "webmozart/assert": "^1.9.1"
+ },
+ "require-dev": {
+ "mockery/mockery": "~1.3.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ },
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "account@ijaap.nl"
+ }
+ ],
+ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
+ },
+ "time": "2020-09-03T19:13:55+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+ "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.0"
+ },
+ "require-dev": {
+ "ext-tokenizer": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-1.x": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
+ },
+ "time": "2020-09-17T18:55:26+00:00"
+ },
+ {
+ "name": "phpspec/prophecy",
+ "version": "1.13.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy.git",
+ "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea",
+ "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.2",
+ "php": "^7.2 || ~8.0, <8.1",
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "sebastian/comparator": "^3.0 || ^4.0",
+ "sebastian/recursion-context": "^3.0 || ^4.0"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "^6.0",
+ "phpunit/phpunit": "^8.0 || ^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.11.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Prophecy\\": "src/Prophecy"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ },
+ {
+ "name": "Marcello Duarte",
+ "email": "marcello.duarte@gmail.com"
+ }
+ ],
+ "description": "Highly opinionated mocking framework for PHP 5.3+",
+ "homepage": "https://github.com/phpspec/prophecy",
+ "keywords": [
+ "Double",
+ "Dummy",
+ "fake",
+ "mock",
+ "spy",
+ "stub"
+ ],
+ "support": {
+ "issues": "https://github.com/phpspec/prophecy/issues",
+ "source": "https://github.com/phpspec/prophecy/tree/1.13.0"
+ },
+ "time": "2021-03-17T13:42:18+00:00"
+ },
+ {
+ "name": "phpstan/phpdoc-parser",
+ "version": "0.4.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpdoc-parser.git",
+ "reference": "98a088b17966bdf6ee25c8a4b634df313d8aa531"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/98a088b17966bdf6ee25c8a4b634df313d8aa531",
+ "reference": "98a088b17966bdf6ee25c8a4b634df313d8aa531",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "consistence/coding-standard": "^3.5",
+ "ergebnis/composer-normalize": "^2.0.2",
+ "jakub-onderka/php-parallel-lint": "^0.9.2",
+ "phing/phing": "^2.16.0",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^0.12.26",
+ "phpstan/phpstan-strict-rules": "^0.12",
+ "phpunit/phpunit": "^6.3",
+ "slevomat/coding-standard": "^4.7.2",
+ "symfony/process": "^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\PhpDocParser\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPDoc parser with support for nullable, intersection and generic types",
+ "support": {
+ "issues": "https://github.com/phpstan/phpdoc-parser/issues",
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/master"
+ },
+ "time": "2020-08-03T20:32:43+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "f6293e1b30a2354e8428e004689671b83871edde"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde",
+ "reference": "f6293e1b30a2354e8428e004689671b83871edde",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.10.2",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.3",
+ "phpunit/php-text-template": "^2.0.2",
+ "sebastian/code-unit-reverse-lookup": "^2.0.2",
+ "sebastian/complexity": "^2.0",
+ "sebastian/environment": "^5.1.2",
+ "sebastian/lines-of-code": "^1.0.3",
+ "sebastian/version": "^3.0.1",
+ "theseer/tokenizer": "^1.2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcov": "*",
+ "ext-xdebug": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-03-28T07:26:59+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8",
+ "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:57:25+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.5.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "fb9b8333f14e3dce976a60ef6a7e05c7c7ed8bfb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fb9b8333f14e3dce976a60ef6a7e05c7c7ed8bfb",
+ "reference": "fb9b8333f14e3dce976a60ef6a7e05c7c7ed8bfb",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.3.1",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.10.1",
+ "phar-io/manifest": "^2.0.1",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.3",
+ "phpspec/prophecy": "^1.12.1",
+ "phpunit/php-code-coverage": "^9.2.3",
+ "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.3",
+ "phpunit/php-timer": "^5.0.2",
+ "sebastian/cli-parser": "^1.0.1",
+ "sebastian/code-unit": "^1.0.6",
+ "sebastian/comparator": "^4.0.5",
+ "sebastian/diff": "^4.0.3",
+ "sebastian/environment": "^5.1.3",
+ "sebastian/exporter": "^4.0.3",
+ "sebastian/global-state": "^5.0.1",
+ "sebastian/object-enumerator": "^4.0.3",
+ "sebastian/resource-operations": "^3.0.3",
+ "sebastian/type": "^2.3.4",
+ "sebastian/version": "^3.0.2"
+ },
+ "require-dev": {
+ "ext-pdo": "*",
+ "phpspec/prophecy-phpunit": "^2.0.1"
+ },
+ "suggest": {
+ "ext-soap": "*",
+ "ext-xdebug": "*"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.5-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ],
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.6"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/donate.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-06-23T05:14:38+00:00"
+ },
+ {
+ "name": "psy/psysh",
+ "version": "v0.10.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/bobthecow/psysh.git",
+ "reference": "e4573f47750dd6c92dca5aee543fa77513cbd8d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/e4573f47750dd6c92dca5aee543fa77513cbd8d3",
+ "reference": "e4573f47750dd6c92dca5aee543fa77513cbd8d3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "nikic/php-parser": "~4.0|~3.0|~2.0|~1.3",
+ "php": "^8.0 || ^7.0 || ^5.5.9",
+ "symfony/console": "~5.0|~4.0|~3.0|^2.4.2|~2.3.10",
+ "symfony/var-dumper": "~5.0|~4.0|~3.0|~2.7"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.2",
+ "hoa/console": "3.17.*"
+ },
+ "suggest": {
+ "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)",
+ "ext-pdo-sqlite": "The doc command requires SQLite to work.",
+ "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.",
+ "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history.",
+ "hoa/console": "A pure PHP readline implementation. You'll want this if your PHP install doesn't already support readline or libedit."
+ },
+ "bin": [
+ "bin/psysh"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "0.10.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Psy\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Justin Hileman",
+ "email": "justin@justinhileman.info",
+ "homepage": "http://justinhileman.com"
+ }
+ ],
+ "description": "An interactive shell for modern PHP.",
+ "homepage": "http://psysh.org",
+ "keywords": [
+ "REPL",
+ "console",
+ "interactive",
+ "shell"
+ ],
+ "support": {
+ "issues": "https://github.com/bobthecow/psysh/issues",
+ "source": "https://github.com/bobthecow/psysh/tree/v0.10.8"
+ },
+ "time": "2021-04-10T16:23:39+00:00"
+ },
+ {
+ "name": "react/promise",
+ "version": "v2.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/promise.git",
+ "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4",
+ "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Promise\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com"
+ }
+ ],
+ "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+ "keywords": [
+ "promise",
+ "promises"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/promise/issues",
+ "source": "https://github.com/reactphp/promise/tree/v2.8.0"
+ },
+ "time": "2020-05-12T15:16:56+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:08:49+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "55f4261989e546dc112258c7a75935a81a7ce382"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382",
+ "reference": "55f4261989e546dc112258c7a75935a81a7ce382",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:49:45+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.7",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:52:27+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+ "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:10:38+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "388b6ced16caa751030f6a69e588299fa09200ac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac",
+ "reference": "388b6ced16caa751030f6a69e588299fa09200ac",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:52:38+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65",
+ "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "http://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:24:23+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/23bd5951f7ff26f12d4e3242864df3e08dec4e49",
+ "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-06-11T13:31:12+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.6",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-28T06:42:11+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172",
+ "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:17:30+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:45:17+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "2.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914",
+ "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/2.3.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-06-15T12:49:02+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "seld/jsonlint",
+ "version": "1.8.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/jsonlint.git",
+ "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57",
+ "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+ },
+ "bin": [
+ "bin/jsonlint"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Seld\\JsonLint\\": "src/Seld/JsonLint/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "JSON Linter",
+ "keywords": [
+ "json",
+ "linter",
+ "parser",
+ "validator"
+ ],
+ "support": {
+ "issues": "https://github.com/Seldaek/jsonlint/issues",
+ "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-11-11T09:19:24+00:00"
+ },
+ {
+ "name": "seld/phar-utils",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/phar-utils.git",
+ "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8674b1d84ffb47cc59a101f5d5a3b61e87d23796",
+ "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Seld\\PharUtils\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be"
+ }
+ ],
+ "description": "PHAR file format utilities, for when PHP phars you up",
+ "keywords": [
+ "phar"
+ ],
+ "support": {
+ "issues": "https://github.com/Seldaek/phar-utils/issues",
+ "source": "https://github.com/Seldaek/phar-utils/tree/master"
+ },
+ "time": "2020-07-07T18:42:57+00:00"
+ },
+ {
+ "name": "slevomat/coding-standard",
+ "version": "6.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slevomat/coding-standard.git",
+ "reference": "696dcca217d0c9da2c40d02731526c1e25b65346"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/696dcca217d0c9da2c40d02731526c1e25b65346",
+ "reference": "696dcca217d0c9da2c40d02731526c1e25b65346",
+ "shasum": ""
+ },
+ "require": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7",
+ "php": "^7.1 || ^8.0",
+ "phpstan/phpdoc-parser": "0.4.5 - 0.4.9",
+ "squizlabs/php_codesniffer": "^3.5.6"
+ },
+ "require-dev": {
+ "phing/phing": "2.16.3",
+ "php-parallel-lint/php-parallel-lint": "1.2.0",
+ "phpstan/phpstan": "0.12.48",
+ "phpstan/phpstan-deprecation-rules": "0.12.5",
+ "phpstan/phpstan-phpunit": "0.12.16",
+ "phpstan/phpstan-strict-rules": "0.12.5",
+ "phpunit/phpunit": "7.5.20|8.5.5|9.4.0"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "6.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "SlevomatCodingStandard\\": "SlevomatCodingStandard"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.",
+ "support": {
+ "issues": "https://github.com/slevomat/coding-standard/issues",
+ "source": "https://github.com/slevomat/coding-standard/tree/6.4.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kukulich",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-10-05T12:39:37+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.5.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
+ "reference": "9d583721a7157ee997f235f327de038e7ea6dac4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4",
+ "reference": "9d583721a7157ee997f235f327de038e7ea6dac4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "bin": [
+ "bin/phpcs",
+ "bin/phpcbf"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "lead"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards"
+ ],
+ "support": {
+ "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
+ "source": "https://github.com/squizlabs/PHP_CodeSniffer",
+ "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
+ },
+ "time": "2020-10-23T02:01:07+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v5.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "0ae3f047bed4edff6fd35b26a9a6bfdc92c953c6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/0ae3f047bed4edff6fd35b26a9a6bfdc92c953c6",
+ "reference": "0ae3f047bed4edff6fd35b26a9a6bfdc92c953c6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5"
+ },
+ "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.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-26T12:52:38+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v5.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "714b47f9196de61a196d86c4bad5f09201b307df"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/714b47f9196de61a196d86c4bad5f09201b307df",
+ "reference": "714b47f9196de61a196d86c4bad5f09201b307df",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-php80": "^1.15"
+ },
+ "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.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-06-12T10:15:01+00:00"
+ },
+ {
+ "name": "symfony/var-dumper",
+ "version": "v5.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-dumper.git",
+ "reference": "46aa709affb9ad3355bd7a810f9662d71025c384"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/46aa709affb9ad3355bd7a810f9662d71025c384",
+ "reference": "46aa709affb9ad3355bd7a810f9662d71025c384",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "^1.15"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<5.4.3",
+ "symfony/console": "<4.4"
+ },
+ "require-dev": {
+ "ext-iconv": "*",
+ "symfony/console": "^4.4|^5.0",
+ "symfony/process": "^4.4|^5.0",
+ "twig/twig": "^2.13|^3.0.4"
+ },
+ "suggest": {
+ "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
+ "ext-intl": "To show region name in time zone dump",
+ "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
+ },
+ "bin": [
+ "Resources/bin/var-dump-server"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions/dump.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\VarDumper\\": ""
+ },
+ "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 mechanisms for walking through any arbitrary PHP variable",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "debug",
+ "dump"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-dumper/tree/v5.3.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-06-24T08:13:00+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "75a63c33a8577608444246075ea0af0d052e452a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
+ "reference": "75a63c33a8577608444246075ea0af0d052e452a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/master"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2020-07-12T23:59:07+00:00"
+ },
+ {
+ "name": "twig/markdown-extra",
+ "version": "v3.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/markdown-extra.git",
+ "reference": "a9fe276dbb7f837c3f4ecc6dad89bcccb9fc8bc9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/a9fe276dbb7f837c3f4ecc6dad89bcccb9fc8bc9",
+ "reference": "a9fe276dbb7f837c3f4ecc6dad89bcccb9fc8bc9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.3",
+ "twig/twig": "^2.4|^3.0"
+ },
+ "require-dev": {
+ "erusev/parsedown": "^1.7",
+ "league/commonmark": "^1.0",
+ "league/html-to-markdown": "^4.8|^5.0",
+ "michelf/php-markdown": "^1.8",
+ "symfony/phpunit-bridge": "^4.4.9|^5.0.9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Twig\\Extra\\Markdown\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "A Twig extension for Markdown",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "html",
+ "markdown",
+ "twig"
+ ],
+ "support": {
+ "source": "https://github.com/twigphp/markdown-extra/tree/v3.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-04-01T12:19:25+00:00"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/Twig.git",
+ "reference": "21578f00e83d4a82ecfa3d50752b609f13de6790"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/21578f00e83d4a82ecfa3d50752b609f13de6790",
+ "reference": "21578f00e83d4a82ecfa3d50752b609f13de6790",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.3"
+ },
+ "require-dev": {
+ "psr/container": "^1.0",
+ "symfony/phpunit-bridge": "^4.4.9|^5.0.9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Twig\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Twig Team",
+ "role": "Contributors"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com",
+ "role": "Project Founder"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "templating"
+ ],
+ "support": {
+ "issues": "https://github.com/twigphp/Twig/issues",
+ "source": "https://github.com/twigphp/Twig/tree/v3.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-05-16T12:14:13+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "1.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozarts/assert.git",
+ "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
+ "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<0.12.20",
+ "vimeo/psalm": "<4.6.1 || 4.6.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5.13"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.10-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "support": {
+ "issues": "https://github.com/webmozarts/assert/issues",
+ "source": "https://github.com/webmozarts/assert/tree/1.10.0"
+ },
+ "time": "2021-03-09T10:59:23+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {
+ "psy/psysh": 0
+ },
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=7.2"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "2.1.0"
+}
diff --git a/app/config/.env.example b/app/config/.env.example
new file mode 100644
index 000000000..15060f7be
--- /dev/null
+++ b/app/config/.env.example
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+# Used as a default to seed config/.env which
+# enables you to use environment variables to configure
+# the aspects of your application that vary by
+# environment.
+#
+# Having this file in production is considered a **SECURITY RISK** and also decreases
+# the boostrap performance of your application.
+#
+# To use this file, first copy it into `config/.env`. Also ensure the related
+# code block for loading this file is uncommented in `config/boostrap.php`
+#
+# In development .env files are parsed by PHP
+# and set into the environment. This provides a simpler
+# development workflow over standard environment variables.
+export APP_NAME="__APP_NAME__"
+export DEBUG="true"
+export APP_ENCODING="UTF-8"
+export APP_DEFAULT_LOCALE="en_US"
+export APP_DEFAULT_TIMEZONE="UTC"
+export SECURITY_SALT="__SALT__"
+
+# Uncomment these to define cache configuration via environment variables.
+#export CACHE_DURATION="+2 minutes"
+#export CACHE_DEFAULT_URL="file://tmp/cache/?prefix=${APP_NAME}_default&duration=${CACHE_DURATION}"
+#export CACHE_CAKECORE_URL="file://tmp/cache/persistent?prefix=${APP_NAME}_cake_core&serialize=true&duration=${CACHE_DURATION}"
+#export CACHE_CAKEMODEL_URL="file://tmp/cache/models?prefix=${APP_NAME}_cake_model&serialize=true&duration=${CACHE_DURATION}"
+
+# Uncomment these to define email transport configuration via environment variables.
+#export EMAIL_TRANSPORT_DEFAULT_URL=""
+
+# Uncomment these to define database configuration via environment variables.
+#export DATABASE_URL="mysql://my_app:secret@localhost/${APP_NAME}?encoding=utf8&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false"
+#export DATABASE_TEST_URL="mysql://my_app:secret@localhost/test_${APP_NAME}?encoding=utf8&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false"
+
+# Uncomment these to define logging configuration via environment variables.
+#export LOG_DEBUG_URL="file://logs/?levels[]=notice&levels[]=info&levels[]=debug&file=debug"
+#export LOG_ERROR_URL="file://logs/?levels[]=warning&levels[]=error&levels[]=critical&levels[]=alert&levels[]=emergency&file=error"
diff --git a/app/config/VERSION b/app/config/VERSION
new file mode 100644
index 000000000..0062ac971
--- /dev/null
+++ b/app/config/VERSION
@@ -0,0 +1 @@
+5.0.0
diff --git a/app/config/app.php b/app/config/app.php
new file mode 100644
index 000000000..1221c1e41
--- /dev/null
+++ b/app/config/app.php
@@ -0,0 +1,428 @@
+ filter_var(env('DEBUG', true), FILTER_VALIDATE_BOOLEAN),
+
+ /**
+ * Configure basic information about the application.
+ *
+ * - namespace - The namespace to find app classes under.
+ * - defaultLocale - The default locale for translation, formatting currencies and numbers, date and time.
+ * - encoding - The encoding used for HTML + database connections.
+ * - base - The base directory the app resides in. If false this
+ * will be auto detected.
+ * - dir - Name of app directory.
+ * - webroot - The webroot directory.
+ * - wwwRoot - The file path to webroot.
+ * - baseUrl - To configure CakePHP to *not* use mod_rewrite and to
+ * use CakePHP pretty URLs, remove these .htaccess
+ * files:
+ * /.htaccess
+ * /webroot/.htaccess
+ * And uncomment the baseUrl key below.
+ * - fullBaseUrl - A base URL to use for absolute links. When set to false (default)
+ * CakePHP generates required value based on `HTTP_HOST` environment variable.
+ * However, you can define it manually to optimize performance or if you
+ * are concerned about people manipulating the `Host` header.
+ * - imageBaseUrl - Web path to the public images directory under webroot.
+ * - cssBaseUrl - Web path to the public css directory under webroot.
+ * - jsBaseUrl - Web path to the public js directory under webroot.
+ * - paths - Configure paths for non class based resources. Supports the
+ * `plugins`, `templates`, `locales` subkeys, which allow the definition of
+ * paths for plugins, view templates and locale files respectively.
+ */
+ 'App' => [
+ 'namespace' => 'App',
+ 'encoding' => env('APP_ENCODING', 'UTF-8'),
+ 'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'),
+ 'defaultTimezone' => env('APP_DEFAULT_TIMEZONE', 'UTC'),
+ 'base' => false,
+ 'dir' => 'src',
+ 'webroot' => 'webroot',
+ 'wwwRoot' => WWW_ROOT,
+ //'baseUrl' => env('SCRIPT_NAME'),
+ 'fullBaseUrl' => false,
+ 'imageBaseUrl' => 'img/',
+ 'cssBaseUrl' => 'css/',
+ 'jsBaseUrl' => 'js/',
+ 'paths' => [
+ 'plugins' => [ROOT . DS . 'plugins' . DS],
+ 'templates' => [ROOT . DS . 'templates' . DS],
+ 'locales' => [ROOT . DS . 'resources' . DS . 'locales' . DS],
+ ],
+ ],
+
+ /**
+ * Security and encryption configuration
+ *
+ * - salt - A random string used in security hashing methods.
+ * The salt value is also used as the encryption key.
+ * You should treat it as extremely sensitive data.
+ */
+ 'Security' => [
+ // Note that we (COmanage) override this in bootstrap.php
+ //'salt' => env('SECURITY_SALT', 'd6ded009aad8fe73e7ebf4f9c170e39e3b0ed0ab9253ed3eb4db03ad6fc07ab4'),
+ ],
+
+ /**
+ * Apply timestamps with the last modified time to static assets (js, css, images).
+ * Will append a querystring parameter containing the time the file was modified.
+ * This is useful for busting browser caches.
+ *
+ * Set to true to apply timestamps when debug is true. Set to 'force' to always
+ * enable timestamping regardless of debug value.
+ */
+ 'Asset' => [
+ //'timestamp' => true,
+ // 'cacheTime' => '+1 year'
+ ],
+
+ /**
+ * Configure the cache adapters.
+ */
+ 'Cache' => [
+ 'default' => [
+ 'className' => 'Cake\Cache\Engine\FileEngine',
+ 'path' => CACHE,
+ 'url' => env('CACHE_DEFAULT_URL', null),
+ ],
+
+ /**
+ * Configure the cache used for general framework caching.
+ * Translation cache files are stored with this configuration.
+ * Duration will be set to '+2 minutes' in bootstrap.php when debug = true
+ * If you set 'className' => 'Null' core cache will be disabled.
+ */
+ '_cake_core_' => [
+ 'className' => 'Cake\Cache\Engine\FileEngine',
+ 'prefix' => 'myapp_cake_core_',
+ 'path' => CACHE . 'persistent/',
+ 'serialize' => true,
+ 'duration' => '+1 years',
+ 'url' => env('CACHE_CAKECORE_URL', null),
+ ],
+
+ /**
+ * Configure the cache for model and datasource caches. This cache
+ * configuration is used to store schema descriptions, and table listings
+ * in connections.
+ * Duration will be set to '+2 minutes' in bootstrap.php when debug = true
+ */
+ '_cake_model_' => [
+ 'className' => 'Cake\Cache\Engine\FileEngine',
+ 'prefix' => 'myapp_cake_model_',
+ 'path' => CACHE . 'models/',
+ 'serialize' => true,
+ 'duration' => '+1 years',
+ 'url' => env('CACHE_CAKEMODEL_URL', null),
+ ],
+
+ /**
+ * Configure the cache for routes. The cached routes collection is built the
+ * first time the routes are processed via `config/routes.php`.
+ * Duration will be set to '+2 seconds' in bootstrap.php when debug = true
+ */
+ '_cake_routes_' => [
+ 'className' => 'Cake\Cache\Engine\FileEngine',
+ 'prefix' => 'myapp_cake_routes_',
+ 'path' => CACHE,
+ 'serialize' => true,
+ 'duration' => '+1 years',
+ 'url' => env('CACHE_CAKEROUTES_URL', null),
+ ],
+ ],
+
+ /**
+ * Configure the Error and Exception handlers used by your application.
+ *
+ * By default errors are displayed using Debugger, when debug is true and logged
+ * by Cake\Log\Log when debug is false.
+ *
+ * In CLI environments exceptions will be printed to stderr with a backtrace.
+ * In web environments an HTML page will be displayed for the exception.
+ * With debug true, framework errors like Missing Controller will be displayed.
+ * When debug is false, framework errors will be coerced into generic HTTP errors.
+ *
+ * Options:
+ *
+ * - `errorLevel` - int - The level of errors you are interested in capturing.
+ * - `trace` - boolean - Whether or not backtraces should be included in
+ * logged errors/exceptions.
+ * - `log` - boolean - Whether or not you want exceptions logged.
+ * - `exceptionRenderer` - string - The class responsible for rendering
+ * uncaught exceptions. If you choose a custom class you should place
+ * the file for that class in src/Error. This class needs to implement a
+ * render method.
+ * - `skipLog` - array - List of exceptions to skip for logging. Exceptions that
+ * extend one of the listed exceptions will also be skipped for logging.
+ * E.g.:
+ * `'skipLog' => ['Cake\Http\Exception\NotFoundException', 'Cake\Http\Exception\UnauthorizedException']`
+ * - `extraFatalErrorMemory` - int - The number of megabytes to increase
+ * the memory limit by when a fatal error is encountered. This allows
+ * breathing room to complete logging or error handling.
+ */
+ 'Error' => [
+ 'errorLevel' => E_ALL,
+ 'exceptionRenderer' => 'Cake\Error\ExceptionRenderer',
+ 'skipLog' => [],
+ 'log' => true,
+ 'trace' => true,
+ ],
+
+ /**
+ * Email configuration.
+ *
+ * By defining transports separately from delivery profiles you can easily
+ * re-use transport configuration across multiple profiles.
+ *
+ * You can specify multiple configurations for production, development and
+ * testing.
+ *
+ * Each transport needs a `className`. Valid options are as follows:
+ *
+ * Mail - Send using PHP mail function
+ * Smtp - Send using SMTP
+ * Debug - Do not send the email, just return the result
+ *
+ * You can add custom transports (or override existing transports) by adding the
+ * appropriate file to src/Mailer/Transport. Transports should be named
+ * 'YourTransport.php', where 'Your' is the name of the transport.
+ */
+ 'EmailTransport' => [
+ 'default' => [
+ 'className' => 'Cake\Mailer\Transport\MailTransport',
+ /*
+ * The following keys are used in SMTP transports:
+ */
+ 'host' => 'localhost',
+ 'port' => 25,
+ 'timeout' => 30,
+ 'username' => null,
+ 'password' => null,
+ 'client' => null,
+ 'tls' => null,
+ 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null),
+ ],
+ ],
+
+ /**
+ * Email delivery profiles
+ *
+ * Delivery profiles allow you to predefine various properties about email
+ * messages from your application and give the settings a name. This saves
+ * duplication across your application and makes maintenance and development
+ * easier. Each profile accepts a number of keys. See `Cake\Mailer\Email`
+ * for more information.
+ */
+ 'Email' => [
+ 'default' => [
+ 'transport' => 'default',
+ 'from' => 'you@localhost',
+ //'charset' => 'utf-8',
+ //'headerCharset' => 'utf-8',
+ ],
+ ],
+
+ /**
+ * Connection information used by the ORM to connect
+ * to your application's datastores.
+ *
+ * ### Notes
+ * - Drivers include Mysql Postgres Sqlite Sqlserver
+ * See vendor\cakephp\cakephp\src\Database\Driver for complete list
+ * - Do not use periods in database name - it may lead to error.
+ * See https://github.com/cakephp/cakephp/issues/6471 for details.
+ * - 'encoding' is recommended to be set to full UTF-8 4-Byte support.
+ * E.g set it to 'utf8mb4' in MariaDB and MySQL and 'utf8' for any
+ * other RDBMS.
+ *
+ * Note for COmanage we read in local/Config/database.php instead
+ */
+ 'Datasources' => [
+ 'default' => [
+ 'className' => 'Cake\Database\Connection',
+ 'driver' => 'Cake\Database\Driver\Mysql',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ /*
+ * CakePHP will use the default DB port based on the driver selected
+ * MySQL on MAMP uses port 8889, MAMP users will want to uncomment
+ * the following line and set the port accordingly
+ */
+ //'port' => 'non_standard_port_number',
+ 'username' => 'my_app',
+ 'password' => 'secret',
+ 'database' => 'my_app',
+ /*
+ * You do not need to set this flag to use full utf-8 encoding (internal default since CakePHP 3.6).
+ */
+ //'encoding' => 'utf8mb4',
+ 'timezone' => 'UTC',
+ 'flags' => [],
+ 'cacheMetadata' => true,
+ // Set to true to get query log for debugging
+ 'log' => false,
+
+ /**
+ * Set identifier quoting to true if you are using reserved words or
+ * special characters in your table or column names. Enabling this
+ * setting will result in queries built using the Query Builder having
+ * identifiers quoted when creating SQL. It should be noted that this
+ * decreases performance because each query needs to be traversed and
+ * manipulated before being executed.
+ */
+ 'quoteIdentifiers' => false,
+
+ /**
+ * During development, if using MySQL < 5.6, uncommenting the
+ * following line could boost the speed at which schema metadata is
+ * fetched from the database. It can also be set directly with the
+ * mysql configuration directive 'innodb_stats_on_metadata = 0'
+ * which is the recommended value in production environments
+ */
+ //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
+
+ 'url' => env('DATABASE_URL', null),
+ ],
+
+ /**
+ * The test connection is used during the test suite.
+ */
+ 'test' => [
+ 'className' => 'Cake\Database\Connection',
+ 'driver' => 'Cake\Database\Driver\Mysql',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ //'port' => 'non_standard_port_number',
+ 'username' => 'my_app',
+ 'password' => 'secret',
+ 'database' => 'test_myapp',
+ //'encoding' => 'utf8mb4',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ 'quoteIdentifiers' => false,
+ 'log' => false,
+ //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
+ 'url' => env('DATABASE_TEST_URL', null),
+ ],
+ ],
+
+ /**
+ * Configures logging options
+ */
+ 'Log' => [
+ 'debug' => [
+ 'className' => 'Cake\Log\Engine\FileLog',
+ 'path' => LOGS,
+ 'file' => 'debug',
+ 'url' => env('LOG_DEBUG_URL', null),
+ 'scopes' => false,
+ 'levels' => ['notice', 'info', 'debug'],
+ ],
+ 'error' => [
+ 'className' => 'Cake\Log\Engine\FileLog',
+ 'path' => LOGS,
+ 'file' => 'error',
+ 'url' => env('LOG_ERROR_URL', null),
+ 'scopes' => false,
+ 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
+ ],
+ // To enable this dedicated query log, you need set your datasource's log flag to true
+ 'queries' => [
+ 'className' => 'Cake\Log\Engine\FileLog',
+ 'path' => LOGS,
+ 'file' => 'queries',
+ 'url' => env('LOG_QUERIES_URL', null),
+ 'scopes' => ['queriesLog'],
+ ],
+ // We define a trace level for what is really debugging, except debug level
+ // will write to stdout instead of the log when debug=true
+ 'trace' => [
+ 'className' => 'Cake\Log\Engine\FileLog',
+ 'path' => LOGS,
+ 'file' => 'trace',
+ 'url' => env('LOG_TRACE_URL', null),
+ 'scopes' => ['trace'],
+ ]
+ ],
+
+ /**
+ * Session configuration.
+ *
+ * Contains an array of settings to use for session configuration. The
+ * `defaults` key is used to define a default preset to use for sessions, any
+ * settings declared here will override the settings of the default config.
+ *
+ * ## Options
+ *
+ * - `cookie` - The name of the cookie to use. Defaults to 'CAKEPHP'. Avoid using `.` in cookie names,
+ * as PHP will drop sessions from cookies with `.` in the name.
+ * - `cookiePath` - The url path for which session cookie is set. Maps to the
+ * `session.cookie_path` php.ini config. Defaults to base path of app.
+ * - `timeout` - The time in minutes the session should be valid for.
+ * Pass 0 to disable checking timeout.
+ * Please note that php.ini's session.gc_maxlifetime must be equal to or greater
+ * than the largest Session['timeout'] in all served websites for it to have the
+ * desired effect.
+ * - `defaults` - The default configuration set to use as a basis for your session.
+ * There are four built-in options: php, cake, cache, database.
+ * - `handler` - Can be used to enable a custom session handler. Expects an
+ * array with at least the `engine` key, being the name of the Session engine
+ * class to use for managing the session. CakePHP bundles the `CacheSession`
+ * and `DatabaseSession` engines.
+ * - `ini` - An associative array of additional ini values to set.
+ *
+ * The built-in `defaults` options are:
+ *
+ * - 'php' - Uses settings defined in your php.ini.
+ * - 'cake' - Saves session files in CakePHP's /tmp directory.
+ * - 'database' - Uses CakePHP's database sessions.
+ * - 'cache' - Use the Cache class to save sessions.
+ *
+ * To define a custom session handler, save it at src/Network/Session/.php.
+ * Make sure the class implements PHP's `SessionHandlerInterface` and set
+ * Session.handler to
+ *
+ * To use database sessions, load the SQL file located at config/schema/sessions.sql
+ */
+ 'Session' => [
+ 'defaults' => 'php',
+ // Switch cookie name to avoid conflict with older versions of Registry
+ // Note this name must match the name used in webroot/auth/*/*
+ 'cookie' => 'REGISTRYPECAKEPHP'
+ ],
+];
diff --git a/app/config/bootstrap.php b/app/config/bootstrap.php
new file mode 100644
index 000000000..da1c9efd0
--- /dev/null
+++ b/app/config/bootstrap.php
@@ -0,0 +1,235 @@
+parse()
+// ->putenv()
+// ->toEnv()
+// ->toServer();
+// }
+
+/*
+ * Read configuration file and inject configuration into various
+ * CakePHP classes.
+ *
+ * By default there is only one configuration file. It is often a good
+ * idea to create multiple configuration files, and separate the configuration
+ * that changes from configuration that does not. This makes deployment simpler.
+ */
+try {
+ Configure::config('default', new PhpConfig());
+ Configure::load('app', 'default', false);
+ // Read site specific configurations from the COmanage Registry local directory
+ Configure::config('default', new PhpConfig(LOCAL . 'config' . DS));
+ Configure::load('database', 'default');
+} catch (\Exception $e) {
+ exit($e->getMessage() . "\n");
+}
+
+/*
+ * Load an environment local configuration file to provide overrides to your configuration.
+ * Notice: For security reasons app_local.php will not be included in your git repo.
+ *
+if (file_exists(CONFIG . 'app_local.php')) {
+ Configure::load('app_local', 'default');
+}*/
+
+/*
+ * When debug = true the metadata cache should only last
+ * for a short time.
+ */
+if (Configure::read('debug')) {
+ Cache::disable();
+ //Configure::write('Cache._cake_model_.duration', '+2 minutes');
+ //Configure::write('Cache._cake_core_.duration', '+2 minutes');
+ // disable router cache during development
+ //Configure::write('Cache._cake_routes_.duration', '+2 seconds');
+}
+
+/*
+ * Set the default server timezone. Using UTC makes time calculations / conversions easier.
+ * Check http://php.net/manual/en/timezones.php for list of valid timezone strings.
+ */
+date_default_timezone_set(Configure::read('App.defaultTimezone'));
+
+/*
+ * Configure the mbstring extension to use the correct encoding.
+ */
+mb_internal_encoding(Configure::read('App.encoding'));
+
+/*
+ * Set the default locale. This controls how dates, number and currency is
+ * formatted and sets the default language to use for translations.
+ */
+ini_set('intl.default_locale', Configure::read('App.defaultLocale'));
+
+/*
+ * Register application error and exception handlers.
+ */
+$isCli = PHP_SAPI === 'cli';
+if ($isCli) {
+ (new ConsoleErrorHandler(Configure::read('Error')))->register();
+} else {
+ (new ErrorHandler(Configure::read('Error')))->register();
+}
+
+/*
+ * Include the CLI bootstrap overrides.
+ */
+if ($isCli) {
+ require CONFIG . 'bootstrap_cli.php';
+}
+
+/*
+ * Set the full base URL.
+ * This URL is used as the base of all absolute links.
+ */
+$fullBaseUrl = Configure::read('App.fullBaseUrl');
+if (!$fullBaseUrl) {
+ $s = null;
+ if (env('HTTPS')) {
+ $s = 's';
+ }
+
+ $httpHost = env('HTTP_HOST');
+ if (isset($httpHost)) {
+ $fullBaseUrl = 'http' . $s . '://' . $httpHost;
+ }
+ unset($httpHost, $s);
+}
+if ($fullBaseUrl) {
+ Router::fullBaseUrl($fullBaseUrl);
+}
+unset($fullBaseUrl);
+
+Cache::setConfig(Configure::consume('Cache'));
+ConnectionManager::setConfig(Configure::consume('Datasources'));
+TransportFactory::setConfig(Configure::consume('EmailTransport'));
+Mailer::setConfig(Configure::consume('Email'));
+Log::setConfig(Configure::consume('Log'));
+// Set the salt based on our local configuration
+$securitySaltFile = LOCAL . DS . "config" . DS . "security.salt";
+// If the file doesn't exist yet, we're probably in SetupCommand, which will create it
+if(file_exists($securitySaltFile)) {
+ $salt = file_get_contents($securitySaltFile);
+ Security::setSalt($salt);
+}
+//Security::setSalt(Configure::consume('Security.salt'));
+
+/*
+ * Setup detectors for mobile and tablet.
+ */
+ServerRequest::addDetector('mobile', function ($request) {
+ $detector = new \Detection\MobileDetect();
+
+ return $detector->isMobile();
+});
+ServerRequest::addDetector('tablet', function ($request) {
+ $detector = new \Detection\MobileDetect();
+
+ return $detector->isTablet();
+});
+
+/*
+ * You can set whether the ORM uses immutable or mutable Time types.
+ * The default changed in 4.0 to immutable types. You can uncomment
+ * below to switch back to mutable types.
+ *
+ * You can enable default locale format parsing by adding calls
+ * to `useLocaleParser()`. This enables the automatic conversion of
+ * locale specific date formats. For details see
+ * @link https://book.cakephp.org/4/en/core-libraries/internationalization-and-localization.html#parsing-localized-datetime-data
+ */
+// \Cake\Database\TypeFactory::build('time')
+// ->useMutable();
+// \Cake\Database\TypeFactory::build('date')
+// ->useMutable();
+// \Cake\Database\TypeFactory::build('datetime')
+// ->useMutable();
+// \Cake\Database\TypeFactory::build('timestamp')
+// ->useMutable();
+// \Cake\Database\TypeFactory::build('datetimefractional')
+// ->useMutable();
+// \Cake\Database\TypeFactory::build('timestampfractional')
+// ->useMutable();
+// \Cake\Database\TypeFactory::build('datetimetimezone')
+// ->useMutable();
+// \Cake\Database\TypeFactory::build('timestamptimezone')
+// ->useMutable();
+// There is no time-specific type in Cake
+TypeFactory::map('time', StringType::class);
+
+/*
+ * Custom Inflector rules, can be set to correctly pluralize or singularize
+ * table, model, controller names or whatever other string is passed to the
+ * inflection functions.
+ */
+//Inflector::rules('plural', ['/^(inflect)or$/i' => '\1ables']);
+//Inflector::rules('irregular', ['red' => 'redlings']);
+//Inflector::rules('uninflected', ['dontinflectme']);
+//Inflector::rules('transliteration', ['/å/' => 'aa']);
+
+Inflector::rules('irregular', ['co_terms_and_condition' => 'co_terms_and_conditions']);
+Inflector::rules('irregular', ['cou' => 'cous']);
+Inflector::rules('irregular', ['meta' => 'meta']);
\ No newline at end of file
diff --git a/app/config/bootstrap_cli.php b/app/config/bootstrap_cli.php
new file mode 100644
index 000000000..fc0dc30bb
--- /dev/null
+++ b/app/config/bootstrap_cli.php
@@ -0,0 +1,35 @@
+= 50.1 is needed to use CakePHP. Please update the `libicu` package of your system.' . PHP_EOL, E_USER_ERROR);
+}
+
+/*
+ * You can remove this if you are confident you have mbstring installed.
+ */
+if (!extension_loaded('mbstring')) {
+ trigger_error('You must enable the mbstring extension to use CakePHP.', E_USER_ERROR);
+}
diff --git a/app/config/routes.php b/app/config/routes.php
new file mode 100644
index 000000000..4fbe6834e
--- /dev/null
+++ b/app/config/routes.php
@@ -0,0 +1,121 @@
+setRouteClass(DashedRoute::class);
+
+$routes->scope('/', function (RouteBuilder $builder) {
+ // Register scoped middleware for in scopes.
+ $builder->registerMiddleware('csrf', new CsrfProtectionMiddleware([
+ 'httponly' => true,
+ ]));
+
+ // BodyParserMiddleware will automatically parse JSON bodies, but we only
+ // want that for API transactions, so we only apply it to the /api scope.
+ $builder->registerMiddleware('bodyparser', new BodyParserMiddleware());
+
+ /*
+ * Apply a middleware to the current route scope.
+ * Requires middleware to be registered through `Application::routes()` with `registerMiddleware()`
+ */
+ $builder->applyMiddleware('csrf');
+
+ /*
+ * Here, we are connecting '/' (base path) to a controller called 'Pages',
+ * its action called 'display', and we pass a param to select the view file
+ * to use (in this case, templates/Pages/home.php)...
+ */
+ $builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
+
+ /*
+ * ...and connect the rest of 'Pages' controller's URLs.
+ */
+ $builder->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
+
+ // Registry API routes
+ Router::scope('/api/v2', function ($routes) {
+ $routes->setExtensions(['json']);
+ $routes->applyMiddleware('bodyparser');
+ // Use setPass to make parameter show up as function parameter
+ // Model specific actions, which will usually have more specific URLs:
+ $routes->post('/api_users/generate/{id}',
+ ['controller' => 'ApiV2', 'action' => 'generateApiKey', 'model' => 'api_users'])
+ ->setPass(['id']);
+ // These establish the usual CRUD options on all models:
+ $routes->delete('/{model}/{id}', ['controller' => 'ApiV2', 'action' => 'delete'])->setPass(['id']);
+ $routes->get('/{model}', ['controller' => 'ApiV2', 'action' => 'index']);
+ $routes->get('/{model}/{id}', ['controller' => 'ApiV2', 'action' => 'view'])->setPass(['id']);
+ $routes->post('/{model}', ['controller' => 'ApiV2', 'action' => 'add']);
+ $routes->put('/{model}/{id}', ['controller' => 'ApiV2', 'action' => 'edit'])->setPass(['id']);
+ });
+
+ /*
+ * Connect catchall routes for all controllers.
+ *
+ * The `fallbacks` method is a shortcut for
+ *
+ * ```
+ * $builder->connect('/:controller', ['action' => 'index']);
+ * $builder->connect('/:controller/:action/*', []);
+ * ```
+ *
+ * You can remove these routes once you've connected the
+ * routes you want in your application.
+ */
+ $builder->fallbacks();
+});
+
+/*
+ * If you need a different set of middleware or none at all,
+ * open new scope and define routes there.
+ *
+ * ```
+ * $routes->scope('/api', function (RouteBuilder $builder) {
+ * // No $builder->applyMiddleware() here.
+ * // Connect API actions here.
+ * });
+ * ```
+ */
diff --git a/app/config/schema/i18n.sql b/app/config/schema/i18n.sql
new file mode 100644
index 000000000..e59d1e651
--- /dev/null
+++ b/app/config/schema/i18n.sql
@@ -0,0 +1,18 @@
+# Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+#
+# Licensed under The MIT License
+# For full copyright and license information, please see the LICENSE.txt
+# Redistributions of files must retain the above copyright notice.
+# MIT License (https://opensource.org/licenses/mit-license.php)
+
+CREATE TABLE i18n (
+ id int NOT NULL auto_increment,
+ locale varchar(6) NOT NULL,
+ model varchar(255) NOT NULL,
+ foreign_key int(10) NOT NULL,
+ field varchar(255) NOT NULL,
+ content text,
+ PRIMARY KEY (id),
+ UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field),
+ INDEX I18N_FIELD(model, foreign_key, field)
+);
diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json
new file mode 100644
index 000000000..00ff6b6fc
--- /dev/null
+++ b/app/config/schema/schema.json
@@ -0,0 +1,183 @@
+{
+ "description": "COmanage Registry Database Schema File",
+ "format": "1",
+ "since": "Registry v5.0.0",
+ "license": "Apache 2, see LICENSE for details",
+ "documentation": "https://spaces.at.internet2.edu/display/COmanage/Database+Schema+Definition",
+
+ "columnLibrary": {
+ "comment": "Columns with names matching those defined here will by default inherit these properties",
+
+ "columns": {
+ "co_id": { "type": "integer", "foreignkey": { "table": "cos", "column": "id" }, "notnull": true },
+ "co_person_id": { "type": "integer", "foreignkey": { "table": "co_people", "column": "id" } },
+ "description": { "type": "string", "size": 128 },
+ "id": { "type": "integer", "autoincrement": true, "primarykey": true },
+ "name": { "type": "string", "size": 128, "notnull": true },
+ "org_identity_id": { "type": "integer", "foreignkey": { "table": "org_identities", "column": "id" } },
+ "status": { "type": "string", "size": 2 },
+ "type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" }, "notnull": true },
+ "valid_from": { "type": "datetime" },
+ "valid_through": { "type": "datetime" }
+ }
+ },
+
+ "tables": {
+ "cos": {
+ "columns": {
+ "id": {},
+ "name": {},
+ "description": {},
+ "status": {}
+ },
+ "indexes": {
+ "cos_i1": {
+ "columns": [ "name" ],
+ "unique": true
+ }
+ },
+ "changelog": true
+ },
+
+ "types": {
+ "columns": {
+ "id": {},
+ "co_id": {},
+ "attribute": { "type": "string", "size": 32 },
+ "name": { "type": "string", "size": 32 },
+ "display_name": { "type": "string", "size": 64 },
+ "edupersonaffiliation": { "type": "string", "size": 32 },
+ "status": {}
+ },
+ "indexes": {
+ "types_i1": { "columns": [ "co_id", "attribute" ] },
+ "types_i2": { "columns": [ "co_id", "attribute", "name" ] }
+ }
+ },
+
+ "api_users": {
+ "columns": {
+ "id": {},
+ "co_id": {},
+ "username": { "type": "string", "size": 64, "notnull": true },
+ "api_key": { "type": "string", "size": 256 },
+ "status": {},
+ "privileged": { "type": "boolean" },
+ "valid_from": {},
+ "valid_through": {},
+ "remote_ip": { "type": "string", "size": 80 }
+ },
+ "indexes": {
+ "api_users_i1": { "columns": [ "co_id" ] },
+ "api_users_i2": { "columns": [ "username" ] }
+ }
+ },
+
+ "cous": {
+ "columns": {
+ "id": {},
+ "co_id": {},
+ "name": {},
+ "description": {},
+ "parent_id": { "type": "integer", "foreignkey": { "table": "cous", "column": "id" } },
+ "lft": { "type": "integer" },
+ "rght": { "type": "integer" }
+ },
+ "indexes": {
+ "cous_i1": { "columns": [ "co_id" ] },
+ "cous_i2": { "columns": [ "name" ] },
+ "cous_i3": { "columns": [ "co_id", "name" ] },
+ "cous_i4": {
+ "comment": "We don't really need an index, but DBAL will create one for all foreign keys if none exists",
+ "columns": [ "parent_id" ]
+ }
+ }
+ },
+
+ "dashboards": {
+ "columns": {
+ "id": {},
+ "co_id": {},
+ "name": {},
+ "description": {}
+ },
+ "indexes": {
+ "dashboards_i1": { "columns": [ "co_id"] }
+ }
+ },
+
+ "co_people": {
+ "columns": {
+ "id": {},
+ "co_id": {},
+ "status": {},
+ "timezone": { "type": "string", "size": 80 },
+ "date_of_birth": { "type": "date" }
+ },
+ "indexes": {
+ "co_people_i1": { "columns": [ "co_id" ] }
+ }
+ },
+
+ "org_identities": {
+ "columns": {
+ "id": {},
+ "co_person_id": { "notnull": true },
+ "status": {},
+ "affiliation": { "type": "string", "size": 32 },
+ "date_of_birth": { "type": "date" },
+ "title": { "type": "string", "size": 128 },
+ "organization": { "type": "string", "size": 128 },
+ "department": { "type": "string", "size": 128 },
+ "valid_from": {},
+ "valid_through": {}
+ },
+ "indexes": {
+ "org_identities_i1": { "columns": [ "co_person_id" ] }
+ }
+ },
+
+ "names": {
+ "columns": {
+ "id": {},
+ "honorific": { "type": "string", "size": 32 },
+ "given": { "type": "string", "size": 128 },
+ "middle": { "type": "string", "size": 128 },
+ "family": { "type": "string", "size": 128 },
+ "suffix": { "type": "string", "size": 32 },
+ "type_id": {},
+ "language": { "type": "string", "size": 16 },
+ "primary_name": { "type": "boolean" }
+ },
+ "indexes": {
+ "names_i1": { "columns": [ "type_id"] }
+ },
+ "mvea": [ "co_person", "org_identity" ],
+ "sourced": true
+ },
+
+ "identifiers": {
+ "XXX": [ "add index for co_provision_target_id+co_person_id" ],
+ "columns": {
+ "id": {},
+ "identifier": { "type": "string", "size": 512 },
+ "type_id": {},
+ "login": { "type": "boolean" },
+ "status": {}
+ },
+ "indexes": {
+ "identifiers_i1": { "columns": [ "identifier", "type_id", "co_person_id" ] },
+ "identifiers_i2": { "columns": [ "identifier", "type_id", "org_identity_id" ] },
+ "identifiers_i3": { "columns": [ "type_id"] }
+ },
+ "mvea": [ "co_person", "org_identity" ],
+ "sourced": true
+ }
+ },
+
+ "drop-tables":[
+ {
+ "comment": "A list of tables to manually drop, not yet implemented"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/config/schema/sessions.sql b/app/config/schema/sessions.sql
new file mode 100644
index 000000000..1aa0a0f54
--- /dev/null
+++ b/app/config/schema/sessions.sql
@@ -0,0 +1,15 @@
+# Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+#
+# Licensed under The MIT License
+# For full copyright and license information, please see the LICENSE.txt
+# Redistributions of files must retain the above copyright notice.
+# MIT License (https://opensource.org/licenses/mit-license.php)
+
+CREATE TABLE `sessions` (
+ `id` char(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
+ `created` datetime DEFAULT CURRENT_TIMESTAMP, -- optional, requires MySQL 5.6.5+
+ `modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- optional, requires MySQL 5.6.5+
+ `data` blob DEFAULT NULL, -- for PostgreSQL use bytea instead of blob
+ `expires` int(10) unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/app/index.php b/app/index.php
new file mode 100644
index 000000000..459176916
--- /dev/null
+++ b/app/index.php
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/phpstan.neon b/app/phpstan.neon
new file mode 100644
index 000000000..28cf5ba94
--- /dev/null
+++ b/app/phpstan.neon
@@ -0,0 +1,8 @@
+parameters:
+ level: 7
+ checkMissingIterableValueType: false
+ treatPhpDocTypesAsCertain: false
+ paths:
+ - src
+ excludes_analyse:
+ - src/Console/Installer.php
diff --git a/app/phpunit.xml.dist b/app/phpunit.xml.dist
new file mode 100644
index 000000000..710712222
--- /dev/null
+++ b/app/phpunit.xml.dist
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+ tests/TestCase/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ src/
+ plugins/*/src/
+
+ src/Console/Installer.php
+
+
+
+
diff --git a/app/plugins/.gitkeep b/app/plugins/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/resources/.gitkeep b/app/resources/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/resources/locales/en_US/default.po b/app/resources/locales/en_US/default.po
new file mode 100644
index 000000000..f3f879763
--- /dev/null
+++ b/app/resources/locales/en_US/default.po
@@ -0,0 +1,411 @@
+# COmanage Registry Localizations
+#
+# Portions licensed to the University Corporation for Advanced Internet
+# Development, Inc. ("UCAID") under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information
+# regarding copyright ownership.
+#
+# UCAID licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# @link https://www.internet2.edu/comanage COmanage Project
+# @package registry
+# @since COmanage Registry v5.0.0
+# @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+# For common code to know which component it is
+msgid "product.code"
+msgstr "registry"
+
+# This should match the ISO 639-1 two letter language code for the translation
+msgid "registry.meta.lang"
+msgstr "en"
+
+msgid "registry.meta.logo"
+msgstr "COmanage Logo"
+
+msgid "registry.meta.powered"
+msgstr "Powered by"
+
+msgid "registry.meta.registry"
+msgstr "COmanage Registry"
+
+### Command Line text
+msgid "registry.cmd.db.noop"
+msgstr "SQL NOT EXECUTED"
+
+msgid "registry.cmd.db.ok"
+msgstr "Database schema update successful"
+
+msgid "registry.cmd.db.schema"
+msgstr "Loading database schema from {0}"
+
+msgid "registry.cmd.opt.admin-username"
+msgstr "Username of initial platform administrator"
+
+msgid "registry.cmd.opt.force"
+msgstr "Force a rerun of setup (only if you know what you are doing)""
+
+msgid "registry.cmd.opt.not"
+msgstr "Calculate changes but do not apply"
+
+# msgid "registry.cmd.se.admin"
+# msgstr "Creating initial administrator permission"
+
+# msgid "registry.cmd.se.admin.user"
+# msgstr "Enter administrator's login username"
+
+msgid "registry.cmd.se.already"
+msgstr "Setup appears to have already run"
+
+msgid "registry.cmd.se.salt"
+msgstr "Generating salt file"
+
+### Controllers (Models)
+msgid "registry.ct.ApiUsers"
+msgstr "{0,plural,=1{API User} other{API Users}}"
+
+msgid "registry.ct.CoPeople"
+msgstr "{0,plural,=1{CO Person} other{CO People}}"
+
+msgid "registry.ct.CoSettings"
+msgstr "{0,plural,=1{CO Setting} other{CO Settings}}"
+
+msgid "registry.ct.Cos"
+msgstr "{0,plural,=1{CO} other{COs}}"
+
+msgid "registry.ct.Cous"
+msgstr "{0,plural,=1{COU} other{COUs}}"
+
+msgid "registry.ct.Dashboards"
+msgstr "{0,plural,=1{Dashboard} other{Dashboards}}"
+
+### Enumerations
+msgid "registry.en.BooleanEnum.0"
+msgstr "False"
+
+msgid "registry.en.BooleanEnum.1"
+msgstr "True"
+
+msgid "registry.en.SetBooleanEnum.0"
+msgstr "Not Set"
+
+msgid "registry.en.SetBooleanEnum.1"
+msgstr "Set"
+
+msgid "registry.en.StatusEnum.A"
+msgstr "Active"
+
+msgid "registry.en.StatusEnum.C"
+msgstr "Confirmed"
+
+msgid "registry.en.StatusEnum.D"
+msgstr "Deleted"
+
+msgid "registry.en.StatusEnum.D2"
+msgstr "Duplicate"
+
+msgid "registry.en.StatusEnum.GP"
+msgstr "Grace Period"
+
+msgid "registry.en.StatusEnum.I"
+msgstr "Invited"
+
+msgid "registry.en.StatusEnum.LK"
+msgstr "Locked"
+
+msgid "registry.en.StatusEnum.N"
+msgstr "Denied"
+
+msgid "registry.en.StatusEnum.P"
+msgstr "Pending"
+
+msgid "registry.en.StatusEnum.PA"
+msgstr "Pending Approval"
+
+msgid "registry.en.StatusEnum.PC"
+msgstr "Pending Confirmation"
+
+msgid "registry.en.StatusEnum.S"
+msgstr "Suspended"
+
+msgid "registry.en.StatusEnum.X"
+msgstr "Declined"
+
+msgid "registry.en.StatusEnum.XP"
+msgstr "Expired"
+
+msgid "registry.en.StatusEnum.Y"
+msgstr "Approved"
+
+msgid "registry.en.SuspendableStatusEnum.A"
+msgstr "Active"
+
+msgid "registry.en.SuspendableStatusEnum.S"
+msgstr "Suspended"
+
+msgid "registry.en.TemplateableStatusEnum.A"
+msgstr "Active"
+
+msgid "registry.en.TemplateableStatusEnum.S"
+msgstr "Suspended"
+
+msgid "registry.en.TemplateableStatusEnum.T"
+msgstr "Template"
+
+msgid "registry.en.YesBooleanEnum.0"
+msgstr "No"
+
+msgid "registry.en.YesBooleanEnum.1"
+msgstr "Yes"
+
+### Error Messages
+msgid "registry.er.api.object"
+msgstr "Did not find \"{0}\" object in request"
+
+msgid "registry.er.api.username.prefix"
+msgstr "API username must begin with \"{0}\""
+
+msgid "registry.er.api.username.suffix"
+msgstr "API username requires a suffix"
+
+msgid "registry.er.auth.api.expired"
+msgstr "API User \"{0}\" has expired"
+
+msgid "registry.er.auth.api.failed"
+msgstr "Authentication Failed"
+
+msgid "registry.er.auth.api.invalid"
+msgstr "Authentication request did not include Username and/or API Key"
+
+msgid "registry.er.auth.api.ip"
+msgstr "Invalid IP Addres \"{0}\" for API User \"{1}\""
+
+msgid "registry.er.auth.api.key"
+msgstr "Invalid API Key provided for \"{0}\""
+
+msgid "registry.er.auth.api.status"
+msgstr "API User \"{0}\" is not Active"
+
+msgid "registry.er.auth.api.toosoon"
+msgstr "API User \"{0}\" is not yet valid"
+
+msgid "registry.er.auth.api.unknown"
+msgstr "Username \"{0}\" not found in api_users table"
+
+msgid "registry.er.coid"
+msgstr "CO ID not found"
+
+msgid "registry.er.cou.parent"
+msgstr "COU Parent ID not valid"
+
+msgid "registry.er.db.config"
+msgstr "Invalid database configuration \"{0}\""
+
+msgid "registry.er.delete.active"
+msgstr "This record is in Active status and cannot be deleted"
+
+msgid "registry.er.edit.comanage"
+msgstr "Cannot edit or delete the COmanage CO"
+
+msgid "registry.er.edit.readonly"
+msgstr "This record is read only and cannot be edited"
+
+msgid "registry.er.exists"
+msgstr "{0} already exists with this name"
+
+msgid "registry.er.fields"
+msgstr "Please recheck these fields: {0}"
+
+msgid "registry.er.fields.primary_link"
+msgstr "The Primary Link {0} is frozen and cannot be changed"
+
+msgid "registry.er.file"
+msgstr "Cannot read file {0}"
+
+msgid "registry.er.input.blank"
+msgstr "Value cannot consist of only blank characters"
+
+msgid "registry.er.input.condreq"
+msgstr "When this value is selected, {0} cannot be empty"
+
+msgid "registry.er.input.invalid"
+msgstr "Invalid character found"
+
+msgid "registry.er.notfound"
+msgstr "{0} not found"
+
+msgid "registry.er.notprov"
+msgstr "{0} not provided"
+
+msgid "registry.er.perm"
+msgstr "Permission Denied"
+
+msgid "registry.er.primary_link"
+msgstr "Could not find value for Primary Link {0}"
+
+msgid "registry.er.save"
+msgstr "Save Failed ({0})"
+
+msgid "registry.er.schema.column"
+msgstr "No type defined for table {0} column {1}"
+
+msgid "registry.er.schema.parse"
+msgstr "Failed to parse file {0}"
+
+### Fields
+### Keys of the form registry.fd.MyModels.field_name[.desc] will apply only to MyModels.field_name
+### Keys of the form registry.fd.field_name[.desc] will apply if no model specific key is found
+msgid "registry.fd.action"
+msgstr "Action"
+
+msgid "registry.fd.api_key"
+msgstr "API Key"
+
+msgid "registry.fd.date_of_birth"
+msgstr "Date of Birth"
+
+msgid "registry.fd.description"
+msgstr "Description"
+
+msgid "registry.fd.family"
+msgstr "Family Name"
+
+msgid "registry.fd.given"
+msgstr "Given Name"
+
+msgid "registry.fd.name"
+msgstr "Name"
+
+msgid "registry.fd.parent_id"
+msgstr "Parent"
+
+msgid "registry.fd.privileged"
+msgstr "Privileged"
+
+msgid "registry.fd.ApiUsers.privileged.desc"
+msgstr "A privileged API user has full access to the CO. Unprivileged API users may be granted specific permissions where supported."
+
+msgid "registry.fd.remote_ip"
+msgstr "IP Address"
+
+msgid "registry.fd.ApiUsers.remote_ip.desc"
+msgstr "If specified, a regular expression describing the IP address(es) from which this API User may connect. Be sure to escape dots (eg: "/10\\.0\\.1\\.150/")."
+
+msgid "registry.fd.required"
+msgstr "Required"
+
+msgid "registry.fd.status"
+msgstr "Status"
+
+msgid "registry.fd.username"
+msgstr "Username"
+
+msgid "registry.fd.ApiUsers.username.desc"
+msgstr "The API User Name must be prefixed with the string \"co_#.\""
+
+msgid "registry.fd.valid_from"
+msgstr "Valid From"
+
+msgid "registry.fd.valid_from.desc"
+msgstr "Leave blank for immediate validity"
+
+msgid "registry.fd.valid_from.tz"
+msgstr "Valid From ({0})"
+
+msgid "registry.fd.valid_through"
+msgstr "Valid Through"
+
+msgid "registry.fd.valid_through.desc"
+msgstr "Leave blank for indefinite validity"
+
+msgid "registry.fd.valid_through.tz"
+msgstr "Valid Through ({0})"
+
+msgid "registry.home.collab"
+msgstr "Available Collaborations"
+
+msgid "registry.home.welcome"
+msgstr "Welcome to {0}."
+
+### Informational (Banner) Messages
+msgid "registry.in.api.cmp"
+msgstr "API Users created in the COmanage CO are given full privileges to all Registry data."
+
+msgid "registry.in.api.key"
+msgstr "This newly generated API Key cannot be recovered. If it is lost a new key must be generated."
+
+### Menu Messages
+msgid "registry.me.co.switch"
+msgstr "Switch Collaboration"
+
+msgid "registry.me.co.co_people"
+msgstr "CO People"
+
+msgid "registry.me.co.configuration"
+msgstr "Configuration"
+
+### Operations (Commands)
+msgid "registry.op.add.a"
+msgstr "Add New {0}"
+
+msgid "registry.op.api.key.generate"
+msgstr "Generate API Key"
+
+msgid "registry.op.api.key.generate.confirm"
+msgstr "Are you sure you wish to generate a new API Key?"
+
+msgid "registry.op.dashboard.configuration"
+msgstr "Manage {0} Configuration"
+
+msgid "registry.op.delete"
+msgstr "Delete"
+
+msgid "registry.op.delete.confirm"
+msgstr "Are you sure you wish to delete this record ({0})?"
+
+msgid "registry.op.duplicate"
+msgstr "Duplicate"
+
+msgid "registry.op.edit"
+msgstr "Edit"
+
+msgid "registry.op.edit.a"
+msgstr "Edit {0}"
+
+msgid "registry.op.login"
+msgstr "Login"
+
+msgid "registry.op.logout"
+msgstr "Logout"
+
+msgid "registry.op.save"
+msgstr "Save"
+
+msgid "registry.op.skip_to_content"
+msgstr "Skip to main content"
+
+msgid "registry.op.view"
+msgstr "View"
+
+msgid "registry.op.view.a"
+msgstr "View {0}"
+
+### Results
+msgid "registry.rs.deleted"
+msgstr "Deleted"
+
+msgid "registry.rs.deleted.a"
+msgstr "{0} Deleted"
+
+msgid "registry.rs.saved"
+msgstr "Saved"
\ No newline at end of file
diff --git a/app/src/Application.php b/app/src/Application.php
new file mode 100644
index 000000000..ea6657cd8
--- /dev/null
+++ b/app/src/Application.php
@@ -0,0 +1,110 @@
+bootstrapCli();
+ }
+
+ /*
+ * Only try to load DebugKit in development mode
+ * Debug Kit should not be installed on a production system
+ */
+ if (Configure::read('debug')) {
+ $this->addPlugin('DebugKit');
+ }
+
+ // Load more plugins here
+ }
+
+ /**
+ * Setup the middleware queue your application will use.
+ *
+ * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to setup.
+ * @return \Cake\Http\MiddlewareQueue The updated middleware queue.
+ */
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ // Note route specific middleware is loaded in routes.php
+
+ $middlewareQueue
+ // Catch any exceptions in the lower layers,
+ // and make an error page/response
+ ->add(new ErrorHandlerMiddleware(Configure::read('Error')))
+
+ // Handle plugin/theme assets like CakePHP normally does.
+ ->add(new AssetMiddleware([
+ 'cacheTime' => Configure::read('Asset.cacheTime'),
+ ]))
+
+ // Add routing middleware.
+ // If you have a large number of routes connected, turning on routes
+ // caching in production could improve performance. For that when
+ // creating the middleware instance specify the cache config name by
+ // using it's second constructor argument:
+ // `new RoutingMiddleware($this, '_cake_routes_')`
+ ->add(new RoutingMiddleware($this));
+
+ return $middlewareQueue;
+ }
+
+ /**
+ * Bootrapping for CLI application.
+ *
+ * That is when running commands.
+ *
+ * @return void
+ */
+ protected function bootstrapCli(): void
+ {
+ try {
+ $this->addPlugin('Bake');
+ } catch (MissingPluginException $e) {
+ // Do not halt if the plugin is missing
+ }
+
+ $this->addPlugin('Migrations');
+
+ // Load more plugins here
+ }
+}
diff --git a/app/src/Command/ConsoleCommand.php b/app/src/Command/ConsoleCommand.php
new file mode 100644
index 000000000..2ec000ef0
--- /dev/null
+++ b/app/src/Command/ConsoleCommand.php
@@ -0,0 +1,86 @@
+err('Unable to load Psy\Shell.');
+ $io->err('');
+ $io->err('Make sure you have installed psysh as a dependency,');
+ $io->err('and that Psy\Shell is registered in your autoloader.');
+ $io->err('');
+ $io->err('If you are using composer run');
+ $io->err('');
+ $io->err('$ php composer.phar require --dev psy/psysh');
+ $io->err('');
+
+ return static::CODE_ERROR;
+ }
+
+ $io->out("You can exit with `CTRL-C` or `exit`");
+ $io->out('');
+
+ Log::drop('debug');
+ Log::drop('error');
+ $io->setLoggers(false);
+ restore_error_handler();
+ restore_exception_handler();
+
+ $psy = new PsyShell();
+ $psy->run();
+ }
+
+ /**
+ * Display help for this console.
+ *
+ * @param \Cake\Console\ConsoleOptionParser $parser The parser to update
+ * @return \Cake\Console\ConsoleOptionParser
+ */
+ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ {
+ $parser->setDescription(
+ 'This shell provides a REPL that you can use to interact with ' .
+ 'your application in a command line designed to run PHP code. ' .
+ 'You can use it to run adhoc queries with your models, or ' .
+ 'explore the features of CakePHP and your application.' .
+ "\n\n" .
+ 'You will need to have psysh installed for this Shell to work.'
+ );
+
+ return $parser;
+ }
+}
diff --git a/app/src/Command/DatabaseCommand.php b/app/src/Command/DatabaseCommand.php
new file mode 100644
index 000000000..2df112b80
--- /dev/null
+++ b/app/src/Command/DatabaseCommand.php
@@ -0,0 +1,312 @@
+addOption('not', [
+ 'short' => 'n',
+ 'boolean' => true,
+ 'help' => __(__('product.code').'.cmd.opt.not')
+ ]);
+
+ return $parser;
+ }
+
+ /**
+ * Execute the Database Command.
+ *
+ * @since COmanage Match v1.0.0, COmanage Registry v5.0.0
+ * @param Arguments $args Command Arguments
+ * @param ConsoleIo $io Console IO
+ * @throws RuntimeException
+ */
+
+ public function execute(Arguments $args, ConsoleIo $io) {
+ // Database schema management. We use Doctrine DBAL rather than Cake's migrations
+ // (phinx) because migrations make development annoying (want to add a field
+ // to a table after you've created it? that's a new migration!), and can't
+ // provide a single representation of a given table (since you're recording
+ // diffs, not desired end state). ADOdb (used in earlier versions) was hard to
+ // debug and poorly maintained. DBAL doesn't have a schema format (like axmls)
+ // but it does everything else, and specifying a schema format is easy.
+
+ // What component are we?
+ $COmponent = __('product.code');
+
+ // First try to parse our schema file
+
+ $schemaFile = ROOT . DS . 'config' . DS . 'schema' . DS . 'schema.json';
+
+ if(!is_readable($schemaFile)) {
+ throw new \RuntimeException(__($COmponent.'.er.file', [$schemaFile]));
+ }
+
+ $io->out(__($COmponent.'.cmd.db.schema', [$schemaFile]));
+
+ $json = file_get_contents($schemaFile);
+
+ $schemaConfig = json_decode($json);
+
+ if(!$schemaConfig) {
+ // json_last_error[_msg]() are pretty useless. If you are debugging here,
+ // it's most likely because of one of the following:
+ // - An unmatched brace { }
+ // - A trailing comma (permitted in PHP but not JSON)
+ // - Single quotes instead of double quotes
+ throw new \RuntimeException(__($COmponent.'.er.schema.parse', [$schemaFile]));
+ }
+
+ // Use the ConnectionManager to get the database config to pass to adodb.
+ $db = ConnectionManager::get('default');
+
+ // $db is a ConnectionInterface object
+ $cfg = $db->config();
+
+ $config = new \Doctrine\DBAL\Configuration();
+
+ $cfargs = [
+ 'dbname' => $cfg['database'],
+ 'user' => $cfg['username'],
+ 'password' => $cfg['password'],
+ 'host' => $cfg['host'],
+ 'driver' => ($cfg['driver'] == 'Cake\Database\Driver\Postgres' ? "pdo_pgsql" : "pdo_mysql")
+ ];
+
+ $conn = DriverManager::getConnection($cfargs, $config);
+
+ $schema = new Schema();
+
+ // Walk through $schemaConfig and build our schema in DBAL format.
+
+ foreach($schemaConfig->tables as $tName => $tCfg) {
+ $table = $schema->createTable($tName);
+
+ foreach($tCfg->columns as $cName => $cCfg) {
+ // We allow "inherited" definitions from the fieldLibrary, so merge together
+ // the configurations (if appropriate)
+
+ $colCfg = (object)array_merge((isset($schemaConfig->columnLibrary->columns->$cName)
+ ? (array)$schemaConfig->columnLibrary->columns->$cName
+ : []),
+ (array)$cCfg);
+
+ if(!isset($colCfg->type)) {
+ throw new \RuntimeException(__('match.er.schema.column', [$tName, $cName]));
+ }
+
+ // For type definitions see https://www.doctrine-project.org/projects/doctrine-dbal/en/2.12/reference/types.html#types
+ $options = [];
+
+ if(isset($colCfg->autoincrement)) {
+ $options['autoincrement'] = $colCfg->autoincrement;
+ }
+
+ if($colCfg->type == "string") {
+ $options['length'] = $colCfg->size;
+ }
+
+ if(isset($colCfg->notnull)) {
+ $options['notnull'] = $colCfg->notnull;
+ } else {
+ $options['notnull'] = false;
+ }
+
+ $table->addColumn($cName, $colCfg->type, $options);
+
+ if(isset($colCfg->primarykey) && $colCfg->primarykey) {
+ $table->setPrimaryKey(["id"]);
+ }
+
+ if(isset($colCfg->foreignkey)) {
+ $table->addForeignKeyConstraint($colCfg->foreignkey->table,
+ [$cName],
+ [$colCfg->foreignkey->column],
+ [],
+ // We name our foreign keys the same way they
+ // were previously named by adodb
+ $tName . "_" . $cName . "_fkey");
+ }
+ }
+
+ // (For Registry) If MVEA models are specified, emit the appropriate
+ // columns and indexes. MVEA attributes must be added before indexes, in
+ // case the table has composite indexes referencing MVEA columns.
+
+ if(!empty($tCfg->mvea)) {
+ $i = 1;
+
+ foreach($tCfg->mvea as $m) {
+ $mColumn = $m . "_id";
+ $fkTable = \Cake\Utility\Inflector::tableize($m);
+
+ // Insert a foreign key to this model and index it
+ $table->addColumn($mColumn, "integer", ['notnull' => false]);
+ $table->addForeignKeyConstraint($fkTable, [$mColumn], ['id'], [], $tName . "_" . $mColumn . "_fkey");
+ $table->addIndex([$mColumn], $tName . "_im" . $i++);
+ }
+ }
+
+ if(isset($tCfg->indexes)) {
+ // We don't autogenerate names for indexes so if the definition of an index
+ // changes DBAL can just rebuild that index instead of recreating every index
+ // on the table. (This should speed up schema updates vs ADOdb.) This does
+ // require each index to be named in the schema file, but we had to do that
+ // in axmls too, even though it rebuilt every index every time through.
+
+ foreach($tCfg->indexes as $iName => $iCfg) {
+ // $flags and $options as passed to Index(), but otherwise undocumented
+ $flags = [];
+ $options = [];
+
+ $table->addIndex($iCfg->columns, $iName, $flags, $options);
+ }
+ }
+
+ // (For Registry) If an attribute is "sourced" it is a CO Person attribute
+ // that is copied via a Pipeline from an Org Identity that was created from
+ // an Org Identity Source, so we need a foreign key into ourself.
+
+ if(isset($tCfg->sourced) && $tCfg->sourced) {
+ $sColumn = "source_" . \Cake\Utility\Inflector::singularize($tName) . "_id";
+
+ // Insert a foreign key to this model and index it
+ $table->addColumn($sColumn, "integer", ['notnull' => false]);
+ $table->addForeignKeyConstraint($table, [$sColumn], ['id'], [], $tName . "_" . $sColumn . "_fkey");
+ $table->addIndex([$sColumn], $tName . "_im" . $i++);
+ }
+
+ // Default is to insert timestamp and changelog fields, unless disabled
+
+ if(!isset($tCfg->timestamps) || $tCfg->timestamps) {
+ // Insert Cake metadata fields
+ $table->addColumn("created", "datetime");
+ $table->addColumn("modified", "datetime", ['notnull' => false]);
+ }
+
+ if(!isset($tCfg->changelog) || $tCfg->changelog) {
+ // Insert ChangelogBehavior metadata fields
+ $clColumn = \Cake\Utility\Inflector::singularize($tName) . "_id";
+ $table->addColumn($clColumn, "integer", ['notnull' => false]);
+ $table->addColumn("revision", "integer", ['notnull' => false]);
+ $table->addColumn("deleted", "boolean", ['notnull' => false]);
+ $table->addColumn("actor_identifier", "string", ['length' => 256, 'notnull' => false]);
+
+ $table->addForeignKeyConstraint($table, [$clColumn], ['id'], [], $tName . "_" . $clColumn . "_fkey");
+ $table->addIndex([$clColumn], $tName . "_icl", [], []);
+ }
+ }
+
+ // This is the SQL that represents the desired state of the database
+ $toSql = $schema->toSql($conn->getDatabasePlatform());
+
+ // SchemaManager provides info about the database
+ $sm = $conn->getSchemaManager();
+
+ // The is the current database representation
+ $curSchema = $sm->createSchema();
+
+ $fromSql = $curSchema->toSql($conn->getDatabasePlatform());
+
+ // Really run the SQL?
+ $doSQL = !$args->getOption('not');
+
+ try {
+ // We manually call compare so we can get the SchemaDiff object. We need
+ // this for toSaveSql(), which we use to avoid dropping undocumented tables
+ // (like the matchgrids, which are dynamically created and so won't be in the
+ // schema file).
+// $diffSql = $curSchema->getMigrateToSql($schema, $conn->getDatabasePlatform());
+ $comparator = new Comparator();
+ $schemaDiff = $comparator->compare($curSchema, $schema);
+
+ $diffSql = $schemaDiff->toSaveSql($conn->getDatabasePlatform());
+
+ // We don't start a transaction since in general we always want to move to
+ // the desired state, and if we fail in flight it's probably a bug that
+ // needs to be fixed.
+
+ foreach($diffSql as $sql) {
+ // XXX At some point do this only if $verbose
+ $io->out($sql);
+
+ if($cfg['driver'] == 'Cake\Database\Driver\Postgres'
+ && preg_match("/^DROP SEQUENCE [a-z]*_id_seq/", $sql)) {
+ // Remove the DROP SEQUENCE statements in $fromSql because they're Postgres automagic
+ // being misinterpreted. (Note toSaveSql might mask this now.)
+ // XXX Maybe debug and file a PR to not emit DROP SEQUENCE on PG for autoincrementesque fields?
+ $io->out("Skipping sequence drop");
+ } else {
+ if($doSQL) {
+ $stmt = $conn->query($sql);
+ // $stmt just returns the query string so we don't bother examining it
+ }
+ }
+ }
+
+ if(!$doSQL) {
+ $io->out(__($COmponent.'.cmd.db.noop'));
+ } else {
+ $io->out(__($COmponent.'.cmd.db.ok'));
+ }
+ }
+ catch(\Exception $e) {
+ $io->out($e->getMessage());
+ }
+
+ // We might run bin/cake schema_cache clear or
+ // bin/cake schema_cache build --connection default
+ // but so far we don't have an example indicating it's needed.
+ }
+}
diff --git a/app/src/Command/SetupCommand.php b/app/src/Command/SetupCommand.php
new file mode 100644
index 000000000..9d7c24e5c
--- /dev/null
+++ b/app/src/Command/SetupCommand.php
@@ -0,0 +1,141 @@
+addOption('admin-username', [
+ 'help' => __('registry.cmd.opt.admin-username')
+ ])->addOption('force', [
+ 'help' => __('registry.cmd.opt.force'),
+ 'boolean' => true
+ ]);
+
+ return $parser;
+ }
+
+ /**
+ * Execute the Setup Command.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Arguments $args Command Arguments
+ * @param ConsoleIo $io Console IO
+ */
+
+ public function execute(Arguments $args, ConsoleIo $io) {
+ global $argv;
+
+ // Check if the security salt file already exists, and if so abort.
+
+ $securitySaltFile = LOCAL . DS . "Config" . DS . "security.salt";
+
+ if(file_exists($securitySaltFile)) {
+ $io->out(__('registry.cmd.se.already'));
+
+ if(!$args->getOption('force')) {
+ exit;
+ }
+ }
+
+ // Before we get going, prompt for whatever information we need in case
+ // the user hits ctrl-c.
+ /*
+ $user = $args->getOption('admin-username');
+
+ while(!$user) {
+ $user = $io->ask(__('match.cmd.se.admin.user'));
+ }
+ */
+ // Set the salt now in case we need it. (Normally this is done in bootstrap.php.)
+ // We'll write it out after we're done with the database updates.
+ $salt = hash('sha256', Security::randomBytes(64));
+ Security::setSalt($salt);
+
+ // Perform database related setup. Start by trying to run the database schema.
+/*
+ // Build the runner with an application and root executable name. (based on bin/cake.php)
+ $runner = new CommandRunner(new Application(dirname(__DIR__) . DS . '..' . DS . 'config'), 'cake');
+ $runner->run([ $argv[0], 'database' ]);
+
+ // Create the initial admin permission
+ $io->out(__('match.cmd.se.admin'));
+
+ $permissionsTable = TableRegistry::get('Permissions');
+ $permission = $permissionsTable->newEntity();
+
+ $permission->username = $user;
+ $permission->matchgrid_id = null;
+ $permission->permission = PermissionEnum::PlatformAdmin;
+
+ if(!$permissionsTable->save($permission)) {
+ throw new \RuntimeException(__('match.er.save', ['Permissions']));
+ }
+
+ // Register the current version for future upgrade purposes
+ // Read the current release from the VERSION file
+ $versionFile = CONFIG . "VERSION";
+
+ $targetVersion = rtrim(file_get_contents($versionFile));
+
+ $metaTable = TableRegistry::get('Meta');
+ $metaTable->setUpgradeVersion($targetVersion, true);
+ */
+ // Write out the salt file
+ $io->out(__('registry.cmd.se.salt'));
+
+ if(file_put_contents($securitySaltFile, $salt)===false) {
+ $err = error_get_last();
+ throw new \RuntimeException($err[message]);
+ }
+
+ // We set 444 to prevent accidental changing of the salt, but also so the
+ // web server user can read it if this script is run by (say) root.
+ // We assume we're not installed on a shared, semi-public server.
+ chmod($securitySaltFile, 0444);
+ }
+}
\ No newline at end of file
diff --git a/app/src/Command/TransmogrifyCommand.php b/app/src/Command/TransmogrifyCommand.php
new file mode 100644
index 000000000..4713dbfc8
--- /dev/null
+++ b/app/src/Command/TransmogrifyCommand.php
@@ -0,0 +1,534 @@
+ [
+ 'source' => 'cm_cos',
+ 'displayField' => 'name'
+ ],
+ 'types' => [
+ 'source' => 'cm_co_extended_types',
+ 'displayField' => 'display_name',
+ 'fieldMap' => [
+ // For some reason, cm_co_extended_types never had created/modified metadata
+ 'created' => '&map_now',
+ 'modified' => '&map_now'
+ ],
+ 'cache' => [ [ 'co_id', 'attribute', 'name' ] ]
+ ],
+ 'api_users' => [
+ 'source' => 'cm_api_users',
+ 'displayField' => 'username',
+ 'booleans' => [ 'privileged' ],
+ 'fieldMap' => [
+ 'password' => 'api_key'
+ ]
+ ],
+ 'cous' => [
+ 'source' => 'cm_cous',
+ 'displayField' => 'name'
+ ],
+ //'dashboards' => [ 'source' => 'cm_co_dashboards' ]
+ 'co_people' => [
+ 'source' => 'cm_co_people',
+ 'displayField' => 'id',
+ 'cache' => [ 'co_id' ]
+ ],
+ 'org_identities' => [
+ 'source' => 'cm_org_identities',
+ 'displayField' => 'id',
+ 'fieldMap' => [
+ 'co_id' => null,
+ 'co_person_id' => '&map_org_identity_co_person_id',
+ 'o' => 'organization',
+ 'ou' => 'department'
+ ],
+ 'cache' => [ 'co_person_id' ]
+ ],
+ 'names' => [
+ 'source' => 'cm_names',
+ 'displayField' => 'id',
+ 'booleans' => [ 'primary_name' ],
+ 'fieldMap' => [
+ // We need to map type_id before we null out type
+ 'type_id' => '&map_name_type',
+ 'type' => null
+ ]
+ ],
+ 'identifiers' => [
+ 'source' => 'cm_identifiers',
+ 'displayField' => 'id',
+ 'booleans' => [ 'login' ],
+ 'fieldMap' => [
+ 'type_id' => '&map_identifier_type',
+ 'type' => null,
+// XXX temporary until tables are migrated
+ 'co_department_id' => null,
+ 'co_group_id' => null,
+ 'co_provisioning_target_id' => null,
+ 'organization_id' => null
+ ]
+ ]
+ ];
+
+ // Table specific field mapping cache
+ protected $cache = [];
+
+ // Make some objects more easily accessible
+ protected $inconn = null;
+
+ /**
+ * Build an Option Parser.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param ConsoleOptionParser $parser ConsoleOptionParser
+ * @return ConsoleOptionParser ConsoleOptionParser
+ */
+
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser {
+ $parser->setEpilog('An optional, space separated list of tables to transmogrify may be specified');
+
+ return $parser;
+ }
+
+ /**
+ * Cache results as configured for the specified table.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $table Table to cache
+ * @param array $row Row of table data
+ */
+
+ protected function cacheResults(string $table, array $row) {
+ if(!empty($this->tables[$table]['cache'])) {
+ // Cache the requested fields. For now, at least, we key on row ID only.
+ foreach($this->tables[$table]['cache'] as $field) {
+ if(is_array($field)) {
+ // This is a list of fields, create a composite key that point to the row ID
+
+ $label = "";
+ $key = "";
+
+ foreach($field as $subfield) {
+ // eg: co_id+attribute+name+
+ $label .= $subfield . "+";
+
+ // eg: 2+Identifier.type+eppn+
+ $key .= $row[$subfield] . "+";
+ }
+
+ $this->cache[$table][$label][$key] = $row['id'];
+ } else {
+ // Map id to the requested field
+ $this->cache[$table]['id'][ $row['id'] ][$field] = $row[$field];
+ }
+ }
+ }
+ }
+
+ /**
+ * Execute the Transmogrify Command.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Arguments $args Command Arguments
+ * @param ConsoleIo $io Console IO
+ */
+
+ public function execute(Arguments $args, ConsoleIo $io) {
+ // Load data from the inbound "transmogrify" database to a newly created
+ // (and empty) v5 database. The schema should already be applied to the
+ // new database.
+
+ // First, open connections to both old and new databases.
+ // Use the Cake ConnectionManager to get the database configs to pass to DBAL.
+ $indb = ConnectionManager::get('transmogrify');
+ $incfg = $indb->config();
+
+ if(empty($incfg)) {
+ throw new \InvalidArgumentException(__("registry.er.db.config", ["transmogrify"]));
+ }
+
+ $outdb = ConnectionManager::get('default');
+ $outcfg = $outdb->config();
+
+ if(empty($incfg)) {
+ throw new \InvalidArgumentException(__("registry.er.db.config", ["default"]));
+ }
+
+ $inconfig = new \Doctrine\DBAL\Configuration();
+
+ $cargs = [
+ 'dbname' => $incfg['database'],
+ 'user' => $incfg['username'],
+ 'password' => $incfg['password'],
+ 'host' => $incfg['host'],
+ 'driver' => ($incfg['driver'] == 'Cake\Database\Driver\Postgres' ? "pdo_pgsql" : "pdo_mysql")
+ ];
+
+ $this->inconn = DriverManager::getConnection($cargs, $inconfig);
+
+ $outconfig = new \Doctrine\DBAL\Configuration();
+
+ $cargs = [
+ 'dbname' => $outcfg['database'],
+ 'user' => $outcfg['username'],
+ 'password' => $outcfg['password'],
+ 'host' => $outcfg['host'],
+ 'driver' => ($outcfg['driver'] == 'Cake\Database\Driver\Postgres' ? "pdo_pgsql" : "pdo_mysql")
+ ];
+
+ $outconn = DriverManager::getConnection($cargs, $outconfig);
+
+ // We accept a list of table names, mostly for testing purposes
+ $atables = $args->getArguments();
+
+ foreach(array_keys($this->tables) as $t) {
+ // If we were given a list of tables see if this table is in the list
+ if(!empty($atables) && !in_array($t, $atables))
+ continue;
+
+ $io->out("===" . $t . "===");
+
+ $count = $this->inconn->fetchOne("SELECT COUNT(*) FROM " . $this->tables[$t]['source']);
+
+ $io->out("= Processing " . $count . " records");
+
+ $insql = "SELECT * FROM " . $this->tables[$t]['source'] . " ORDER BY id ASC";
+ $stmt = $this->inconn->query($insql);
+
+ $tally = 0;
+
+ while($row = $stmt->fetch()) {
+ if(!empty($row[ $this->tables[$t]['displayField'] ])) {
+ $io->out($row[ $this->tables[$t]['displayField'] ] . "...", 0);
+ }
+
+ try {
+ // Do this before fixBooleans since we'll insert some
+ $this->fixChangelog($t, $row);
+
+ $this->fixBooleans($t, $row);
+
+ $this->mapFields($t, $row);
+
+ $outconn->insert($t, $row);
+
+ $this->cacheResults($t, $row);
+ }
+ catch(ForeignKeyConstraintViolationException $e) {
+ // A foreign key associated with this record did not load, so we can't
+ // load this record. This can happen, eg, because the source_field_id
+ // did not load, perhaps because it was associated with an Org Identity
+ // not linked to a CO Person that was not migrated.
+
+ $io->err("WARNING: Skipping record " . $row['id'] . " due to invalid foreign key: " . $e->getMessage());
+ }
+ catch(\InvalidArgumentException $e) {
+ // If we can't find a value for mapping we skip the record
+ // (ie: mapFields basically requires a successful mapping)
+
+ $io->err("WARNING: Skipping record " . $row['id'] . ": " . $e->getMessage());
+ }
+
+ $tally++;
+ $io->out(floor(($tally * 100)/$count) . "% done");
+ }
+
+ $max = $this->inconn->fetchColumn('SELECT MAX(id) FROM ' . $this->tables[$t]['source']);
+ $max++;
+
+ $io->out("= New max: " . $max);
+
+ // Strictly speaking we should use prepared statements, but we control the
+ // data here, and also we're executing a maintenance operation (so query
+ // optimization is less important)
+ $outsql = "ALTER SEQUENCE " . $t . "_id_seq RESTART WITH " . $max;
+ $outconn->query($outsql);
+ }
+ }
+
+ /**
+ * Find the CO for a row of table data, based on a foreign key.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $row Row of table data
+ * @return int CO ID
+ * @throws InvalidArgumentException
+ */
+
+ protected function findCoId(array $row) {
+ // By the time we're called, we should have transmogrified the Org Identity
+ // and CO Person data, so we can just walk the caches
+
+ if(!empty($row['co_person_id'])) {
+ if(isset($this->cache['co_people']['id'][ $row['co_person_id'] ]['co_id'])) {
+ return $this->cache['co_people']['id'][ $row['co_person_id'] ]['co_id'];
+ }
+ } elseif(!empty($row['org_identity_id'])) {
+ // Map the OrgIdentity to a CO Person, then to the CO
+ if(!empty($this->cache['org_identities']['id'][ $row['org_identity_id'] ]['co_person_id'])) {
+ $coPersonId = $this->cache['org_identities']['id'][ $row['org_identity_id'] ]['co_person_id'];
+
+ if(isset($this->cache['co_people']['id'][ $coPersonId ]['co_id'])) {
+ return $this->cache['co_people']['id'][ $coPersonId ]['co_id'];
+ }
+ }
+ }
+
+ throw new \InvalidArgumentException('CO not found for record');
+ }
+
+ /**
+ * Translate booleans to string literals to work around DBAL Postgres boolean handling.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $table Table Name
+ * @param array $row Row of attributes, fixed in place
+ */
+
+ protected function fixBooleans(string $table, array &$row) {
+ $attrs = ['deleted'];
+
+ // We could introspect this from the schema file...
+ if(!empty($this->tables[$table]['booleans'])) {
+ $attrs = array_merge($attrs, $this->tables[$table]['booleans']);
+ }
+
+ foreach($attrs as $a) {
+ if(isset($row[$a]) && gettype($row[$a]) == 'boolean') {
+ // DBAL Postgres boolean handling seems to be somewhat buggy, see history in
+ // this issue: https://github.com/doctrine/dbal/issues/1847
+ // We need to (more generically than this hack) convert from boolean to char
+ // to avoid errors on insert
+ $row[$a] = ($row[$a] ? 't' : 'f');
+ }
+ }
+ }
+
+ /**
+ * Populate empty Changelog data from legacy records.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $table Table Name
+ * @param array $row Row of attributes, fixed in place
+ */
+
+ protected function fixChangelog(string $table, array &$row) {
+ if(array_key_exists('deleted', $row) && is_null($row['deleted'])) {
+ $row['deleted'] = false;
+ }
+
+ if(array_key_exists('revision', $row) && is_null($row['revision'])) {
+ $row['revision'] = 0;
+ }
+
+ if(array_key_exists('actor_identifier', $row) && is_null($row['actor_identifier'])) {
+ $row['actor_identifier'] = 'Transmogrification';
+ }
+
+ // The parent FK should remain NULL since this is the original record.
+ }
+
+ /**
+ * Map fields that have been renamed from Registry Classic to Registry PE.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $table Table Name
+ * @param array $row Row of attributes, fixed in place
+ * @throws InvalidArgumentException
+ */
+
+ protected function mapFields(string $table, array &$row) {
+ // oldname => newname, or &newname, which is a function to call.
+ // Note functions can returns more than one mapping
+ $fields = [];
+
+ if(!empty($this->tables[$table]['fieldMap'])) {
+ $fields = $this->tables[$table]['fieldMap'];
+ }
+
+ foreach($fields as $oldname => $newname) {
+ if(!$newname) {
+ // This attribute doesn't map, so simply unset it
+ unset($row[$oldname]);
+ } elseif($newname[0] == '&') {
+ // This is a function to map the field, in which case we reuse the old name
+ $f = substr($newname, 1);
+
+ // We always pass the entire row so the mapping function can implement
+ // whatever logic it needs
+ $row[$oldname] = $this->$f($row);
+
+ if(!$row[$oldname]) {
+ throw new \InvalidArgumentException("Could not find value for $table $oldname");
+ }
+ } else {
+ // Copy the value to the new name, then unset the old name
+ $row[$newname] = $row[$oldname];
+ unset($row[$oldname]);
+ }
+ }
+ }
+
+ /**
+ * Map an identifier type string to a foreign key.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $row Row of table data (ignored)
+ * @return int type_id
+ */
+
+ protected function map_identifier_type(array $row) {
+ return $this->map_type($row, 'Identifier.type', $this->findCoId($row));
+ }
+
+ /**
+ * Map a name type string to a foreign key.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $row Row of table data (ignored)
+ * @return int type_id
+ */
+
+ protected function map_name_type(array $row) {
+ return $this->map_type($row, 'Name.type', $this->findCoId($row));
+ }
+
+ /**
+ * Return a timestamp equivalent to now.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $row Row of table data (ignored)
+ * @return string Timestamp
+ */
+
+ protected function map_now(array $row) {
+ if(empty($this->cache['now'])) {
+ $created = new \Datetime('now');
+ $this->cache['now'] = $created->format('Y-m-d H:i:s');
+ }
+
+ return $this->cache['now'];
+ }
+
+ /**
+ * Map an Org Identity ID to a CO Person ID
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $row Row of Org Identity table data
+ * @return int CO Person ID
+ */
+
+ protected function map_org_identity_co_person_id(array $row) {
+ // PE eliminates OrgIdentityLink, so we need to map each Org Identity to
+ // a CO Person ID. This is a bit trickier than it sounds, since an Org Identity
+ // could have been relinked.
+
+ // Before Transmogrification, we require that Org Identities are unpooled.
+ // (This is probably how most deployments are set up, but there may be some
+ // legacy deployments out there.) This ensures whatever CO Person the Org
+ // Identity currently maps to through CoOrgIdentityLink is in the same CO.
+
+ // There may be multiple mappings if the Org Identity was relinked. Basically
+ // we're going to lose the multiple mappings, since we can only return one
+ // value here. (Ideally, we would inject multiple OrgIdentities into the new
+ // table, but this ends up being rather tricky, since we have to figure out
+ // what row id to assign, and for the moment we don't have a mechanism to
+ // do that.) Historical information remains available in history_records,
+ // and if the deployer keeps an archive of the old database.
+
+ // To figure out which co_person_id to use, we pull the record with the
+ // highest revision number. Note we might be transmogrifying a deleted row,
+ // so we can't ignore deleted rows here.
+
+ if(empty($this->cache['org_identities']['co_people'])) {
+ //$this->io('Populating org identity map...');
+
+ // We pull deleted rows because we might be migrating deleted rows
+ $mapsql = "SELECT * FROM cm_co_org_identity_links";
+ $stmt = $this->inconn->query($mapsql);
+
+ while($r = $stmt->fetch()) {
+ if(!empty($r['org_identity_id'])) {
+ $this->cache['org_identities']['co_people'][ $r['org_identity_id'] ][ $r['revision'] ] = $r['co_person_id'];
+ }
+ }
+ }
+
+ if(!empty($this->cache['org_identities']['co_people'][ $row['id'] ])) {
+ // Return the record with the highest revision number
+ $rev = max(array_keys($this->cache['org_identities']['co_people'][ $row['id'] ]));
+
+ return $this->cache['org_identities']['co_people'][ $row['id'] ][$rev];
+ }
+
+ return null;
+ }
+
+ /**
+ * Map a type string to a foreign key.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $row Row of table data (ignored)
+ * @param string $type Type to map (types:attribute)
+ * @param int $coId CO ID
+ * @return int type_id
+ * @throws InvalidArgumentException
+ */
+
+ protected function map_type(array $row, string $type, $coId) {
+ if(!$coId) {
+ throw new \InvalidArgumentException("CO ID not provided for $type " . $row['id']);
+ }
+
+ $key = $coId . "+" . $type . "+" . $row['type'] . "+";
+
+ if(empty($this->cache['types']['co_id+attribute+name+'][$key])) {
+ throw new \InvalidArgumentException("Type not found for " . $key);
+ }
+
+ return $this->cache['types']['co_id+attribute+name+'][$key];
+ }
+}
diff --git a/app/src/Console/Installer.php b/app/src/Console/Installer.php
new file mode 100644
index 000000000..8d98c7709
--- /dev/null
+++ b/app/src/Console/Installer.php
@@ -0,0 +1,246 @@
+getIO();
+
+ $rootDir = dirname(dirname(__DIR__));
+
+ static::createAppLocalConfig($rootDir, $io);
+ static::createWritableDirectories($rootDir, $io);
+
+ static::setFolderPermissions($rootDir, $io);
+ static::setSecuritySalt($rootDir, $io);
+
+ $class = 'Cake\Codeception\Console\Installer';
+ if (class_exists($class)) {
+ $class::customizeCodeceptionBinary($event);
+ }
+ }
+
+ /**
+ * Create config/app_local.php file if it does not exist.
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @return void
+ */
+ public static function createAppLocalConfig($dir, $io)
+ {
+ $appLocalConfig = $dir . '/config/app_local.php';
+ $appLocalConfigTemplate = $dir . '/config/app_local.example.php';
+ if (!file_exists($appLocalConfig)) {
+ copy($appLocalConfigTemplate, $appLocalConfig);
+ $io->write('Created `config/app_local.php` file');
+ }
+ }
+
+ /**
+ * Create the `logs` and `tmp` directories.
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @return void
+ */
+ public static function createWritableDirectories($dir, $io)
+ {
+ foreach (static::WRITABLE_DIRS as $path) {
+ $path = $dir . '/' . $path;
+ if (!file_exists($path)) {
+ mkdir($path);
+ $io->write('Created `' . $path . '` directory');
+ }
+ }
+ }
+
+ /**
+ * Set globally writable permissions on the "tmp" and "logs" directory.
+ *
+ * This is not the most secure default, but it gets people up and running quickly.
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @return void
+ */
+ public static function setFolderPermissions($dir, $io)
+ {
+ // ask if the permissions should be changed
+ if ($io->isInteractive()) {
+ $validator = function ($arg) {
+ if (in_array($arg, ['Y', 'y', 'N', 'n'])) {
+ return $arg;
+ }
+ throw new Exception('This is not a valid answer. Please choose Y or n.');
+ };
+ $setFolderPermissions = $io->askAndValidate(
+ 'Set Folder Permissions ? (Default to Y) [Y,n]? ',
+ $validator,
+ 10,
+ 'Y'
+ );
+
+ if (in_array($setFolderPermissions, ['n', 'N'])) {
+ return;
+ }
+ }
+
+ // Change the permissions on a path and output the results.
+ $changePerms = function ($path) use ($io) {
+ $currentPerms = fileperms($path) & 0777;
+ $worldWritable = $currentPerms | 0007;
+ if ($worldWritable == $currentPerms) {
+ return;
+ }
+
+ $res = chmod($path, $worldWritable);
+ if ($res) {
+ $io->write('Permissions set on ' . $path);
+ } else {
+ $io->write('Failed to set permissions on ' . $path);
+ }
+ };
+
+ $walker = function ($dir) use (&$walker, $changePerms) {
+ $files = array_diff(scandir($dir), ['.', '..']);
+ foreach ($files as $file) {
+ $path = $dir . '/' . $file;
+
+ if (!is_dir($path)) {
+ continue;
+ }
+
+ $changePerms($path);
+ $walker($path);
+ }
+ };
+
+ $walker($dir . '/tmp');
+ $changePerms($dir . '/tmp');
+ $changePerms($dir . '/logs');
+ }
+
+ /**
+ * Set the security.salt value in the application's config file.
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @return void
+ */
+ public static function setSecuritySalt($dir, $io)
+ {
+ $newKey = hash('sha256', Security::randomBytes(64));
+ static::setSecuritySaltInFile($dir, $io, $newKey, 'app_local.php');
+ }
+
+ /**
+ * Set the security.salt value in a given file
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @param string $newKey key to set in the file
+ * @param string $file A path to a file relative to the application's root
+ * @return void
+ */
+ public static function setSecuritySaltInFile($dir, $io, $newKey, $file)
+ {
+ $config = $dir . '/config/' . $file;
+ $content = file_get_contents($config);
+
+ $content = str_replace('__SALT__', $newKey, $content, $count);
+
+ if ($count == 0) {
+ $io->write('No Security.salt placeholder to replace.');
+
+ return;
+ }
+
+ $result = file_put_contents($config, $content);
+ if ($result) {
+ $io->write('Updated Security.salt value in config/' . $file);
+
+ return;
+ }
+ $io->write('Unable to update Security.salt value.');
+ }
+
+ /**
+ * Set the APP_NAME value in a given file
+ *
+ * @param string $dir The application's root directory.
+ * @param \Composer\IO\IOInterface $io IO interface to write to console.
+ * @param string $appName app name to set in the file
+ * @param string $file A path to a file relative to the application's root
+ * @return void
+ */
+ public static function setAppNameInFile($dir, $io, $appName, $file)
+ {
+ $config = $dir . '/config/' . $file;
+ $content = file_get_contents($config);
+ $content = str_replace('__APP_NAME__', $appName, $content, $count);
+
+ if ($count == 0) {
+ $io->write('No __APP_NAME__ placeholder to replace.');
+
+ return;
+ }
+
+ $result = file_put_contents($config, $content);
+ if ($result) {
+ $io->write('Updated __APP_NAME__ value in config/' . $file);
+
+ return;
+ }
+ $io->write('Unable to update __APP_NAME__ value.');
+ }
+}
diff --git a/app/src/Controller/ApiUsersController.php b/app/src/Controller/ApiUsersController.php
new file mode 100644
index 000000000..fd5eb2a3c
--- /dev/null
+++ b/app/src/Controller/ApiUsersController.php
@@ -0,0 +1,72 @@
+ [
+ 'delete' => ['platformAdmin', 'coAdmin'],
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'generate' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => ['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ];
+
+ /**
+ * Generate a new API Key.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $id API User ID (This is really an int, but Cake throws an error since it wants to pass type string)
+ */
+
+ public function generate(string $id) {
+ // We don't autogenerate after add because we'd have to interfere with performRedirect.
+
+ try {
+ $this->set('vv_obj', $this->ApiUsers->get($id));
+ $this->set('vv_api_key', $this->ApiUsers->generateKey((int)$id));
+ }
+ catch(Exception $e) {
+ $this->Flash->error($e->getMessage());
+ }
+
+ // Let the view render, but tell it to use a different fields file
+ $this->set('vv_fields_inc', 'fields-generate.inc');
+ $this->set('vv_title', __('registry.op.api.key.generate'));
+
+ $this->render('/Standard/add-edit-view');
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/ApiV2Controller.php b/app/src/Controller/ApiV2Controller.php
new file mode 100644
index 000000000..04407d826
--- /dev/null
+++ b/app/src/Controller/ApiV2Controller.php
@@ -0,0 +1,315 @@
+request->getParam('model');
+ // $this->name = Models
+ // We override $this->name (which is ApiV2) to make it match to the expected
+ // behavior for UI calls (which is Models, eg "Cous"). We need to do this
+ // before RegistryAuthComponent runs.
+ $modelsName = Inflector::camelize($reqModel);
+ $this->name = $modelsName;
+ // Similarly, for compatibility with UI related calls we load the model
+ $this->$modelsName = TableRegistry::getTableLocator()->get($modelsName);
+ $this->tableName = $this->$modelsName->getTable();
+
+ // We want API auth, not Web Auth
+ $this->RegistryAuth->setConfig('apiUser', true);
+ }
+
+ /**
+ * Handle an add action for a Standard object.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function add() {
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $tableName = models
+ $tableName = $this->tableName;
+
+ $json = $this->request->getData(); // Parsed by BodyParserMiddleware
+
+ if(empty($json[$modelsName])) {
+ $this->llog('debug', $modelsName . " object not found in request");
+ throw new BadRequestException(__('registry.er.api.object', [$modelsName]));
+ }
+
+ $results = [];
+
+ foreach($json[$modelsName] as $rec) {
+ try {
+ $obj = $this->$modelsName->newEntity($rec);
+
+ if($this->$modelsName->saveOrFail($obj)) {
+ $results[] = ['id' => $obj->id];
+ }
+ }
+ catch(\Exception $e) {
+ // The default exception error isn't particularly user friendly, so
+ // we dig into the entity errors and try to make a message from there.
+ $err = $this->exceptionToError($e);
+
+ $this->llog('debug', $err);
+ $results[] = ['error' => $err];
+ }
+ }
+
+ $this->set('vv_results', $results);
+
+ // Let the view render
+ $this->render('/Standard/api/v2/json/add-edit');
+ }
+
+ /**
+ * Callback run prior to the request rendering.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ * @return EventInterface
+ */
+
+ public function beforeRender(\Cake\Event\EventInterface $event) {
+ $this->set('vv_model_name', $this->name);
+ $this->set('vv_table_name', $this->tableName);
+
+ return parent::beforeRender($event);
+ }
+
+ /**
+ * Handle a delete action for a Standard object.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Integer $id Object ID
+ */
+
+ public function delete($id) {
+ // $this->name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+
+ // Make sure the requested object exists
+ try {
+ $obj = $this->$modelsName->findById($id)->firstOrFail();
+
+// XXX document AR-CO-1 when we implement hard delete/changelog
+// note similar logic in StandardController
+ $this->$modelsName->deleteOrFail($obj);
+
+ // Render an empty view
+ $this->render('/Standard/api/v2/json/delete');
+ }
+ catch(\Exception $e) {
+ // findById throws Cake\Datasource\Exception\RecordNotFoundException
+
+ // Rethrow the error so it formats correctly
+ throw new BadRequestException($this->exceptionToError($e));
+ }
+ }
+
+ /**
+ * Handle an edit action for a Standard object.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Integer $id Object ID
+ */
+
+ public function edit($id) {
+ // $this->name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+ // $tableName = models
+ $tableName = $this->$modelsName->getTable();
+
+ $query = $this->$modelsName->findById($id);
+
+ try {
+ // Pull the current record
+ $obj = $query->firstOrFail();
+
+ if(method_exists($this->$modelsName, "isReadOnly") && $this->$modelsName->isReadOnly($obj)) {
+ throw new BadRequestException(__('registry.er.edit.readonly'));
+ }
+
+ $json = $this->request->getData(); // Parsed by BodyParserMiddleware
+
+ if(empty($json[$modelsName])) {
+ throw new BadRequestException(__('registry.er.api.object', [$modelsName]));
+ }
+
+ $obj = $this->$modelsName->patchEntity($obj, $json[$modelsName]);
+
+ $this->$modelsName->saveOrFail($obj);
+
+ // Let the view render
+ $this->render('/Standard/api/v2/json/add-edit');
+ }
+ catch(\Exception $e) {
+ // findById throws Cake\Datasource\Exception\RecordNotFoundException
+
+ // The default exception error isn't particularly user friendly, so
+ // we dig into the entity errors and try to make a message from there.
+ $err = $this->exceptionToError($e);
+
+ $this->llog('debug', $err);
+ $results[] = ['error' => $err];
+
+ throw new BadRequestException($this->exceptionToError($e));
+ }
+ }
+
+ /**
+ * Convert an Exception to an error string suitable for the REST response,
+ * including field validation errors.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Exception $e Exception
+ * @return string Error string
+ */
+
+ protected function exceptionToError(\Exception $e): string {
+ // Default error
+ $err = $e->getMessage();
+
+ if(method_exists($e, "getEntity")) {
+ // Check for field validation errors
+ $errors = $e->getEntity()->getErrors();
+
+ if(!empty($errors)) {
+ // Flatten the array into a text string
+
+ $byAttr = [];
+
+ foreach($errors as $attr => $msgs) {
+ $byAttr[] = $attr . ": " . implode(',', array_values($msgs));
+ }
+
+ $err = implode(';', $byAttr);
+ }
+ }
+
+ return $err;
+ }
+
+ /**
+ * Generate an API Key for an API User.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $id API User ID
+ */
+
+ public function generateApiKey(string $id) {
+ // $id is always an int, but (1) in theory could be a string if we ever
+ // switched to UUIDs, and (2) ControllerFactory::invoke() (as triggered by
+ // the configuration in routes.php) only supports type string.
+
+ // Just let exceptions pop up the stack
+ $api_key = $this->ApiUsers->generateKey((int)$id);
+
+ $this->set('vv_results', ['api_key' => $api_key]);
+
+ // Let the view render
+ $this->render('/Standard/api/v2/json/add-edit');
+ }
+
+ /**
+ * Generate an index for a set of Standard Objects.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function index() {
+ // $modelsName = Models
+ $modelsName = $this->name;
+
+ $query = $this->$modelsName->find();
+
+ // PrimaryLinkTrait
+ $link = $this->getPrimaryLink(true);
+
+ // We automatically allow API calls to be filtered on primary link
+ if(!empty($link->attr) && !empty($link->value)) {
+ $query = $query->where([$link->attr => $link->value]);
+ }
+
+ // This magically makes REST calls paginated... can use eg direction=,
+ // sort=, limit=, page=
+ $this->set($this->tableName, $this->Paginator->paginate($query));
+
+ // Let the view render
+ $this->render('/Standard/api/v2/json/index');
+ }
+
+ /**
+ * Generate a view for a set of Standard Objects.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function view($id = null) {
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $tableName = models
+ $tableName = $this->$modelsName->getTable();
+
+ if(empty($id)) {
+ throw new InvalidArgumentException(__('registry.er.notprov', ['id']));
+ }
+
+ $obj = $this->$modelsName->findById($id)->firstOrFail();
+
+ $this->set($tableName, [$obj]);
+
+ // Let the view render
+ $this->render('/Standard/api/v2/json/index');
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php
new file mode 100644
index 000000000..a76fa6a27
--- /dev/null
+++ b/app/src/Controller/AppController.php
@@ -0,0 +1,417 @@
+loadComponent('RequestHandler');
+
+ // Add a detector so we can tell restful from non-restful calls
+ // Add a detector so we can call request->is('restful') (though note we no longer
+ // really support XML format...)
+ $request = $this->getRequest();
+
+ $request->addDetector('restful', function($request) {
+ // $request->is(json|xml) well check the mimetype
+ return ($request->is('json') || $request->is('xml'));
+ });
+
+ // COmanage specific component that handles authn/z processintg
+ $this->loadComponent('RegistryAuth');
+
+ $ChangelogEventListener = new ChangelogEventListener($this->RegistryAuth);
+ EventManager::instance()->on($ChangelogEventListener);
+
+ // We use Paginator in the REST API as well
+ $this->loadComponent('Paginator');
+
+ if(!$this->request->is('restful')) {
+ // Initialization for non-RESTful
+ $this->loadComponent('Flash');
+
+ /*
+ * Enable the following components for recommended CakePHP security settings.
+ * see https://book.cakephp.org/3.0/en/controllers/components/security.html
+ *
+ * In general, we don't need these protections for transactional API calls.
+ */
+ $this->loadComponent('Security');
+
+ // CSRF Protection is enabled via in Middleware via Application.php.
+ }
+ }
+
+ /**
+ * Callback run prior to the request action.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ */
+
+ public function beforeFilter(\Cake\Event\EventInterface $event) {
+ // Determine the timezone
+ $this->setTZ();
+
+ // Determine the requested CO
+ $this->setCO();
+
+ return parent::beforeFilter($event);
+ }
+
+ /**
+ * Callback run prior to the view rendering.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ */
+
+ public function beforeRender(\Cake\Event\EventInterface $event) {
+ // $this->name = Models
+ $modelsName = $this->name;
+
+ // Views can also inspect the request object to determine the current
+ // action, but it seems slightly easier to do it once here.
+ $this->set('vv_action', $this->request->getParam('action'));
+
+ if(isset($this->RegistryAuth)) {
+ // Components might not be loaded on error, so check
+ $this->set('vv_menu_permissions', $this->RegistryAuth->getMenuPermissions());
+ }
+
+ // Pull the set of COs this user is a member of, for rendering via menuMain
+ $Cos = TableRegistry::getTableLocator()->get("Cos");
+
+// XXX filter this based on the current user's eligibility (user should have one active or grace period role)
+// and also filter only Active COs, etc
+// - do this in CosTable or in RegistryAuth?
+ $this->set('vv_available_cos', $Cos->find()->toArray());
+
+ // For breadcrumbs, do we have a target model, and if so is it a configuration
+ // model (eg: ApiUsers) or an object model (eg: CoPeople)?
+ if(isset($this->$modelsName) // May not be set under certain error conditions
+ && method_exists($this->$modelsName, "getIsConfigurationTable")) {
+ $this->set('vv_is_configuration_model', $this->$modelsName->getIsConfigurationTable());
+ }
+
+ return parent::beforeRender($event);
+ }
+
+ /**
+ * Get the current CO.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return \App\Model\Entity\Co Co Entity or null
+ */
+
+ public function getCO(): ?\App\Model\Entity\Co {
+ if(!$this->cur_co) {
+ $this->setCO();
+ }
+
+ // We'll return null if no CO, since some contexts may need to know that
+ return $this->cur_co;
+ }
+
+ /**
+ * Get the current CO ID.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return int CO ID, or null
+ */
+
+ public function getCOID(): ?int {
+ $cur_co = $this->getCO();
+
+ return $cur_co ? $cur_co->id : null;
+ }
+
+ /**
+ * Obtain information about the Standard Object's Primary Link, if set.
+ * The $vv_primary_link view variable is also set.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param boolean $lookup If true, get the value of the primary link, not just the attribute
+ * @return object Object holding the primary link attribute, and optionally its value
+ * @throws \RuntimeException
+ */
+
+ protected function getPrimaryLink(bool $lookup=false) {
+ // Did we already figure this out? (But only if $lookup)
+ if($lookup && isset($this->cur_pl->value)) {
+ return $this->cur_pl;
+ }
+
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $modelName = Model
+ $modelName = \Cake\Utility\Inflector::singularize($modelsName);
+
+ $this->cur_pl = new \stdClass();
+
+ // PrimaryLinkTrait
+ if(method_exists($this->$modelsName, "getPrimaryLink")
+ && $this->$modelsName->getPrimaryLink()) {
+ $this->cur_pl->attr = $this->$modelsName->getPrimaryLink();
+ $this->set('vv_primary_link', $this->cur_pl->attr);
+
+ if($lookup) {
+ // Try to find a value
+
+ if($this->request->is('get')) {
+ // If this action allows unkeyed, asserted primary link IDs, check the query
+ // string (eg: 'add' or 'index' allow matchgrid_id to be passed in)
+ if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action'))
+ && $this->request->getQuery($this->cur_pl->attr)) {
+ $this->cur_pl->value = $this->request->getQuery($this->cur_pl->attr);
+ } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) {
+ // Try to map the requested object ID
+ $param = (int)$this->request->getParam('pass.0');
+
+ if(!empty($param)) {
+ $this->cur_pl->value = $this->$modelsName->calculatePrimaryLinkId($param);
+ }
+ }
+ } elseif($this->request->is('post') && $this->request->getParam('action') != 'delete') {
+ // Post = add, where we can have a list of objects and nothing in /objects/{id}
+ // We don't support different primary links across objects, so we throw an error
+ // if different parent keys are provided.
+
+ $linkValue = null;
+
+ // Data in API format
+ $reqData = $this->request->getData($modelsName);
+
+ if(!$reqData
+ // Don't create $reqData if the POST data is also empty
+ && !empty($this->request->getData())) {
+ // Data in POST format
+ $reqData[] = $this->request->getData();
+ }
+
+ if(!empty($reqData)) {
+ foreach($reqData as $rec) {
+ if(!empty($rec[$this->cur_pl->attr])) {
+ if(!$linkValue) {
+ // This is the first record we've seen, use this primary link value
+ $linkValue = $rec[$this->cur_pl->attr];
+ } elseif($linkValue != $rec[$this->cur_pl->attr]) {
+ // We don't support multiple records with different parents
+ throw new \InvalidArgumentException('All records must have the same primary link'); // XXX I18n
+ }
+ }
+
+ $this->cur_pl->value = $linkValue;
+ }
+ } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) {
+ // Try to map the requested object ID (this is probably a delete, so no attribute in post body)
+ $param = (int)$this->request->getParam('pass.0');
+
+ if(!empty($param)) {
+ $this->cur_pl->value = $this->$modelsName->calculatePrimaryLinkId($param);
+ }
+ }
+ } elseif($this->request->is('put') || $this->request->getParam('action') == 'delete') {
+ // Put = edit, so we should look up the parent ID via the object itself
+ if($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) {
+ // Try to map the requested object ID (this is probably a delete, so no attribute in post body)
+ $param = (int)$this->request->getParam('pass.0');
+
+ if(!empty($param)) {
+ $this->cur_pl->value = $this->$modelsName->calculatePrimaryLinkId($param);
+ }
+ }
+ }
+
+ if(empty($this->cur_pl->value) && !$this->$modelsName->allowEmptyPrimaryLink()) {
+ throw new \RuntimeException(__('registry.er.primary_link', [ $this->cur_pl->attr ]));
+ }
+ }
+
+ if(!empty($this->cur_pl->value)) {
+ // Look up the link value to find the related entity
+
+ $linkModelName = $this->$modelsName->getPrimaryLinkTableName();
+ $linkModel = TableRegistry::get($linkModelName);
+
+ $this->set('vv_primary_link_model', $linkModelName);
+
+ try {
+ $this->set('vv_primary_link_obj', $linkModel->findById($this->cur_pl->value)->firstOrFail());
+ }
+ catch(RecordNotFoundException $e) {
+ $this->llog('error', "Could not find value '" . $this->cur_pl->value . "' for primary link object " . $linkModelName);
+ // Mask this with a generic UnauthorizedException
+ throw new UnauthorizedException(__('registry.er.perm'));
+ }
+ }
+ }
+
+ return $this->cur_pl;
+ }
+
+ /**
+ * Determine the (requested) current CO and make it available to the
+ * rest of the application.
+ *
+ * @since COmanage Registry v5.0.0
+ * @throws Cake\Datasource\Exception\RecordNotFoundException
+ * @throws \InvalidArgumentException
+ */
+
+// XXX rewrite this and getPrimaryLink based on Match AppController when we
+// have an indirect model (eg: co_person_role) that has a parent other than CO
+ protected function setCO() {
+ if($this->cur_co) {
+ // Nothing to do...
+ return;
+ }
+
+ // $this->name = Models, unless we're in an API call
+ $modelsName = $this->name;
+
+ $attrs = $this->request->getAttributes();
+
+ // Unlike Match, where the Matchgrid is embedded in the request API URL,
+ // Registry API calls are more similar to UI calls, where we may or may
+ // not be able to find the CO ID directly in the URL.
+ if($this->request->is('restful')
+ && !empty($attrs['params']['model'])) {
+ $modelsName = \Cake\Utility\Inflector::camelize($attrs['params']['model']);
+ $this->$modelsName = TableRegistry::getTableLocator()->get($modelsName);
+ }
+
+ if(!method_exists($this->$modelsName, "requiresCO")
+ || !$this->$modelsName->requiresCO()) {
+ // Nothing to do, CO not required by this model/controller
+ return;
+ }
+
+ // Not all models have CO as their primary link. This will also
+ // trigger setting of the viewVar for breadcrumbs and anything else.
+ $link = $this->getPrimaryLink(true);
+
+ // Try to find the requested CO
+ $coid = null;
+
+ // If the parent model is CO, then getPrimaryLink has already done our work
+ if($link->attr == 'co_id') {
+ $coid = $link->value;
+ } else {
+ // XXX map (see Match)
+ }
+
+ if(!$coid
+ && !$this->$modelsName->allowEmptyCO()
+ && !$this->request->is('restful')) {
+ // If we get this far without a CO ID, something went wrong.
+ throw new \RuntimeException(__('registry.er.coid'));
+ }
+
+ if($coid) {
+ $this->loadModel('Cos');
+
+ // This throws Cake\Datasource\Exception\RecordNotFoundException which
+ // we just let pass up the stack.
+ $this->cur_co = $this->Cos->findById($coid)->firstOrFail();
+
+ // While the COmanage CO cannot be suspended (AR-CO-2), this is enforced
+ // at cos/edit, not here.
+
+ if($this->cur_co->status == TemplateableStatusEnum::Active) {
+ $this->set('vv_cur_co', $this->cur_co);
+ }
+ }
+ }
+
+ /**
+ * Determine the current timezone and make it available to the
+ * rest of the application.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ protected function setTZ() {
+ // $this->name = Models
+ $modelsName = $this->name;
+
+ // See if we've collected it from the browser in a previous page load. Otherwise
+ // use the system default. If the user set a preferred timezone, we'll catch that below.
+
+ $tz = date_default_timezone_get();
+
+ if(!empty($_COOKIE['cm_registry_tz_auto'])) {
+ // We have an auto-detected timezone from a previous page render from the browser.
+ // Note we don't call date_default_timezone_set() because we still want to record
+ // times internally in UTC (at the expense of having to convert back and forth).
+ $tz = $_COOKIE['cm_registry_tz_auto'];
+ }
+
+// XXX need to implement person-specific timezone detection (after CoPerson model and
+// login authentication are implemented)
+
+ $this->set('vv_tz', $tz);
+
+ if($this->$modelsName->behaviors()->has('Timezone')) {
+ // Tell TimezoneBehavior what the current timezone is
+ $this->$modelsName->setTimeZone($tz);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/CoPeopleController.php b/app/src/Controller/CoPeopleController.php
new file mode 100644
index 000000000..a7f710ffa
--- /dev/null
+++ b/app/src/Controller/CoPeopleController.php
@@ -0,0 +1,64 @@
+ [
+ 'canvas' => ['platformAdmin', 'coAdmin'],
+ 'delete' => ['platformAdmin', 'coAdmin'],
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => ['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ];
+
+ /**
+ * Handle a canvas request.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param integer $id CO Person ID
+ */
+ // XXX docblock
+
+ public function canvas($id) {
+ $this->edit($id);
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/Component/RegistryAuthComponent.php b/app/src/Controller/Component/RegistryAuthComponent.php
new file mode 100644
index 000000000..29cbfd721
--- /dev/null
+++ b/app/src/Controller/Component/RegistryAuthComponent.php
@@ -0,0 +1,369 @@
+llog('error', "Empty value(s) received for PHP_AUTH_USER and/or PHP_AUTH_PW");
+ throw new \InvalidArgumentException(__('registry.er.auth.api.invalid'));
+ }
+
+ $ApiUsers = TableRegistry::getTableLocator()->get('ApiUsers');
+
+ try {
+ // validateKey takes care of all validity logic, as well as rehashing (if needed)
+ if($ApiUsers->validateKey($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $_SERVER['REMOTE_ADDR'])) {
+ $this->authenticatedUser = $_SERVER['PHP_AUTH_USER'];
+ $this->authenticatedApiUser = true;
+ $this->llog('debug', "Authenticated API User \"" . $this->authenticatedUser . "\"");
+
+ return true;
+ }
+ }
+ catch(\Exception $e) {
+ $this->llog('debug', "User authentication failed: " . $e->getMessage());
+ throw new \InvalidArgumentException($e->getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * Authorize an API User.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return bool True if authorization was successful.
+ * @throws InvalidArgumentException
+ */
+
+ protected function authorizeApiUser(EventInterface $event) {
+ $controller = $event->getSubject();
+
+ // API authorization works a bit different from UI authorization, in that
+ // access is generally not Controller specific.
+
+ $ApiUsers = TableRegistry::getTableLocator()->get('ApiUsers');
+
+ try {
+ // The CO might be NULL if there is no CO ID in the current context
+ // (eg: /index/cos). In that case, we use CO ID 1 (COmanage CO), which is
+ // the proxy for "root" access.
+
+ $CO = $controller->getCO();
+
+ $priv = $ApiUsers->getUserPrivilege($this->authenticatedUser, ($CO ? $CO->id : 1));
+ }
+ catch(\InvalidArgumentException $e) {
+ // User unknown or similar, probably should have been caught in authenticateApiUser
+ $this->llog('debug', "User authorization failed: " . $e->getMessage());
+ throw $e;
+ }
+
+ if(!$priv) {
+ // XXX to deal with unprivileged API users we'll need some mechanism to call
+ // into the controller (or plugin controller) to allow it to determine if
+ // we're authorized
+
+ $this->llog('error', "Unprivileged User NOT IMPLEMENTED");
+ throw new \InvalidArgumentException("NOT IMPLEMENTED");
+ }
+
+ return true;
+ }
+
+ /**
+ * Callback run prior to the request action.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ */
+
+ public function beforeFilter(EventInterface $event) {
+ $controller = $event->getSubject();
+ $request = $controller->getRequest();
+ $session = $request->getSession();
+
+ if($this->getConfig('apiUser')) {
+ // There are no unauthenticated API calls, so always require a valid user
+
+ try {
+ if($this->authenticateApiUser()) {
+ $this->authorizeApiUser($event);
+ }
+ }
+ catch(RecordNotFoundException $e) {
+ // Requested record does not exist. For privileged API users, we can return
+ // a RecordNotFoundException, otherwise we recast to generate permission denied.
+ $this->llog('debug', "User authorization failed: " . $e->getMessage());
+
+ $ApiUsers = TableRegistry::getTableLocator()->get('ApiUsers');
+
+ if($ApiUsers->getUserPrivilege($this->authenticatedUser, 1)) {
+ throw $e;
+ } else {
+ throw new UnauthorizedException(__('registry.er.auth.api.failed'));
+ }
+ }
+ catch(\Exception $e) {
+ $this->llog('debug', $e->getMessage());
+ // Obfuscate the error message, which is available in the logs
+ throw new UnauthorizedException(__('registry.er.auth.api.failed'));
+ }
+ } else {
+ // Certain requests do not require authentication
+
+ // XXX is this too broad, or are all Pages permitted? Also, should this move
+ // into Controller::isAuthorized?
+ if($controller->getName() == 'Pages') {
+ return true;
+ }
+
+ // Do we have an authenticated user session?
+
+ // Note we don't stuff anything into the session anymore, the only attribute
+ // is the username, which is actually loaded by login.php.
+
+ $auth = $session->read('Auth');
+
+ if(!empty($auth['external']['user'])) {
+ // We have a valid user name that is *authenticated* for the current request.
+ // Note we haven't checked authorization, but this is how the authorization
+ // checks can get the authenticated username.
+ $controller->set('vv_user', ['username' => $auth['external']['user']]);
+ $this->authenticatedUser = $auth['external']['user'];
+
+ $id = null;
+ $passed = $request->getParam('pass');
+
+ if(!empty($passed[0])) {
+ $id = (int)$passed[0];
+ }
+
+ if($this->calculatePermission($request->getParam('action'), $id)) {
+ // Authorization successful
+ return true;
+ }
+
+ if(Configure::read('debug')) {
+ // For testing purposes throw an error, but in production we want to
+ // redirect to /login
+ $controller->Flash->error("Authorization Failed (RegistryAuthComponent)");
+ return $controller->redirect("/");
+ }
+ }
+
+ // No authentication, redirect to login
+
+ // We want to come back to where we started
+ $session->write('Auth.target', $request->getRequestTarget());
+
+ return $controller->redirect("/auth/login/login.php");
+ }
+ }
+
+ /**
+ * Calculate permissions for this action.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $action Action requested
+ * @param int $id Subject id, if applicable
+ * @return bool true if the action is permitted, false otherwise
+ * @throws UnauthorizedException
+ */
+
+ protected function calculatePermission(string $action, ?int $id=null): bool {
+ $controller = $this->_registry->getController();
+
+ $perms = $controller->calculatePermissions($id);
+
+ if(!isset($perms[$action])) {
+ throw new UnauthorizedException('Invalid Request (RegistryAuthComponent)');
+ }
+
+ return $perms[$action];
+ }
+
+ /**
+ * Calculate permissions for a Result Set.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param ResultSet $rs Result Set
+ * @return array Array of permissions keyed on record ID
+ */
+
+ public function calculatePermissionsForResultSet(ResultSet $rs): array {
+ $controller = $this->_registry->getController();
+
+ // We return an array since this is intended to be passed to a view
+ $ret = [];
+
+ $rs->rewind();
+
+ while($rs->valid()) {
+ $o = $rs->current();
+
+ $ret[ $o->id ] = $controller->calculatePermissions($o->id);
+
+ $rs->next();
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Calculate permissions for use in a view.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $action Action requested
+ * @param int $id Subject id, if applicable
+ * @return array Array of permissions, suitable for the view
+ */
+
+ public function calculatePermissionsForView(string $action, ?int $id=null): array {
+ $controller = $this->_registry->getController();
+
+ return $controller->calculatePermissions($id);
+ }
+
+ /**
+ * Obtain the identifier of the currently authenticated user.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return string The authenticated user identifier or false if no authenticated user
+ */
+
+ public function getAuthenticatedUser() {
+ return $this->authenticatedUser;
+ }
+
+ /**
+ * Obtain permissions suitable for menu rendering, specifically by
+ * templates/element/menuMain.php.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return array Array of permissions
+ */
+
+ public function getMenuPermissions() {
+ $permissions = [];
+
+// XXX need to set permissions according to current user's roles
+ // Can manage CO People in the current CO
+ $permissions['co_people'] = true;
+
+ // Can access the Configuration Dashboard for the current CO
+ $permissions['configuration'] = true;
+
+ return $permissions;
+ }
+
+ /**
+ * Determine if the current user is an API user.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return bool True if the current user is an API user
+ */
+
+ public function isApiUser() {
+ return $this->authenticatedApiUser;
+ }
+
+ /**
+ * Determine if the current user is a CO Administrator.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return bool True if the current user is a CO Administrator
+ */
+
+ public function isCoAdmin(?int $coId) {
+// XXX hardcoded for now until we've bootstrapped the COmanage CO
+// XXX we should cache the lookup when we actually do a db query
+ return ($this->authenticatedUser == 'admin');
+ }
+
+ /**
+ * Determine if the current user is a Platform Administrator.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return bool True if the current user is a Platform Administrator
+ */
+
+ public function isPlatformAdmin() {
+// XXX hardcoded for now until we've bootstrapped the COmanage CO
+// XXX we should cache the lookup when we actually do a db query
+ return ($this->authenticatedUser == 'admin');
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/CosController.php b/app/src/Controller/CosController.php
new file mode 100644
index 000000000..01b5a450f
--- /dev/null
+++ b/app/src/Controller/CosController.php
@@ -0,0 +1,77 @@
+ [
+ 'delete' => ['platformAdmin'],
+ 'duplicate' => ['platformAdmin'],
+ 'edit' => ['platformAdmin'],
+ 'view' => ['platformAdmin']
+ ],
+ // Actions that are permitted on readonly entities (besides view)
+ 'readOnly' => ['duplicate'],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => ['platformAdmin'],
+ 'index' => ['platformAdmin'],
+ 'select' => ['authenticatedUser']
+ ]
+ ];
+
+ /*
+ * XXX implement, also REST API
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Integer $id CO ID
+ */
+
+ public function duplicate(int $id) {
+
+ }
+
+ /**
+ * Provide a set of COs to operate on.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function select() {
+ // Population of vv_available_cos is currently done in AppController
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/CousController.php b/app/src/Controller/CousController.php
new file mode 100644
index 000000000..df3251485
--- /dev/null
+++ b/app/src/Controller/CousController.php
@@ -0,0 +1,81 @@
+ [
+ 'delete' => ['platformAdmin', 'coAdmin'],
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => ['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ];
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event Cake Event
+ */
+
+ public function beforeRender(\Cake\Event\EventInterface $event) {
+ if(!$this->request->is('restful')) {
+ // Pull the set of potential Parent COUs
+
+ switch($this->request->getParam('action')) {
+ case 'add':
+ $this->set('parents', $this->Cous->potentialParents($this->getCOID(), null, true));
+ break;
+ case 'edit':
+ $p = $this->request->getParam('pass');
+ $couId = (int)$p[0];
+ $this->set('parents', $this->Cous->potentialParents($this->getCOID(), $couId, true));
+ break;
+ case 'index':
+ $this->set('parents', $this->Cous->potentialParents($this->getCOID()));
+ break;
+ default:
+ break;
+ }
+ }
+
+ return parent::beforeRender($event);
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/DashboardsController.php b/app/src/Controller/DashboardsController.php
new file mode 100644
index 000000000..f6abfc6ff
--- /dev/null
+++ b/app/src/Controller/DashboardsController.php
@@ -0,0 +1,130 @@
+ [
+/*
+ 'delete' => ['platformAdmin', 'coAdmin'],
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']*/
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'configuration' => ['platformAdmin', 'coAdmin'],
+ 'dashboard' => ['platformAdmin', 'coAdmin'] // XXX this is not the correct long term permission
+/* 'add' => ['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ */
+ ]
+ ];
+
+ /**
+ * Render the CO Configuration Dashboard.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function configuration() {
+ $cur_co = $this->getCO();
+
+ $this->set('vv_title', __('registry.op.dashboard.configuration', $cur_co->name));
+
+ // Construct the set of configuration items. For everything except CO Settings
+ // we want to order by the localized text string.
+
+ // We're assuming that the permission for each of these items is the same as for
+ // configuration() itself, ie: CMP or CO Admin. But plausibly some of this stuff
+ // could be delegated to (eg) a COU Admin at some point...
+
+ $configMenuItems = [
+ __('registry.ct.ApiUsers', [99]) => [
+ 'icon' => 'vpn_key',
+ 'controller' => 'api_users',
+ 'action' => 'index'
+ ],
+ __('registry.ct.Cous', [99]) => [
+ 'icon' => 'people_outline',
+ 'controller' => 'cous',
+ 'action' => 'index'
+ ]
+ ];
+
+ ksort($configMenuItems);
+
+ // Insert CO Settings to the front of the list
+
+ $configMenuItems = array_merge([
+ __('registry.ct.CoSettings', [99]) => [
+ 'icon' => 'settings',
+ 'controller' => 'co_settings',
+ 'action' => 'add'
+ ]],
+ $configMenuItems
+ );
+
+ $this->set('vv_configuration_menu_items', $configMenuItems);
+
+ $platformMenuItems = [];
+
+ if($this->getCOID() == 1) {
+ // Also pass the platform menu items
+
+ $platformMenuItems = [
+ __('registry.ct.Cos', [99]) => [
+ 'icon' => 'build', // XXX kind of want house here, but maybe need newer material icons?
+ 'controller' => 'cos',
+ 'action' => 'index'
+ ]
+ ];
+ }
+
+ ksort($platformMenuItems);
+
+ $this->set('vv_platform_menu_items', $platformMenuItems);
+ }
+
+ /**
+ * Render a Dashboard.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id Dashboard ID
+ */
+
+ public function dashboard(?int $id=null) {
+ // XXX placeholder
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/ErrorController.php b/app/src/Controller/ErrorController.php
new file mode 100644
index 000000000..27dfde501
--- /dev/null
+++ b/app/src/Controller/ErrorController.php
@@ -0,0 +1,70 @@
+loadComponent('RequestHandler');
+ }
+
+ /**
+ * beforeFilter callback.
+ *
+ * @param \Cake\Event\EventInterface $event Event.
+ * @return \Cake\Http\Response|null|void
+ */
+ public function beforeFilter(EventInterface $event)
+ {
+ }
+
+ /**
+ * beforeRender callback.
+ *
+ * @param \Cake\Event\EventInterface $event Event.
+ * @return \Cake\Http\Response|null|void
+ */
+ public function beforeRender(EventInterface $event)
+ {
+ parent::beforeRender($event);
+
+ $this->viewBuilder()->setTemplatePath('Error');
+ }
+
+ /**
+ * afterFilter callback.
+ *
+ * @param \Cake\Event\EventInterface $event Event.
+ * @return \Cake\Http\Response|null|void
+ */
+ public function afterFilter(EventInterface $event)
+ {
+ }
+}
diff --git a/app/src/Controller/PagesController.php b/app/src/Controller/PagesController.php
new file mode 100644
index 000000000..5ad47405e
--- /dev/null
+++ b/app/src/Controller/PagesController.php
@@ -0,0 +1,75 @@
+redirect('/');
+ }
+ if (in_array('..', $path, true) || in_array('.', $path, true)) {
+ throw new ForbiddenException();
+ }
+ $page = $subpage = null;
+
+ if (!empty($path[0])) {
+ $page = $path[0];
+ }
+ if (!empty($path[1])) {
+ $subpage = $path[1];
+ }
+ $this->set(compact('page', 'subpage'));
+
+ try {
+ return $this->render(implode('/', $path));
+ } catch (MissingTemplateException $exception) {
+ if (Configure::read('debug')) {
+ throw $exception;
+ }
+ throw new NotFoundException();
+ }
+
+ return $this->render();
+ }
+}
diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php
new file mode 100644
index 000000000..5be3fa589
--- /dev/null
+++ b/app/src/Controller/StandardController.php
@@ -0,0 +1,633 @@
+name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+ // $tableName = models
+ $tableName = $table->getTable();
+
+ if($this->request->is('post')) {
+ // Try to save
+ $obj = $table->newEntity($this->request->getData());
+
+ // This throws \Cake\ORM\Exception\RolledbackTransactionException if aborted
+ // in afterSave
+ if($table->save($obj)) {
+ $this->Flash->success(__('registry.rs.saved'));
+
+ return $this->generateRedirect($obj->id);
+ }
+
+ $errors = $obj->getErrors();
+
+ if(!empty($errors)) {
+ $this->Flash->error(__('registry.er.fields', [ implode(',',
+ array_map(function($v) { return __('registry.fd.'.$v); },
+ array_keys($errors))) ]));
+ } else {
+ $this->Flash->error(__('registry.er.save', [$modelsName]));
+ }
+
+ // Pass $obj as context so the view can render validation errors
+ $this->set('vv_obj', $obj);
+ } else {
+ // Create an empty entity for FormHelper
+
+ $this->set('vv_obj', $table->newEmptyEntity());
+ }
+
+ // PrimaryLinkTrait
+ $this->getPrimaryLink();
+
+ // AutoViewVarsTrait
+ $this->populateAutoViewVars();
+
+ // Default title is add new object
+ $this->set('vv_title', __('registry.op.add.a', __('registry.ct.'.$modelsName, [1])));
+
+ // Let the view render
+ $this->render('/Standard/add-edit-view');
+ }
+
+ /**
+ * Standard operations before the view is rendered.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param EventInterface $event BeforeRender event
+ * @return \Cake\Http\Response HTTP Response
+ */
+
+// XXX can we merge calls ot (eg) getPrimaryLink and populateAutoViewVars here?
+ public function beforeRender(\Cake\Event\EventInterface $event) {
+ // $this->name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+
+ $this->getRequiredFields();
+
+ // Set the display field as a view var to make it available to the views
+ $this->set('vv_display_field', $table->getDisplayField());
+
+ // Populate permissions info, which uses the requested object ID if one
+ // was provided. As a first approximation, those actions that permit lookup
+ // primary link are also those that pass an $id that can be used to establish
+ // permissions, and also Cos (which has no primary link).
+
+ $id = null;
+
+ $params = $this->request->getParam('pass');
+
+ if(!empty($params[0])) {
+ if((method_exists($table, "getPrimaryLink")
+ && $table->allowLookupPrimaryLink($this->request->getParam('action')))
+ ||
+ $modelsName == 'Cos') {
+ $id = (int)$params[0];
+ }
+ }
+
+ $this->set('vv_permissions', $this->RegistryAuth->calculatePermissionsForView($this->request->getParam('action'), $id));
+
+ return parent::beforeRender($event);
+ }
+
+ /**
+ * Default implementation for calculating permissions for standard controllers,
+ * intended to be overridden by controllers with more speciific requirements.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id Record ID if relevant, or null
+ * @return array Array of permissions
+ */
+
+ public function calculatePermissions(?int $id): array {
+ $ret = [];
+
+ // $this->name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+
+ // Do we have an authenticated user?
+ $authenticatedUser = (bool)$this->RegistryAuth->getAuthenticatedUser();
+
+ // Is this user a Platform Administrator?
+ $platformAdmin = $this->RegistryAuth->isPlatformAdmin();
+
+ // Is this user a CO Administrator?
+ $coAdmin = $this->RegistryAuth->isCoAdmin($this->getCOID());
+
+ // Is this record read only?
+ $readOnly = false;
+
+ if($id) {
+ $readOnlyActions = ['view'];
+
+ // Does this table have an isReadOnly call?
+
+ if(method_exists($table, "isReadOnly")) {
+ // Pull the record so we can interrogate it
+
+ $obj = $table->get($id);
+
+ $readOnly = $table->isReadOnly($obj);
+
+ if(!empty($this->permissions['readOnly'])) {
+ // Merge in controller specific actions permitted on read only entities
+ $readOnlyActions = array_merge($readOnlyActions, $this->permissions['readOnly']);
+ }
+ }
+
+ // Permissions for actions that operate over individual entities
+
+ foreach($this->permissions['entity'] as $action => $roles) {
+ $ok = false;
+
+ if(!$readOnly || in_array($action, $readOnlyActions)) {
+ foreach($roles as $role) {
+ // eg: $role = "platformAdmin", which corresponds to the variables set, above
+ if($$role) {
+ $ok = true;
+ break;
+ }
+ }
+ }
+
+ $ret[$action] = $ok;
+ }
+ } else {
+ // Permissions for actions that operate over tables
+
+ foreach($this->permissions['table'] as $action => $roles) {
+ $ok = false;
+
+ foreach($roles as $role) {
+ // eg: $role = "platformAdmin", which corresponds to the variables set, above
+ if($$role) {
+ $ok = true;
+ break;
+ }
+ }
+
+ $ret[$action] = $ok;
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Handle a delete action for a Standard object.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Integer $id Object ID
+ */
+
+ public function delete($id) {
+ // $this->name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+
+ // Allow a delete via a POST or DELETE
+ $this->request->allowMethod(['post', 'delete']);
+
+ // Make sure the requested object exists
+ try {
+ $obj = $table->findById($id)->firstOrFail();
+
+// XXX throw 404 on RESTful not found?
+// XXX document AR-CO-1 when we implement hard delete/changelog
+ $table->deleteOrFail($obj);
+
+ // Use the display field to generate the flash message
+
+ $field = $table->getDisplayField();
+
+ if(!empty($obj->$field)) {
+ $this->Flash->success(__('registry.rs.deleted.a', [$obj->$field]));
+ } else {
+ $this->Flash->success(__('registry.rs.deleted'));
+ }
+
+ // Return to index since there is no delete view
+ return $this->generateRedirect();
+ }
+ catch(\Exception $e) {
+ // findById throws Cake\Datasource\Exception\RecordNotFoundException
+ $errors = $obj->getErrors();
+
+ if(!empty($errors)) {
+ // Format is [field => [rule => error]]
+ $errstr = "";
+
+ foreach($errors as $f => $r) {
+ foreach($r as $rule => $err) {
+ $errstr .= $err . ",";
+ }
+ }
+
+ $this->Flash->error(rtrim($errstr, ","));
+ } else {
+ $this->Flash->error($e->getMessage());
+ }
+
+ // The record is still valid, so redirect back to it
+ return $this->redirect(['action' => 'edit', $id]);
+ }
+ }
+
+ /**
+ * Handle an edit action for a Standard object.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Integer $id Object ID
+ */
+
+ public function edit($id) {
+ // $this->name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+ // $tableName = models
+ $tableName = $table->getTable();
+
+ // We use findById() rather than get() so we can apply subsequent
+ // query modifications via traits
+ $query = $table->findById($id);
+
+ // AssociationTrait
+/*
+ if(method_exists($table, "getEditContains")) {
+ $query = $query->contain($table->getEditContains());
+ }*/
+
+ try {
+ // Pull the current record
+ $obj = $query->firstOrFail();
+
+ if(method_exists($table, "isReadOnly")) {
+ // If this is a read only record, redirect to view
+ if($table->isReadOnly($obj)) {
+ $redirect = [
+ 'action' => 'view',
+ $obj->id
+ ];
+
+ return $this->redirect($redirect);
+ }
+ }
+
+ if($this->request->is(['post', 'put'])) {
+ // This is an update request
+ $opts = [];
+
+ // AssociationTrait
+ /*
+ if(method_exists($table, "getPatchAssociated")) {
+ $opts['associated'] = $table->getPatchAssociated();
+ }*/
+
+ // Attempt the update the record
+ $table->patchEntity($obj, $this->request->getData(), $opts);
+
+ // This throws \Cake\ORM\Exception\RolledbackTransactionException if aborted
+ // in afterSave
+ if($table->save($obj)) {
+ $this->Flash->success(__('registry.rs.saved'));
+
+ return $this->generateRedirect();
+ }
+
+ $errors = $obj->getErrors();
+
+ if(!empty($errors)) {
+ $this->Flash->error(__('registry.er.fields', [ implode(',',
+ array_map(function($v) { return __('registry.fd.'.$v); },
+ array_keys($errors))) ]));
+ } else {
+ $this->Flash->error(__('registry.er.save', [$modelsName]));
+ }
+ }
+ }
+ catch(\Exception $e) {
+ // findById throws Cake\Datasource\Exception\RecordNotFoundException
+
+ $this->Flash->error($e->getMessage());
+ return $this->generateRedirect();
+ }
+
+ $this->set('vv_obj', $obj);
+ // XXX should we also set '$model'? cake seems to autopopulate edit fields just fine without it
+ // note index() uses $tableName, not 'vv_objs' or event 'vv_table_name'
+
+ // PrimaryLinkTrait
+ $this->getPrimaryLink();
+
+ // AutoViewVarsTrait
+ $this->populateAutoViewVars($obj);
+
+ // Default view title is edit object display field
+ $field = $table->getDisplayField();
+
+ if(!empty($obj->$field)) {
+ $this->set('vv_title', __('registry.op.edit.a', $obj->$field));
+ } else {
+ $this->set('vv_title', __('registry.op.edit.a', __('registry.ct.'.$modelsName, [1])));
+ }
+
+ // Let the view render
+ $this->render('/Standard/add-edit-view');
+ }
+
+ /**
+ * Generate a redirect for a Standard Object operation.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id ID of object to redirect to
+ * @return \Cake\Http\Response
+ */
+
+ public function generateRedirect(?int $id) {
+ $redirect = [];
+
+ if($this->request->getParam('action') == 'add' && $id) {
+ // Redirect to the edit view of the record just added
+ // (if the user has add permission, they probably have edit permission)
+
+ $redirect = [
+ 'action' => 'edit',
+ $id
+ ];
+ } else {
+ // Default is to redirect to the index view
+ $redirect = ['action' => 'index'];
+
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->attr) && !empty($link->value)) {
+ $redirect['?'] = [$link->attr => $link->value];
+ }
+ }
+
+ return $this->redirect($redirect);
+ }
+
+ /**
+ * Build a list of required fields suitable for FieldHelper
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ protected function getRequiredFields() {
+ // $this->name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+
+ // Build a list of required fields for FieldHelper
+ $reqFields = [];
+
+ $validator = $table->getValidator();
+ $fields = $validator->getIterator();
+
+ foreach($fields as $name => $cfg) {
+ if(!$validator->isEmptyAllowed($name, ($this->request->getParam('action') == 'add'))) {
+ $reqFields[] = $name;
+ }
+ }
+
+ $this->set('vv_required_fields', $reqFields);
+ }
+
+ /**
+ * Generate an index for a set of Standard Objects.
+ *
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function index() {
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+ // $tableName = models
+ $tableName = $table->getTable();
+
+ $query = null;
+
+ // PrimaryLinkTrait
+ $link = $this->getPrimaryLink(true);
+
+ // AutoViewVarsTrait
+ $this->populateAutoViewVars();
+
+ if(!empty($link->attr)) {
+ // If a link attribute is defined but no value is provided, then query
+ // where the link attribute is NULL
+ $query = $table->find()->where([$link->attr => $link->value]);
+ } else {
+ $query = $table->find();
+ }
+
+ // QueryModificationTrait
+ if(method_exists($table, "getIndexContains")
+ && $table->getIndexContains()) {
+ $query->contain($table->getIndexContains());
+ }
+
+ $resultSet = $this->Paginator->paginate($query);
+
+ $this->set($tableName, $resultSet);
+ $this->set('vv_permission_set', $this->RegistryAuth->calculatePermissionsForResultSet($resultSet));
+
+ // Default index view title is model name
+ $this->set('vv_title', __('registry.ct.'.$modelsName, [99]));
+
+ // Let the view render
+ $this->render('/Standard/index');
+ }
+
+ /**
+ * Populate any auto view variables, as requested via AutoViewVarsTrait.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param object $obj Current object (eg: from edit), if set
+ */
+
+ protected function populateAutoViewVars(object $obj=null) {
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+
+ // Populate certain view vars (eg: selects) automatically.
+
+ // AutoViewVarsTrait
+ if(method_exists($table, "getAutoViewVars")
+ && $table->getAutoViewVars()) {
+ foreach($table->getAutoViewVars() as $vvar => $avv) {
+ switch($avv['type']) {
+ case 'enum':
+ // We just want the localized text strings for the defined constants
+ $class = '\\App\\Lib\\Enum\\'.$avv['class'];
+ $this->set($vvar, $class::getLocalizedConsts());
+ break;
+ // "auxiliary" and "select" do basically the same thing, but the former
+ // returns the full object and the latter just returns a hash suitable
+ // for a select
+ case 'auxiliary':
+// XXX add list as in match?
+ case 'select':
+ // We assume $modelName has a direct relationship to $avv['model']
+ $avvmodel = $avv['model'];
+ $this->loadModel($avvmodel);
+
+ if($avv['type'] == 'auxiliary') {
+ $query = $this->$avvmodel->find();
+ } else {
+ $query = $this->$avvmodel->find('list');
+ }
+
+ if(!empty($avv['find'])) {
+ if($avv['find'] == 'filterPrimaryLink') {
+ // We're filtering the requested model, not our current model.
+ // See if the requested key is available, and if so run the find.
+
+ $linkFilter = $table->getPrimaryLink();
+
+ if($linkFilter) {
+ // Try to find the $linkFilter value
+ $v = null;
+
+ // We might have been passed an object with the current value
+ if($obj && !empty($obj->$linkFilter)) {
+ $v = $obj->$linkFilter;
+ } elseif(!empty($this->request->getQuery($linkFilter))) {
+ $v = $this->request->getQuery($linkFilter);
+ }
+// XXX also need to check getData()?
+// XXX shouldn't this use $this->getPrimaryLink() instead? Or maybe move $this->primaryLink
+// to PrimaryLinkTrait and call it there?
+
+ if($v) {
+ $query = $query->find($avv['find'], [$linkFilter => $v]);
+ }
+ }
+ } else {
+ // Use the specified finder, if configured
+ $query = $query->find($avv['find']);
+ }
+ }
+
+ $this->set($vvar, $query->toArray());
+ break;
+ default:
+// XXX I18n? and in match?
+ throw new \LogicException('Unknonwn Auto View Var Type {0}', [$avv['type']]);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle a view action for a Standard object.
+ *
+ * @since COmanage Registry v6.0.0
+ * @param Integer $id Object ID
+ */
+
+ public function view($id = null) {
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $table = the actual table object
+ $table = $this->$modelsName;
+ // $tableName = models
+ $tableName = $table->getTable();
+
+ // We use findById() rather than get() so we can apply subsequent
+ // query modifications via traits
+ $query = $table->findById($id);
+
+ // AssociationTrait
+/*
+ if(method_exists($table, "getViewContains")) {
+ $query = $query->contain($table->getViewContains());
+ }*/
+
+ try {
+ // Pull the current record
+ $obj = $query->firstOrFail();
+ }
+ catch(\Exception $e) {
+ // findById throws Cake\Datasource\Exception\RecordNotFoundException
+
+ $this->Flash->error($e->getMessage());
+ return $this->generateRedirect();
+ }
+
+ $this->set('vv_obj', $obj);
+
+ // PrimaryLinkTrait
+ $this->getPrimaryLink();
+
+ // AutoViewVarsTrait
+ // We still used this in view() to map select values
+ $this->populateAutoViewVars($obj);
+
+ // Default view title is view object display field
+ $field = $table->getDisplayField();
+
+ if(!empty($obj->$field)) {
+ $this->set('vv_title', __('registry.op.view.a', $obj->$field));
+ } else {
+ $this->set('vv_title', __('registry.op.view.a', __('registry.ct.'.$modelsName, [1])));
+ }
+
+ // Let the view render
+ $this->render('/Standard/add-edit-view');
+ }
+}
\ No newline at end of file
diff --git a/app/src/Lib/Enum/BooleanEnum.php b/app/src/Lib/Enum/BooleanEnum.php
new file mode 100644
index 000000000..adb67195d
--- /dev/null
+++ b/app/src/Lib/Enum/BooleanEnum.php
@@ -0,0 +1,35 @@
+getConstants();
+
+ $className = substr(strrchr(get_called_class(), '\\'), 1);
+
+ foreach(array_values($consts) as $key) {
+ $ret[$key] = __('registry.en.'.$className.'.'.$key);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Get the values for the constants in the Enumeration.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return array Array of enumeration values
+ */
+
+ public static function getConstValues() : array {
+ // Get the keys for this enum
+ $reflect = new ReflectionClass(get_called_class());
+
+ $consts = $reflect->getConstants();
+
+ return array_values($consts);
+ }
+}
\ No newline at end of file
diff --git a/app/src/Lib/Enum/StatusEnum.php b/app/src/Lib/Enum/StatusEnum.php
new file mode 100644
index 000000000..851cdbe15
--- /dev/null
+++ b/app/src/Lib/Enum/StatusEnum.php
@@ -0,0 +1,48 @@
+RegistryAuth = $Auth;
+ }
+
+ /**
+ * Before save event listener.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Event $event Cake Event
+ * @param EntityInterface $entity Entity subject of the event (ie: object to be saved)
+ * @param ArrayObject $options Save options
+ */
+
+ public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options) {
+ // Tweak the options so ChangelogBehavior can see who performed the save
+
+ if($this->RegistryAuth) {
+ $options['actor'] = $this->RegistryAuth->getAuthenticatedUser();
+ $options['apiuser'] = $this->RegistryAuth->isApiUser();
+ }
+ }
+
+ /**
+ * Define the list of implemented events.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return array Array of implemented events and associated configuration.
+ */
+
+ public function implementedEvents(): array {
+ return [
+ 'Model.beforeSave' => [
+ 'callable' => 'beforeSave',
+ // We need this beforeSave to run before the ChangelogBehavior beforeSave
+ 'priority' => -100
+ ]
+ ];
+ }
+}
diff --git a/app/src/Lib/Random/RandomString.php b/app/src/Lib/Random/RandomString.php
new file mode 100644
index 000000000..693b8a922
--- /dev/null
+++ b/app/src/Lib/Random/RandomString.php
@@ -0,0 +1,68 @@
+autoViewVars;
+ }
+
+ /**
+ * Set the auto view variables.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $vars Array of auto view variables
+ */
+
+ public function setAutoViewVars($vars) {
+ $this->autoViewVars = $vars;
+ }
+}
diff --git a/app/src/Lib/Traits/CoLinkTrait.php b/app/src/Lib/Traits/CoLinkTrait.php
new file mode 100644
index 000000000..7832474d8
--- /dev/null
+++ b/app/src/Lib/Traits/CoLinkTrait.php
@@ -0,0 +1,129 @@
+allowEmptyCO;
+ }
+
+ /**
+ * Check to see whether the specified action is allowed to assert a CO ID
+ * directly (ie: not via lookup of an associated record).
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $action Action
+ * @return boolean true if permitted, false otherwise
+ */
+
+ public function allowUnkeyedCO(string $action) {
+ return in_array($action, $this->unkeyedActions, true);
+ }
+
+ /**
+ * Calculate the CO ID associated with the requested object ID.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id Object ID
+ * @return int CO ID
+ * @throws Cake\Datasource\Exception\RecordNotFoundException
+ */
+
+ public function calculateCoId(int $id) {
+ // For now we assume we have a direct foreign key to Cos.
+
+ $obj = $this->findById($id)->firstOrFail();
+
+ return $obj->co_id;
+ }
+
+ /**
+ * Determine if the associated controller requires a CO ID.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return boolean True if a CO ID is required, false otherwise
+ */
+
+ public function requiresCO() {
+ return $this->requiresCO;
+ }
+
+ /**
+ * Set if the associated controller normally requires a CO ID, whether the
+ * CO ID can be empty.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param boolean $allowEmpty True if the CO ID is permitted to be empty
+ */
+
+ public function setAllowEmptyCO(bool $allowEmpty) {
+ $this->allowEmptyCO = $allowEmpty;
+ }
+
+ /**
+ * Set whether the CO can be asserted directly.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param boolean $allowEmpty true if the CO can be asserted directly
+ */
+
+ public function setAllowUnkeyedPrimaryCO(array $actions) {
+ $this->unkeyedActions = array_merge($this->unkeyedActions, $actions);
+ }
+
+ /**
+ * Set if the associated controller requires a CO ID.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param boolean $required Boolean True if a Matchgrid ID is required, false otherwise
+ */
+
+ public function setRequiresCO(bool $required) {
+ $this->requiresCO = $required;
+ }
+}
diff --git a/app/src/Lib/Traits/LabeledLogTrait.php b/app/src/Lib/Traits/LabeledLogTrait.php
new file mode 100644
index 000000000..c36b7949d
--- /dev/null
+++ b/app/src/Lib/Traits/LabeledLogTrait.php
@@ -0,0 +1,68 @@
+allowEmpty;
+ }
+
+ /**
+ * Check to see whether the specified action allows a record ID to be passed
+ * in the URL, which can be used to lookup a primary link.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $action Action
+ * @return boolean true if permitted, false otherwise
+ */
+
+ public function allowLookupPrimaryLink(string $action) {
+ return in_array($action, $this->lookupActions, true);
+ }
+
+ /**
+ * Check to see whether the specified action is allowed to assert a primary link ID
+ * directly (ie: not via lookup of an associated record).
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $action Action
+ * @return boolean true if permitted, false otherwise
+ */
+
+ public function allowUnkeyedPrimaryLink(string $action) {
+ return in_array($action, $this->unkeyedActions, true);
+ }
+
+ /**
+ * Calculate the Primary Link ID associated with the requested object ID.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id Object ID
+ * @return int Primary Link ID
+ * @throws Cake\Datasource\Exception\RecordNotFoundException
+ */
+
+ public function calculatePrimaryLinkId(int $id) {
+ $obj = $this->findById($id)->firstOrFail();
+
+ return $obj->{$this->primaryLink};
+ }
+
+ /**
+ * Generate an ORM Query for the Primary Link.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param CakeORMQuery $query Cake ORM Query
+ * @param array $options Cake ORM Query options
+ * @return CakeORMQuery Cake ORM Query
+ */
+
+ public function findFilterPrimaryLink(\Cake\ORM\Query $query, array $options) {
+ return $query->where([$this->getPrimaryLink() => $options[$this->primaryLink]]);
+ }
+
+ /**
+ * Obtain the primary link.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return string Primary link attribute
+ */
+
+ public function getPrimaryLink() {
+ return $this->primaryLink;
+ }
+
+ /**
+ * Obtain the primary link's table name.
+ *
+ * @since COmanage Registry v5.0.0
+ * @return string Primary link table name
+ */
+
+ public function getPrimaryLinkTableName() {
+ return $this->primaryLinkTable;
+ }
+
+ /**
+ * Set whether the primary link is permitted to be empty.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param boolean $allowEmpty true if the primary link is permitted to be empty
+ */
+
+ public function setAllowEmptyPrimaryLink(bool $allowEmpty) {
+ $this->allowEmpty = $allowEmpty;
+ }
+
+ /**
+ * Set whether the primary link can be resolved via the object ID in the URL.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param boolean $allowEmpty true if the primary link can be resolved via the URL ID
+ */
+
+ public function setAllowLookupPrimaryLink(array $actions) {
+ $this->lookupActions = array_merge($this->lookupActions, $actions);
+ }
+
+ /**
+ * Set the primary link attribute.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $field Primary link attribute
+ */
+
+ public function setPrimaryLink($field) {
+ $this->primaryLink = $field;
+
+ // Calculate the table name for future reference
+ if(preg_match('/^(.*?)_id$/', $field, $f)) {
+ $this->primaryLinkTable = \Cake\Utility\Inflector::camelize(\Cake\Utility\Inflector::pluralize($f[1]));
+ }
+ }
+
+ /**
+ * Set whether the primary link can be asserted directly.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param boolean $allowEmpty true if the primary link can be asserted directly
+ */
+
+ public function setAllowUnkeyedPrimaryLink(array $actions) {
+ $this->unkeyedActions = array_merge($this->unkeyedActions, $actions);
+ }
+}
diff --git a/app/src/Lib/Traits/QueryModificationTrait.php b/app/src/Lib/Traits/QueryModificationTrait.php
new file mode 100644
index 000000000..c188302d6
--- /dev/null
+++ b/app/src/Lib/Traits/QueryModificationTrait.php
@@ -0,0 +1,57 @@
+indexContains;
+ }
+
+ /**
+ * Set containable models for index actions.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $contains Containable models
+ */
+
+ public function setIndexContains(array $contains) {
+ $this->indexContains = $contains;
+ }
+}
diff --git a/app/src/Lib/Traits/RulesTrait.php b/app/src/Lib/Traits/RulesTrait.php
new file mode 100644
index 000000000..c49e1abb7
--- /dev/null
+++ b/app/src/Lib/Traits/RulesTrait.php
@@ -0,0 +1,100 @@
+addUpdate(
+ [$this, 'ruleFreezePrimaryLink'],
+ 'freezePrimaryLink',
+ ['errorField' => $this->getPrimaryLink()]
+ );
+ }
+
+ // Add table specific rules
+
+ if(method_exists($this, "buildTableRules")) {
+ $this->buildTableRules($rules);
+ }
+
+ return $rules;
+ }
+
+ // Only Application Rules that apply to multiple Tables should be defined
+ // here, so as not to create noise in this file or add unnecessary functions.
+
+ /**
+ * Application Rule to reject changes to the primary link. This is more of a
+ * Security Rule than an Application Rule, but for now we don't distinguish
+ * between the two types.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Entity $entity Entity to be validated
+ * @param array $options Application rule options
+ * @return boolean true if the Rule check passes, false otherwise
+ */
+
+ public function ruleFreezePrimaryLink($entity, $options) {
+ $want = $entity->get($this->getPrimaryLink());
+ $have = $entity->getOriginal($this->getPrimaryLink());
+
+ // If the two values differ throw an error. Note this should only be called
+ // on update(), so we shouldn't need to check the original for null (as it
+ // might be on add).
+
+ if($want !== $have) {
+ return __('registry.er.fields.primary_link', [$this->getPrimaryLink()]);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Lib/Traits/TableMetaTrait.php b/app/src/Lib/Traits/TableMetaTrait.php
new file mode 100644
index 000000000..f43e3cfce
--- /dev/null
+++ b/app/src/Lib/Traits/TableMetaTrait.php
@@ -0,0 +1,57 @@
+confTable;
+ }
+
+ /**
+ * Set if this Table represents Registry configuration (vs objects).
+ *
+ * @since COmanage Registry v5.0.0
+ * @param array $vars Array of auto view variables
+ */
+
+ public function setIsConfigurationTable(bool $confTable) {
+ $this->confTable = $confTable;
+ }
+}
diff --git a/app/src/Lib/Traits/ValidationTrait.php b/app/src/Lib/Traits/ValidationTrait.php
new file mode 100644
index 000000000..b8adf03d1
--- /dev/null
+++ b/app/src/Lib/Traits/ValidationTrait.php
@@ -0,0 +1,171 @@
+setProvider('conditionalRequire', [
+ * 'inArray' => [valuesThatRequireOtherField],
+ * 'require' => 'field_to_require',
+ * 'label' => 'field label, for use in error message']);
+ *
+ * Note this validation rule is applied on the value that must always be set,
+ * which will result in the validation error being unintuitively placed on the
+ * "wrong" attribute.
+ *
+ * @since COmanage Common v1.0.0
+ * @param string $value Value to validate
+ * @param array $context Validation context
+ * @return mixed True if $value validates, or an error string otherwise
+ */
+
+ public function validateConditionalRequire($value, array $context) {
+ // What component are we?
+ $COmponent = __('product.code');
+
+ if(!empty($value)
+ && in_array($value, $context['providers']['conditionalRequire']['inArray'])
+ && empty($context['data'][ $context['providers']['conditionalRequire']['require'] ])) {
+ return __($COmponent.'.er.input.condreq', [$context['providers']['conditionalRequire']['label']]);
+ }
+
+ return true;
+ }
+
+ /**
+ * Determine if a string submitted from a form is valid input.
+ *
+ * @since COmanage Common v1.0.0
+ * @param string $value Value to validate
+ * @param array $context Validation context
+ * @return mixed True if $value validates, or an error string otherwise
+ */
+
+ public function validateInput($value, array $context) {
+ // By default, we'll accept anything except < and >. Arguably, we should accept
+ // anything at all for input (and filter only on output), but this was agreed to
+ // as an extra "line of defense" against unsanitized HTML output, since there are
+ // currently no known cases where user-entered input should permit angle brackets.
+
+// XXX we previously supported 'filter'. 'flags', and 'invalidchars' as arguments, do we still need to?
+
+ // What component are we?
+ $COmponent = __('product.code');
+
+ // Perform a basic string search.
+
+ $invalid = "<>";
+
+ if(strlen($value) != strcspn($value, $invalid)) {
+ // Mismatch, implying bad input
+ return __($COmponent.'.er.input.invalid');
+ }
+
+ // We require at least one non-whitespace character (CO-1551)
+ if(!preg_match('/\S/', $value)) {
+ return __($COmponent.'.er.input.blank');
+ }
+
+ return true;
+ }
+
+ /**
+ * Determine if a string submitted from a form is a valid language.
+ *
+ * @since COmanage Common v1.0.0
+ * @param string $value Value to validate
+ * @param array $context Validation context
+ * @return mixed True if $value validates, or an error string otherwise
+ */
+
+ public function validateLanguage($value, array $context) {
+// XXX this was previously done by examining $cm_texts[$cm_lang]['en.language']
+// we need a new way to enumerate permitted language codes
+ /*
+ if(!in_array($value, array_values(timezone_identifiers_list()))) {
+ return __($COmponent.'.er.input.invalid');
+ }*/
+
+ return true;
+ }
+
+ /**
+ * Determine if a string submitted from a form is valid SQL identifier.
+ *
+ * @since COmanage Common v1.0.0
+ * @param string $value Value to validate
+ * @param array $context Validation context
+ * @return mixed True if $value validates, or an error string otherwise
+ */
+
+ public function validateSqlIdentifier($value, array $context) {
+ // What component are we?
+ $COmponent = __('product.code');
+
+ // Valid (portable) SQL identifiers begin with a letter or underscore, and
+ // subsequent characters can also include digits. We'll be a little stricter
+ // than we need to be for now by only accepting A-Z, when in fact certain
+ // additional characters (like á) are also acceptable.
+
+ if(!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $value)) {
+ return __($COmponent.'.er.input.invalid');
+ }
+
+ return true;
+ }
+
+ /**
+ * Determine if a string submitted from a form is a valid timezone.
+ *
+ * @since COmanage Common v1.0.0
+ * @param string $value Value to validate
+ * @param array $context Validation context
+ * @return mixed True if $value validates, or an error string otherwise
+ */
+
+ public function validateTimeZone($value, array $context) {
+ if(!in_array($value, array_values(timezone_identifiers_list()))) {
+ return __($COmponent.'.er.input.invalid');
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Behavior/ChangelogBehavior.php b/app/src/Model/Behavior/ChangelogBehavior.php
new file mode 100644
index 000000000..d1acdd2ff
--- /dev/null
+++ b/app/src/Model/Behavior/ChangelogBehavior.php
@@ -0,0 +1,174 @@
+getSubject();
+ $alias = $subject->getAlias();
+
+ LogBehavior::strace($alias, 'Changelog converting delete to update');
+
+ $entity->deleted = true;
+ $subject->saveOrFail($entity, ['checkRules' => false, 'archive' => false]);
+
+ // Stop the delete from actually happening
+ $event->stopPropagation();
+
+ // But return success
+ return true;
+ }
+
+ /**
+ * Adjust find query conditions for changelog.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Event $event The beforeFind event
+ * @param Query $query Query
+ * @param ArrayObject $options The options for the query
+ * @param boolean $primary Whether or not this is the root query (vs an associated query)
+ */
+
+ public function beforeFind(\Cake\Event\Event $event, \Cake\ORM\Query $query, \ArrayObject $options, bool $primary) {
+ $subject = $event->getSubject();
+ $table = $subject->getTable();
+ $alias = $subject->getAlias();
+ $parentfk = Inflector::singularize($table) . "_id";
+
+ LogBehavior::strace($alias, 'Changelog altering find conditions');
+
+ // XXX add support for archived, revision, etc
+ // XXX if specific id is requested, do not modify query
+
+ // We use IS NOT TRUE to check for null || false, since pre-Changelog data
+ // may have null instead of false.
+ // (Alternately we could join two clauses for false || IS NULL.)
+// Transmogrification will backfill this... do we still need it? Are there any tables
+// that will be not-changelog but might become changelog?
+ $query->where([$alias . '.deleted IS NOT true'])
+ ->where([$alias . '.' . $parentfk . ' IS NULL']);
+
+ // XXX need to also check parent key IS NULL
+ }
+
+ /**
+ * Handle changelog archive during (before) save of object.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Event $event The beforeSave event
+ * @param EntityInterface $entity Entity
+ * @param ArrayObject $options Options
+ */
+
+ public function beforeSave(\Cake\Event\Event $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options) {
+ // XXX prevent updates to deleted and archived records
+ // Cake Book suggests doing this with Application Rules... can we define those in the Behavior?
+ // or perhaps in beforeMarshal? https://book.cakephp.org/3.0/en/orm/saving-data.html#modifying-request-data-before-building-entities
+
+ if(isset($options['archive']) && !$options['archive']) {
+ // Archiving disabled for this request, don't do anything
+ return;
+ }
+
+ $subject = $event->getSubject();
+ $table = $subject->getTable();
+ $alias = $subject->getAlias();
+ $parentfk = Inflector::singularize($table) . "_id";
+
+ $actor = '';
+
+ if(!empty($options['actor'])) {
+ // The actor information is passed via $options, which is populated by ChangelogEventListener
+
+ if(isset($options['apiuser']) && $options['apiuser']) {
+ $actor = 'API:';
+ }
+
+ $actor .= $options['actor'];
+
+ // Make sure the string fits
+ $actor = substr($actor, 0, 256);
+ }
+
+ if(empty($entity->id)) {
+ // This is an add, just set default metadata
+
+ LogBehavior::strace($alias, 'Changelog setting default changelog metadata on add');
+ $entity->deleted = false;
+ $entity->revision = 0;
+ $entity->actor_identifier = $actor;
+
+ return;
+ } else {
+ // This is an edit, so copy on write
+
+ LogBehavior::strace($alias, 'Changelog creating archive copy of record ' . $entity->id);
+
+// XXX start a transaction that gets finished in afterSave
+// we're normally already in a transaction (if atomic), maybe we don't need to manage another one?
+ $class = get_class($entity);
+
+ // We disable setters because we want an exact copy of the original record
+ $archive = new $class($entity->getOriginalValues(), ['useSetters' => false]);
+
+ // Update the appropriate attributes
+ unset($archive->id);
+ $archive->$parentfk = $entity->id;
+
+ // We actually want to update the *current* record with $actor, not the
+ // archived copy (which was presumably updated by the previous $actor)
+ $entity->actor_identifier = $actor;
+
+ // We also increment the revision on the entity, not the archive
+ $entity->revision++;
+
+ // Cake 3 doesn't have callbacks=false, so we use the archive flag so we
+ // don't recurse indefinitely. We also skip validation in case (eg) validation
+ // rules changed since the original record was created.
+ $subject->saveOrFail($archive, ['checkRules' => false, 'archive' => false]);
+
+ return;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Behavior/LogBehavior.php b/app/src/Model/Behavior/LogBehavior.php
new file mode 100644
index 000000000..c4519a810
--- /dev/null
+++ b/app/src/Model/Behavior/LogBehavior.php
@@ -0,0 +1,73 @@
+getSubject();
+ $label = getmypid() . "/" . $subject->getAlias() . ": ";
+// XXX can we inject IP address of requester (where available)?
+
+ Log::info($label . 'beforeFind', ['scope' => ['trace']]);
+ }
+
+ // XXX docblock
+
+ public function beforeSave(\Cake\Event\Event $event, $entity, \ArrayObject $options) {
+ }
+
+// XXX don't define log() since it will collide with LogTrait?
+
+ public function trace(string $msg) {
+
+ }
+
+ public static function strace(string $name, string $msg) {
+ $label = getmypid() . "/" . $name . ": ";
+
+ Log::info($label . $msg, ['scope' => ['trace']]);
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Behavior/TimezoneBehavior.php b/app/src/Model/Behavior/TimezoneBehavior.php
new file mode 100644
index 000000000..b3d6310d1
--- /dev/null
+++ b/app/src/Model/Behavior/TimezoneBehavior.php
@@ -0,0 +1,90 @@
+tz && $this->tz != 'UTC') {
+ // There's some Cake support for doing timezone conversion, but it's not really
+ // well documented, so we use PHP calls directly.
+
+ $localTZ = new \DateTimeZone($this->tz);
+
+ foreach($this->fields as $f) {
+ if(!empty($data[$f])) {
+ // This returns a DateTime object adjusting for localTZ
+ $offsetDT = new \DateTime($data[$f], $localTZ);
+
+ // strftime converts a timestamp according to server localtime (which should be UTC)
+ $data[$f] = strftime("%F %T", $offsetDT->getTimestamp());
+ }
+ }
+ }
+// XXX
+// - adjust back after find
+// - afterfind isn't a thing anymore, perhaps use a mutator?
+// - note that's on the entity, not the table, so we can't just use the Behavior, maybe need a trait
+// - maybe move this behavior to a trait on the entity and do this entirely with accessors and mutators?
+// - or on retrieve we should assert values are UTC then convert in the view
+// https://stackoverflow.com/questions/49280448/how-to-handle-users-timezone-and-utc-sync-between-application-and-database-in-c
+ }
+
+ /**
+ * Set the current timezone.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $tz Timezone, eg as determined by AppController::beforeFilter
+ */
+
+ public function setTimeZone(string $tz) {
+ $this->tz = $tz;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/ApiUser.php b/app/src/Model/Entity/ApiUser.php
new file mode 100644
index 000000000..832ca40de
--- /dev/null
+++ b/app/src/Model/Entity/ApiUser.php
@@ -0,0 +1,64 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ // By default, we don't want to allow api_key to be set directly over the API
+ // (or via the UI, but we control that by not exposing a field). Only generate()
+ // can set api_key. (AR-ApiUser-4)
+ 'api_key' => false
+ ];
+
+ /**
+ * Hash (bcrypt) an API Key on save.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $apiKey Unhashed API Key
+ * @return string Hashed API Key
+ */
+
+ protected function _setApiKey($apiKey) {
+ // Note setters are disabled by ChangelogBehavior in order to prevent (eg)
+ // rehashing the hash on archive.
+
+ if(!empty($apiKey)) {
+ return (new DefaultPasswordHasher)->hash($apiKey);
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Co.php b/app/src/Model/Entity/Co.php
new file mode 100644
index 000000000..6db629df6
--- /dev/null
+++ b/app/src/Model/Entity/Co.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/CoPerson.php b/app/src/Model/Entity/CoPerson.php
new file mode 100644
index 000000000..ede40d399
--- /dev/null
+++ b/app/src/Model/Entity/CoPerson.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Cou.php b/app/src/Model/Entity/Cou.php
new file mode 100644
index 000000000..32dcdb36d
--- /dev/null
+++ b/app/src/Model/Entity/Cou.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Dashboard.php b/app/src/Model/Entity/Dashboard.php
new file mode 100644
index 000000000..b5517f5a0
--- /dev/null
+++ b/app/src/Model/Entity/Dashboard.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Identifier.php b/app/src/Model/Entity/Identifier.php
new file mode 100644
index 000000000..0ce1203b3
--- /dev/null
+++ b/app/src/Model/Entity/Identifier.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Name.php b/app/src/Model/Entity/Name.php
new file mode 100644
index 000000000..81845bb1a
--- /dev/null
+++ b/app/src/Model/Entity/Name.php
@@ -0,0 +1,93 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+
+ /**
+ * Generate a common (full) name.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param bool $showHonorific If true, return honorific as part of name
+ * @return string Formatted name
+ */
+
+ protected function _getCommonName($showHonorific = false) {
+ // Name order is a bit tricky. We'll use the language encoding as our hint,
+ // although it isn't perfect. This could be replaced with a more sophisticated
+ // test as requirements evolve.
+
+ $cn = "";
+
+ if(empty($this->language)
+ || !in_array($this->language, ['hu', 'ja', 'ko', 'za-Hans', 'za-Hant'])) {
+ // Western order. Do not show honorific by default.
+
+ if($showHonorific && !empty($this->honorific)) {
+ $cn .= ($cn != "" ? ' ' : '') . $this->honorific;
+ }
+
+ if(!empty($this->given)) {
+ $cn .= ($cn != "" ? ' ' : '') . $this->given;
+ }
+
+ if(!empty($this->middle)) {
+ $cn .= ($cn != "" ? ' ' : '') . $this->middle;
+ }
+
+ if(!empty($this->family)) {
+ $cn .= ($cn != "" ? ' ' : '') . $this->family;
+ }
+
+ if(!empty($this->suffix)) {
+ $cn .= ($cn != "" ? ' ' : '') . $this->suffix;
+ }
+ } else {
+ // Switch to Eastern order. It's not clear what to do with some components.
+
+ if(!empty($this->family)) {
+ $cn .= ($cn != "" ? ' ' : '') . $this->family;
+ }
+
+ if(!empty($this->given)) {
+ $cn .= ($cn != "" ? ' ' : '') . $this->given;
+ }
+ }
+
+ return $cn;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/OrgIdentity.php b/app/src/Model/Entity/OrgIdentity.php
new file mode 100644
index 000000000..77580ed21
--- /dev/null
+++ b/app/src/Model/Entity/OrgIdentity.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/ApiUsersTable.php b/app/src/Model/Table/ApiUsersTable.php
new file mode 100644
index 000000000..46610628a
--- /dev/null
+++ b/app/src/Model/Table/ApiUsersTable.php
@@ -0,0 +1,350 @@
+addBehavior('Changelog');
+ $this->addBehavior('Timestamp');
+ $this->addBehavior('Timezone');
+
+ // ApiUsers are configuration
+ $this->setIsConfigurationTable(true);
+
+ // Define associations
+ $this->belongsTo('Cos');
+
+ $this->setDisplayField('username');
+
+ $this->setPrimaryLink('co_id');
+ $this->setAllowLookupPrimaryLink(['generate', 'generateApiKey']);
+ $this->setRequiresCO(true);
+
+ $this->setAutoViewVars([
+ 'statuses' => [
+ 'type' => 'enum',
+ 'class' => 'SuspendableStatusEnum'
+ ]
+ ]);
+ }
+
+ /**
+ * Define business rules to supplement the default trait implementation.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param RulesChecker $rules RulesChecker object
+ * @return RulesChecker
+ */
+
+ public function buildTableRules(RulesChecker $rules): RulesChecker {
+ // We don't want to perform the uniqueness check until after then namespacing
+ // check in order to avoid information leakage. This requires more complicated
+ // rule building.
+
+ $rules->add(function($entity, $options) use($rules) {
+ // AR-ApiUser-3 For namespacing purposes, API Users are named with a prefix consisting of the string "co_#.".
+ $ret = $this->ruleIsUsernameValid($entity, $options);
+
+ if($ret !== true) {
+ // Return the error message
+ return $ret;
+ }
+
+ // AR-ApiUser-3 API usernames must be unique across the entire platform.
+ $rule = $rules->isUnique(['username'], __('registry.er.exists', [__('registry.ct.ApiUsers', [1])]));
+
+ return $rule($entity, $options);
+ },
+ 'isUsernameValid',
+ ['errorField' => 'username']);
+
+ return $rules;
+ }
+
+ /**
+ * Generate (and save) an API Key for the specified API User.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $id API User ID
+ * @return string API Key
+ */
+
+ public function generateKey(int $id) {
+ $token = RandomString::generateAppKey();
+
+ // Note hashing happens in the entity (ApiUser.php)
+ $apiUser = $this->get($id);
+ $apiUser->api_key = $token;
+
+ $this->save($apiUser);
+
+ return $token;
+ }
+
+ /**
+ * Obtain an API User's priviledged status. Note this function will not validate
+ * any aspects of the record (status, valid_from, etc) -- use validateKey for that.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $username API Username
+ * @param int $coId CO ID
+ * @return boolean True if $username is a privileged API user, false otherwise
+ * @throws InvalidArgumentException
+ */
+
+ public function getUserPrivilege(string $username, int $coId) {
+ $apiUser = $this->find()->where(['username' => $username])->first();
+
+ if(empty($apiUser)) {
+ throw new \InvalidArgumentException(__('registry.er.auth.api.unknown', [$username]));
+ }
+
+ return $apiUser->privileged;
+ }
+
+ /**
+ * Application Rule to determine if the current entity username is valid.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Entity $entity Entity to be validated
+ * @param array $options Application rule options
+ * @return boolean true if the Rule check passes, false otherwise
+ */
+
+ public function ruleIsUsernameValid($entity, $options) {
+ // We need to pull the CO data to check the name
+
+ if(!$entity->co_id) {
+ return __('registry.er.coid');
+ }
+
+ $Cos = TableRegistry::getTableLocator()->get('Cos');
+
+ $co = $Cos->get($entity->co_id);
+
+ if(!$co) {
+ return __('registry.er.notfound', [__('registry.ct.cos', [1])]);
+ }
+
+ $prefix = "co_" . $co->id . ".";
+
+ // Return false if the prefix doesn't match the CO ID
+ if(strncmp($entity->username, $prefix, strlen($prefix))) {
+ return __('registry.er.api.username.prefix', [$prefix]);
+ }
+
+ // Or if there's nothing after the dot
+ if(strlen($entity->username) == strlen($prefix)) {
+ return __('registry.er.api.username.suffix');
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate an API Key.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param string $username API Username
+ * @param string $apiKey API Key to validate
+ * @param string $remoteIp IP Address of request
+ * @return boolean true if the API Key validates
+ * @throws InvalidArgumentException
+ */
+
+ public function validateKey(string $username, string $apiKey, string $remoteIp) {
+ // First pull the ApiUser record for $username. Note we don't know which
+ // CO we're querying for, so $username requires the CO name as a prefix
+ // (except for legacy usernames, which are assumed to be part of the
+ // COmanage CO).
+
+ // We could add where clauses to filter on status, etc, but by manually
+ // examining the record we can provide better error information.
+ $apiUser = $this->find()->where(['username' => $username])->first();
+
+ if(empty($apiUser)) {
+ throw new \InvalidArgumentException(__('registry.er.auth.api.unknown', [$username]));
+ }
+
+ // First validate the key. We use the FallbackPasswordHasher because API Users
+ // that were created in version prior to 5.0.0 use Cake 2's SHA-1 hashing.
+ // We can detect that here and rehash the password, but only when the apiuser
+ // authenticates.
+
+ $Hasher = new FallbackPasswordHasher([
+ 'hashers' => [
+ 'Default' => [],
+ 'Weak' => ['hashType' => 'sha1']
+ ]
+ ]);
+
+ if(!$Hasher->check($apiKey, $apiUser->api_key)) {
+ throw new \InvalidArgumentException('registry.er.auth.api.key', [$username]);
+ }
+
+ if($Hasher->needsRehash($apiUser->api_key)) {
+ // We'll rehash passwords even if subsequent eligibility checks fail
+ \Cake\Log\Log::write('debug', "Rehashing password for API User \"" . $username . "\"");
+
+ $apiUser->api_key = $apiKey;
+ // We disable rules checking to permit legacy usernames (those not prefixed
+ // with the CO name to remain)
+ $this->save($apiUser, ['checkRules' => false]);
+ }
+
+ // Is the ApiUser active?
+ if($apiUser->status != SuspendableStatusEnum::Active) {
+ throw new \InvalidArgumentException(__('registry.er.auth.api.status', [$username]));
+ }
+
+ // Are we within the validity window, if applicable?
+ $now = Chronos::now();
+
+ if($apiUser->valid_from
+ && $now->lt($apiUser->valid_from)) {
+ throw new \InvalidArgumentException(__('registry.er.auth.api.toosoon', [$username]));
+ }
+
+ if($apiUser->valid_through
+ && $now->gt($apiUser->valid_through)) {
+ throw new \InvalidArgumentException(__('registry.er.auth.api.expired', [$username]));
+ }
+
+ // Perform the IP Address check
+ if($apiUser->remote_ip
+ && !preg_match($apiUser->remote_ip, $remoteIp)) {
+ throw new \InvalidArgumentException(__('registry.er.auth.api.ip', [$remoteIp, $username]));
+ }
+
+ return true;
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $validator->add(
+ 'co_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('co_id');
+
+ $validator->add(
+ 'username',
+ 'length',
+ [ 'rule' => [ 'maxLength', 64 ] ]
+ );
+ $validator->add(
+ 'username',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->notEmpty('username');
+
+ $validator->add(
+ 'api_key',
+ 'length',
+ [ 'rule' => [ 'maxLength', 256 ] ]
+ );
+ $validator->allowEmpty('api_key');
+
+ $validator->add(
+ 'status',
+ 'content',
+ [ 'rule' => [ 'inList', [
+ SuspendableStatusEnum::Active,
+ SuspendableStatusEnum::Suspended
+ ] ] ]
+ );
+ $validator->notEmpty('status');
+
+ $validator->add(
+ 'privileged',
+ 'content',
+ [ 'rule' => [ 'boolean' ] ]
+ );
+ $validator->allowEmpty('privileged');
+
+ $validator->add(
+ 'valid_from',
+ 'content',
+ [ 'rule' => [ 'datetime' ] ]
+ );
+ $validator->allowEmpty('valid_from');
+
+ $validator->add(
+ 'valid_through',
+ 'content',
+ [ 'rule' => [ 'datetime' ] ]
+ );
+ $validator->allowEmpty('valid_through');
+
+ $validator->add(
+ 'remote_ip',
+ 'length',
+ [ 'rule' => [ 'maxLength', 80 ] ]
+ );
+ $validator->allowEmpty('remote_ip');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/CoPeopleTable.php b/app/src/Model/Table/CoPeopleTable.php
new file mode 100644
index 000000000..ca4eaa5a7
--- /dev/null
+++ b/app/src/Model/Table/CoPeopleTable.php
@@ -0,0 +1,136 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ // CO People are not configuration
+ $this->setIsConfigurationTable(false);
+
+ // Define associations
+ $this->belongsTo('Cos');
+
+ $this->hasOne('PrimaryName', [
+ 'className' => 'Names'
+ ])
+ ->setConditions(['PrimaryName.primary_name' => true]);
+ $this->hasMany('Names')
+ ->setDependent(true);
+
+// XXX can we change this to Name?
+ $this->setDisplayField('id');
+
+ $this->setPrimaryLink('co_id');
+ $this->setRequiresCO(true);
+ $this->setAllowLookupPrimaryLink(['canvas']);
+
+ $this->setIndexContains(['PrimaryName']);
+
+ $this->setAutoViewVars([
+ 'statuses' => [
+ 'type' => 'enum',
+ 'class' => 'StatusEnum'
+ ]
+ ]);
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $validator->add(
+ 'co_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('co_id');
+
+ $validator->add(
+ 'status',
+ 'content',
+// XXX if this works, backport to other tables
+ [ 'rule' => [ 'inList', StatusEnum::getConstValues() ]]
+/* [ 'rule' => [ 'inList', [
+ TemplateableStatusEnum::Active,
+ TemplateableStatusEnum::Suspended,
+ TemplateableStatusEnum::Template
+ ] ] ]*/
+ );
+ $validator->notEmpty('status');
+
+ $validator->add(
+ 'timezone',
+ 'content',
+ [ 'rule' => [ 'validateTimeZone' ],
+ 'provider' => 'table' ]
+ );
+ $validator->allowEmpty('timezone');
+
+ $validator->add(
+ 'date_of_birth',
+ 'content',
+ [ 'rule' => 'date' ]
+ );
+ $validator->allowEmpty('date_of_birth');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/CosTable.php b/app/src/Model/Table/CosTable.php
new file mode 100644
index 000000000..4ebfaafa5
--- /dev/null
+++ b/app/src/Model/Table/CosTable.php
@@ -0,0 +1,220 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ // COs are configuration
+ $this->setIsConfigurationTable(true);
+
+ // Define associations
+
+ $this->hasMany('ApiUsers')
+ ->setDependent(true);
+ $this->hasMany('CoPeople')
+ ->setDependent(true)
+ ->setCascadeCallbacks(true);
+ $this->hasMany('Cous')
+ ->setDependent(true);
+ $this->hasMany('Dashboards')
+ ->setDependent(true);
+
+ $this->setDisplayField('name');
+
+ $this->setAutoViewVars([
+ 'statuses' => [
+ 'type' => 'enum',
+ 'class' => 'TemplateableStatusEnum'
+ ]
+ ]);
+ }
+
+ /**
+ * Define business rules.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param RulesChecker $rules RulesChecker object
+ * @return RulesChecker
+ */
+
+ public function buildRules(RulesChecker $rules): RulesChecker {
+ // AR-CO-2 The COmanage CO cannot be renamed or deleted
+ // Note add() sets the rule for create+update, where addDelete() sets the rule additionally for delete
+ $rules->add([$this, 'ruleIsCOmanageCO'],
+ 'isCOmanageCO',
+ ['errorField' => 'name']);
+
+ $rules->addDelete([$this, 'ruleIsCOmanageCO'],
+ 'isCOmanageCO',
+ ['errorField' => 'name']);
+
+ // AR-CO-3 Two COs cannot share the same name
+// XXX CO-1736 In general, these checks should be case insensitive
+// (ie: I shouldn't be able to create a CO called "comanage", similarly COUs etc)
+// Also, with CO-1845 maybe unique ignores non-alphanumeric
+ $rules->add($rules->isUnique(['name'], __('registry.er.exists', [__('registry.ct.Cos', [1])])));
+
+ // AR-CO-5 A CO cannot be deleted if it is in Active status
+ // This basically requires two steps to delete a CO (set to Suspended),
+ // reducing the likelihood of accidentally deleting a CO.
+ $rules->addDelete([$this, 'ruleIsActive'],
+ 'isActive',
+ ['errorField' => 'status']);
+
+ return $rules;
+ }
+
+ /*
+ public function duplicate($id) {
+ // XXX document AR-CO-4, use TableMetaTrait to determine which tables are configuration
+ }*/
+
+ /**
+ * Determine if this is a Read Only record.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Entity $entity Cake Entity
+ * @return boolean true if the entity is read only, false otherwise
+ */
+
+ public function isReadOnly($entity) {
+ // The COmanage CO is read only
+
+ return ($entity->name == 'COmanage');
+ }
+
+ /**
+ * Application Rule to determine if the current entity is the COmanage CO.
+ *
+ * @since COmanage Registyr v5.0.0
+ * @param Entity $entity Entity to be validated
+ * @param array $options Application rule options
+ * @return boolean true if the Rule check passes, false otherwise
+ */
+
+ public function ruleIsCOmanageCO($entity, $options) {
+ // We want negative logic since we want to fail if we're editing the COmanage CO
+ if($entity->name == 'COmanage') {
+ return __('registry.er.edit.comanage');
+ }
+
+ return true;
+ }
+
+ /**
+ * Application Rule to determine if the current entity is not Active.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Entity $entity Entity to be validated
+ * @param array $options Application rule options
+ * @return boolean true if the Rule check passes, false otherwise
+ */
+
+ public function ruleIsActive($entity, $options) {
+ // We want negative logic since we want to fail if the record is Active
+ if($entity->status == TemplateableStatusEnum::Active) {
+ return __('registry.er.delete.active');
+ }
+
+ return true;
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $validator->add(
+ 'name',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->add(
+ 'name',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->notEmpty('name');
+
+ $validator->add(
+ 'description',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->add(
+ 'description',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->allowEmpty('description');
+
+ $validator->add(
+ 'status',
+ 'content',
+ [ 'rule' => [ 'inList', [
+ TemplateableStatusEnum::Active,
+ TemplateableStatusEnum::Suspended,
+ TemplateableStatusEnum::Template
+ ] ] ]
+ );
+ $validator->notEmpty('status');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/CousTable.php b/app/src/Model/Table/CousTable.php
new file mode 100644
index 000000000..a14514efa
--- /dev/null
+++ b/app/src/Model/Table/CousTable.php
@@ -0,0 +1,214 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+ $this->addBehavior('Tree');
+
+ // COUs are configuration
+ $this->setIsConfigurationTable(true);
+
+ // Define associations
+ $this->belongsTo('Cos');
+
+ $this->setDisplayField('name');
+
+ $this->setPrimaryLink('co_id');
+ $this->setRequiresCO(true);
+ }
+
+ /**
+ * Define business rules to supplement the default trait implementation.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param RulesChecker $rules RulesChecker object
+ * @return RulesChecker
+ */
+
+ public function buildTableRules(RulesChecker $rules): RulesChecker {
+ // AR-CO-3 Two COUs within the same CO cannot share the same name
+ $rules->add($rules->isUnique(['name', 'co_id'], __('registry.er.exists', [__('registry.ct.Cous', [1])])));
+
+ // This is not an Application Rule per se, but the parent_id must be a valid
+ // potential parent
+ $rules->add([$this, 'rulePotentialParent'],
+ 'potentialParent',
+ ['errorField' => 'parent_id']);
+
+ return $rules;
+ }
+
+ /**
+ * Assemble the set of potential parent COUs.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param int $coId CO ID
+ * @param int $id COU ID to determine potential parents of, or null for any (or a new) COU
+ * @param bool $hierarchy Render the hierarchy in the name
+ * @return Array Array of COU IDs and COU Names
+ */
+
+ public function potentialParents(int $coId, int $id=null, bool $hierarchy=false) {
+ // Note prior to v5 we filtered child COUs, meaning a COU couldn't be reassigned
+ // to be a child of a current child. It's not clear why we imposed that restriction.
+
+ $query = null;
+
+ if($hierarchy) {
+ $query = $this->find('treeList', ['spacer' => '-']);
+ } else {
+ $query = $this->find('list');
+ }
+
+ $query = $query->where(['co_id' => $coId])
+ // true overrides the default Cake order for treeList so we get
+ // our items sorted alphabetically instead of by tree ID
+ ->order(['name' => 'ASC'], true);
+
+ if($id) {
+ $query = $query->where(['id <>' => $id]);
+ }
+
+ return $query->toArray();
+ }
+
+ /**
+ * Application Rule to determine if the parent ID is a potential parent.
+ *
+ * @since COmanage Registyr v5.0.0
+ * @param Entity $entity Entity to be validated
+ * @param array $options Application rule options
+ * @return boolean true if the Rule check passes, false otherwise
+ */
+
+ public function rulePotentialParent($entity, $options) {
+ // We want negative logic since we want to fail if we're editing the COmanage CO
+ if(!empty($entity->parent_id)) {
+ $potentialParents = $this->potentialParents($entity->co_id, (!empty($entity->id) ? $entity->id : null));
+
+ if(!isset($potentialParents[$entity->parent_id])) {
+ return __('registry.er.cou.parent');
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $validator->add(
+ 'co_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('co_id');
+
+ $validator->add(
+ 'name',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->add(
+ 'name',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->notEmpty('name');
+
+ $validator->add(
+ 'description',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->add(
+ 'description',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->allowEmpty('description');
+
+ $validator->add(
+ 'parent_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->allowEmpty('parent_id');
+
+ $validator->add(
+ 'lft',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->allowEmpty('lft');
+
+ $validator->add(
+ 'rght',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->allowEmpty('rght');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/DashboardsTable.php b/app/src/Model/Table/DashboardsTable.php
new file mode 100644
index 000000000..bb122a092
--- /dev/null
+++ b/app/src/Model/Table/DashboardsTable.php
@@ -0,0 +1,65 @@
+addBehavior('Changelog');
+ $this->addBehavior('Timestamp');
+ $this->addBehavior('Timezone');
+
+ // Dashboards are configuration
+ $this->setIsConfigurationTable(true);
+
+ // Define associations
+ $this->belongsTo('Cos');
+
+ $this->setDisplayField('name');
+
+ $this->setPrimaryLink('co_id');
+ $this->setRequiresCO(true);
+ $this->setAllowUnkeyedPrimaryCO(['configuration', 'dashboard']);
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/NamesTable.php b/app/src/Model/Table/NamesTable.php
new file mode 100644
index 000000000..8e57c9979
--- /dev/null
+++ b/app/src/Model/Table/NamesTable.php
@@ -0,0 +1,192 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ // Names are not configuration
+ $this->setIsConfigurationTable(false);
+
+ // Define associations
+ $this->belongsTo('CoPeople');
+ $this->belongsTo('OrgIdentity');
+
+// XXX can we make this a function (generateCn)?
+ $this->setDisplayField('given');
+
+ $this->setPrimaryLink('co_person_id');
+ $this->setRequiresCO(true);
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.0.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ // One of CO Person ID or Org Identity ID is required
+// XXX Test this via the API?
+ $validator->add(
+ 'co_person_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('co_person_id', null, function($context) {
+ return empty($context['data']['org_identity_id']);
+ });
+
+ $validator->add(
+ 'org_identity_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('org_identity_id', null, function($context) {
+ return empty($context['data']['co_person_id']);
+ });
+
+ $validator->add(
+ 'honorific',
+ 'length',
+ [ 'rule' => [ 'maxLength', 32 ] ]
+ );
+ $validator->add(
+ 'honorific',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->allowEmpty('honorific');
+
+ $validator->add(
+ 'given',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->add(
+ 'given',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->notEmpty('given');
+
+ $validator->add(
+ 'middle',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->add(
+ 'middle',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->allowEmpty('middle');
+
+ $validator->add(
+ 'family',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->add(
+ 'family',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->allowEmpty('family');
+
+ $validator->add(
+ 'suffix',
+ 'length',
+ [ 'rule' => [ 'maxLength', 32 ] ]
+ );
+ $validator->add(
+ 'suffix',
+ 'content',
+ [ 'rule' => [ 'validateInput' ],
+ 'provider' => 'table' ]
+ );
+ $validator->allowEmpty('suffix');
+
+// XXX need to do something to validate type (test via API)
+
+ $validator->add(
+ 'language',
+ 'content',
+ [ 'rule' => [ 'validateLanguage' ],
+ 'provider' => 'table' ]
+ );
+ $validator->allowEmpty('language');
+
+ $validator->add(
+ 'primary_name',
+ 'content',
+ [ 'rule' => [ 'boolean' ] ]
+ );
+ $validator->allowEmpty('primary_name');
+
+ $validator->add(
+ 'source_name_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->allowEmpty('source_name_id');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/View/AjaxView.php b/app/src/View/AjaxView.php
new file mode 100644
index 000000000..dd9d7d984
--- /dev/null
+++ b/app/src/View/AjaxView.php
@@ -0,0 +1,46 @@
+response = $this->response->withType('ajax');
+ }
+}
diff --git a/app/src/View/AppView.php b/app/src/View/AppView.php
new file mode 100644
index 000000000..7ccc7c2e9
--- /dev/null
+++ b/app/src/View/AppView.php
@@ -0,0 +1,41 @@
+loadHelper('Html');`
+ *
+ * @return void
+ */
+ public function initialize(): void
+ {
+ }
+}
diff --git a/app/src/View/Cell/.gitkeep b/app/src/View/Cell/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php
new file mode 100644
index 000000000..7224baa1c
--- /dev/null
+++ b/app/src/View/Helper/FieldHelper.php
@@ -0,0 +1,323 @@
+
+ info
+ ' . $info . '
+';
+ }
+
+ /**
+ * Emit a form control.
+ *
+ * @since COmanage Registry v6.0.0
+ * @param string $fieldName Form field
+ * @param array $options FormHelper control options
+ * @param string $labelText Label text (fieldName language key used by default)
+ * @return string HTML for control
+ */
+
+ public function control(string $fieldName,
+ array $options=[],
+ string $labelText=null) {
+ $coptions = $options;
+ $coptions['label'] = false;
+ $coptions['readonly'] = !$this->editable;
+ // Selects, Checkboxes, and Radio Buttons use "disabled"
+ $coptions['disabled'] = $coptions['readonly'];
+
+ // Generate HTML for the control itself
+ $liClass = "";
+
+ // Handle datetime controls specially
+ if($fieldName == 'valid_from' || $fieldName == 'valid_through') {
+ // Append the timezone to the label
+ $label = __("registry.fd.".$fieldName.".tz", [$this->_View->get('vv_tz')]);
+
+ // Render these fields as datepickers instead of plain text boxes
+ $coptions['class'] = 'datepicker-' . ($fieldName == 'valid_from' ? "f" : "u");
+
+ $entity = $this->_View->get('vv_obj');
+
+ if(!empty($entity->$fieldName)) {
+ // Adjust the time back to the user's timezone
+ $coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->_View->get('vv_tz'));
+ }
+
+ $controlCode = $this->Form->text($fieldName, $coptions);
+ $liClass = " modelbox-data";
+ } else {
+ $controlCode = $this->Form->control($fieldName, $coptions);
+ }
+
+ return $this->startLine($liClass)
+ . $this->formNameDiv($fieldName, $labelText)
+ . $this->formInfoDiv($controlCode)
+ . $this->endLine();
+ }
+
+ /**
+ * End a set of form controls.
+ *
+ * @since COmanage Registry v6.0.0
+ * @return string Control Set end HTML
+ */
+
+ public function endControlSet() {
+ $this->modelName = null;
+
+ return "\n";
+ }
+
+ /**
+ * End a form line.
+ *
+ * @since COmanage Registry v6.0.0
+ * @return string Line end HTML
+ */
+
+ protected function endLine() {
+ return "\n";
+ }
+
+ /**
+ * Generate a form info (control, value) box.
+ *
+ * @since COmanage Registry v6.0.0
+ * @param string $content Content HTML
+ * @return string Form Info HTML
+ */
+
+ protected function formInfoDiv(string $content) {
+ return '
+ ' . $content . '
+
';
+ }
+
+ /**
+ * Generate a form name (label, description) box.
+ *
+ * @since COmanage Registry v6.0.0
+ * @param string $fieldName Form field
+ * @param string $labelText Label text (fieldName language key used by default)
+ * @return string Form Name HTML
+ */
+
+ protected function formNameDiv(string $fieldName, string $labelText=null) {
+ $label = $labelText;
+ $desc = null;
+
+ // We'll accept a fieldName of the form other_models.0.foo for forms that
+ // request associated data. Note, however, that model names for language
+ // keys are OtherModel, so we'll need to inflect.
+
+ $mn = $this->modelName;
+ $fn = $fieldName;
+
+ if(strpos($fieldName, '.') !== false) {
+ // othermodels.0.field
+
+ $bits = explode('.', $fieldName, 3);
+ $mn = Inflector::classify($bits[0]);
+ $fn = $bits[2];
+ }
+
+ // First try to autogenerate the field label (if we weren't given one).
+
+ if(!$label) {
+ // We autogenerate field labels and descriptions from the field name.
+ // Fields of the form foo_id map to the singular form of registry.ct.foos.
+ // All others map first to registry.fd.Model.foo, then to registry.fd.foo
+ // if no Model specific key is found.
+
+ $label = __("registry.fd.".$mn.".".$fn);
+
+ if($label == "registry.fd.".$mn.".".$fn) {
+ // Model specific label not found, try again
+
+ $f = null;
+
+ if(preg_match('/^(.*?)_id$/', $fn, $f)) {
+ // Map foriegn keys (foo_id) to the controller label
+ $label = __("registry.ct.".Inflector::pluralize($f[1]), [1]);
+ } else {
+ // Just look up the key
+ $label = __("registry.fd.".$fn);
+ }
+ }
+ }
+
+ // We try to automagically determine if a description for the field exists by
+ // looking for the corresponding .desc language translation.
+
+ $desc = __("registry.fd.".$mn.".".$fn.".desc");
+
+ if($desc == "registry.fd.".$mn.".".$fn.".desc") {
+ $desc = __("registry.fd.".$fn.".desc");
+ }
+
+ // If the description is the literal key we just generated, there is no description
+ if($desc == "registry.fd.".$fn.".desc") {
+ $desc = null;
+ }
+
+ return '
" . $this->Form->postLink(
+ __('registry.op.delete'),
+ ['action' => 'delete', $vv_obj->id],
+// XXX should be configurable which field we put in, maybe displayField?
+ ['confirm' => __('registry.op.delete.confirm', [$vv_obj->id]),
+ 'class' => 'deletebutton']
+ ) . "
";
+ print "
\n";
+}
+
+// By default, the form will POST to the current controller
+// Note we need to open the form for view so Cake will autopopulate values
+print $this->Form->create($vv_obj);
+
+$linkId = null;
+
+if(!empty($vv_primary_link)) {
+ if(!empty($this->request->getQuery($vv_primary_link))) {
+ $linkId = $this->request->getQuery($vv_primary_link);
+ } elseif(!empty($this->request->getData($vv_primary_link))) {
+ $linkId = $this->request->getData($vv_primary_link);
+ } elseif(!empty($vv_obj->$vv_primary_link)) {
+ $linkId = $vv_obj->$vv_primary_link;
+ }
+}
+
+print $this->Field->startControlSet($this->name,
+ $vv_action,
+ ($vv_action == 'add' || $vv_action == 'edit'),
+ $vv_required_fields);
+
+// We allow the fields.inc file to be specified for Controllers that have more
+// complicated/non-default actions.
+$fieldsFile = "fields.inc";
+
+if(!empty($vv_fields_inc)) {
+ $fieldsFile = $vv_fields_inc;
+}
+
+include(ROOT . DS . "templates" . DS . $modelsName . DS . $fieldsFile);
+
+if($vv_action == 'add' || $vv_action == 'edit') {
+ // We don't want/need to output these for view actions
+
+ if(!empty($linkId)) {
+ // Hidden values used to link to parent objects (eg: matchgrid_id)
+ print $this->Form->hidden($vv_primary_link, ['value' => $linkId]);
+ }
+
+ print $this->Field->submit(__('registry.op.save'));
+}
+
+print $this->Form->end();
+
+print $this->Field->endControlSet();
+
+// XXX insert changelog metadata (+nav? or maybe we should have a dedicate index view that shows all records in revision order?)
diff --git a/app/templates/Standard/api/v2/json/add-edit.php b/app/templates/Standard/api/v2/json/add-edit.php
new file mode 100644
index 000000000..5e0586e3c
--- /dev/null
+++ b/app/templates/Standard/api/v2/json/add-edit.php
@@ -0,0 +1,30 @@
+ $vv_results]);
+}
\ No newline at end of file
diff --git a/app/templates/Standard/api/v2/json/delete.php b/app/templates/Standard/api/v2/json/delete.php
new file mode 100644
index 000000000..b2bed4bd6
--- /dev/null
+++ b/app/templates/Standard/api/v2/json/delete.php
@@ -0,0 +1,26 @@
+template;
+
+$responseMeta = [
+ 'resource' => $vv_model_name,
+ 'version' => '2'
+];
+
+if($action == 'index') {
+ $responseMeta['totalResults'] = $this->Paginator->counter('{{count}}');
+ $responseMeta['startIndex'] = $this->Paginator->counter('{{start}}');
+ $responseMeta['itemsPerPage'] = $this->Paginator->counter('{{current}}'); // confusingly this is different than ->current()
+ $responseMeta['currentPage'] = $this->Paginator->current();
+ $responseMeta['pageCount'] = $this->Paginator->total();
+}
+
+$metaAttrs = ['created', 'modified', 'revision', 'deleted', 'actor_identifier'];
+
+// Inflect the table name to get the changelog parent record key
+$pkey = \Cake\Utility\Inflector::singularize($vv_table_name) . "_id";
+$metaAttrs[] = $pkey;
+
+$results = [];
+
+foreach($$vv_table_name as $r) {
+ $rec = $r;
+ $meta = [];
+
+ foreach($metaAttrs as $a) {
+ $meta[$a] = $rec[$a];
+ unset($rec[$a]);
+ }
+
+ $rec['meta'] = $meta;
+ $results[] = $rec;
+}
+
+print json_encode(["responseMeta" => $responseMeta, $vv_model_name => $results]);
\ No newline at end of file
diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php
new file mode 100644
index 000000000..e2bebfcc0
--- /dev/null
+++ b/app/templates/Standard/index.php
@@ -0,0 +1,271 @@
+name = Models
+$modelsName = $this->name;
+// $tablename = models
+// XXX backport to match?
+$tableName = \Cake\Utility\Inflector::tableize(\Cake\Utility\Inflector::singularize($this->name));
+
+// Our default link actions, in order of preference, unless the column config overrides it
+$linkActions = ['edit', 'view'];
+
+// Read the index configuration ($indexColumns) for this model
+include(ROOT . DS . "templates" . DS . $modelsName . DS . "columns.inc");
+
+// $linkFilter is used for models that belong to a specific parent model (eg: co_id)
+$linkFilter = [];
+
+if(!empty($vv_primary_link) && !empty($this->request->getQuery($vv_primary_link))) {
+ $linkFilter = [$vv_primary_link => $this->request->getQuery($vv_primary_link)];
+}
+
+function _column_key($modelsName, $c, $tz=null) {
+ if(strpos($c, "_id", strlen($c)-3)) {
+ // Key is of the form field_id, use .ct label instead
+ $k = \Cake\Utility\Inflector::classify(\Cake\Utility\Inflector::pluralize(substr($c, 0, strlen($c)-3)));
+
+ return __('registry.ct.'.$k, [1]);
+ }
+
+ // Look for a model specific key first
+ $label = __('registry.fd.'.$modelsName.'.'.$c);
+
+ if($label != 'registry.fd.'.$modelsName.'.'.$c) {
+ return $label;
+ }
+
+ if($tz) {
+ // If there is a timezone aware label, use that
+ $label = __('registry.fd.'.$c.'.tz', [$tz]);
+
+ if($label != 'registry.fd.'.$c.'.tz') {
+ return $label;
+ }
+ }
+
+ // Otherwise look for the general key
+ return __('registry.fd.'.$c);
+}
+?>
+