From a584671c2d6237a71ae146309ce08776b7419269 Mon Sep 17 00:00:00 2001
From: Benn Oshrin
Date: Fri, 21 Sep 2018 15:09:22 -0400
Subject: [PATCH] Initial commit of COmanage Match (not yet ready for
production use)
---
LICENSE | 202 +
NOTICE | 86 +
README.md | 12 +
app/bin/cake | 46 +
app/bin/cake.bat | 27 +
app/bin/cake.php | 12 +
app/composer.json | 55 +
app/composer.lock | 2737 ++
app/config/VERSION | 1 +
app/config/app.php | 391 +
app/config/bootstrap.php | 234 +
app/config/bootstrap_cli.php | 38 +
app/config/database.php.dist | 76 +
app/config/paths.php | 90 +
app/config/requirements.php | 39 +
app/config/routes.php | 145 +
app/index.php | 16 +
app/logs | 1 +
app/plugins/empty | 0
app/src/Application.php | 57 +
app/src/Auth/EnvAuthenticate.php | 121 +
app/src/Command/DatabaseCommand.php | 114 +
app/src/Command/SetupCommand.php | 141 +
app/src/Console/Installer.php | 246 +
app/src/Controller/AppController.php | 193 +
.../Controller/AttributeGroupsController.php | 58 +
app/src/Controller/AttributesController.php | 60 +
.../Component/AuthorizationComponent.php | 199 +
app/src/Controller/ErrorController.php | 68 +
app/src/Controller/MatchgridsController.php | 254 +
app/src/Controller/PagesController.php | 84 +
app/src/Controller/PermissionsController.php | 56 +
app/src/Controller/RulesController.php | 60 +
app/src/Controller/StandardController.php | 366 +
.../Controller/SystemsOfRecordController.php | 60 +
app/src/Controller/TierApiController.php | 599 +
app/src/Controller/UsersController.php.not | 70 +
app/src/Lib/Enum/ConfidenceModeEnum.php | 35 +
app/src/Lib/Enum/PermissionEnum.php | 38 +
app/src/Lib/Enum/ReferenceIdEnum.php | 35 +
app/src/Lib/Enum/ResolutionModeEnum.php | 35 +
app/src/Lib/Enum/SearchTypeEnum.php | 37 +
app/src/Lib/Enum/StandardEnum.php | 58 +
app/src/Lib/Enum/StatusEnum.php | 35 +
app/src/Lib/Identifier/ReferenceIdService.php | 34 +
app/src/Lib/Identifier/Sequence.php | 39 +
app/src/Lib/Identifier/Uuid.php | 45 +
app/src/Lib/Match/AttributeManager.php | 183 +
app/src/Lib/Match/MatchService.php | 725 +
app/src/Lib/Match/MatchgridBuilder.php | 264 +
app/src/Lib/Match/PostgresService.php | 87 +
app/src/Lib/Match/ResultManager.php | 276 +
app/src/Lib/Traits/AssociationTrait.php | 107 +
app/src/Lib/Traits/AutoViewVarsTrait.php | 57 +
app/src/Lib/Traits/MatchgridLinkTrait.php | 89 +
app/src/Lib/Traits/PrimaryLinkTrait.php | 70 +
app/src/Locale/en_US/default.po | 353 +
app/src/Model/Behavior/empty | 0
app/src/Model/Entity/Attribute.php | 40 +
app/src/Model/Entity/AttributeGroup.php | 40 +
app/src/Model/Entity/Matchgrid.php | 40 +
app/src/Model/Entity/Permission.php | 40 +
app/src/Model/Entity/Rule.php | 40 +
app/src/Model/Entity/RuleAttribute.php | 40 +
app/src/Model/Entity/SystemOfRecord.php | 40 +
app/src/Model/Table/AttributeGroupsTable.php | 83 +
app/src/Model/Table/AttributesTable.php | 178 +
app/src/Model/Table/MatchgridsTable.php | 208 +
app/src/Model/Table/MetaTable.php | 78 +
app/src/Model/Table/PermissionsTable.php | 121 +
app/src/Model/Table/RuleAttributesTable.php | 90 +
app/src/Model/Table/RulesTable.php | 132 +
app/src/Model/Table/SystemsOfRecordTable.php | 103 +
app/src/Shell/ConsoleShell.php | 81 +
app/src/Template/AttributeGroups/columns.inc | 32 +
app/src/Template/AttributeGroups/fields.inc | 31 +
app/src/Template/Attributes/columns.inc | 35 +
app/src/Template/Attributes/fields.inc | 48 +
app/src/Template/Element/Flash/default.ctp | 10 +
app/src/Template/Element/Flash/error.ctp | 6 +
app/src/Template/Element/Flash/success.ctp | 6 +
app/src/Template/Element/breadcrumbs.ctp | 89 +
app/src/Template/Element/footer.ctp | 34 +
app/src/Template/Element/javascript.ctp | 191 +
app/src/Template/Element/menuMain.ctp | 110 +
app/src/Template/Element/menuUser.ctp | 67 +
app/src/Template/Email/html/default.ctp | 20 +
app/src/Template/Email/text/default.ctp | 16 +
app/src/Template/Error/error400.ctp | 38 +
app/src/Template/Error/error500.ctp | 43 +
.../Template/Layout/Email/html/default.ctp | 24 +
.../Template/Layout/Email/text/default.ctp | 16 +
app/src/Template/Layout/ajax.ctp | 16 +
app/src/Template/Layout/default.ctp | 167 +
app/src/Template/Layout/error.ctp | 47 +
app/src/Template/Layout/rest.ctp | 4 +
app/src/Template/Layout/rss/default.ctp | 11 +
app/src/Template/Matchgrids/columns.inc | 50 +
app/src/Template/Matchgrids/fields.inc | 66 +
app/src/Template/Matchgrids/manage.ctp | 34 +
app/src/Template/Matchgrids/pending.ctp | 54 +
app/src/Template/Matchgrids/reconcile.ctp | 68 +
app/src/Template/Pages/home.ctp | 83 +
app/src/Template/Permissions/columns.inc | 37 +
app/src/Template/Permissions/fields.inc | 60 +
app/src/Template/Rules/columns.inc | 39 +
app/src/Template/Rules/fields.inc | 95 +
app/src/Template/Standard/add-edit-view.ctp | 69 +
app/src/Template/Standard/index.ctp | 155 +
app/src/Template/SystemsOfRecord/columns.inc | 36 +
app/src/Template/SystemsOfRecord/fields.inc | 32 +
app/src/Template/TierApi/response.ctp | 32 +
app/src/View/AjaxView.php | 49 +
app/src/View/AppView.php | 45 +
app/src/View/Helper/FieldHelper.php | 148 +
app/tests/Fixture/empty | 0
app/tests/TestCase/ApplicationTest.php | 46 +
app/tests/TestCase/Controller/Component/empty | 0
.../Controller/PagesControllerTest.php | 97 +
app/tests/TestCase/Model/Behavior/empty | 0
app/tests/TestCase/View/Helper/empty | 0
app/tests/bootstrap.php | 12 +
app/tmp | 1 +
app/webroot/.htaccess | 5 +
app/webroot/auth/login/login.php | 47 +
app/webroot/auth/logout/logout.php | 39 +
app/webroot/css/base.css | 455 +
app/webroot/css/cake.css | 525 +
app/webroot/css/co-base.css | 1878 ++
app/webroot/css/co-responsive.css | 497 +
.../Font-Awesome-4.6.3/css/font-awesome.css | 2203 ++
.../css/font-awesome.css.map | 7 +
.../css/font-awesome.min.css | 4 +
.../Font-Awesome-4.6.3/fonts/FontAwesome.otf | Bin 0 -> 124988 bytes
.../fonts/fontawesome-webfont.eot | Bin 0 -> 76518 bytes
.../fonts/fontawesome-webfont.svg | 685 +
.../fonts/fontawesome-webfont.ttf | Bin 0 -> 152796 bytes
.../fonts/fontawesome-webfont.woff | Bin 0 -> 90412 bytes
.../fonts/fontawesome-webfont.woff2 | Bin 0 -> 71896 bytes
.../material-icons/MaterialIcons-Regular.eot | Bin 0 -> 143258 bytes
.../MaterialIcons-Regular.ijmap | 1 +
.../material-icons/MaterialIcons-Regular.svg | 2373 ++
.../material-icons/MaterialIcons-Regular.ttf | Bin 0 -> 128180 bytes
.../material-icons/MaterialIcons-Regular.woff | Bin 0 -> 57620 bytes
.../MaterialIcons-Regular.woff2 | Bin 0 -> 44300 bytes
.../fonts/material-icons/material-icons.css | 36 +
.../notosans_bold/NotoSans-Bold-webfont.eot | Bin 0 -> 160752 bytes
.../notosans_bold/NotoSans-Bold-webfont.svg | 21104 ++++++++++++++++
.../notosans_bold/NotoSans-Bold-webfont.ttf | Bin 0 -> 479772 bytes
.../notosans_bold/NotoSans-Bold-webfont.woff | Bin 0 -> 210896 bytes
.../css/fonts/notosans_bold/stylesheet.css | 12 +
.../NotoSans-Regular-webfont.eot | Bin 0 -> 160807 bytes
.../NotoSans-Regular-webfont.svg | 21104 ++++++++++++++++
.../NotoSans-Regular-webfont.ttf | Bin 0 -> 476480 bytes
.../NotoSans-Regular-webfont.woff | Bin 0 -> 210300 bytes
.../css/fonts/notosans_regular/stylesheet.css | 11 +
app/webroot/css/home.css | 240 +
app/webroot/css/jquery/jquery-3.2.1.min.js | 4 +
.../jquery-ui-1.12.1.custom/AUTHORS.txt | 333 +
.../jquery-ui-1.12.1.custom/LICENSE.txt | 43 +
.../external/jquery/jquery.js | 11008 ++++++++
.../images/ui-bg_glass_75_d0e5f5_1x400.png | Bin 0 -> 336 bytes
.../images/ui-bg_glass_85_dfeffc_1x400.png | Bin 0 -> 341 bytes
.../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 332 bytes
.../ui-bg_gloss-wave_30_8dbbdd_500x100.png | Bin 0 -> 5782 bytes
.../ui-bg_inset-hard_100_f5f8f9_1x100.png | Bin 0 -> 333 bytes
.../ui-bg_inset-hard_100_fcfdfd_1x100.png | Bin 0 -> 292 bytes
.../images/ui-icons_000_256x240.png | Bin 0 -> 5331 bytes
.../images/ui-icons_217bc0_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_469bdd_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_6da8d5_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_d8e7f3_256x240.png | Bin 0 -> 4549 bytes
.../jquery/jquery-ui-1.12.1.custom/index.html | 559 +
.../jquery-ui-1.12.1.custom/jquery-ui.css | 1312 +
.../jquery-ui-1.12.1.custom/jquery-ui.js | 18706 ++++++++++++++
.../jquery-ui-1.12.1.custom/jquery-ui.min.css | 7 +
.../jquery-ui-1.12.1.custom/jquery-ui.min.js | 13 +
.../jquery-ui.structure.css | 886 +
.../jquery-ui.structure.min.css | 5 +
.../jquery-ui.theme.css | 443 +
.../jquery-ui.theme.min.css | 5 +
.../jquery-ui-1.12.1.custom/package.json | 74 +
.../css/jquery/metisMenu/metisMenu.css | 99 +
app/webroot/css/jquery/metisMenu/metisMenu.js | 375 +
.../css/jquery/metisMenu/metisMenu.js.map | 1 +
.../css/jquery/metisMenu/metisMenu.min.css | 10 +
.../css/jquery/metisMenu/metisMenu.min.js | 11 +
app/webroot/css/mdl/mdl-1.3.0/LICENSE | 212 +
app/webroot/css/mdl/mdl-1.3.0/bower.json | 29 +
app/webroot/css/mdl/mdl-1.3.0/material.css | 11476 +++++++++
app/webroot/css/mdl/mdl-1.3.0/material.js | 3996 +++
.../css/mdl/mdl-1.3.0/material.min.css | 9 +
.../css/mdl/mdl-1.3.0/material.min.css.map | 1 +
app/webroot/css/mdl/mdl-1.3.0/material.min.js | 10 +
.../css/mdl/mdl-1.3.0/material.min.js.map | 1 +
app/webroot/css/mdl/mdl-1.3.0/package.json | 77 +
.../mdl-selectfield.min.css | 3 +
.../mdl-selectfield.min.js | 2 +
app/webroot/favicon.ico | Bin 0 -> 1150 bytes
app/webroot/font/cakedingbats-webfont.eot | Bin 0 -> 75538 bytes
app/webroot/font/cakedingbats-webfont.svg | 78 +
app/webroot/font/cakedingbats-webfont.ttf | Bin 0 -> 75412 bytes
app/webroot/font/cakedingbats-webfont.woff | Bin 0 -> 43484 bytes
app/webroot/font/cakedingbats-webfont.woff2 | Bin 0 -> 35456 bytes
app/webroot/img/COmanage-Logo-LG-onBlue.png | Bin 0 -> 26246 bytes
app/webroot/img/COmanage-Logo-LG-onWhite.png | Bin 0 -> 26262 bytes
app/webroot/img/cake-logo.png | Bin 0 -> 2683 bytes
app/webroot/img/cake.icon.png | Bin 0 -> 943 bytes
app/webroot/img/cake.logo.svg | 41 +
app/webroot/img/cake.power.gif | Bin 0 -> 201 bytes
app/webroot/index.php | 40 +
app/webroot/js/comanage.js | 224 +
app/webroot/js/jquery/jquery-3.2.1.min.js | 4 +
.../jquery-ui-1.12.1.custom/AUTHORS.txt | 333 +
.../jquery-ui-1.12.1.custom/LICENSE.txt | 43 +
.../external/jquery/jquery.js | 11008 ++++++++
.../images/ui-bg_glass_75_d0e5f5_1x400.png | Bin 0 -> 336 bytes
.../images/ui-bg_glass_85_dfeffc_1x400.png | Bin 0 -> 341 bytes
.../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 332 bytes
.../ui-bg_gloss-wave_30_8dbbdd_500x100.png | Bin 0 -> 5782 bytes
.../ui-bg_inset-hard_100_f5f8f9_1x100.png | Bin 0 -> 333 bytes
.../ui-bg_inset-hard_100_fcfdfd_1x100.png | Bin 0 -> 292 bytes
.../images/ui-icons_000_256x240.png | Bin 0 -> 5331 bytes
.../images/ui-icons_217bc0_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_469bdd_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_6da8d5_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4549 bytes
.../images/ui-icons_d8e7f3_256x240.png | Bin 0 -> 4549 bytes
.../jquery/jquery-ui-1.12.1.custom/index.html | 559 +
.../jquery-ui-1.12.1.custom/jquery-ui.css | 1312 +
.../jquery-ui-1.12.1.custom/jquery-ui.js | 18706 ++++++++++++++
.../jquery-ui-1.12.1.custom/jquery-ui.min.css | 7 +
.../jquery-ui-1.12.1.custom/jquery-ui.min.js | 13 +
.../jquery-ui.structure.css | 886 +
.../jquery-ui.structure.min.css | 5 +
.../jquery-ui.theme.css | 443 +
.../jquery-ui.theme.min.css | 5 +
.../jquery-ui-1.12.1.custom/package.json | 74 +
app/webroot/js/jquery/metisMenu/metisMenu.css | 99 +
app/webroot/js/jquery/metisMenu/metisMenu.js | 375 +
.../js/jquery/metisMenu/metisMenu.js.map | 1 +
.../js/jquery/metisMenu/metisMenu.min.css | 10 +
.../js/jquery/metisMenu/metisMenu.min.js | 11 +
app/webroot/js/jquery/spin.license.txt | 22 +
app/webroot/js/jquery/spin.min.js | 2 +
.../js/js-cookie/js.cookie-2.1.3.min.js | 2 +
app/webroot/js/mdl/mdl-1.3.0/LICENSE | 212 +
app/webroot/js/mdl/mdl-1.3.0/bower.json | 29 +
app/webroot/js/mdl/mdl-1.3.0/material.css | 11476 +++++++++
app/webroot/js/mdl/mdl-1.3.0/material.js | 3996 +++
app/webroot/js/mdl/mdl-1.3.0/material.min.css | 9 +
.../js/mdl/mdl-1.3.0/material.min.css.map | 1 +
app/webroot/js/mdl/mdl-1.3.0/material.min.js | 10 +
.../js/mdl/mdl-1.3.0/material.min.js.map | 1 +
app/webroot/js/mdl/mdl-1.3.0/package.json | 77 +
.../mdl-selectfield.min.css | 3 +
.../mdl-selectfield.min.js | 2 +
local/Config/.gitignore | 1 +
261 files changed, 164395 insertions(+)
create mode 100644 LICENSE
create mode 100644 NOTICE
create mode 100644 README.md
create mode 100755 app/bin/cake
create mode 100644 app/bin/cake.bat
create mode 100644 app/bin/cake.php
create mode 100644 app/composer.json
create mode 100644 app/composer.lock
create mode 100644 app/config/VERSION
create mode 100644 app/config/app.php
create mode 100644 app/config/bootstrap.php
create mode 100644 app/config/bootstrap_cli.php
create mode 100644 app/config/database.php.dist
create mode 100644 app/config/paths.php
create mode 100644 app/config/requirements.php
create mode 100644 app/config/routes.php
create mode 100644 app/index.php
create mode 120000 app/logs
create mode 100644 app/plugins/empty
create mode 100644 app/src/Application.php
create mode 100644 app/src/Auth/EnvAuthenticate.php
create mode 100644 app/src/Command/DatabaseCommand.php
create mode 100644 app/src/Command/SetupCommand.php
create mode 100644 app/src/Console/Installer.php
create mode 100644 app/src/Controller/AppController.php
create mode 100644 app/src/Controller/AttributeGroupsController.php
create mode 100644 app/src/Controller/AttributesController.php
create mode 100644 app/src/Controller/Component/AuthorizationComponent.php
create mode 100644 app/src/Controller/ErrorController.php
create mode 100644 app/src/Controller/MatchgridsController.php
create mode 100644 app/src/Controller/PagesController.php
create mode 100644 app/src/Controller/PermissionsController.php
create mode 100644 app/src/Controller/RulesController.php
create mode 100644 app/src/Controller/StandardController.php
create mode 100644 app/src/Controller/SystemsOfRecordController.php
create mode 100644 app/src/Controller/TierApiController.php
create mode 100644 app/src/Controller/UsersController.php.not
create mode 100644 app/src/Lib/Enum/ConfidenceModeEnum.php
create mode 100644 app/src/Lib/Enum/PermissionEnum.php
create mode 100644 app/src/Lib/Enum/ReferenceIdEnum.php
create mode 100644 app/src/Lib/Enum/ResolutionModeEnum.php
create mode 100644 app/src/Lib/Enum/SearchTypeEnum.php
create mode 100644 app/src/Lib/Enum/StandardEnum.php
create mode 100644 app/src/Lib/Enum/StatusEnum.php
create mode 100644 app/src/Lib/Identifier/ReferenceIdService.php
create mode 100644 app/src/Lib/Identifier/Sequence.php
create mode 100644 app/src/Lib/Identifier/Uuid.php
create mode 100644 app/src/Lib/Match/AttributeManager.php
create mode 100644 app/src/Lib/Match/MatchService.php
create mode 100644 app/src/Lib/Match/MatchgridBuilder.php
create mode 100644 app/src/Lib/Match/PostgresService.php
create mode 100644 app/src/Lib/Match/ResultManager.php
create mode 100644 app/src/Lib/Traits/AssociationTrait.php
create mode 100644 app/src/Lib/Traits/AutoViewVarsTrait.php
create mode 100644 app/src/Lib/Traits/MatchgridLinkTrait.php
create mode 100644 app/src/Lib/Traits/PrimaryLinkTrait.php
create mode 100644 app/src/Locale/en_US/default.po
create mode 100644 app/src/Model/Behavior/empty
create mode 100644 app/src/Model/Entity/Attribute.php
create mode 100644 app/src/Model/Entity/AttributeGroup.php
create mode 100644 app/src/Model/Entity/Matchgrid.php
create mode 100644 app/src/Model/Entity/Permission.php
create mode 100644 app/src/Model/Entity/Rule.php
create mode 100644 app/src/Model/Entity/RuleAttribute.php
create mode 100644 app/src/Model/Entity/SystemOfRecord.php
create mode 100644 app/src/Model/Table/AttributeGroupsTable.php
create mode 100644 app/src/Model/Table/AttributesTable.php
create mode 100644 app/src/Model/Table/MatchgridsTable.php
create mode 100644 app/src/Model/Table/MetaTable.php
create mode 100644 app/src/Model/Table/PermissionsTable.php
create mode 100644 app/src/Model/Table/RuleAttributesTable.php
create mode 100644 app/src/Model/Table/RulesTable.php
create mode 100644 app/src/Model/Table/SystemsOfRecordTable.php
create mode 100644 app/src/Shell/ConsoleShell.php
create mode 100644 app/src/Template/AttributeGroups/columns.inc
create mode 100644 app/src/Template/AttributeGroups/fields.inc
create mode 100644 app/src/Template/Attributes/columns.inc
create mode 100644 app/src/Template/Attributes/fields.inc
create mode 100644 app/src/Template/Element/Flash/default.ctp
create mode 100644 app/src/Template/Element/Flash/error.ctp
create mode 100644 app/src/Template/Element/Flash/success.ctp
create mode 100644 app/src/Template/Element/breadcrumbs.ctp
create mode 100644 app/src/Template/Element/footer.ctp
create mode 100644 app/src/Template/Element/javascript.ctp
create mode 100644 app/src/Template/Element/menuMain.ctp
create mode 100644 app/src/Template/Element/menuUser.ctp
create mode 100644 app/src/Template/Email/html/default.ctp
create mode 100644 app/src/Template/Email/text/default.ctp
create mode 100644 app/src/Template/Error/error400.ctp
create mode 100644 app/src/Template/Error/error500.ctp
create mode 100644 app/src/Template/Layout/Email/html/default.ctp
create mode 100644 app/src/Template/Layout/Email/text/default.ctp
create mode 100644 app/src/Template/Layout/ajax.ctp
create mode 100644 app/src/Template/Layout/default.ctp
create mode 100644 app/src/Template/Layout/error.ctp
create mode 100644 app/src/Template/Layout/rest.ctp
create mode 100644 app/src/Template/Layout/rss/default.ctp
create mode 100644 app/src/Template/Matchgrids/columns.inc
create mode 100644 app/src/Template/Matchgrids/fields.inc
create mode 100644 app/src/Template/Matchgrids/manage.ctp
create mode 100644 app/src/Template/Matchgrids/pending.ctp
create mode 100644 app/src/Template/Matchgrids/reconcile.ctp
create mode 100644 app/src/Template/Pages/home.ctp
create mode 100644 app/src/Template/Permissions/columns.inc
create mode 100644 app/src/Template/Permissions/fields.inc
create mode 100644 app/src/Template/Rules/columns.inc
create mode 100644 app/src/Template/Rules/fields.inc
create mode 100644 app/src/Template/Standard/add-edit-view.ctp
create mode 100644 app/src/Template/Standard/index.ctp
create mode 100644 app/src/Template/SystemsOfRecord/columns.inc
create mode 100644 app/src/Template/SystemsOfRecord/fields.inc
create mode 100644 app/src/Template/TierApi/response.ctp
create mode 100644 app/src/View/AjaxView.php
create mode 100644 app/src/View/AppView.php
create mode 100644 app/src/View/Helper/FieldHelper.php
create mode 100644 app/tests/Fixture/empty
create mode 100644 app/tests/TestCase/ApplicationTest.php
create mode 100644 app/tests/TestCase/Controller/Component/empty
create mode 100644 app/tests/TestCase/Controller/PagesControllerTest.php
create mode 100644 app/tests/TestCase/Model/Behavior/empty
create mode 100644 app/tests/TestCase/View/Helper/empty
create mode 100644 app/tests/bootstrap.php
create mode 120000 app/tmp
create mode 100644 app/webroot/.htaccess
create mode 100644 app/webroot/auth/login/login.php
create mode 100644 app/webroot/auth/logout/logout.php
create mode 100644 app/webroot/css/base.css
create mode 100644 app/webroot/css/cake.css
create mode 100644 app/webroot/css/co-base.css
create mode 100644 app/webroot/css/co-responsive.css
create mode 100644 app/webroot/css/fonts/Font-Awesome-4.6.3/css/font-awesome.css
create mode 100644 app/webroot/css/fonts/Font-Awesome-4.6.3/css/font-awesome.css.map
create mode 100644 app/webroot/css/fonts/Font-Awesome-4.6.3/css/font-awesome.min.css
create mode 100644 app/webroot/css/fonts/Font-Awesome-4.6.3/fonts/FontAwesome.otf
create mode 100644 app/webroot/css/fonts/Font-Awesome-4.6.3/fonts/fontawesome-webfont.eot
create mode 100644 app/webroot/css/fonts/Font-Awesome-4.6.3/fonts/fontawesome-webfont.svg
create mode 100644 app/webroot/css/fonts/Font-Awesome-4.6.3/fonts/fontawesome-webfont.ttf
create mode 100644 app/webroot/css/fonts/Font-Awesome-4.6.3/fonts/fontawesome-webfont.woff
create mode 100644 app/webroot/css/fonts/Font-Awesome-4.6.3/fonts/fontawesome-webfont.woff2
create mode 100644 app/webroot/css/fonts/material-icons/MaterialIcons-Regular.eot
create mode 100644 app/webroot/css/fonts/material-icons/MaterialIcons-Regular.ijmap
create mode 100644 app/webroot/css/fonts/material-icons/MaterialIcons-Regular.svg
create mode 100644 app/webroot/css/fonts/material-icons/MaterialIcons-Regular.ttf
create mode 100644 app/webroot/css/fonts/material-icons/MaterialIcons-Regular.woff
create mode 100644 app/webroot/css/fonts/material-icons/MaterialIcons-Regular.woff2
create mode 100644 app/webroot/css/fonts/material-icons/material-icons.css
create mode 100644 app/webroot/css/fonts/notosans_bold/NotoSans-Bold-webfont.eot
create mode 100644 app/webroot/css/fonts/notosans_bold/NotoSans-Bold-webfont.svg
create mode 100644 app/webroot/css/fonts/notosans_bold/NotoSans-Bold-webfont.ttf
create mode 100644 app/webroot/css/fonts/notosans_bold/NotoSans-Bold-webfont.woff
create mode 100644 app/webroot/css/fonts/notosans_bold/stylesheet.css
create mode 100644 app/webroot/css/fonts/notosans_regular/NotoSans-Regular-webfont.eot
create mode 100644 app/webroot/css/fonts/notosans_regular/NotoSans-Regular-webfont.svg
create mode 100644 app/webroot/css/fonts/notosans_regular/NotoSans-Regular-webfont.ttf
create mode 100644 app/webroot/css/fonts/notosans_regular/NotoSans-Regular-webfont.woff
create mode 100644 app/webroot/css/fonts/notosans_regular/stylesheet.css
create mode 100644 app/webroot/css/home.css
create mode 100644 app/webroot/css/jquery/jquery-3.2.1.min.js
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/AUTHORS.txt
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/LICENSE.txt
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/external/jquery/jquery.js
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-bg_glass_75_d0e5f5_1x400.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-bg_glass_85_dfeffc_1x400.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-bg_glass_95_fef1ec_1x400.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-bg_gloss-wave_30_8dbbdd_500x100.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-bg_inset-hard_100_f5f8f9_1x100.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-bg_inset-hard_100_fcfdfd_1x100.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-icons_000_256x240.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-icons_217bc0_256x240.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-icons_2e83ff_256x240.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-icons_469bdd_256x240.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-icons_6da8d5_256x240.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-icons_cd0a0a_256x240.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/images/ui-icons_d8e7f3_256x240.png
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/index.html
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/jquery-ui.css
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/jquery-ui.js
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/jquery-ui.min.css
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/jquery-ui.min.js
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/jquery-ui.structure.css
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/jquery-ui.structure.min.css
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/jquery-ui.theme.css
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/jquery-ui.theme.min.css
create mode 100644 app/webroot/css/jquery/jquery-ui-1.12.1.custom/package.json
create mode 100644 app/webroot/css/jquery/metisMenu/metisMenu.css
create mode 100644 app/webroot/css/jquery/metisMenu/metisMenu.js
create mode 100644 app/webroot/css/jquery/metisMenu/metisMenu.js.map
create mode 100644 app/webroot/css/jquery/metisMenu/metisMenu.min.css
create mode 100644 app/webroot/css/jquery/metisMenu/metisMenu.min.js
create mode 100644 app/webroot/css/mdl/mdl-1.3.0/LICENSE
create mode 100644 app/webroot/css/mdl/mdl-1.3.0/bower.json
create mode 100644 app/webroot/css/mdl/mdl-1.3.0/material.css
create mode 100644 app/webroot/css/mdl/mdl-1.3.0/material.js
create mode 100644 app/webroot/css/mdl/mdl-1.3.0/material.min.css
create mode 100644 app/webroot/css/mdl/mdl-1.3.0/material.min.css.map
create mode 100644 app/webroot/css/mdl/mdl-1.3.0/material.min.js
create mode 100644 app/webroot/css/mdl/mdl-1.3.0/material.min.js.map
create mode 100644 app/webroot/css/mdl/mdl-1.3.0/package.json
create mode 100644 app/webroot/css/mdl/mdl-selectfield-1.0.2/mdl-selectfield.min.css
create mode 100644 app/webroot/css/mdl/mdl-selectfield-1.0.2/mdl-selectfield.min.js
create mode 100644 app/webroot/favicon.ico
create mode 100644 app/webroot/font/cakedingbats-webfont.eot
create mode 100644 app/webroot/font/cakedingbats-webfont.svg
create mode 100644 app/webroot/font/cakedingbats-webfont.ttf
create mode 100644 app/webroot/font/cakedingbats-webfont.woff
create mode 100644 app/webroot/font/cakedingbats-webfont.woff2
create mode 100755 app/webroot/img/COmanage-Logo-LG-onBlue.png
create mode 100755 app/webroot/img/COmanage-Logo-LG-onWhite.png
create mode 100644 app/webroot/img/cake-logo.png
create mode 100644 app/webroot/img/cake.icon.png
create mode 100644 app/webroot/img/cake.logo.svg
create mode 100644 app/webroot/img/cake.power.gif
create mode 100644 app/webroot/index.php
create mode 100644 app/webroot/js/comanage.js
create mode 100644 app/webroot/js/jquery/jquery-3.2.1.min.js
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/AUTHORS.txt
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/LICENSE.txt
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/external/jquery/jquery.js
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-bg_glass_75_d0e5f5_1x400.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-bg_glass_85_dfeffc_1x400.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-bg_glass_95_fef1ec_1x400.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-bg_gloss-wave_30_8dbbdd_500x100.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-bg_inset-hard_100_f5f8f9_1x100.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-bg_inset-hard_100_fcfdfd_1x100.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-icons_000_256x240.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-icons_217bc0_256x240.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-icons_2e83ff_256x240.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-icons_469bdd_256x240.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-icons_6da8d5_256x240.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-icons_cd0a0a_256x240.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/images/ui-icons_d8e7f3_256x240.png
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/index.html
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/jquery-ui.css
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/jquery-ui.js
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/jquery-ui.min.css
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/jquery-ui.min.js
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/jquery-ui.structure.css
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/jquery-ui.structure.min.css
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/jquery-ui.theme.css
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/jquery-ui.theme.min.css
create mode 100644 app/webroot/js/jquery/jquery-ui-1.12.1.custom/package.json
create mode 100644 app/webroot/js/jquery/metisMenu/metisMenu.css
create mode 100644 app/webroot/js/jquery/metisMenu/metisMenu.js
create mode 100644 app/webroot/js/jquery/metisMenu/metisMenu.js.map
create mode 100644 app/webroot/js/jquery/metisMenu/metisMenu.min.css
create mode 100644 app/webroot/js/jquery/metisMenu/metisMenu.min.js
create mode 100644 app/webroot/js/jquery/spin.license.txt
create mode 100644 app/webroot/js/jquery/spin.min.js
create mode 100644 app/webroot/js/js-cookie/js.cookie-2.1.3.min.js
create mode 100644 app/webroot/js/mdl/mdl-1.3.0/LICENSE
create mode 100644 app/webroot/js/mdl/mdl-1.3.0/bower.json
create mode 100644 app/webroot/js/mdl/mdl-1.3.0/material.css
create mode 100644 app/webroot/js/mdl/mdl-1.3.0/material.js
create mode 100644 app/webroot/js/mdl/mdl-1.3.0/material.min.css
create mode 100644 app/webroot/js/mdl/mdl-1.3.0/material.min.css.map
create mode 100644 app/webroot/js/mdl/mdl-1.3.0/material.min.js
create mode 100644 app/webroot/js/mdl/mdl-1.3.0/material.min.js.map
create mode 100644 app/webroot/js/mdl/mdl-1.3.0/package.json
create mode 100644 app/webroot/js/mdl/mdl-selectfield-1.0.2/mdl-selectfield.min.css
create mode 100644 app/webroot/js/mdl/mdl-selectfield-1.0.2/mdl-selectfield.min.js
create mode 100644 local/Config/.gitignore
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..d6456956
--- /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 00000000..ffe3ffae
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,86 @@
+COmanage Registry
+
+Copyright (C) 2010-2018
+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 Contributer License Agreement by,
+
+ Spherical Cow Group
+ https://sphericalcowgroup.com
+
+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
+
+ 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
+
+ spin.js (app/webroot/js/jquery/spin*)
+ MIT License
+ http://spin.js.org
+
+ Shibboleth Embedded Discovery Service (app/webroot/js/eds)
+ Apache 2.0
+ https://shibboleth.net/products/embedded-discovery-service.html
+
+ Superfish (app/webroot/js/superfish)
+ MIT License
+ https://superfish.joelbirch.co
+
+---------------------------------------------------------------------------
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..0123528d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+# COmanage Match
+
+COmanage Match is a utility for identifying potential duplicate records from multiple authoritatize
+systems. COmanage Match is a product of the COmanage Project.
+
+More information is available in the [COmanage wiki](https://spaces.at.internet2.edu/display/COmanage),
+including
+
+- [Technical Manual](https://spaces.at.internet2.edu/display/COmanage/COmanage+Match+Technical+Manual)
+- [Product Roadmap](https://spaces.at.internet2.edu/display/COmanage/COmanage+Product+Roadmap)
+- [Email Lists](https://spaces.at.internet2.edu/display/COmanage/Email+Lists)
+- [Issue Tracker](https://bugs.internet2.edu/jira/projects/CO)
diff --git a/app/bin/cake b/app/bin/cake
new file mode 100755
index 00000000..2547a4f4
--- /dev/null
+++ b/app/bin/cake
@@ -0,0 +1,46 @@
+#!/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"
+}
+
+CONSOLE=$(dirname -- "$(canonicalize "$0")")
+APP=$(dirname "$CONSOLE")
+
+if [ $(basename $0) != 'cake' ]
+then
+ exec php "$CONSOLE"/cake.php $(basename $0) "$@"
+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 00000000..ad137822
--- /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 00000000..320ee364
--- /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 00000000..6b3354ae
--- /dev/null
+++ b/app/composer.json
@@ -0,0 +1,55 @@
+{
+ "name": "cakephp/app",
+ "description": "CakePHP skeleton app",
+ "homepage": "https://cakephp.org",
+ "type": "project",
+ "license": "MIT",
+ "require": {
+ "php": ">=5.6",
+ "adodb/adodb-php": "^5.20",
+ "cakephp/cakephp": "3.6.*",
+ "cakephp/migrations": "^1.0",
+ "cakephp/plugin-installer": "^1.0",
+ "josegonzalez/dotenv": "2.*",
+ "mobiledetect/mobiledetectlib": "2.*"
+ },
+ "require-dev": {
+ "cakephp/bake": "^1.1",
+ "cakephp/cakephp-codesniffer": "^3.0",
+ "cakephp/debug_kit": "^3.6",
+ "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 compatibility.",
+ "phpunit/phpunit": "Allows automated tests to be run without system-wide install.",
+ "cakephp/cakephp-codesniffer": "Allows to check the code against the coding standards used in CakePHP."
+ },
+ "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",
+ "post-autoload-dump": "Cake\\Composer\\Installer\\PluginInstaller::postAutoloadDump",
+ "check": [
+ "@test",
+ "@cs-check"
+ ],
+ "cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests",
+ "cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests",
+ "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 00000000..42139705
--- /dev/null
+++ b/app/composer.lock
@@ -0,0 +1,2737 @@
+{
+ "_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#composer-lock-the-lock-file",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "486ee95971d8d7984bd7085037dc3921",
+ "packages": [
+ {
+ "name": "adodb/adodb-php",
+ "version": "v5.20.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ADOdb/ADOdb.git",
+ "reference": "f601748cca1ccb86dfd427620a1692a70e681075"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ADOdb/ADOdb/zipball/f601748cca1ccb86dfd427620a1692a70e681075",
+ "reference": "f601748cca1ccb86dfd427620a1692a70e681075",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "adodb.inc.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause",
+ "LGPL-2.1"
+ ],
+ "authors": [
+ {
+ "name": "John Lim",
+ "email": "jlim@natsoft.com",
+ "role": "Author"
+ },
+ {
+ "name": "Damien Regad",
+ "role": "Current maintainer"
+ },
+ {
+ "name": "Mark Newnham",
+ "role": "Developer"
+ }
+ ],
+ "description": "ADOdb is a PHP database abstraction layer library",
+ "homepage": "http://adodb.org/",
+ "keywords": [
+ "abstraction",
+ "database",
+ "layer",
+ "library",
+ "php"
+ ],
+ "time": "2016-12-21T17:19:42+00:00"
+ },
+ {
+ "name": "aura/intl",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/auraphp/Aura.Intl.git",
+ "reference": "7fce228980b19bf4dee2d7bbd6202a69b0dde926"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/auraphp/Aura.Intl/zipball/7fce228980b19bf4dee2d7bbd6202a69b0dde926",
+ "reference": "7fce228980b19bf4dee2d7bbd6202a69b0dde926",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Aura\\Intl\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aura.Intl Contributors",
+ "homepage": "https://github.com/auraphp/Aura.Intl/contributors"
+ }
+ ],
+ "description": "The Aura Intl package provides internationalization tools, specifically message translation.",
+ "homepage": "https://github.com/auraphp/Aura.Intl",
+ "keywords": [
+ "g11n",
+ "globalization",
+ "i18n",
+ "internationalization",
+ "intl",
+ "l10n",
+ "localization"
+ ],
+ "time": "2017-01-20T05:00:11+00:00"
+ },
+ {
+ "name": "cakephp/cakephp",
+ "version": "3.6.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/cakephp.git",
+ "reference": "9a67c9d7158d5e299418f9956d1a8af39128cf57"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/cakephp/zipball/9a67c9d7158d5e299418f9956d1a8af39128cf57",
+ "reference": "9a67c9d7158d5e299418f9956d1a8af39128cf57",
+ "shasum": ""
+ },
+ "require": {
+ "aura/intl": "^3.0.0",
+ "cakephp/chronos": "^1.0.1",
+ "ext-intl": "*",
+ "ext-mbstring": "*",
+ "php": ">=5.6.0",
+ "psr/log": "^1.0.0",
+ "zendframework/zend-diactoros": "^1.4.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<5.7"
+ },
+ "replace": {
+ "cakephp/cache": "self.version",
+ "cakephp/collection": "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/i18n": "self.version",
+ "cakephp/log": "self.version",
+ "cakephp/orm": "self.version",
+ "cakephp/utility": "self.version",
+ "cakephp/validation": "self.version"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^3.0",
+ "phpunit/phpunit": "^5.7.14|^6.0"
+ },
+ "suggest": {
+ "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()"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Cake\\": "src/"
+ },
+ "files": [
+ "src/Core/functions.php",
+ "src/Collection/functions.php",
+ "src/I18n/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"
+ ],
+ "time": "2018-07-08T18:02:23+00:00"
+ },
+ {
+ "name": "cakephp/chronos",
+ "version": "1.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/chronos.git",
+ "reference": "30f5b26bcf76a5e53ecc274700ad1ec49dc05567"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/chronos/zipball/30f5b26bcf76a5e53ecc274700ad1ec49dc05567",
+ "reference": "30f5b26bcf76a5e53ecc274700ad1ec49dc05567",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|^7"
+ },
+ "require-dev": {
+ "athletic/athletic": "~0.1",
+ "cakephp/cakephp-codesniffer": "^3.0",
+ "phpbench/phpbench": "@dev",
+ "phpstan/phpstan": "^0.6.4",
+ "phpunit/phpunit": "<6.0 || ^7.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"
+ ],
+ "time": "2018-07-11T18:51:56+00:00"
+ },
+ {
+ "name": "cakephp/migrations",
+ "version": "1.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/migrations.git",
+ "reference": "a5612adfd2efa8c90d29cb3b0c969de872a99eda"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/migrations/zipball/a5612adfd2efa8c90d29cb3b0c969de872a99eda",
+ "reference": "a5612adfd2efa8c90d29cb3b0c969de872a99eda",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/cache": "~3.2",
+ "cakephp/orm": "~3.2",
+ "php": ">=5.5.9",
+ "robmorgan/phinx": "0.8.1"
+ },
+ "require-dev": {
+ "cakephp/bake": "@stable",
+ "cakephp/cakephp": "~3.2",
+ "cakephp/cakephp-codesniffer": "^3.0",
+ "phpunit/phpunit": "~4.1"
+ },
+ "suggest": {
+ "cakephp/bake": "Required if you want to generate 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 3.0 based on Phinx",
+ "homepage": "https://github.com/cakephp/migrations",
+ "keywords": [
+ "cakephp",
+ "migrations"
+ ],
+ "time": "2017-12-12T21:01:38+00:00"
+ },
+ {
+ "name": "cakephp/plugin-installer",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/plugin-installer.git",
+ "reference": "41373d0678490502f45adc7be88aa22d24ac1843"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/plugin-installer/zipball/41373d0678490502f45adc7be88aa22d24ac1843",
+ "reference": "41373d0678490502f45adc7be88aa22d24ac1843",
+ "shasum": ""
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "dev-master",
+ "composer/composer": "1.0.*@dev",
+ "phpunit/phpunit": "^4.8|^5.7|^6.0"
+ },
+ "type": "composer-installer",
+ "extra": {
+ "class": "Cake\\Composer\\Installer\\PluginInstaller"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Composer\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "http://cakephp.org"
+ }
+ ],
+ "description": "A composer installer for CakePHP 3.0+ plugins.",
+ "time": "2017-12-24T21:09:29+00:00"
+ },
+ {
+ "name": "josegonzalez/dotenv",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/josegonzalez/php-dotenv.git",
+ "reference": "ff3461f2960737f54054dff4fef3482a2bb9682b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/josegonzalez/php-dotenv/zipball/ff3461f2960737f54054dff4fef3482a2bb9682b",
+ "reference": "ff3461f2960737f54054dff4fef3482a2bb9682b",
+ "shasum": ""
+ },
+ "require": {
+ "m1/env": "2.*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "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"
+ ],
+ "time": "2017-01-03T01:04:05+00:00"
+ },
+ {
+ "name": "m1/env",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/m1/Env.git",
+ "reference": "d87eddd031f2aa5450fa04bb1325de8a489b3cd0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/m1/Env/zipball/d87eddd031f2aa5450fa04bb1325de8a489b3cd0",
+ "reference": "d87eddd031f2aa5450fa04bb1325de8a489b3cd0",
+ "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"
+ ],
+ "time": "2016-10-06T19:31:28+00:00"
+ },
+ {
+ "name": "mobiledetect/mobiledetectlib",
+ "version": "2.8.30",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/serbanghita/Mobile-Detect.git",
+ "reference": "5500bbbf312fe77ef0c7223858dad84fe49ee0c3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/5500bbbf312fe77ef0c7223858dad84fe49ee0c3",
+ "reference": "5500bbbf312fe77ef0c7223858dad84fe49ee0c3",
+ "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"
+ ],
+ "time": "2017-12-18T10:38:51+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"
+ ],
+ "time": "2016-08-06T14:39:51+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+ "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "time": "2016-10-10T12:19:37+00:00"
+ },
+ {
+ "name": "robmorgan/phinx",
+ "version": "v0.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/phinx.git",
+ "reference": "7a19de5bebc59321edd9613bc2a667e7f96224ec"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/phinx/zipball/7a19de5bebc59321edd9613bc2a667e7f96224ec",
+ "reference": "7a19de5bebc59321edd9613bc2a667e7f96224ec",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4",
+ "symfony/config": "~2.8|~3.0",
+ "symfony/console": "~2.8|~3.0",
+ "symfony/yaml": "~2.8|~3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.26|^5.0"
+ },
+ "bin": [
+ "bin/phinx"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phinx\\": "src/Phinx"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Woody Gilk",
+ "email": "woody.gilk@gmail.com",
+ "homepage": "http://shadowhand.me",
+ "role": "Developer"
+ },
+ {
+ "name": "Rob Morgan",
+ "email": "robbym@gmail.com",
+ "homepage": "https://robmorgan.id.au",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Richard Quadling",
+ "email": "rquadling@gmail.com",
+ "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"
+ ],
+ "time": "2017-06-05T13:30:19+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v3.4.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "72689b934d6c6ecf73eca874e98933bf055313c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/72689b934d6c6ecf73eca874e98933bf055313c9",
+ "reference": "72689b934d6c6ecf73eca874e98933bf055313c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8",
+ "symfony/filesystem": "~2.8|~3.0|~4.0"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<3.3",
+ "symfony/finder": "<3.3"
+ },
+ "require-dev": {
+ "symfony/dependency-injection": "~3.3|~4.0",
+ "symfony/finder": "~3.3|~4.0",
+ "symfony/yaml": "~3.0|~4.0"
+ },
+ "suggest": {
+ "symfony/yaml": "To use the yaml reference dumper"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.4-dev"
+ }
+ },
+ "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": "Symfony Config Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-01-21T19:05:02+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v3.4.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "1b97071a26d028c9bd4588264e101e14f6e7cd00"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/1b97071a26d028c9bd4588264e101e14f6e7cd00",
+ "reference": "1b97071a26d028c9bd4588264e101e14f6e7cd00",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8",
+ "symfony/debug": "~2.8|~3.0|~4.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<3.4",
+ "symfony/process": "<3.3"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "symfony/config": "~3.3|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/event-dispatcher": "~2.8|~3.0|~4.0",
+ "symfony/lock": "~3.4|~4.0",
+ "symfony/process": "~3.3|~4.0"
+ },
+ "suggest": {
+ "psr/log-implementation": "For using the console logger",
+ "symfony/event-dispatcher": "",
+ "symfony/lock": "",
+ "symfony/process": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.4-dev"
+ }
+ },
+ "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": "Symfony Console Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-05-23T05:02:55+00:00"
+ },
+ {
+ "name": "symfony/debug",
+ "version": "v4.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/debug.git",
+ "reference": "dbe0fad88046a755dcf9379f2964c61a02f5ae3d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/debug/zipball/dbe0fad88046a755dcf9379f2964c61a02f5ae3d",
+ "reference": "dbe0fad88046a755dcf9379f2964c61a02f5ae3d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "psr/log": "~1.0"
+ },
+ "conflict": {
+ "symfony/http-kernel": "<3.4"
+ },
+ "require-dev": {
+ "symfony/http-kernel": "~3.4|~4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Debug\\": ""
+ },
+ "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": "Symfony Debug Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-06-08T09:39:36+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v4.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "562bf7005b55fd80d26b582d28e3e10f2dd5ae9c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/562bf7005b55fd80d26b582d28e3e10f2dd5ae9c",
+ "reference": "562bf7005b55fd80d26b582d28e3e10f2dd5ae9c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.1-dev"
+ }
+ },
+ "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": "Symfony Filesystem Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-05-30T07:26:09+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+ "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ },
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "time": "2018-04-30T19:57:29+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "3296adf6a6454a050679cde90f95350ad604b171"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171",
+ "reference": "3296adf6a6454a050679cde90f95350ad604b171",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8-dev"
+ }
+ },
+ "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"
+ ],
+ "time": "2018-04-26T10:06:28+00:00"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "v3.4.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "eab73b6c21d27ae4cd037c417618dfd4befb0bfe"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/eab73b6c21d27ae4cd037c417618dfd4befb0bfe",
+ "reference": "eab73b6c21d27ae4cd037c417618dfd4befb0bfe",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8"
+ },
+ "conflict": {
+ "symfony/console": "<3.4"
+ },
+ "require-dev": {
+ "symfony/console": "~3.4|~4.0"
+ },
+ "suggest": {
+ "symfony/console": "For validating YAML files using the lint command"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "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": "Symfony Yaml Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-01-21T19:05:02+00:00"
+ },
+ {
+ "name": "zendframework/zend-diactoros",
+ "version": "1.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-diactoros.git",
+ "reference": "63d920d1c9ebc009d860c3666593a66298727dd6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/63d920d1c9ebc009d860c3666593a66298727dd6",
+ "reference": "63d920d1c9ebc009d860c3666593a66298727dd6",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "psr/http-message": "^1.0"
+ },
+ "provide": {
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "phpunit/phpunit": "^5.7.16 || ^6.0.8",
+ "zendframework/zend-coding-standard": "~1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8.x-dev",
+ "dev-develop": "1.9.x-dev",
+ "dev-release-2.0": "2.0.x-dev"
+ }
+ },
+ "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"
+ ],
+ "psr-4": {
+ "Zend\\Diactoros\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "description": "PSR HTTP Message implementations",
+ "homepage": "https://github.com/zendframework/zend-diactoros",
+ "keywords": [
+ "http",
+ "psr",
+ "psr-7"
+ ],
+ "time": "2018-07-09T21:17:27+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "ajgl/breakpoint-twig-extension",
+ "version": "0.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ajgarlag/AjglBreakpointTwigExtension.git",
+ "reference": "360ec6351ad7e1968ee78abb31430046c2e04fc5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ajgarlag/AjglBreakpointTwigExtension/zipball/360ec6351ad7e1968ee78abb31430046c2e04fc5",
+ "reference": "360ec6351ad7e1968ee78abb31430046c2e04fc5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6",
+ "twig/twig": "^1.14|^2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5",
+ "symfony/framework-bundle": "^2.7|^3.2",
+ "symfony/twig-bundle": "^2.7|^3.2"
+ },
+ "suggest": {
+ "ext-xdebug": "The Xdebug extension is required for the breakpoint to work",
+ "symfony/framework-bundle": "The framework bundle to integrate the extension into Symfony",
+ "symfony/twig-bundle": "The twig bundle to integrate the extension into Symfony"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Ajgl\\Twig\\Extension\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Antonio J. García Lagar",
+ "email": "aj@garcialagar.es",
+ "homepage": "http://aj.garcialagar.es",
+ "role": "developer"
+ }
+ ],
+ "description": "Twig extension to set breakpoints",
+ "homepage": "https://github.com/ajgarlag/AjglBreakpointTwigExtension",
+ "keywords": [
+ "Xdebug",
+ "breakpoint",
+ "twig"
+ ],
+ "time": "2017-11-20T13:04:11+00:00"
+ },
+ {
+ "name": "aptoma/twig-markdown",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/aptoma/twig-markdown.git",
+ "reference": "64a9c5c7418c08faf91c4410b34bdb65fb25c23d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/aptoma/twig-markdown/zipball/64a9c5c7418c08faf91c4410b34bdb65fb25c23d",
+ "reference": "64a9c5c7418c08faf91c4410b34bdb65fb25c23d",
+ "shasum": ""
+ },
+ "require": {
+ "twig/twig": "~1.12"
+ },
+ "require-dev": {
+ "codeclimate/php-test-reporter": "dev-master",
+ "erusev/parsedown": "^1.6",
+ "knplabs/github-api": "~1.2",
+ "league/commonmark": "~0.5",
+ "michelf/php-markdown": "~1",
+ "phpunit/phpunit": "~4.0",
+ "satooshi/php-coveralls": "~0.6"
+ },
+ "suggest": {
+ "knplabs/github-api": "Needed for using GitHub's Markdown engine provided through their API.",
+ "michelf/php-markdown": "Original Markdown engine with MarkdownExtra."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Aptoma": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Joris Berthelot",
+ "email": "joris@berthelot.tel"
+ },
+ {
+ "name": "Gunnar Lium",
+ "email": "gunnar@aptoma.com"
+ }
+ ],
+ "description": "Twig extension to work with Markdown content",
+ "keywords": [
+ "markdown",
+ "twig"
+ ],
+ "time": "2015-10-23T20:27:08+00:00"
+ },
+ {
+ "name": "asm89/twig-cache-extension",
+ "version": "1.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/asm89/twig-cache-extension.git",
+ "reference": "630ea7abdc3fc62ba6786c02590a1560e449cf55"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/asm89/twig-cache-extension/zipball/630ea7abdc3fc62ba6786c02590a1560e449cf55",
+ "reference": "630ea7abdc3fc62ba6786c02590a1560e449cf55",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2",
+ "twig/twig": "^1.0|^2.0"
+ },
+ "require-dev": {
+ "doctrine/cache": "~1.0"
+ },
+ "suggest": {
+ "psr/cache-implementation": "To make use of PSR-6 cache implementation via PsrCacheAdapter."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alexander",
+ "email": "iam.asm89@gmail.com"
+ }
+ ],
+ "description": "Cache fragments of templates directly within Twig.",
+ "homepage": "https://github.com/asm89/twig-cache-extension",
+ "keywords": [
+ "cache",
+ "extension",
+ "twig"
+ ],
+ "time": "2017-01-10T22:04:15+00:00"
+ },
+ {
+ "name": "cakephp/bake",
+ "version": "1.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/bake.git",
+ "reference": "6c2d86bf7d39262b63716c150dedcb02d56e53c1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/bake/zipball/6c2d86bf7d39262b63716c150dedcb02d56e53c1",
+ "reference": "6c2d86bf7d39262b63716c150dedcb02d56e53c1",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/cakephp": "^3.5.10",
+ "cakephp/plugin-installer": "^1.0",
+ "php": ">=5.6.0",
+ "wyrihaximus/twig-view": "^4.2.1"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^3.0",
+ "phpunit/phpunit": "^5.7 | ^6.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 3",
+ "homepage": "https://github.com/cakephp/bake",
+ "keywords": [
+ "bake",
+ "cakephp"
+ ],
+ "time": "2018-02-07T17:03:13+00:00"
+ },
+ {
+ "name": "cakephp/cakephp-codesniffer",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/cakephp-codesniffer.git",
+ "reference": "d77ac81199f2f1e5a8d8ebf96a5d6d7cd4e0542b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/cakephp-codesniffer/zipball/d77ac81199f2f1e5a8d8ebf96a5d6d7cd4e0542b",
+ "reference": "d77ac81199f2f1e5a8d8ebf96a5d6d7cd4e0542b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4",
+ "squizlabs/php_codesniffer": "^3.0.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "<6.0"
+ },
+ "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": "http://cakephp.org",
+ "keywords": [
+ "codesniffer",
+ "framework"
+ ],
+ "time": "2017-12-21T20:01:35+00:00"
+ },
+ {
+ "name": "cakephp/debug_kit",
+ "version": "3.16.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/debug_kit.git",
+ "reference": "aabaecb032f7e91d2c4df9c79806992cf60e07ee"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/debug_kit/zipball/aabaecb032f7e91d2c4df9c79806992cf60e07ee",
+ "reference": "aabaecb032f7e91d2c4df9c79806992cf60e07ee",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/cakephp": "^3.6.0",
+ "cakephp/chronos": "^1.0.0",
+ "cakephp/plugin-installer": "^1.0.0",
+ "composer/composer": "^1.3.0",
+ "jdorn/sql-formatter": "^1.2.0",
+ "php": ">=5.6.0"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^3.0",
+ "phpunit/phpunit": "^5.7.14|^6.0"
+ },
+ "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": "http://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"
+ ],
+ "time": "2018-06-11T02:33:52+00:00"
+ },
+ {
+ "name": "composer/ca-bundle",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/ca-bundle.git",
+ "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d2c0a83b7533d6912e8d516756ebd34f893e9169",
+ "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "ext-pcre": "*",
+ "php": "^5.3.2 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
+ "psr/log": "^1.0",
+ "symfony/process": "^2.5 || ^3.0 || ^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "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"
+ ],
+ "time": "2018-03-29T19:57:20+00:00"
+ },
+ {
+ "name": "composer/composer",
+ "version": "1.6.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/composer.git",
+ "reference": "b184a92419cc9a9c4c6a09db555a94d441cb11c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/composer/zipball/b184a92419cc9a9c4c6a09db555a94d441cb11c9",
+ "reference": "b184a92419cc9a9c4c6a09db555a94d441cb11c9",
+ "shasum": ""
+ },
+ "require": {
+ "composer/ca-bundle": "^1.0",
+ "composer/semver": "^1.0",
+ "composer/spdx-licenses": "^1.2",
+ "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
+ "php": "^5.3.2 || ^7.0",
+ "psr/log": "^1.0",
+ "seld/cli-prompt": "^1.0",
+ "seld/jsonlint": "^1.4",
+ "seld/phar-utils": "^1.0",
+ "symfony/console": "^2.7 || ^3.0 || ^4.0",
+ "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
+ "symfony/finder": "^2.7 || ^3.0 || ^4.0",
+ "symfony/process": "^2.7 || ^3.0 || ^4.0"
+ },
+ "conflict": {
+ "symfony/console": "2.8.38"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7",
+ "phpunit/phpunit-mock-objects": "^2.3 || ^3.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": "1.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\": "src/Composer"
+ }
+ },
+ "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"
+ }
+ ],
+ "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
+ "homepage": "https://getcomposer.org/",
+ "keywords": [
+ "autoload",
+ "dependency",
+ "package"
+ ],
+ "time": "2018-05-04T09:44:59+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "1.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573",
+ "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.5 || ^5.0.5",
+ "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.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"
+ ],
+ "time": "2016-08-30T16:08:34+00:00"
+ },
+ {
+ "name": "composer/spdx-licenses",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/spdx-licenses.git",
+ "reference": "cb17687e9f936acd7e7245ad3890f953770dec1b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/cb17687e9f936acd7e7245ad3890f953770dec1b",
+ "reference": "cb17687e9f936acd7e7245ad3890f953770dec1b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
+ "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "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"
+ ],
+ "time": "2018-04-30T10:33:04+00:00"
+ },
+ {
+ "name": "dnoegel/php-xdg-base-dir",
+ "version": "0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dnoegel/php-xdg-base-dir.git",
+ "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/265b8593498b997dc2d31e75b89f053b5cc9621a",
+ "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "@stable"
+ },
+ "type": "project",
+ "autoload": {
+ "psr-4": {
+ "XdgBaseDir\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "implementation of xdg base directory specification for php",
+ "time": "2014-10-24T07:27:01+00:00"
+ },
+ {
+ "name": "jakub-onderka/php-console-color",
+ "version": "0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JakubOnderka/PHP-Console-Color.git",
+ "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/e0b393dacf7703fc36a4efc3df1435485197e6c1",
+ "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "require-dev": {
+ "jakub-onderka/php-code-style": "1.0",
+ "jakub-onderka/php-parallel-lint": "0.*",
+ "jakub-onderka/php-var-dump-check": "0.*",
+ "phpunit/phpunit": "3.7.*",
+ "squizlabs/php_codesniffer": "1.*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "JakubOnderka\\PhpConsoleColor": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jakub Onderka",
+ "email": "jakub.onderka@gmail.com",
+ "homepage": "http://www.acci.cz"
+ }
+ ],
+ "time": "2014-04-08T15:00:19+00:00"
+ },
+ {
+ "name": "jakub-onderka/php-console-highlighter",
+ "version": "v0.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git",
+ "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/7daa75df45242c8d5b75a22c00a201e7954e4fb5",
+ "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5",
+ "shasum": ""
+ },
+ "require": {
+ "jakub-onderka/php-console-color": "~0.1",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "jakub-onderka/php-code-style": "~1.0",
+ "jakub-onderka/php-parallel-lint": "~0.5",
+ "jakub-onderka/php-var-dump-check": "~0.1",
+ "phpunit/phpunit": "~4.0",
+ "squizlabs/php_codesniffer": "~1.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "JakubOnderka\\PhpConsoleHighlighter": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jakub Onderka",
+ "email": "acci@acci.cz",
+ "homepage": "http://www.acci.cz/"
+ }
+ ],
+ "time": "2015-04-20T18:58:01+00:00"
+ },
+ {
+ "name": "jasny/twig-extensions",
+ "version": "v1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jasny/twig-extensions.git",
+ "reference": "30bdf3a3903c021544f36332c9d5d4d563527da4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jasny/twig-extensions/zipball/30bdf3a3903c021544f36332c9d5d4d563527da4",
+ "reference": "30bdf3a3903c021544f36332c9d5d4d563527da4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0.0 | >=5.6.0",
+ "twig/twig": "^2.0 | ^1.12"
+ },
+ "require-dev": {
+ "ext-intl": "*",
+ "ext-pcre": "*",
+ "jasny/php-code-quality": "^2.1",
+ "phpunit/phpunit": "^5.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"
+ ],
+ "time": "2017-09-13T07:38:01+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"
+ ],
+ "time": "2014-01-12T16:20:24+00:00"
+ },
+ {
+ "name": "justinrainbow/json-schema",
+ "version": "5.2.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/justinrainbow/json-schema.git",
+ "reference": "8560d4314577199ba51bf2032f02cd1315587c23"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23",
+ "reference": "8560d4314577199ba51bf2032f02cd1315587c23",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.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"
+ ],
+ "time": "2018-02-14T22:26:30+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v3.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "e57b3a09784f846411aa7ed664eedb73e3399078"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/e57b3a09784f846411aa7ed664eedb73e3399078",
+ "reference": "e57b3a09784f846411aa7ed664eedb73e3399078",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0|~5.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-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"
+ ],
+ "time": "2018-01-25T21:31:33+00:00"
+ },
+ {
+ "name": "psy/psysh",
+ "version": "v0.8.17",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/bobthecow/psysh.git",
+ "reference": "5069b70e8c4ea492c2b5939b6eddc78bfe41cfec"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/5069b70e8c4ea492c2b5939b6eddc78bfe41cfec",
+ "reference": "5069b70e8c4ea492c2b5939b6eddc78bfe41cfec",
+ "shasum": ""
+ },
+ "require": {
+ "dnoegel/php-xdg-base-dir": "0.1",
+ "jakub-onderka/php-console-highlighter": "0.3.*",
+ "nikic/php-parser": "~1.3|~2.0|~3.0",
+ "php": ">=5.3.9",
+ "symfony/console": "~2.3.10|^2.4.2|~3.0|~4.0",
+ "symfony/var-dumper": "~2.7|~3.0|~4.0"
+ },
+ "require-dev": {
+ "hoa/console": "~3.16|~1.14",
+ "phpunit/phpunit": "^4.8.35|^5.4.3",
+ "symfony/finder": "~2.1|~3.0|~4.0"
+ },
+ "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-develop": "0.8.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Psy/functions.php"
+ ],
+ "psr-4": {
+ "Psy\\": "src/Psy/"
+ }
+ },
+ "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"
+ ],
+ "time": "2017-12-28T16:14:16+00:00"
+ },
+ {
+ "name": "seld/cli-prompt",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/cli-prompt.git",
+ "reference": "a19a7376a4689d4d94cab66ab4f3c816019ba8dd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/a19a7376a4689d4d94cab66ab4f3c816019ba8dd",
+ "reference": "a19a7376a4689d4d94cab66ab4f3c816019ba8dd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Seld\\CliPrompt\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be"
+ }
+ ],
+ "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type",
+ "keywords": [
+ "cli",
+ "console",
+ "hidden",
+ "input",
+ "prompt"
+ ],
+ "time": "2017-03-18T11:32:45+00:00"
+ },
+ {
+ "name": "seld/jsonlint",
+ "version": "1.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/jsonlint.git",
+ "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d15f59a67ff805a44c50ea0516d2341740f81a38",
+ "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3 || ^7.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"
+ ],
+ "time": "2018-01-24T12:46:19+00:00"
+ },
+ {
+ "name": "seld/phar-utils",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/phar-utils.git",
+ "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/7009b5139491975ef6486545a39f3e6dad5ac30a",
+ "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a",
+ "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": [
+ "phra"
+ ],
+ "time": "2015-10-13T18:44:15+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
+ "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d7c00c3000ac0ce79c96fcbfef86b49a71158cd1",
+ "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.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": "http://www.squizlabs.com/php-codesniffer",
+ "keywords": [
+ "phpcs",
+ "standards"
+ ],
+ "time": "2017-12-19T21:44:46+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v4.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/84714b8417d19e4ba02ea78a41a975b3efaafddb",
+ "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.1-dev"
+ }
+ },
+ "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": "Symfony Finder Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-06-19T21:38:16+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php72",
+ "version": "v1.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php72.git",
+ "reference": "8eca20c8a369e069d4f4c2ac9895144112867422"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/8eca20c8a369e069d4f4c2ac9895144112867422",
+ "reference": "8eca20c8a369e069d4f4c2ac9895144112867422",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php72\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2018-01-31T17:43:24+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v4.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a",
+ "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.1-dev"
+ }
+ },
+ "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": "Symfony Process Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-05-31T10:17:53+00:00"
+ },
+ {
+ "name": "symfony/var-dumper",
+ "version": "v4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-dumper.git",
+ "reference": "6d63cc74f3e2d4961411ccb77389a00332653104"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6d63cc74f3e2d4961411ccb77389a00332653104",
+ "reference": "6d63cc74f3e2d4961411ccb77389a00332653104",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php72": "~1.5"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
+ },
+ "require-dev": {
+ "ext-iconv": "*",
+ "twig/twig": "~1.34|~2.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"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "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": "Symfony mechanism for exploring and dumping PHP variables",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "debug",
+ "dump"
+ ],
+ "time": "2018-01-29T09:06:29+00:00"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v1.35.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/Twig.git",
+ "reference": "daa657073e55b0a78cce8fdd22682fddecc6385f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/daa657073e55b0a78cce8fdd22682fddecc6385f",
+ "reference": "daa657073e55b0a78cce8fdd22682fddecc6385f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "psr/container": "^1.0",
+ "symfony/debug": "~2.7",
+ "symfony/phpunit-bridge": "~3.3@dev"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.35-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Twig_": "lib/"
+ },
+ "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": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com",
+ "role": "Project Founder"
+ },
+ {
+ "name": "Twig Team",
+ "homepage": "http://twig.sensiolabs.org/contributors",
+ "role": "Contributors"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "http://twig.sensiolabs.org",
+ "keywords": [
+ "templating"
+ ],
+ "time": "2017-09-27T18:06:46+00:00"
+ },
+ {
+ "name": "umpirsky/twig-php-function",
+ "version": "v0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/umpirsky/twig-php-function.git",
+ "reference": "53b4b1eb0c5eacbd7d66c504b7d809c79b4bedbc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/umpirsky/twig-php-function/zipball/53b4b1eb0c5eacbd7d66c504b7d809c79b4bedbc",
+ "reference": "53b4b1eb0c5eacbd7d66c504b7d809c79b4bedbc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "twig/twig": "~1.12"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "~2.0",
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Umpirsky\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Saša Stamenković",
+ "email": "umpirsky@gmail.com"
+ }
+ ],
+ "description": "Call (almost) any PHP function from your Twig templates.",
+ "time": "2016-03-12T16:36:32+00:00"
+ },
+ {
+ "name": "wyrihaximus/twig-view",
+ "version": "4.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/WyriHaximus/TwigView.git",
+ "reference": "0bdce795bbf7f667209a9ed4a2d4b4db485e110c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/WyriHaximus/TwigView/zipball/0bdce795bbf7f667209a9ed4a2d4b4db485e110c",
+ "reference": "0bdce795bbf7f667209a9ed4a2d4b4db485e110c",
+ "shasum": ""
+ },
+ "require": {
+ "ajgl/breakpoint-twig-extension": "^0.3.0",
+ "aptoma/twig-markdown": "^2.0",
+ "asm89/twig-cache-extension": "^1.0",
+ "cakephp/cakephp": "^3.5",
+ "jasny/twig-extensions": "^1.0",
+ "php": "^5.6 || ^7.0",
+ "twig/twig": "^1.18",
+ "umpirsky/twig-php-function": "0.1"
+ },
+ "require-dev": {
+ "cakephp/bake": "^1.5",
+ "cakephp/debug_kit": "^3.0",
+ "phake/phake": "^1.0.4",
+ "phpunit/phpunit": "^5.7.14",
+ "squizlabs/php_codesniffer": "^1.5.6",
+ "wyrihaximus/phpunit-class-reflection-helpers": "dev-master"
+ },
+ "type": "cakephp-plugin",
+ "autoload": {
+ "psr-4": {
+ "WyriHaximus\\TwigView\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "ceesjank@gmail.com",
+ "homepage": "http://wyrihaximus.net/"
+ }
+ ],
+ "description": "Twig powered View for CakePHP3",
+ "keywords": [
+ "cakephp",
+ "cakephp3",
+ "twig",
+ "view"
+ ],
+ "time": "2018-01-28T22:18:29+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {
+ "psy/psysh": 0
+ },
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=5.6"
+ },
+ "platform-dev": []
+}
diff --git a/app/config/VERSION b/app/config/VERSION
new file mode 100644
index 00000000..3eefcb9d
--- /dev/null
+++ b/app/config/VERSION
@@ -0,0 +1 @@
+1.0.0
diff --git a/app/config/app.php b/app/config/app.php
new file mode 100644
index 00000000..306c7b14
--- /dev/null
+++ b/app/config/app.php
@@ -0,0 +1,391 @@
+ filter_var(env('DEBUG', false), 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.
+ * - 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'),
+ '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' => [APP . 'Template' . DS],
+ 'locales' => [APP . 'Locale' . 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', '22a0feb8115478995505ee6275de27f42ff79e9876794c6762cb75ea853986af'),
+ ],
+
+ /**
+ * 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,
+ ],
+
+ /**
+ * Configure the cache adapters.
+ */
+ 'Cache' => [
+ 'default' => [
+ 'className' => 'File',
+ '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' => 'Null', // 'File',
+ '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' => 'Null', //'File',
+ 'prefix' => 'myapp_cake_model_',
+ 'path' => CACHE . 'models/',
+ 'serialize' => true,
+ 'duration' => '+1 years',
+ 'url' => env('CACHE_CAKEMODEL_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\Network\Exception\NotFoundException', 'Cake\Network\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' => 'Mail',
+ // 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.
+ * Do not use periods in database name - it may lead to error.
+ * See https://github.com/cakephp/cakephp/issues/6471 for details.
+ * Drivers include Mysql Postgres Sqlite Sqlserver
+ * See vendor\cakephp\cakephp\src\Database\Driver for complete list
+ *
+ * Note for COmanage we read in local/Config/database.php instead
+ *
+ 'Datasources' => [
+ 'default' => [
+ 'className' => 'Cake\Database\Connection',
+ 'driver' => 'Cake\Database\Driver\Postgres',
+ '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' => 'comatch',
+ 'password' => '',
+ 'database' => 'matchtest',
+ 'encoding' => 'utf8',
+ 'timezone' => 'UTC',
+ 'flags' => [],
+ 'cacheMetadata' => true,
+ 'log' => true, //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\Postgres',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ //'port' => 'non_standard_port_number',
+ 'username' => 'my_app',
+ 'password' => 'secret',
+ 'database' => 'test_myapp',
+ 'encoding' => 'utf8',
+ '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'],
+ ],
+ ],
+
+ /**
+ * 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 Registry
+ // Note this name must match the name used in webroot/auth/*/*
+ 'cookie' => 'MATCHCAKEPHP'
+ ],
+];
diff --git a/app/config/bootstrap.php b/app/config/bootstrap.php
new file mode 100644
index 00000000..7316d8f0
--- /dev/null
+++ b/app/config/bootstrap.php
@@ -0,0 +1,234 @@
+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 Match 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.
+ * You can use a file like app_local.php to provide local overrides to your
+ * shared configuration.
+ */
+//Configure::load('app_local', 'default');
+
+// This is set in app.php
+//Configure::write('debug', false);
+
+/*
+ * 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');
+}
+
+/*
+ * Set server timezone to UTC. You can change it to another timezone of your
+ * choice but 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('UTC');
+
+/*
+ * 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 __DIR__ . '/bootstrap_cli.php';
+}
+
+/*
+ * Set the full base URL.
+ * This URL is used as the base of all absolute links.
+ *
+ * If you define fullBaseUrl in your config file you can remove this.
+ */
+if (!Configure::read('App.fullBaseUrl')) {
+ $s = null;
+ if (env('HTTPS')) {
+ $s = 's';
+ }
+
+ $httpHost = env('HTTP_HOST');
+ if (isset($httpHost)) {
+ Configure::write('App.fullBaseUrl', 'http' . $s . '://' . $httpHost);
+ }
+ unset($httpHost, $s);
+}
+
+Cache::setConfig(Configure::consume('Cache'));
+ConnectionManager::setConfig(Configure::consume('Datasources'));
+Email::setConfigTransport(Configure::consume('EmailTransport'));
+Email::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'));
+
+/*
+ * The default crypto extension in 3.0 is OpenSSL.
+ * If you are migrating from 2.x uncomment this code to
+ * use a more compatible Mcrypt based implementation
+ */
+//Security::engine(new \Cake\Utility\Crypto\Mcrypt());
+
+/*
+ * 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();
+});
+
+/*
+ * Enable immutable time objects in the ORM.
+ *
+ * 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/3.0/en/core-libraries/internationalization-and-localization.html#parsing-localized-datetime-data
+ */
+Type::build('time')
+ ->useImmutable();
+Type::build('date')
+ ->useImmutable();
+Type::build('datetime')
+ ->useImmutable();
+Type::build('timestamp')
+ ->useImmutable();
+
+/*
+ * 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']);
+// Cake doesn't handle multi-word inflection well
+Inflector::rules('irregular', ['systemOfRecord' => 'systemsOfRecord']);
+Inflector::rules('irregular', ['system_of_record' => 'systems_of_record']);
+Inflector::rules('irregular', ['systems_of_record' => 'systems_of_record']);
+//Inflector::rules('uninflected', ['dontinflectme']);
+//Inflector::rules('transliteration', ['/å/' => 'aa']);
+
+/*
+ * Plugins need to be loaded manually, you can either load them one by one or all of them in a single call
+ * Uncomment one of the lines below, as you need. make sure you read the documentation on Plugin to use more
+ * advanced ways of loading plugins
+ *
+ * Plugin::loadAll(); // Loads all plugins at once
+ * Plugin::load('Migrations'); //Loads a single plugin named Migrations
+ *
+ */
+
+/*
+ * Only try to load DebugKit in development mode
+ * Debug Kit should not be installed on a production system
+ */
+//if (Configure::read('debug')) {
+// This mucks with CSS, maybe just remove it entirely?
+// Plugin::load('DebugKit', ['bootstrap' => true]);
+//}
\ 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 00000000..85e119e9
--- /dev/null
+++ b/app/config/bootstrap_cli.php
@@ -0,0 +1,38 @@
+ [
+ 'default' => [
+ 'className' => 'Cake\Database\Connection',
+ // Postgres is currently the only supported backend for COmanage Match
+ 'driver' => 'Cake\Database\Driver\Postgres',
+ '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' => 'XXX',
+ 'password' => 'XXX',
+ 'database' => 'XXX',
+ 'encoding' => 'utf8',
+ 'timezone' => 'UTC',
+ 'flags' => [],
+ 'cacheMetadata' => true,
+ '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\Postgres',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ //'port' => 'non_standard_port_number',
+ 'username' => 'my_app',
+ 'password' => 'secret',
+ 'database' => 'test_myapp',
+ 'encoding' => 'utf8',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ 'quoteIdentifiers' => false,
+ 'log' => false,
+ //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
+ 'url' => env('DATABASE_TEST_URL', null),
+ ],
+ ]
+];
\ No newline at end of file
diff --git a/app/config/paths.php b/app/config/paths.php
new file mode 100644
index 00000000..c933a904
--- /dev/null
+++ b/app/config/paths.php
@@ -0,0 +1,90 @@
+connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
+
+ /**
+ * ...and connect the rest of 'Pages' controller's URLs.
+ */
+ $routes->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
+
+ /**
+ * TIER ID Match API Routes
+ */
+
+ // Match Request
+// XXX should we use TierApiV1Controller? or v1match?
+ $routes->get('/api/:matchgrid_id/v1/matchRequests/:id', ['controller' => 'TierApi', 'action' => 'viewMatchRequest']);
+ $routes->get('/api/:matchgrid_id/v1/matchRequests', ['controller' => 'TierApi', 'action' => 'viewMatchRequests']);
+ $routes->put('/api/:matchgrid_id/v1/people/:sor/:sorid', ['controller' => 'TierApi', 'action' => 'match']);
+ $routes->post('/api/:matchgrid_id/v1/people/:sor/:sorid', ['controller' => 'TierApi', 'action' => 'search']);
+// XXX do we need the ?* here? (it breaks matchRequests -> viewMatchRequests)
+ $routes->get('/api/:matchgrid_id/v1/people/:sor/:sorid?*', ['controller' => 'TierApi', 'action' => 'search']);
+ $routes->delete('/api/:matchgrid_id/v1/people/:sor/:sorid', ['controller' => 'TierApi', 'action' => 'remove']);
+ $routes->get('/api/:matchgrid_id/v1/people/:sor/:sorid', ['controller' => 'TierApi', 'action' => 'current']);
+ $routes->get('/api/:matchgrid_id/v1/people/:sor', ['controller' => 'TierApi', 'action' => 'inventory']);
+ $routes->put('/api/:matchgrid_id/v1/referenceIds/:id', ['controller' => 'TierApi', 'action' => 'merge']);
+
+ /**
+ * Connect catchall routes for all controllers.
+ *
+ * Using the argument `DashedRoute`, the `fallbacks` method is a shortcut for
+ * `$routes->connect('/:controller', ['action' => 'index'], ['routeClass' => 'DashedRoute']);`
+ * `$routes->connect('/:controller/:action/*', [], ['routeClass' => 'DashedRoute']);`
+ *
+ * Any route class can be used with this method, such as:
+ * - DashedRoute
+ * - InflectedRoute
+ * - Route
+ * - Or your own route class
+ *
+ * You can remove these routes once you've connected the
+ * routes you want in your application.
+ */
+ $routes->fallbacks(DashedRoute::class);
+});
+
+/**
+ * Load all plugin routes. See the Plugin documentation on
+ * how to customize the loading of plugin routes.
+ *
+ * As of Cake v3.6 it is no longer necessary to call this
+ * Plugin::routes();
+ */
+
+
+/* XXX clean up this before commit
+// check getParams
+
+// Create a route that only responds to GET requests.
+$routes->get(
+ '/cooks/:id',
+ ['controller' => 'Users', 'action' => 'view'],
+ 'users:view'
+);
+
+// Create a route that only responds to PUT requests
+$routes->put(
+ '/cooks/:id',
+ ['controller' => 'Users', 'action' => 'update'],
+ 'users:update'
+);
+
+$routes->connect(
+ '/:controller/:id',
+ ['action' => 'view']
+)->setPatterns(['id' => '[0-9]+']);
+
+
+$routes->connect(
+ '/cooks/:action/*', ['controller' => 'Users']
+);
+
+Router::connect(
+ '/voot/groups/:memberid/:groupid',
+ array('controller' => 'voot', 'action' => 'groups')
+);
+
+Router::connect(
+ '/voot/groups/:memberid',
+ array('controller' => 'voot', 'action' => 'groups')
+);
+
+Router::connect(
+ '/voot/people/:memberid/:groupid',
+ array('controller' => 'voot', 'action' => 'people')
+);
+*/
\ No newline at end of file
diff --git a/app/index.php b/app/index.php
new file mode 100644
index 00000000..45917691
--- /dev/null
+++ b/app/index.php
@@ -0,0 +1,16 @@
+add(ErrorHandlerMiddleware::class)
+
+ // Handle plugin/theme assets like CakePHP normally does.
+ ->add(AssetMiddleware::class)
+
+ // Add routing middleware.
+ ->add(new RoutingMiddleware($this));
+
+ // Enable CSRF protection using Cake v3.5+ approach.
+ // Initially, we use the default options, except a different CSRF cookie
+ // name to avoid conflicts with Registry.
+ $middlewareQueue->add(new CsrfProtectionMiddleware(['cookieName' => 'matchCsrfToken']));
+
+ return $middlewareQueue;
+ }
+}
diff --git a/app/src/Auth/EnvAuthenticate.php b/app/src/Auth/EnvAuthenticate.php
new file mode 100644
index 00000000..3640b459
--- /dev/null
+++ b/app/src/Auth/EnvAuthenticate.php
@@ -0,0 +1,121 @@
+getUser($request);
+ }
+
+ /**
+ * Get a user based on information in the request. Primarily used by stateless authentication
+ * systems like basic and digest auth.
+ *
+ * @since COmanage Match v1.0.0
+ * @param ServerRequest $request Cake ServerRequest
+ * @return Array Array of user information, or false
+ */
+
+ public function getUser(ServerRequest $request) {
+ Log::write('debug', 'EnvAuthenticate::getUser()');
+
+ $user = $request->getSession()->read('Auth.external.user');
+
+ Log::write('debug', 'EnvAuthenticate::getUser() = ' . $user);
+
+ if(!$user) {
+ return false;
+ }
+
+ // It's not clear we really need to use an array here, it just seems to be
+ // the convention of the Cake Authenticate classes.
+ return [ 'username' => $user ];
+ }
+
+ /**
+ * Handle an unauthenticated request.
+ *
+ * @since COmanage Match v1.0.0
+ * @param ServerRequest $request Cake ServerRequest
+ * @param Response $response Cake Response
+ * @return Response Cake Response
+ */
+
+ public function unauthenticated(ServerRequest $request, Response $response) {
+ $externalUser = $request->getSession()->read('Auth.external.user');
+
+ Log::write('debug', 'EnvAuthenticate::unauthenticated() = ' . $externalUser);
+
+ if(!empty($externalUser)) {
+ // Back from authentication
+
+ Log::write('debug', 'EnvAuthenticate::unauthenticated() Back from authentication');
+ return null;
+ }
+
+ Log::write('debug', 'EnvAuthenticate::unauthenticated() Redirecting to authentication');
+
+ // We need to figure out the URL to redirect to. This is basically $request->here(),
+ // except that includes the server base (eg "/match"), and the subsequent redirect
+ // issued by AuthController will have that added in automatically.
+
+ // Note here() and $here return different strings. here() includes query params.
+ $target = $request->getRequestTarget();
+
+ Log::write('debug', 'EnvAuthenticate::unauthenticated() Redirect target: ' . $target);
+
+ // Rather than pass the target URL through the browser, we'll just stuff it
+ // into the session
+ $request->getSession()->write('Auth.target', $target);
+
+ // Send the request to the web server login target
+ $newresponse = $response->withLocation("/match/auth/login/login.php");
+ return $newresponse;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Command/DatabaseCommand.php b/app/src/Command/DatabaseCommand.php
new file mode 100644
index 00000000..8837ec54
--- /dev/null
+++ b/app/src/Command/DatabaseCommand.php
@@ -0,0 +1,114 @@
+config();
+
+ // We only support Postgres (at least for now)
+ if($cfg['driver'] != "Cake\Database\Driver\Postgres") {
+ // We could also use $this->abort(), but then we'd have to $io->out the error
+ throw new \RuntimeException(__('match.er.db.driver' , [ $cfg['driver'] ]));
+ }
+
+ // This ADOdb label actually supports postgres 8+, but Match requires 9.1 or latergamma681
+
+ $dbc = ADONewConnection('postgres9');
+
+ if(!$dbc->Connect($cfg['host'],
+ $cfg['username'],
+ $cfg['password'],
+ $cfg['database'])) {
+ throw new \RuntimeException(__('match.er.db.connect', [$dbc->ErrorMsg()]));
+ }
+
+ $schemaFile = ROOT . DS . 'config' . DS . 'schema' . DS . 'schema.xml';
+
+ if(!is_readable($schemaFile)) {
+ throw new \RuntimeException(__('match.er.file', [$schemaFile]));
+ }
+
+ $io->out(__('match.cmd.db.schema', [$schemaFile]));
+
+ $schema = new \adoSchema($dbc);
+ // ParseSchema is generating bad SQL for Postgres. eg:
+ // ALTER TABLE cm_cos ALTER COLUMN id SERIAL
+ // which (1) should be ALTER TABLE cm_cos ALTER COLUMN id TYPE SERIAL
+ // and (2) SERIAL isn't usable in an ALTER TABLE statement
+ // So we continue on error (CO-1570)
+ $schema->ContinueOnError(true);
+
+ // If we add support for MySQL, check to see if the translation from
+ // boolean to TINYINT performed by Registry is still required.
+ $sql = $schema->ParseSchema($schemaFile);
+
+ switch($schema->ExecuteSchema($sql)) {
+ case 2: // !!!
+ $io->out(__('match.cmd.db.ok'));
+ break;
+ default:
+ $io->out(__('match.er.db.schema'));
+ break;
+ }
+
+ $dbc->Disconnect();
+
+ // 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 00000000..f631308e
--- /dev/null
+++ b/app/src/Command/SetupCommand.php
@@ -0,0 +1,141 @@
+addOption('admin-username', [
+ 'help' => __('match.cmd.opt.admin-username')
+ ])->addOption('force', [
+ 'help' => __('match.cmd.opt.force'),
+ 'boolean' => true
+ ]);
+
+ return $parser;
+ }
+
+ /**
+ * Execute the Setup Command.
+ *
+ * @since COmanage Match v1.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(__('match.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(__('match.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/Console/Installer.php b/app/src/Console/Installer.php
new file mode 100644
index 00000000..722ef8be
--- /dev/null
+++ b/app/src/Console/Installer.php
@@ -0,0 +1,246 @@
+getIO();
+
+ $rootDir = dirname(dirname(__DIR__));
+
+ static::createAppConfig($rootDir, $io);
+ static::createWritableDirectories($rootDir, $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, ['Y', 'y'])) {
+ static::setFolderPermissions($rootDir, $io);
+ }
+ } else {
+ static::setFolderPermissions($rootDir, $io);
+ }
+
+ static::setSecuritySalt($rootDir, $io);
+
+ if (class_exists('\Cake\Codeception\Console\Installer')) {
+ \Cake\Codeception\Console\Installer::customizeCodeceptionBinary($event);
+ }
+ }
+
+ /**
+ * Create the config/app.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 createAppConfig($dir, $io)
+ {
+ $appConfig = $dir . '/config/app.php';
+ $defaultConfig = $dir . '/config/app.default.php';
+ if (!file_exists($appConfig)) {
+ copy($defaultConfig, $appConfig);
+ $io->write('Created `config/app.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)
+ {
+ // Change the permissions on a path and output the results.
+ $changePerms = function ($path, $perms, $io) {
+ // Get permission bits from stat(2) result.
+ $currentPerms = fileperms($path) & 0777;
+ if (($currentPerms & $perms) == $perms) {
+ return;
+ }
+
+ $res = chmod($path, $currentPerms | $perms);
+ if ($res) {
+ $io->write('Permissions set on ' . $path);
+ } else {
+ $io->write('Failed to set permissions on ' . $path);
+ }
+ };
+
+ $walker = function ($dir, $perms, $io) use (&$walker, $changePerms) {
+ $files = array_diff(scandir($dir), ['.', '..']);
+ foreach ($files as $file) {
+ $path = $dir . '/' . $file;
+
+ if (!is_dir($path)) {
+ continue;
+ }
+
+ $changePerms($path, $perms, $io);
+ $walker($path, $perms, $io);
+ }
+ };
+
+ $worldWritable = bindec('0000000111');
+ $walker($dir . '/tmp', $worldWritable, $io);
+ $changePerms($dir . '/tmp', $worldWritable, $io);
+ $changePerms($dir . '/logs', $worldWritable, $io);
+ }
+
+ /**
+ * 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.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/AppController.php b/app/src/Controller/AppController.php
new file mode 100644
index 00000000..3599ac19
--- /dev/null
+++ b/app/src/Controller/AppController.php
@@ -0,0 +1,193 @@
+loadComponent('RequestHandler', [
+ // As of Cake v3.6.7 need to disable this to suppress v4.0.0 deprecation warnings.
+ 'enableBeforeRedirect' => false
+ ]);
+
+ $this->loadComponent('Flash');
+
+ $this->loadComponent('Auth', [
+ // We want to use isAuthorized in each controller for request authorization
+ 'authorize' => [
+ 'Controller'
+ ],
+ // This corresponds to EnvAuthenticate
+ 'authenticate' => [
+ 'Env'
+ ],
+ ]);
+
+ $this->loadComponent('Paginator');
+
+ /*
+ * Enable the following components for recommended CakePHP security settings.
+ * see https://book.cakephp.org/3.0/en/controllers/components/security.html
+ */
+ $this->loadComponent('Security');
+
+ // CSRF Protection is enabled via in Middleware via Application.php.
+
+ // This is the COmanage AuthorizationComponent, not to be confused with
+ // Cake's AuthComponent, or the use of Controller Authorization.
+ $this->loadComponent('Authorization');
+ }
+
+ /**
+ * Callback run prior to the request action.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Event $event Cake Event
+ */
+
+ public function beforeFilter(\Cake\Event\Event $event) {
+ parent::beforeFilter($event);
+
+ // Determine the requested Matchgrid
+ $this->setMatchgrid();
+ }
+
+ /**
+ * Callback run prior to the view rendering.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Event $event Cake Event
+ */
+
+ public function beforeRender(\Cake\Event\Event $event) {
+ parent::beforeRender($event);
+
+ // The current user, if authenticated
+ $this->set('vv_user', $this->request->getSession()->read('Auth.User'));
+
+ // The current Matchgrid, as determined in beforeFilter()
+ $mgid = null;
+
+ if($this->cur_mg) {
+ $mgid = $this->cur_mg->id;
+ }
+
+ // Available Matchgrids
+ $this->loadModel('Matchgrids');
+ $this->set('vv_matchgrids', $this->Matchgrids->find('list')->find('activeMatchGrids')->toArray());
+
+ // The set of menu permissions, so the layout knows what to render
+ if($this->Authorization) {
+ // Ordinarily $this->Authorization will be set, but under certain error conditions
+ // it won't, which will prevent error messages from rendering
+
+ $this->set('vv_menu_permissions',
+ $this->Authorization->menuPermissions($this->request->getSession()->read('Auth.User.username'), $mgid));
+ }
+ }
+
+ /**
+ * Determine the (requested) current Matchgrid and make it available to the
+ * rest of the application.
+ *
+ * @since COmanage Match v1.0.0
+ * @throws Cake\Datasource\Exception\RecordNotFoundException
+ * @throws \InvalidArgumentException
+ */
+
+ protected function setMatchgrid() {
+ // $this->name = Models
+ $modelsName = $this->name;
+
+ if(!method_exists($this->$modelsName, "requiresMatchgrid")
+ || !$this->$modelsName->requiresMatchgrid()) {
+ // Nothing to do, matchgrid not required by this model/controller
+ return;
+ }
+
+ // Try to find the requested matchgrid
+ $mgid = null;
+
+ if($this->request->is('get')) {
+ // If this action allows unkeyed, asserted matchgrid IDs, check the query string
+ // (eg: 'add' or 'index' allow matchgrid_id to be passed in)
+ if($this->$modelsName->allowUnkeyedMatchgrid($this->request->getParam('action'))) {
+ $mgid = $this->request->getQuery('matchgrid_id');
+ }
+ }
+
+ if(!$mgid) {
+ // Try to map the requested object ID
+ $param = (int)$this->request->getParam('pass.0');
+
+ if(!empty($param)) {
+ try {
+ $mgid = $this->$modelsName->calculateMatchgridId($param);
+ }
+ catch(Exception $e) {
+ // Ignore errors and keep trying
+ }
+ }
+ }
+
+ if(!$mgid) {
+ // If we get this far without a Matchgrid ID, something went wrong.
+ throw new RuntimeException(__('match.er.mgid'));
+ }
+
+ if($mgid) {
+ $this->loadModel('Matchgrids');
+
+ // This throws Cake\Datasource\Exception\RecordNotFoundException which
+ // we just let pass up the stack.
+ $this->cur_mg = $this->Matchgrids->findById($mgid)->firstOrFail();
+ $this->set('vv_cur_mg', $this->cur_mg);
+ }
+ }
+}
diff --git a/app/src/Controller/AttributeGroupsController.php b/app/src/Controller/AttributeGroupsController.php
new file mode 100644
index 00000000..5b954b32
--- /dev/null
+++ b/app/src/Controller/AttributeGroupsController.php
@@ -0,0 +1,58 @@
+Authorization->isPlatformAdmin($user['username']);
+
+ $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $this->mgid);
+
+ $p = [
+ 'add' => $platformAdmin || $mgAdmin,
+ 'delete' => $platformAdmin || $mgAdmin,
+ 'edit' => $platformAdmin || $mgAdmin,
+ 'index' => $platformAdmin || $mgAdmin,
+ 'view' => false
+ ];
+
+ $this->set('vv_permissions', $p);
+ return $p[$this->request->getParam('action')];
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/AttributesController.php b/app/src/Controller/AttributesController.php
new file mode 100644
index 00000000..ea193240
--- /dev/null
+++ b/app/src/Controller/AttributesController.php
@@ -0,0 +1,60 @@
+cur_mg->id) ? $this->cur_mg->id : null;
+
+ $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']);
+
+ $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid);
+
+ $p = [
+ 'add' => $platformAdmin || $mgAdmin,
+ 'delete' => $platformAdmin || $mgAdmin,
+ 'edit' => $platformAdmin || $mgAdmin,
+ 'index' => $platformAdmin || $mgAdmin,
+ 'view' => false
+ ];
+
+ $this->set('vv_permissions', $p);
+ return $p[$this->request->getParam('action')];
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/Component/AuthorizationComponent.php b/app/src/Controller/Component/AuthorizationComponent.php
new file mode 100644
index 00000000..80f5df1b
--- /dev/null
+++ b/app/src/Controller/Component/AuthorizationComponent.php
@@ -0,0 +1,199 @@
+Permissions = TableRegistry::get('Permissions');
+ }
+
+ /**
+ * Calculate Match permissions for the specified user.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $username Username of subject to obtain permissions for
+ * @return Array of authorizations, as documented above
+ */
+
+ protected function getPermissions($username) {
+ if(!empty($this->userPermissions[$username])) {
+ return $this->userPermissions[$username];
+ }
+
+ $this->userPermissions[$username] = [
+ // Platform Admin
+ 'cmadmin' => false,
+ // Matchgrid Permissions, keyed on Matchgrid ID
+ 'matchgrids' => []
+ ];
+
+ // Pull the permissions from the database
+ $perms = $this->Permissions->findForUser($username);
+
+ foreach($perms as $mgid => $p) {
+ if($p == PermissionEnum::None) {
+ // Skip None permissions.
+ continue;
+ }
+
+ if($mgid) {
+ // Currently Permissions are hierarchical (ie: MatchgridAdmin implies
+ // ReconciliationManager), but this could change in the future, so we
+ // track everything separately.
+ $this->userPermissions[$username]['matchgrids'][$mgid][$p] = true;
+ } elseif($p == PermissionEnum::PlatformAdmin) {
+ $this->userPermissions[$username]['cmadmin'] = true;
+ }
+ }
+
+ return $this->userPermissions[$username];
+ }
+
+ /**
+ * Obtain the Matchgrid Permission for the specified user.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $username Username
+ * @param Integer $matchgridId Matchgrid ID
+ * @return PermissionEnum Permission
+ */
+
+ public function getGridPermissions($username, $matchgridId) {
+ $perms = $this->getPermissions($username);
+
+ if(!isset($perms['matchgrids'][$matchgridId])) {
+ return [];
+ }
+
+ return $perms['matchgrids'][$matchgridId];
+ }
+
+ /**
+ * Determine if the specified user is a match administrator for the specified matchgrid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $username Username
+ * @param Integer $matchgridId Matchgrid ID
+ * @return boolean true if $username is a match administrator for $matchgridId
+ */
+
+ public function isMatchAdmin($username, $matchgridId) {
+ $perms = $this->getPermissions($username);
+
+ if($matchgridId
+ && isset($perms['matchgrids'][$matchgridId][PermissionEnum::MatchgridAdmin])
+ && $perms['matchgrids'][$matchgridId][PermissionEnum::MatchgridAdmin]) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine if the specified user is a platform administrator.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $username Username
+ * @return boolean true if $username is a platform administrator
+ */
+
+ public function isPlatformAdmin($username) {
+ $perms = $this->getPermissions($username);
+
+ return $perms['cmadmin'];
+ }
+
+ /**
+ * Determine if the specified user is a reconciliation manager for the specified matchgrid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $username Username
+ * @param Integer $matchgridId Matchgrid ID
+ * @return boolean true if $username is a reconciliation manager for $matchgridId
+ */
+
+ public function isReconciliationManager($username, $matchgridId) {
+ $perms = $this->getPermissions($username);
+
+ if($matchgridId
+ && isset($perms['matchgrids'][$matchgridId][PermissionEnum::ReconciliationManager])
+ && $perms['matchgrids'][$matchgridId][PermissionEnum::ReconciliationManager]) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Obtain permissions for rendering menu options
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $username Username of subject to obtain permissions for
+ * @param Integer $matchgridId Matchgrid ID to obtain permissions for, if known
+ * @return Array of authorizations, keyed on menu item
+ */
+
+ public function menuPermissions($username, $matchgridId=null) {
+ $perms = $this->getPermissions($username);
+
+ $platformAdmin = $this->isPlatformAdmin($username);
+ $mgAdmin = $this->isMatchAdmin($username, $matchgridId);
+ $recMgr = $this->isReconciliationManager($username, $matchgridId);
+
+ return [
+ // Manage configuration of the current matchgrid
+ 'attributes' => $platformAdmin || $mgAdmin,
+ 'attribute_groups' => $platformAdmin || $mgAdmin,
+ 'rules' => $platformAdmin || $mgAdmin,
+ 'systems_of_record' => $platformAdmin || $mgAdmin,
+ // Permissions specific to a matchgrid
+ 'gridroles' => $perms['matchgrids'],
+ // Overall permission to manage the matchgrids
+ 'matchgrids' => $platformAdmin,
+ // Overall permission to manage permissions
+ 'permissions' => $platformAdmin
+ ];
+ }
+}
diff --git a/app/src/Controller/ErrorController.php b/app/src/Controller/ErrorController.php
new file mode 100644
index 00000000..b7d4231b
--- /dev/null
+++ b/app/src/Controller/ErrorController.php
@@ -0,0 +1,68 @@
+loadComponent('RequestHandler');
+ }
+
+ /**
+ * beforeFilter callback.
+ *
+ * @param \Cake\Event\Event $event Event.
+ * @return \Cake\Http\Response|null|void
+ */
+ public function beforeFilter(Event $event)
+ {
+ }
+
+ /**
+ * beforeRender callback.
+ *
+ * @param \Cake\Event\Event $event Event.
+ * @return \Cake\Http\Response|null|void
+ */
+ public function beforeRender(Event $event)
+ {
+ parent::beforeRender($event);
+
+ $this->viewBuilder()->setTemplatePath('Error');
+ }
+
+ /**
+ * afterFilter callback.
+ *
+ * @param \Cake\Event\Event $event Event.
+ * @return \Cake\Http\Response|null|void
+ */
+ public function afterFilter(Event $event)
+ {
+ }
+}
diff --git a/app/src/Controller/MatchgridsController.php b/app/src/Controller/MatchgridsController.php
new file mode 100644
index 00000000..f377c604
--- /dev/null
+++ b/app/src/Controller/MatchgridsController.php
@@ -0,0 +1,254 @@
+request->getParam('action'),
+ ['manage', 'pending', 'reconcile'])) {
+ $this->Matchgrids->setRequiresMatchgrid(true);
+ }
+
+ parent::beforeFilter($event);
+ }
+
+ /**
+ * Build the Matchgrid based on the current configuration.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Integer $id Matchgrid ID
+ */
+
+ public function build(int $id) {
+ try {
+ $this->Matchgrids->build($id);
+ $this->Flash->success(__('match.rs.build'));
+ }
+ catch(Exception $e) {
+ $this->Flash->error(__('match.er.build', [$e->getMessage()]));
+ }
+
+ return $this->redirect(['action' => 'index']);
+ }
+
+ /**
+ * Authorization for this Controller, called by Auth component
+ * - postcondition: $vv_permissions set with calculated permissions for this Controller
+ *
+ * @since COmanage Match v1.0.0
+ * @param Array $user Array of user data
+ * @return Boolean True if authorized for the current action, false otherwise
+ */
+
+ public function isAuthorized(Array $user) {
+ $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']);
+
+ $mgid = isset($this->cur_mg->id) ? $this->cur_mg->id : null;
+
+ $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid);
+
+ $recMgr = $this->Authorization->isReconciliationManager($user['username'], $mgid);
+
+ $p = [
+ 'add' => $platformAdmin,
+ 'build' => $platformAdmin || $mgAdmin,
+ 'delete' => $platformAdmin,
+ 'edit' => $platformAdmin,
+ 'index' => $platformAdmin,
+ 'manage' => $platformAdmin || $mgAdmin,
+ 'pending' => $platformAdmin || $mgAdmin || $recMgr,
+ 'reconcile' => $platformAdmin || $mgAdmin || $recMgr,
+ 'view' => false
+ ];
+
+ $this->set('vv_permissions', $p);
+ return $p[$this->request->getParam('action')];
+ }
+
+ /**
+ * Manage a matchgrid. This is the main landing page for matchgrid related operations.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $id Matchgrid ID
+ */
+
+ public function manage(string $id) {
+ $this->set('vv_title', __('match.op.manage.a', [$this->cur_mg->table_name]));
+ }
+
+ /**
+ * Display the set of pending requests.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $id Matchgrid ID
+ */
+
+ public function pending(string $id) {
+ try {
+ $MatchService = new \App\Lib\Match\MatchService();
+
+ $MatchService->connect();
+ $MatchService->setConfig((int)$id);
+
+ $results = $MatchService->getRequests('pending');
+
+ // Although we're passing the $id as provided by the user, it has been
+ // vetted since MatchgridLinkTrait will pull the current Matchgrid from
+ // the database before we get here and throw an error if $id is invalid.
+ $this->set('vv_matchgrid_id', $id);
+ $this->set('vv_pending', $results->getRawResults());
+ }
+ catch(Exception $e) {
+ $this->Flash->error(__('match.er.reconcile', [$e->getMessage()]));
+ }
+
+ $this->set('vv_title', __('match.op.reconcile.a', [$this->cur_mg->table_name]));
+ }
+
+ /**
+ * Display the set of pending requests.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $id Matchgrid ID
+ */
+
+ public function reconcile(string $id) {
+ // There's roughly similar logic in TierApiController::doViewMatchRequest,
+ // which could perhaps at some point be consolidated or moved to a trait...
+
+ try {
+ $MatchService = new \App\Lib\Match\MatchService();
+ $AttributeManager = new \App\Lib\Match\AttributeManager();
+
+ $MatchService->connect();
+ $MatchService->setConfig((int)$id);
+
+ if($this->request->is('post')) {
+ // We've got the requested resolution
+
+ $req = $this->request->getData();
+
+ if(empty($req['rowid']) || empty($req['referenceid'])) {
+ // Just throw an exception that we'll catch and render below
+ throw new \RuntimeException(__('match.er.args', ['reconcile']));
+ }
+
+ $rowid = (int)$req['rowid'];
+
+ // Pull the request so we know which sor+sorid we're working with.
+ // Plausibly this could also have been passed through the form, but it
+ // seems easier to just pass the row ID.
+ $results = $MatchService->getRequest($rowid);
+
+ $row = $results->getRawResults();
+
+ $sor = $row[$rowid]['sor'];
+ $sorid = $row[$rowid]['sorid'];
+
+ // Cake Form tampering protection should ensure that $req['referenceid']
+ // is valid and one we originally proposed.
+ $refId = $MatchService->attachReferenceId($sor, $sorid, $AttributeManager, $req['referenceid']);
+
+ $this->Flash->success(__('match.rs.refid.assigned', [$refId]));
+
+ // Redirect back to list of pending requests
+ return $this->redirect([
+ 'action' => 'pending',
+ $this->cur_mg->id
+ ]);
+ } else { // is get
+ // Find and render the candidates for the pending record
+
+ $rowId = (int)$this->request->getQuery('rowid');
+
+ if(!$rowId) {
+ // Throw an error to be caught below
+ throw new \RuntimeException(__('match.er.args', ['reconcile']));
+ }
+
+ $results = $MatchService->getRequest($rowId);
+ $candidates = [];
+
+ if($results->count() == 0) {
+ // No such request ID
+ throw new \RuntimeException(__('match.er.reconcile.notfound', [$rowId]));
+ }
+
+ // Parse the original request
+ $origReq = $results->getRawResults();
+
+ if(!empty($origReq[$rowId]['referenceId'])) {
+ // This is a resolved request
+ throw new \RuntimeException(__('match.er.reconcile.done', [$rowId]));
+ }
+
+ // Extract the SOR and SORID
+ $sor = $origReq[$rowId]['sor'];
+ $sorid = $origReq[$rowId]['sorid'];
+
+ // Use AttributeManager to parse the current record back into database format for searching
+ // Note we have the raw version, but this remunging of the json is "easier" than adding
+ // another interface to AttributeManager for the moment.
+ $AttributeManager = new \App\Lib\Match\AttributeManager();
+ // We have an array but parseFromJSON wants an object
+ $AttributeManager->parseFromJSON(json_decode(json_encode($results->getResultsForJson('current'))));
+
+ $results = $MatchService->searchReferenceId($sor, $sorid, $AttributeManager);
+
+ // Count could be 0 if we failed to match any rules at all (canonical or potential)
+ if($results->count() > 0) {
+ $candidates = $results->getRawResults();
+ }
+
+ // Insert the original request as "new"
+ $candidates[] = $origReq[$rowId];
+
+ $this->set('vv_candidates', $candidates);
+ // Also set the original request separately to make it easier for the view
+ $this->set('vv_request', $origReq[$rowId]);
+ $this->set('vv_title', __('match.op.reconcile.request', [$sor, $sorid]));
+ } // is post
+ }
+ catch(Exception $e) {
+ $this->Flash->error(__('match.er.reconcile', [$e->getMessage()]));
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/PagesController.php b/app/src/Controller/PagesController.php
new file mode 100644
index 00000000..63debee9
--- /dev/null
+++ b/app/src/Controller/PagesController.php
@@ -0,0 +1,84 @@
+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 {
+ $this->render(implode('/', $path));
+ } catch (MissingTemplateException $exception) {
+ if (Configure::read('debug')) {
+ throw $exception;
+ }
+ throw new NotFoundException();
+ }
+ }
+
+ /**
+ * Authorization for this Controller, called by Auth component
+ * - postcondition: $vv_permissions set with calculated permissions for this Controller
+ *
+ * @since COmanage Match v1.0.0
+ * @param Array $user Array of user data
+ * @return Boolean True if authorized for the current action, false otherwise
+ */
+ public function isAuthorized(Array $user) {
+ // For now always return true
+ // (Alternate, add to beforeFilter(): $this->Auth->allow(['display']);)
+
+ return true;
+ }
+}
diff --git a/app/src/Controller/PermissionsController.php b/app/src/Controller/PermissionsController.php
new file mode 100644
index 00000000..52a59623
--- /dev/null
+++ b/app/src/Controller/PermissionsController.php
@@ -0,0 +1,56 @@
+Authorization->isPlatformAdmin($user['username']);
+
+ $p = [
+ 'add' => $platformAdmin,
+ 'delete' => $platformAdmin,
+ 'edit' => $platformAdmin,
+ 'index' => $platformAdmin,
+ 'view' => false
+ ];
+
+ $this->set('vv_permissions', $p);
+ return $p[$this->request->getParam('action')];
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/RulesController.php b/app/src/Controller/RulesController.php
new file mode 100644
index 00000000..4b9397c2
--- /dev/null
+++ b/app/src/Controller/RulesController.php
@@ -0,0 +1,60 @@
+cur_mg->id) ? $this->cur_mg->id : null;
+
+ $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']);
+
+ $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid);
+
+ $p = [
+ 'add' => $platformAdmin || $mgAdmin,
+ 'delete' => $platformAdmin || $mgAdmin,
+ 'edit' => $platformAdmin || $mgAdmin,
+ 'index' => $platformAdmin || $mgAdmin,
+ 'view' => false
+ ];
+
+ $this->set('vv_permissions', $p);
+ return $p[$this->request->getParam('action')];
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php
new file mode 100644
index 00000000..4db053c2
--- /dev/null
+++ b/app/src/Controller/StandardController.php
@@ -0,0 +1,366 @@
+name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+ // $tableName = models
+ $tableName = $this->$modelsName->getTable();
+
+ if($this->request->is('post')) {
+ // Try to save
+ $obj = $this->$modelsName->newEntity($this->request->getData());
+
+ // This throws \Cake\ORM\Exception\RolledbackTransactionException if aborted
+ // in afterSave
+ if($this->$modelsName->save($obj)) {
+ $this->Flash->success(__('match.rs.saved'));
+
+ return $this->generateRedirect();
+ }
+
+ $this->Flash->error(__('match.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', $this->$modelsName->newEntity());
+ }
+
+ // PrimaryLinkTrait
+ $this->getPrimaryLink();
+
+ // AutoViewVarsTrait
+ $this->populateAutoViewVars();
+
+ // Default title is add new object
+ $this->set('vv_title', __('match.op.add.a', __('match.ct.'.$tableName, [1])));
+
+ // Let the view render
+ $this->render('/Standard/add-edit-view');
+ }
+
+ /**
+ * Handle a delete action for a Standard object.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Integer $id Object ID
+ */
+
+ public function delete($id) {
+ // $this->name = Models (ie: from ModelsTable)
+ $modelsName = $this->name;
+
+ // Allow a delete via a POST or DELETE
+ $this->request->allowMethod(['post', 'delete']);
+
+ // Make sure the requested object exists
+ try {
+ $obj = $this->$modelsName->findById($id)->firstOrFail();
+
+ if($this->$modelsName->delete($obj)) {
+ // Use the display field to generate the flash message
+
+ $field = $this->$modelsName->getDisplayField();
+
+ if(!empty($obj->$field)) {
+ $this->Flash->success(__('match.rs.deleted.a', [$obj->$field]));
+ } else {
+ $this->Flash->success(__('match.rs.deleted'));
+ }
+ } else {
+ // It's hard to get a specific failure reason to render...
+ $this->Flash->error(__('match.er.delete'));
+ }
+ }
+ catch(\Exception $e) {
+ // findById throws Cake\Datasource\Exception\RecordNotFoundException
+
+ $this->Flash->error($e->getMessage());
+ }
+
+ // Always return to index since there is no delete view
+ return $this->generateRedirect();
+ }
+
+ /**
+ * Handle an edit action for a Standard object.
+ *
+ * @since COmanage Match v1.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);
+
+ // AssociationTrait
+ if(method_exists($this->$modelsName, "getEditContains")) {
+ $query = $query->contain($this->$modelsName->getEditContains());
+ }
+
+ try {
+ // Pull the current record
+ $obj = $query->firstOrFail();
+
+ if($this->request->is(['post', 'put'])) {
+ // This is an update request
+ $opts = [];
+
+ // AssociationTrait
+ if(method_exists($this->$modelsName, "getPatchAssociated")) {
+ $opts['associated'] = $this->$modelsName->getPatchAssociated();
+ }
+
+ // Attempt the update the record
+ $this->$modelsName->patchEntity($obj, $this->request->getData(), $opts);
+
+ // This throws \Cake\ORM\Exception\RolledbackTransactionException if aborted
+ // in afterSave
+ if($this->$modelsName->save($obj)) {
+ $this->Flash->success(__('match.rs.saved'));
+
+ return $this->generateRedirect();
+ }
+
+ $this->Flash->error(__('match.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);
+
+ // PrimaryLinkTrait
+ $this->getPrimaryLink();
+
+ // AutoViewVarsTrait
+ $this->populateAutoViewVars();
+
+ // Default view title is edit object display field
+ $field = $this->$modelsName->getDisplayField();
+
+ if(!empty($obj->$field)) {
+ $this->set('vv_title', __('match.op.edit.a', $obj->$field));
+ } else {
+ $this->set('vv_title', __('match.op.edit.a', __('match.ct.'.$tableName, [1])));
+ }
+
+ // Let the view render
+ $this->render('/Standard/add-edit-view');
+ }
+
+ /**
+ * Generate a redirect for a Standard Object operation.
+ *
+ * @since COmanage Match v1.0.0
+ * @return \Cake\Http\Response
+ */
+
+ public function generateRedirect() {
+ $redirect = ['action' => 'index'];
+
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link)) {
+ $redirect[ $link['linkattr'] ] = $link['linkvalue'];
+ }
+
+ return $this->redirect($redirect);
+ }
+
+ /**
+ * Obtain information about the Standard Object's Primary Link, if set.
+ * The $vv_primary_link view variable is also set.
+ *
+ * @since COmanage Match v1.0.0
+ * @param boolean $lookup If true, get the value of the primary link, not just the attribute
+ * @return array Array holding the primary link attribute, and optionally its value
+ * @throws \RuntimeException
+ */
+
+ protected function getPrimaryLink(bool $lookup=false) {
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $modelName = Model
+ $modelName = \Cake\Utility\Inflector::singularize($this->name);
+
+ $ret = [];
+
+ // PrimaryLinkTrait
+ if(method_exists($this->$modelsName, "getPrimaryLink")
+ && $this->$modelsName->getPrimaryLink()) {
+ $ret['linkattr'] = $this->$modelsName->getPrimaryLink();
+ $this->set('vv_primary_link', $ret['linkattr']);
+
+ if($lookup) {
+ // Try to find a value
+ if($this->request->getQuery($ret['linkattr'])) {
+ $ret['linkvalue'] = $this->request->getQuery($ret['linkattr']);
+ } elseif($this->request->getData($ret['linkattr'])) {
+ $ret['linkvalue'] = $this->request->getData($ret['linkattr']);
+ } elseif($this->request->getData($modelName . "." . $ret['linkattr'])) {
+ $ret['linkvalue'] = $this->request->getData($modelName . "." . $ret['linkattr']);
+ } else {
+ throw new \RuntimeException(__('match.er.primary_link', [ $ret['linkattr'] ]));
+ }
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Generate an index for a set of Standard Objects.
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+ public function index() {
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $tableName = models
+ $tableName = $this->$modelsName->getTable();
+
+ $query = null;
+
+ // PrimaryLinkTrait
+ $link = $this->getPrimaryLink();
+
+ if(!empty($link['linkattr'])) {
+ $query = $this->$modelsName->find()->where([$link['linkattr'] => $this->request->getQuery($link['linkattr'])]);
+ } else {
+ $query = $this->$modelsName->find();
+ }
+
+ $this->set($tableName, $this->Paginator->paginate($query));
+
+ // Default index view title is model name
+ $this->set('vv_title', __('match.ct.'.$tableName, [99]));
+
+ // Let the view render
+ $this->render('/Standard/index');
+ }
+
+ /**
+ * Populate any auto view variables, as requested via AutoViewVarsTrait.
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+ protected function populateAutoViewVars() {
+ // $this->name = Models
+ $modelsName = $this->name;
+
+ // Populate certain view vars (eg: selects) automatically.
+
+ // AutoViewVarsTrait
+ if(method_exists($this->$modelsName, "getAutoViewVars")
+ && $this->$modelsName->getAutoViewVars()) {
+ foreach($this->$modelsName->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':
+ 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 = $this->$modelsName->getPrimaryLink();
+
+ if($linkFilter) {
+// XXX also need to check getData()?
+ $v = $this->request->getQuery($linkFilter);
+
+ 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:
+ throw new \LogicException('Unknonwn Auto View Var Type {0}', [$avv['type']]);
+ break;
+ }
+ }
+ }
+ }
+
+// XXX still need to generalize this
+/*
+ public function view($id = null) {
+ $matchgrid = $this->Matchgrids->findById($id)->firstOrFail();
+ $this->set(compact('matchgrid'));
+ }*/
+}
\ No newline at end of file
diff --git a/app/src/Controller/SystemsOfRecordController.php b/app/src/Controller/SystemsOfRecordController.php
new file mode 100644
index 00000000..57598641
--- /dev/null
+++ b/app/src/Controller/SystemsOfRecordController.php
@@ -0,0 +1,60 @@
+cur_mg->id) ? $this->cur_mg->id : null;
+
+ $platformAdmin = $this->Authorization->isPlatformAdmin($user['username']);
+
+ $mgAdmin = $this->Authorization->isMatchAdmin($user['username'], $mgid);
+
+ $p = [
+ 'add' => $platformAdmin || $mgAdmin,
+ 'delete' => $platformAdmin || $mgAdmin,
+ 'edit' => $platformAdmin || $mgAdmin,
+ 'index' => $platformAdmin || $mgAdmin,
+ 'view' => false
+ ];
+
+ $this->set('vv_permissions', $p);
+ return $p[$this->request->getParam('action')];
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/TierApiController.php b/app/src/Controller/TierApiController.php
new file mode 100644
index 00000000..4c2d685e
--- /dev/null
+++ b/app/src/Controller/TierApiController.php
@@ -0,0 +1,599 @@
+loadComponent('RequestHandler');
+// $this->loadComponent('Flash');
+/*
+ $this->loadComponent('Auth', [
+ 'authorize' => 'Controller',
+ 'authenticate' => [
+ 'Env'
+ ],
+ 'unauthorizedRedirect' => '/match/webroot/auth/login'
+/
+ 'loginAction' => [
+ 'controller' => 'Users',
+ 'action' => 'login'
+ ],
+ // If unauthorized, return them to page they were just on
+ 'unauthorizedRedirect' => $this->referer()
+ /
+ ]);*/
+
+ // Allow the display action so our PagesController
+ // continues to work. Also enable the read only actions.
+// $this->Auth->allow(['display', 'view', 'index']);
+
+// XXX do we want to reenable either of these in some limited mode?
+ /*
+ * Enable the following components for recommended CakePHP security settings.
+ * see https://book.cakephp.org/3.0/en/controllers/components/security.html
+ */
+// $this->loadComponent('Security');
+
+// XXX this is deprecated, look at https://book.cakephp.org/3.0/en/controllers/middleware.html#csrf-middleware
+// $this->loadComponent('Csrf');
+ }
+
+ /**
+ * Handle an API Request Current Values request, ie: GET /v1/people/sor/sorid
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+ public function current() {
+// XXX some overlap with inventory()
+ $result = null;
+ $statusCode = 200;
+
+ $matchgridId = (int)$this->request->getParam('matchgrid_id');
+ $sor = $this->request->getParam('sor');
+ $sorid = $this->request->getParam('sorid');
+
+ try {
+ $MatchService = new \App\Lib\Match\MatchService();
+
+ $MatchService->connect();
+ $MatchService->setConfig($matchgridId);
+
+ $results = $MatchService->getSorAttributes($sor, $sorid);
+// XXX $results->getResultsForJson(); but $result is an array, not a Result Manager
+
+ if($results->count()==0) {
+ $statusCode = 404;
+ } else {
+ $result = $results->getResultsForJson('current');
+ }
+ // We shouldn't get more than one row
+
+ $MatchService->disconnect();
+ }
+ catch(\Exception $e) {
+ $statusCode = 500;
+ $result['error'] = $e->getMessage();
+ Log::write('error', $e->getMessage());
+ }
+
+ $this->viewBuilder()->setLayout('rest');
+
+ $this->response = $this->response->withStatus($statusCode);
+
+ // Set the result data and render the default API response view
+ $this->set('vv_result', $result);
+ $this->render('response');
+ }
+
+ /**
+ * Dispatch an API request.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $func Function to call
+ */
+
+// XXX merge all calls to use this
+ protected function dispatch(string $func) {
+/* XXX check to see if requested vars exist, throw error otherwise
+ $sor = $this->request->getParam('sor');
+ $sorId = $this->request->getParam('sorid');*/
+ $matchgridId = (int)$this->request->getParam('matchgrid_id');
+
+ try {
+ $MatchService = new \App\Lib\Match\MatchService();
+
+ $MatchService->connect();
+ $MatchService->setConfig($matchgridId);
+
+ $this->$func($MatchService);
+
+ $MatchService->disconnect();
+ }
+ catch(\Exception $e) {
+ $statusCode = 500;
+ $this->result['error'] = $e->getMessage();
+ Log::write('error', $e->getMessage());
+ }
+
+ $this->viewBuilder()->setLayout('rest');
+
+ $this->response = $this->response->withStatus($this->statusCode);
+
+ // Set the result data and render the default API response view
+ $this->set('vv_result', $this->result);
+ $this->render('response');
+ }
+
+// XXX docblock
+
+ protected function doMatchRequest(bool $searchOnly=false) {
+ $statusCode = 500;
+ $result = [];
+// debug('people controller match');
+
+// XXX need to expose api URL somewhere (https://server/match/api/matchgrid_id/) so
+// admins can configure other end
+ $matchgridId = (int)$this->request->getParam('matchgrid_id');
+ $sor = $this->request->getParam('sor');
+// XXX We should do an authz check on $sor/$matchgridId, based on requester
+// and also verify they were provided
+// and also for all the other calls (current, remove, etc)
+ $sorid = $this->request->getParam('sorid');
+ // Request attributes are here (json body)
+ $json = $this->request->input('json_decode');
+// XXX for $searchOnly, attributes can come from GET, see $this->request->getQueryParams()
+// if we don't implement this at first commit throw NOT IMPLEMENTED 500 error if GET
+
+// XXX walk through and add appropriate logging
+// should debugging be localized? probably not
+ Log::write('debug', $sor . "/" . $sorid . ($searchOnly ? " Search" : " Match") . " request received for matchgrid " . $matchgridId);
+
+ try {
+// XXX Maybe move this test higher?
+ if(empty($json)) {
+ throw new \InvalidArgumentException('No JSON record found or body not successfully parsed');
+ }
+
+ $AttributeManager = new \App\Lib\Match\AttributeManager();
+ $MatchService = new \App\Lib\Match\MatchService();
+
+ $MatchService->connect();
+
+ $AttributeManager->parseFromJSON($json);
+
+ Log::write('debug', $sor . "/" . $sorid . " Obtaining configuration for matchgrid " . $matchgridId);
+ $MatchService->setConfig($matchgridId);
+
+// XXX should we start a transaction? and maybe call connect/disconnect from here
+// (poc did not set up transactions)
+
+ if(!$searchOnly
+ && !$AttributeManager->getRequestedReferenceId()
+ && ($curid = $MatchService->searchSorId($sor, $sorid))) {
+ // If we already have a record for sor+sorid and no referenceId was
+ // provided, this is an Update Match Attributes request.
+
+ Log::write('debug', $sor . "/". $sorid . " Updating existing SOR attributes for Row ID " . $curid);
+
+ $MatchService->updateSorAttributes($curid, $AttributeManager);
+
+ $statusCode = 200;
+ } elseif(!$searchOnly && $AttributeManager->getRequestedReferenceId()) {
+ // Forced Reconciliation request. Skip the search and jump to the insert.
+ // (attachReferenceId will insert or update as appropriate.)
+
+ // If no attributes were provided in the JSON, then this is a Reassign
+ // Reference Identifier request, which we handle basically the same way.
+ // MatchService::update will figure out what to do.
+
+ $referenceId = $MatchService->attachReferenceId($sor, $sorid, $AttributeManager, $AttributeManager->getRequestedReferenceId());
+
+ $result = ['referenceId' => $referenceId];
+
+ $statusCode = 200;
+ } else {
+ // Perform a search, and insert or update if not Search Only
+ $results = $MatchService->searchReferenceId($sor, $sorid, $AttributeManager);
+
+ if($results->count() == 0) {
+ // No match
+
+ if($searchOnly) {
+ $statusCode = 404;
+ } else {
+ $referenceId = $MatchService->assignReferenceId($sor, $sorid, $AttributeManager);
+
+ $result = ['referenceId' => $referenceId];
+
+ $statusCode = 201;
+ }
+ // XXX or return 404 depending on read-only/read-write
+ } elseif($results->getConfidenceMode() == ConfidenceModeEnum::Canonical) {
+ // Exact match
+
+ $refIds = $results->getReferenceIds();
+
+ if(!empty($refIds[0])) {
+ if(!$searchOnly) {
+ $MatchService->attachReferenceId($sor, $sorid, $AttributeManager, $refIds[0]);
+ }
+
+ $result = ['referenceId' => $refIds[0]];
+ }
+
+ $statusCode = 200;
+ } else {
+ // Potential match
+
+ // Pull the SOR configuration
+
+ $SoR = TableRegistry::get('SystemsOfRecord');
+
+// XXX custom finder?
+// XXX this will throw 500 if $sor is not defined
+// XXX maybe merge this into the authz check?
+ $sorobj = $SoR->find('all')->where(['matchgrid_id' => $matchgridId, 'label' => $sor])->firstOrFail();
+
+ if(!$searchOnly && $sorobj->resolution_mode == ResolutionModeEnum::External) {
+ $statusCode = 202;
+
+ $matchRequest = $MatchService->insertPending($sor, $sorid, $AttributeManager);
+
+ $result['matchRequest'] = $matchRequest;
+ } elseif($sorobj->resolution_mode == ResolutionModeEnum::Interactive) {
+ $statusCode = 300;
+
+ $result['candidates'] = $results->getResultsForJson();
+ }
+ // XXX else throw an error?
+ }
+ }
+
+ $MatchService->disconnect();
+ }
+// try/catch and coerce errors into a 400 or 500 response
+ catch(\InvalidArgumentException $e) {
+ $statusCode = 400;
+ $result['error'] = $e->getMessage();
+ Log::write('error', $e->getMessage());
+// XXX put the error in the json body somewhere?
+ }
+ catch(\RuntimeException $e) {
+ $statusCode = 500;
+ $result['error'] = $e->getMessage();
+ Log::write('error', $e->getMessage());
+ }
+ catch(\Exception $e) {
+ $statusCode = 500;
+ $result['error'] = $e->getMessage();
+ Log::write('error', $e->getMessage());
+ }
+
+ Log::write('debug', $sor . "/" . $sorid . " Preparing response with status code " . $statusCode);
+
+ $this->viewBuilder()->setLayout('rest');
+
+ $this->response = $this->response->withStatus($statusCode); // Can also pass a custom phrase
+
+ // Set the result data and render the default API response view
+ $this->set('vv_result', $result);
+ $this->render('response');
+ }
+
+ // empty
+// debug($this->data);
+ // this has server vars
+// debug($this->request->getServerParams());
+ // empty
+// debug($this->request->getQuery());
+// debug($this->request->getQueryParams());
+ // empty
+// debug($this->request->getData());
+
+ /**
+ * Handle an API Join Reference Identifiers request, ie: PUT /v1/referenceIds/id
+ *
+ * @since COmanage Match v1.0.0
+ * @param MatchService $MatchService Match Service
+ */
+
+ protected function doMerge(\App\Lib\Match\MatchService $MatchService) {
+// XXX check that value is provided
+ // Reference ID we want to keep / merge to
+ $targetId = $this->request->getParam('id');
+
+ $json = $this->request->input('json_decode');
+
+ if($targetId && !empty($json->referenceIds)) {
+ $MatchService->merge($targetId, $json->referenceIds);
+
+ $this->statusCode = 200;
+ }
+ /*
+ if(!empty($this->request->getQuery('status'))) {
+ // Obtain pending/resolved requests
+
+// XXX should we filter_var getQuery? (for now it's not needed, but maybe if getRequests changed?)
+// XXX should check that 'status' is set?
+ $r = $MatchService->getRequests($this->request->getQuery('status'));
+ } elseif(!empty($this->request->getQuery('referenceId'))) {
+ // Obtain SOR Records request
+
+ $r = $MatchService->getRequestsForReferenceId($this->request->getQuery('referenceId'));
+ }
+
+ $this->result['matchRequests'] = $r->getResultsForJson('pending');
+ $this->statusCode = 200;*/
+ }
+
+ /**
+ * Handle an API Match Request request, ie: GET /v1/matchRequest/id
+ *
+ * @since COmanage Match v1.0.0
+ * @param MatchService $MatchService Match Service
+ */
+
+ protected function doViewMatchRequest(\App\Lib\Match\MatchService $MatchService) {
+// XXX should check that 'id' is set?
+ $results = $MatchService->getRequest((int)$this->request->getParam('id'));
+
+ if($results->count() == 0) {
+ // No such request ID
+ $this->statusCode = 404;
+
+ return;
+ }
+
+ // Parse the original request
+ $origReq = $results->getResultsForJson('current');
+
+ if(!empty($origReq['referenceId'])) {
+ // This is a resolved request, so handle it a bit differently
+ $this->statusCode = 200;
+ $this->result = $origReq;
+
+ return;
+ }
+
+// XXX it's plausible (but unlikely) that a pending match could becoume canonically resolvable
+// after it has been submitted (eg: if some bad conflicting data was cleaned up)
+ $this->statusCode = 300;
+
+ // Extract the SOR and SORID
+ $sor = $origReq['sorAttributes']['sor'];
+ $sorid = null;
+
+ foreach($origReq['sorAttributes']['identifiers'] as $id) {
+ if($id['type'] == 'sor') {
+ $sorid = $id['identifier'];
+ break;
+ }
+ }
+
+ // Use AttributeManager to parse the current record back into database format for searching
+ $AttributeManager = new \App\Lib\Match\AttributeManager();
+ // We have an array but parseFromJSON wants an object
+ $AttributeManager->parseFromJSON(json_decode(json_encode($origReq)));
+
+ $results = $MatchService->searchReferenceId($sor, $sorid, $AttributeManager);
+
+ // Count could be 0 if we failed to match any rules at all (canonical or potential)
+ if($results->count() > 0) {
+ $this->result['candidates'] = $results->getResultsForJson('search');
+ }
+
+ // Insert the original request as "new"
+ $origReq['referenceId'] = 'new';
+ $this->result['candidates'][] = $origReq;
+ }
+
+ /**
+ * Handle various API Match Requests requests, ie: GET /v1/matchRequests
+ *
+ * @since COmanage Match v1.0.0
+ * @param MatchService $MatchService Match Service
+ */
+
+ protected function doViewMatchRequests(\App\Lib\Match\MatchService $MatchService) {
+ if(!empty($this->request->getQuery('status'))) {
+ // Obtain pending/resolved requests
+
+// XXX should we filter_var getQuery? (for now it's not needed, but maybe if getRequests changed?)
+// XXX should check that 'status' is set?
+ $r = $MatchService->getRequests($this->request->getQuery('status'));
+ } elseif(!empty($this->request->getQuery('referenceId'))) {
+ // Obtain SOR Records request
+
+ $r = $MatchService->getRequestsForReferenceId($this->request->getQuery('referenceId'));
+ }
+
+ $this->result['matchRequests'] = $r->getResultsForJson('pending');
+ $this->statusCode = 200;
+ }
+
+// XXX docbock
+ public function inventory() {
+ $result = [];
+ $statusCode = 200;
+
+ $matchgridId = (int)$this->request->getParam('matchgrid_id');
+ $sor = $this->request->getParam('sor');
+
+ try {
+ $MatchService = new \App\Lib\Match\MatchService();
+
+ $MatchService->connect();
+ $MatchService->setConfig($matchgridId);
+
+ $result['sorids'] = $MatchService->getSorIds($sor);
+
+ $MatchService->disconnect();
+ }
+ catch(\Exception $e) {
+ $statusCode = 500;
+ $result['error'] = $e->getMessage();
+ Log::write('error', $e->getMessage());
+ }
+
+ $this->viewBuilder()->setLayout('rest');
+
+ $this->response = $this->response->withStatus($statusCode);
+
+ // Set the result data and render the default API response view
+ $this->set('vv_result', $result);
+ $this->render('response');
+ }
+
+// XXX docblock
+
+ public function isAuthorized(Array $user) {
+ //debug('isAuthorized');
+// debug($this->request->session()->read('Auth'));
+ return true;
+
+ // By default deny access.
+ return false;
+ }
+
+ /**
+ * Handle an API Reference Identifier (Match) request, ie: PUT /v1/people/sor/sorid
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+ public function match() {
+ $this->doMatchRequest();
+ }
+
+ /**
+ * Handle an Join Reference Identifiers request, ie: PUT /v1/referenceIds/id
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+// XXX what should the authz be on this?
+ public function merge() {
+ $this->dispatch('doMerge');
+ }
+
+ /**
+ * Handle an API Delete Current Values request
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+ public function remove() {
+// XXX should authz for this be configurable? ie: perhaps only admins can do this, and not SORs
+ $result = [];
+ $statusCode = 200;
+
+ $matchgridId = (int)$this->request->getParam('matchgrid_id');
+ $sor = $this->request->getParam('sor');
+ $sorId = $this->request->getParam('sorid');
+
+ try {
+ $MatchService = new \App\Lib\Match\MatchService();
+
+ $MatchService->connect();
+ $MatchService->setConfig($matchgridId);
+
+ if(!$MatchService->remove($sor, $sorId)) {
+ $statusCode = 404;
+ }
+
+ $MatchService->disconnect();
+ }
+// XXX Should we return 404 on NOT FOUND?
+ catch(\Exception $e) {
+ $statusCode = 500;
+ $result['error'] = $e->getMessage();
+ Log::write('error', $e->getMessage());
+ }
+
+ $this->viewBuilder()->setLayout('rest');
+
+ $this->response = $this->response->withStatus($statusCode);
+
+ // Set the result data and render the default API response view
+ $this->set('vv_result', $result);
+ $this->render('response');
+ }
+
+ /**
+ * Handle an API Search-Only request, ie: POST or (not yet supported) GET /v1/people/sor/sorid
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+ public function search() {
+// XXX make sure not to return pending records
+ $this->doMatchRequest(true);
+ }
+
+ /**
+ * Handle an API Match Request request, ie: GET /v1/matchRequest/id
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+ public function viewMatchRequest() {
+ $this->dispatch('doViewMatchRequest');
+ }
+
+ /**
+ * Handle various API Match Requests requests, ie: GET /v1/matchRequests
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+ public function viewMatchRequests() {
+ $this->dispatch('doViewMatchRequests');
+ }
+}
\ No newline at end of file
diff --git a/app/src/Controller/UsersController.php.not b/app/src/Controller/UsersController.php.not
new file mode 100644
index 00000000..fb591f3a
--- /dev/null
+++ b/app/src/Controller/UsersController.php.not
@@ -0,0 +1,70 @@
+Auth->allow(['logout']);
+ }
+
+ public function login() {
+ /*
+ Log::write('debug', 'UsersController::login()');
+
+ Log::write('debug', 'target=' . $request->session()->read('Auth.target'));
+ Log::write('debug', 'user=' . $request->session()->read('Auth.external.user'));
+
+ $this->Auth->setUser($user);
+ return $this->redirect($request->session()->read('Auth.target'));
+
+ /*
+debug('in login');
+ if ($this->request->is('post')) {
+ $user = $this->Auth->identify();
+debug($user);
+ if ($user) {
+ $this->Auth->setUser($user);
+ return $this->redirect($this->Auth->redirectUrl());
+ }
+ $this->Flash->error('Your username or password is incorrect.');
+ }*/
+ }
+}
diff --git a/app/src/Lib/Enum/ConfidenceModeEnum.php b/app/src/Lib/Enum/ConfidenceModeEnum.php
new file mode 100644
index 00000000..caec3e6a
--- /dev/null
+++ b/app/src/Lib/Enum/ConfidenceModeEnum.php
@@ -0,0 +1,35 @@
+getConstants();
+
+ $className = substr(strrchr(get_called_class(), '\\'), 1);
+
+ foreach(array_values($consts) as $key) {
+ $ret[$key] = __('match.en.'.$className.'.'.$key);
+ }
+
+ return $ret;
+ }
+}
\ 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 00000000..c597b4ef
--- /dev/null
+++ b/app/src/Lib/Enum/StatusEnum.php
@@ -0,0 +1,35 @@
+_sequence') or something
+ }
+}
\ No newline at end of file
diff --git a/app/src/Lib/Identifier/Uuid.php b/app/src/Lib/Identifier/Uuid.php
new file mode 100644
index 00000000..6ec1eaa7
--- /dev/null
+++ b/app/src/Lib/Identifier/Uuid.php
@@ -0,0 +1,45 @@
+ attribute => value
+ *
+ * For complex attributes, we concatenate the components with :, creating
+ * a single attribute key for the array. So, for example:
+ *
+ * [_default][dateOfBirth]
+ * [official][names:given]
+ * [official][names:family]
+ * [national][identifiers:identifier]
+ */
+ protected $attributes = [];
+
+ // Requested referenceId for forced reconciliation request
+ protected $requestedReferenceId = null;
+
+ /**
+ * Determine if any attributes were parsed from the source request.
+ *
+ * @since COmanage Match v1.0.0
+ * @return boolean True if at least one attribute was parsed, false otherwise
+ */
+
+ public function attributesAvailable() {
+ return !empty($this->attributes);
+ }
+
+ /**
+ * Obtain the requested Reference ID, if provided in the request.
+ *
+ * @since COmanage Match v1.0.0
+ * @return string Requested Reference ID, or null
+ */
+
+ public function getRequestedReferenceId() {
+ return $this->requestedReferenceId;
+ }
+
+ /**
+ * Obtain the value for an attribute based on an Attribute object.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Attribute $attribute Attribute, including nested AttributeGroup object if set
+ * @return string Attribute value or null
+ */
+
+ public function getValueByAttribute(\App\Model\Entity\Attribute $attribute) {
+ // We can have three types of attributes here:
+ // (1) simple, eg "dateOfBirth", context = "_default" (or NULL)
+ // (2) typed, eg "identifiers:identifier/national", context = type (eg: "national")
+ // (3) grouped, eg "names:given" where attribute_group_id is not null, context = group name
+
+ // The attribute group name needs to match the type, eg for names/type=official
+ // define attribute_groups:name as "official". We'll need a better solution though
+ // if we ever had two different types of "official" attributes.
+
+ if($attribute->attribute_group_id) {
+ // Type 3 (Grouped)
+ return $this->getValueByContext($attribute->api_name, $attribute->attribute_group->name);
+ } elseif(strpos($attribute->api_name, '/') !== false) {
+ // Type 2 (Typed)
+ $a = explode('/', $attribute->api_name);
+ return $this->getValueByContext($a[0], $a[1]);
+ } else {
+ // Type 1 (Simple)
+ return $this->getValueByContext($attribute->api_name);
+ }
+ }
+
+ /**
+ * Obtain the value for an attribute based on the attribute name and context.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $attribute Attribute name
+ * @param string $context Attribute context (type or group label)
+ * @return string Attribute value or null
+ */
+
+ public function getValueByContext(string $attribute, string $context="_default") {
+ if(!empty($this->attributes[$context][$attribute])) {
+ return $this->attributes[$context][$attribute];
+ }
+
+ return null;
+ }
+
+ /**
+ * Load a JSON object (as returned from json_decode) into the Attribute Manager.
+ *
+ * @since COmanage Match v1.0.0
+ * @param stdClass $json JSON Object from json_decode
+ * @throws RuntimeException
+ */
+
+ public function parseFromJSON(\stdClass $json) {
+ // First grab the requested Reference ID, if specified.
+
+ if(!empty($json->referenceId)) {
+ $this->requestedReferenceId = $json->referenceId;
+ }
+
+ // We parse all attributes given (under "sorAttributes"), regardless of what
+ // a given matchgrid's configuration is.
+
+ if(empty($json->sorAttributes)) {
+ // This might legitimately be empty, eg for Reassign Reference Identifier,
+ // which may have no attributes.
+ return;
+ }
+
+ // We need to "flatten" attributes from wire (hierarchical) notation to
+ // column (flat) notation.
+ foreach($json->sorAttributes as $k => $v) {
+ switch(gettype($v)) {
+ case 'array':
+ // We have a set of objects of type $k. We group on type.
+ foreach($v as $vobject) {
+ // Group is default unless type is set
+ $g = !empty($vobject->type) ? $vobject->type : '_default';
+
+ foreach($vobject as $vk => $vv) {
+ if($vk == 'type') {
+ continue;
+ }
+
+ // We'll use $k and $vk to construct the attribute name, eg names:given
+
+ $this->attributes[$g][$k.':'.$vk] = $vv;
+ }
+ }
+ break;
+ case 'integer':
+ case 'string':
+ // A simple attribute, eg: dateofBirth: 00-00-0000
+ $this->attributes['_default'][$k] = $v;
+ break;
+ default:
+ throw new RuntimeException(__('match.er.parse.json', [gettype($v), $k]));
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/Lib/Match/MatchService.php b/app/src/Lib/Match/MatchService.php
new file mode 100644
index 00000000..02820c30
--- /dev/null
+++ b/app/src/Lib/Match/MatchService.php
@@ -0,0 +1,725 @@
+generateReferenceId();
+ Log::write('debug', $sor . "/" . $sorid . " Generated reference ID for request: " . $referenceId);
+
+ $this->insert($sor, $sorid, $attributes, $referenceId);
+
+ return $referenceId;
+ }
+
+ /**
+ * Attach a Reference ID for the specified record, which may or may not already
+ * be in the Matchgrid as a pending record.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $sor SOR Label
+ * @param string $sorid SOR Record ID
+ * @param AttributeManager $attributes Request Attributes
+ * @param string $referenceId Requested Reference ID, or "new" to assign a new Reference ID
+ * @return string Newly assigned Reference ID
+ */
+
+ public function attachReferenceId(string $sor, string $sorid, AttributeManager $attributes, string $referenceId) {
+ $refId = $referenceId;
+
+ if($referenceId == 'new') {
+ // Assign a new reference ID and upsert
+ $refId = $this->generateReferenceId();
+
+ Log::write('debug', $sor . "/" . $sorid . " Generated reference ID for request: " . $referenceId);
+ } else {
+ Log::write('debug', $sor . "/" . $sorid . " Attaching reference ID for request: " . $referenceId);
+ }
+
+ $this->upsert($sor, $sorid, $attributes, $refId);
+
+ return $refId;
+ }
+
+ /**
+ * Generate a Reference ID in accordance with the current configuration.
+ *
+ * @since COmanage Match v1.0.0
+ * @return string Newly generated Reference ID
+ */
+
+ protected function generateReferenceId() {
+// XXX read the matchgrid config and instantiate the appropriate backend
+// need to pass database config/connection to Sequence (or maybe to all, and they
+// can ignore it if not needed, eg UUID)
+// Note $this->dbc is available via PostgresService
+// We could also recreate matchgrid with reference_id column type uuid (supported by postgres)
+// Should we build matchgrid with the appropriate column type (int or uuid) and then make it not-changeable?
+
+ $IdService = new \App\Lib\Identifier\Uuid;
+
+ return $IdService->generate();
+ }
+
+ /**
+ * Obtain a specified match request
+ *
+ * @since COmanage Match v1.0.0
+ * @param int $id Match Request ID
+ * @return ResultManager Result Manager
+ * @throws RuntimeException
+ */
+
+ public function getRequest(int $id) {
+ $results = new ResultManager;
+
+ $results->setConfig($this->mgConfig->attributes);
+
+ $sql = "SELECT *
+ FROM " . $this->mgTable . "
+ WHERE id=?";
+
+ $row = $this->dbc->GetRow($sql, [$id]);
+
+ if($row === false) {
+ throw new \RuntimeException($this->dbc->errorMsg());
+ }
+
+ $results->add($row);
+
+ return $results;
+ }
+
+ /**
+ * Obtain match requests of a given status.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $status "pending" or "resolved"
+ * @return ResultManager Result Manager
+ * @throws RuntimeException
+ */
+
+ public function getRequests(string $status) {
+ $results = new ResultManager;
+
+ $results->setConfig($this->mgConfig->attributes);
+
+ $sql = "SELECT *
+ FROM " . $this->mgTable . "
+ WHERE resolution_time IS " . ($status == 'resolved' ? "NOT " : "") . " NULL";
+
+ $stmt = $this->dbc->Prepare($sql);
+
+ $r = $this->dbc->Execute($stmt);
+
+ if(!$r) {
+ throw new \RuntimeException($this->dbc->errorMsg());
+ }
+
+ while($row = $r->fetchRow()) {
+ $results->add($row);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Obtain match requests for a given Reference ID.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $referenceId Reference ID
+ * @return ResultManager Result Manager
+ * @throws RuntimeException
+ */
+
+ public function getRequestsForReferenceId(string $referenceId) {
+ $results = new ResultManager;
+
+ $results->setConfig($this->mgConfig->attributes);
+
+ $sql = "SELECT *
+ FROM " . $this->mgTable . "
+ WHERE referenceid=?";
+
+ $stmt = $this->dbc->Prepare($sql);
+
+ $r = $this->dbc->Execute($stmt, [$referenceId]);
+
+ if(!$r) {
+ throw new \RuntimeException($this->dbc->errorMsg());
+ }
+
+ while($row = $r->fetchRow()) {
+ $results->add($row);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Obtain the current attributes for an SOR record.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $sor SOR Label
+ * @param string $sorid SOR Record Identifier
+ * @return ResultManager Result Manager
+ * @throws RuntimeException
+ */
+
+ public function getSorAttributes(string $sor, string $sorid) {
+ $results = new ResultManager;
+
+ $results->setConfig($this->mgConfig->attributes);
+
+ $sql = "SELECT *
+ FROM " . $this->mgTable . "
+ WHERE sor=?
+ AND sorid=?";
+
+ $attrs = $this->dbc->GetRow($sql, [$sor, $sorid]);
+
+ if($attrs === false) {
+ throw new \RuntimeException($this->dbc->errorMsg());
+ }
+
+ $results->add($attrs);
+
+ return $results;
+ }
+
+ /**
+ * Obtain a list of all SOR IDs for a given SOR.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $sor SOR Label
+ * @return array Array of SOR IDs, which may be empty if none found
+ * @throws RuntimeException
+ */
+
+ public function getSorIds(string $sor) {
+ $sql = "SELECT sorid
+ FROM " . $this->mgTable . "
+ WHERE sor=?";
+
+ $sorids = $this->dbc->GetCol($sql, [$sor]);
+
+ if($sorids === false) {
+ throw new \RuntimeException($this->dbc->errorMsg());
+ }
+
+ return $sorids;
+ }
+
+ /**
+ * Insert a new record into the Matchgrid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $sor SOR Label
+ * @param string $sorid SOR Record ID
+ * @param AttributeManager $attributes Request Attributes
+ * @param string $referenceId Requested Reference ID, or "new" to assign a new Reference ID
+ * @return string Row ID
+ */
+
+ protected function insert(string $sor, string $sorid, AttributeManager $attributes, string $referenceId=null) {
+// XXX test for error on already existing SOR+SORID and handle "gracefully"
+// note attachReferenceId handles upsert for forced reconciliation request
+// what about attribute update request?
+// XXX should we check that sor + sorid doesn't exist already (maybe in search())? The UNIQUE constraint should
+// throw an error if it does, can we catch that and coerce it into a more informative error?
+// Though if it's unreconciled we could replace it (PoC did that)
+
+// XXX the poc did a delete/insert but that loses the original request time
+
+ // Request time is now, resolution time is also now if a referenceId was specified.
+ $requestTime = gmdate('Y-m-d H:i:s');
+ $resolutionTime = ($referenceId ? $requestTime : null);
+
+ $attrs = ['sor', 'sorid', 'referenceid', 'request_time', 'resolution_time'];
+ $vals = [$sor, $sorid, $referenceId, $requestTime, $resolutionTime];
+
+ // Walk the list of configured attributes and build a list
+ foreach($this->mgConfig->attributes as $attr) {
+ // Only add this attribute if there is a value specified
+ $val = $attributes->getValueByAttribute($attr);
+
+ if($val) {
+ $attrs[] = $attr->name;
+ $vals[] = $val;
+ }
+ }
+
+ // "RETURNING id" is not portable SQL
+// XXX should we be using adodb's parameter generator?
+ $sql = "INSERT INTO " . $this->mgTable . " (" . implode(",", $attrs) . ")
+ VALUES (" . str_repeat("?,", count($vals)-1) . "?)
+ RETURNING id";
+
+ $stmt = $this->dbc->Prepare($sql);
+
+ $rowid = $this->dbc->GetOne($stmt, $vals);
+
+ if(!$rowid) {
+ Log::write('error', $this->dbc->errorMsg());
+// XXX throw an exception
+ print $this->dbc->errorMsg();
+ }
+
+ Log::write('debug', "Inserted new matchgrid entry at row ID " . $rowid);
+
+ return $rowid;
+ }
+
+ /**
+ * Insert a new, pending record into the Matchgrid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $sor SOR Label
+ * @param string $sorid SOR Record ID
+ * @param AttributeManager $attributes Request Attributes
+ * @return string Row ID
+ */
+
+ public function insertPending(string $sor, string $sorid, AttributeManager $attributes) {
+ return $this->insert($sor, $sorid, $attributes);
+ }
+
+ /**
+ * Merge deprecated referenceIds to a primary ID.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $targetId Target Reference ID (to keep)
+ * @param array $referenceIds Array of deprecated referenceIds (to merge)
+ * @throws RuntimeException
+ */
+
+ public function merge(string $targetId, array $referenceIds) {
+// XXX this should at least generate a history record, if not update resolution_time
+ $sql = "UPDATE " . $this->mgTable . "
+ SET referenceid=?
+ WHERE referenceid IN (" . str_repeat("?,", count($referenceIds)-1) . "?)";
+
+ $vals = array_merge([$targetId], $referenceIds);
+
+ $stmt = $this->dbc->Prepare($sql);
+
+ if(!$this->dbc->Execute($stmt, $vals)) {
+ throw new \RuntimeException($this->dbc->errorMsg());
+ }
+ }
+
+ /**
+ * Remove an entry from the matchgrid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $sor SOR Label
+ * @param string $sorid SOR Record Identifier
+ * @param boolean True if an entry was removed, false if no entry was found
+ * @throws RuntimeException
+ */
+
+ public function remove(string $sor, string $sorid) {
+ $sql = "DELETE
+ FROM " . $this->mgTable . "
+ WHERE sor=?
+ AND sorid=?
+ RETURNING id"; // Postgres SQL Extension
+
+ // This should only ever match zero or one rows
+ $ret = $this->dbc->GetOne($sql, [$sor, $sorid]);
+
+ if($ret === false) {
+ throw new \RuntimeException($this->dbc->errorMsg());
+ }
+
+ return !empty($ret);
+ }
+
+ /**
+ * Perform a search of the matchgrid according to the current configuration.
+ *
+ * @since COmanage Match v1.0.0
+ * @param ConfidenceModeEnum $mode Confidence Mode
+ * @param string $sor SOR Label
+ * @param string $sorid SOR ID
+ * @param AttributeManager $attributes Search attibutes
+ * @param string $referenceId Reference ID, if known
+ * @return ResultManager Result Manager
+ * @throws LogicException
+ * @throws RuntimeException
+ */
+
+ protected function search(string $mode,
+ string $sor,
+ string $sorid,
+ AttributeManager $attributes,
+ string $referenceId=null) { // XXX do we need this?
+ $results = new ResultManager;
+
+ $results->setConfidenceMode($mode);
+ $results->setConfig($this->mgConfig->attributes);
+ // XXX if $referenceId is provided, should we only look for sor+sorid+referenceid (ignoring $attributes?)
+
+ $ruleObjs = ($mode == ConfidenceModeEnum::Canonical ? "canonical" : "potential") . "_rules";
+
+ Log::write('debug', $sor . "/" . $sorid . " Searching with confidence mode " . $mode);
+
+ foreach($this->mgConfig->$ruleObjs as $rule) {
+// XXX need to implement strtolower, preg_replace, etc (see PoC buildAttributeSql)
+ $sql = "SELECT *
+ FROM " . $this->mgTable . "
+ WHERE referenceid IS NOT NULL"; // Don't match pending requests
+
+ $vals = [];
+
+ foreach($rule->rule_attributes as $ruleattr) {
+ if($ruleattr->search_type == SearchTypeEnum::Skip) {
+ continue;
+ }
+
+ // If we don't have a value for this attribute then we can't process this rule
+ $val = $attributes->getValueByAttribute($ruleattr->attribute);
+
+ if(!$val) {
+ Log::write('debug', $sor . "/" . $sorid . " No value found for attribute " . $ruleattr->attribute->name . " skipping rule " . $rule->name);
+ continue 2;
+ }
+
+
+ $andclause = "";
+ $colclause = "";
+
+ // The column name
+ $colclause = $ruleattr->attribute->name;
+
+ // XXX we only want search_type=E for canonical rules (should we enforce this here, or just at config time?)
+ // XXX complain if there are no Exact rules? Maybe in the UI during configuration?
+ // XXX document in wiki https://spaces.at.internet2.edu/display/COmanage/Match+Attributes
+ // how we handle all this
+ switch($ruleattr->search_type) {
+ case SearchTypeEnum::Distance:
+ $maxdistance = (int)($ruleattr->attribute->search_distance)+1;
+ $andclause = "LEVENSHTEIN_LESS_EQUAL("
+ . $colclause
+ . ",?,"
+ . $ruleattr->attribute->search_distance
+ . ") < "
+ . $maxdistance;
+ break;
+ case SearchTypeEnum::Exact:
+// XXX should we be using adodb's parameter generator?
+ $andclause = $colclause . "=?";
+ break;
+ case SearchTypeEnum::Substring:
+// XXX document that initial position is 1, not 0 (ie: from 1 for 3 = SMI for SMITH)
+ $andclause = "SUBSTRING("
+ . $colclause
+ . " FROM "
+ . $ruleattr->attribute->search_substr_from
+ . " FOR "
+ . $ruleattr->attribute->search_substr_for
+ . ") = SUBSTRING(? FROM "
+ . $ruleattr->attribute->search_substr_from
+ . " FOR "
+ . $ruleattr->attribute->search_substr_for
+ . ")";
+ break;
+ default:
+ throw new LogicException(__('matchgrid.er.search_type', [$ruleattr->search_type]));
+ break;
+ }
+
+ $sql .= " AND " . $andclause;
+ $vals[] = $val;
+ }
+
+// XXX Maybe add a special setting to enable logging of SQL?
+// XXX add timing on SQL queries? or overall transaction?
+LOG::write('debug', $sor . "/" . $sorid . " SQL: " . $sql);
+ $stmt = $this->dbc->Prepare($sql);
+
+ $r = $this->dbc->Execute($stmt, $vals);
+
+ if(!$r) {
+ throw new \RuntimeException($this->dbc->errorMsg());
+ }
+
+ $count = 0;
+
+ while($row = $r->fetchRow()) {
+ $results->add($row);
+ $count++;
+ }
+
+ Log::write('debug', $sor . "/" . $sorid . " Matched " . $count . " candidate(s) using rule " . $rule->name);
+
+ if($mode == ConfidenceModeEnum::Canonical && $count > 0) {
+ // We stop processing Canonical rules if any match
+ break;
+ }
+ }
+
+ return $results;
+
+ // XXX review POC for additional logic implemented in searchDatabase() and buildAttributeSql()
+ }
+
+ /**
+ * Attempt to obtain a reference ID based on search attributes.
+// XXX should this be renamed? @return is ResultManager, not ReferenceId(string)
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $sor Requesting System of Record
+ * @param string $sorid Requesting SOR's Identifier
+ * @param AttributeManager $attributes Attribute Manager, holding search attributes
+ * @return ResultManager Result Manager
+ */
+
+// XXX maybe the public function should be "search" (and "insert"), and the private
+// functions should be doSearch/doInsert, or similar
+ public function searchReferenceId(string $sor, string $sorid, AttributeManager $attributes) {
+// Log::write('debug', "Match request received for matchgrid " . $matchgridId . ": " . $sor . "/" . $sorid);
+
+// throw error if not obtained
+ // First try canonical matches
+ $canonicalMatches = $this->search(ConfidenceModeEnum::Canonical, $sor, $sorid, $attributes);//, $referenceId);
+
+// XXX add some logging on match candidates? or maybe in search()
+ switch($canonicalMatches->count()) {
+ case 1:
+ // Exact match, return
+ return $canonicalMatches;
+ break;
+ case 0:
+ // Fall through and try potential matches
+ break;
+ default:
+ // Multiple canonical matches: demote to potential
+ $canonicalMatches->setConfidenceMode(ConfidenceModeEnum::Potential);
+// XXX should we continue with other potential rules and merge results?
+ return $canonicalMatches;
+ break;
+ }
+
+ // Next try potential matches
+ $potentialMatches = $this->search(ConfidenceModeEnum::Potential, $sor, $sorid, $attributes);
+
+ return $potentialMatches;
+ }
+
+ /**
+ * Search for an existing record for $sor + $sorid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $sor Requesting System of Record
+ * @param string $sorid Requesting SOR's Identifier
+ * @return int Row ID
+ */
+
+ public function searchSorId(string $sor, string $sorid) {
+// XXX need to implement strtolower, preg_replace, etc (see PoC buildAttributeSql)
+ $sql = "SELECT id
+ FROM " . $this->mgTable . "
+ WHERE sor=?
+ AND sorid=?";
+
+ $vals = [$sor, $sorid];
+
+ $stmt = $this->dbc->Prepare($sql);
+
+ $r = $this->dbc->GetOne($stmt, $vals);
+
+ return $r;
+
+// Log::write('debug', $sor . "/" . $sorid . " Matched " . $count . " candidate(s) using rule " . $rule->name);
+ }
+
+ /**
+ * Obtain and store the configuration for the specified matchgrid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param int $matchgridId Matchgrid ID
+ */
+
+ public function setConfig(int $matchgridId) {
+ $Matchgrids = TableRegistry::get('Matchgrids');
+
+// XXX what error does this throw if not found?
+ $this->mgConfig = $Matchgrids->findById($matchgridId)
+ ->contain(['Attributes' => 'AttributeGroups',
+ 'CanonicalRules' => [
+// XXX we already pull attributes above, but this makes it easier to access them in search()
+ 'RuleAttributes' => ['Attributes' => 'AttributeGroups'],
+ 'sort' => ['CanonicalRules.ordr' => 'ASC']
+ ],
+ 'PotentialRules' => [
+ 'RuleAttributes' => ['Attributes' => 'AttributeGroups'],
+ 'sort' => ['PotentialRules.ordr' => 'ASC']
+ ]])
+ ->firstOrFail();
+
+// XXX prefix should be inserted in parent call? or maybe as virtual field?
+// XXX throw an error if matchgrid is not active
+ $this->mgTable = "mg_" . $this->mgConfig->table_name;
+ }
+
+ /**
+ * Update an existing Matchgrid record.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $rowid Row ID
+ * @param AttributeManager $attributes Request Attributes
+ * @param string $referenceId Requested Reference ID
+ * @return string Row ID
+ * @throws RuntimeException
+ */
+
+ protected function update(string $rowid, AttributeManager $attributes, string $referenceId=null) {
+// XXX note update doesn't allow sor/sorid to be changed
+// XXX create a history record
+ // We don't update request time
+ $resolutionTime = ($referenceId ? gmdate('Y-m-d H:i:s') : null);
+
+ $attrs = ['referenceid', 'resolution_time'];
+ $vals = [$referenceId, $resolutionTime];
+
+ // Walk the list of configured attributes and build a list, but only if attributes
+ // were provided in the request. (eg: Reassign Reference Identifier does not require
+ // attributes to be provided.)
+
+ if($attributes->attributesAvailable()) {
+ foreach($this->mgConfig->attributes as $attr) {
+ // Only add this attribute if there is a value specified
+ $val = $attributes->getValueByAttribute($attr);
+
+ if($val) {
+ $attrs[] = $attr->name;
+ $vals[] = $val;
+ }
+ }
+ }
+
+ $sql = "UPDATE " . $this->mgTable . "
+ SET ";
+
+ foreach($attrs as $a) {
+ $sql .= $a . "=?,";
+ }
+
+ // Toss the last comma
+ $sql = rtrim($sql, ",");
+
+ $sql .= " WHERE id=?";
+ $vals[] = $rowid;
+
+ $stmt = $this->dbc->Prepare($sql);
+
+ if(!$this->dbc->Execute($stmt, $vals)) {
+ throw new \RuntimeException($this->dbc->errorMsg());
+ }
+
+ Log::write('debug', "Updated matchgrid entry at row ID " . $rowid);
+
+ // Return $rowid for consistency with insert()
+ return $rowid;
+ }
+
+ /**
+ * Update the Attributes associated with an existing Matchgrid record.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $rowid Row ID
+ * @param AttributeManager $attributes Request Attributes
+ * @return string Row ID
+ * @throws RuntimeException
+ */
+
+ public function updateSorAttributes(string $rowid, AttributeManager $attributes) {
+ return $this->update($rowid, $attributes);
+ }
+
+ /**
+ * Insert or update a record in the Matchgrid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $sor SOR Label
+ * @param string $sorid SOR Record ID
+ * @param AttributeManager $attributes Request Attributes
+ * @param string $referenceId Requested Reference ID, or "new" to assign a new Reference ID
+ * @return string Row ID
+ */
+
+ protected function upsert(string $sor, string $sorid, AttributeManager $attributes, string $referenceId=null) {
+ // Dispatch to insert() or update()
+
+// XXX This should be a SELECT .. FOR UPDATE, but only if we're in a transaction
+ $sql = "SELECT id
+ FROM " . $this->mgTable . "
+ WHERE sor=" . $this->dbc->Param('a') . "
+ AND sorid=" . $this->dbc->Param('b');
+
+ $stmt = $this->dbc->Prepare($sql);
+
+ $rowid = $this->dbc->GetOne($stmt, [$sor, $sorid]);
+
+ if($rowid) {
+ return $this->update($rowid, $attributes, $referenceId);
+ } else {
+ return $this->insert($sor, $sorid, $attributes, $referenceId);
+ }
+ }
+}
diff --git a/app/src/Lib/Match/MatchgridBuilder.php b/app/src/Lib/Match/MatchgridBuilder.php
new file mode 100644
index 00000000..8822c286
--- /dev/null
+++ b/app/src/Lib/Match/MatchgridBuilder.php
@@ -0,0 +1,264 @@
+connect();
+
+ // Convert the configuration given into an ADOdb AXMLS document
+ $xml = $this->configToSchema($dbc, $tablename, $attributes);
+
+ // Execute the XML schema
+ $this->runSchema($dbc, $xml);
+
+ // Disconnect
+ $dbc->Disconnect();
+ }
+
+ /**
+ * Convert a Matchgrid Attribute configuration into an ADODB schema.
+ *
+ * @since COmanage Match v1.0.0
+ * @param ADOConnection $dbc ADOdb Connection Object
+ * @param string $tablename Name of Matchgrid table
+ * @param array $attributes Array of Attributes
+ * @return string XML Document holding schema
+ */
+
+ protected function configToSchema($dbc, string $tablename, array $attributes) {
+ // We use Cake's XML library because it's simpler to work with.
+ // This requires constructing an array.
+
+ // There are various mandatory columns that we hardcode here.
+ $fields = [
+ // Primary Key
+ [
+ '@name' => 'id',
+ '@type' => 'I',
+ 'key' => [],
+ 'autoincrement' => []
+ ],
+ // XXX maybe SOR Label and ID should be UI configured so @size can be set?
+ // SOR Label
+ [
+ '@name' => 'sor',
+ '@type' => 'C',
+ '@size' => '64'
+ ],
+ // See also ResultManager::getResultsForJson special handling
+ // SOR ID
+ [
+ '@name' => 'sorid',
+ '@type' => 'C',
+ '@size' => '64'
+ ],
+ // Reference ID
+ [
+ '@name' => 'referenceid',
+ '@type' => 'C',
+ '@size' => '64'
+ ],
+ // Request Time
+ [
+ '@name' => 'request_time',
+ '@type' => 'T'
+ ],
+ // Resolution Time
+ [
+ '@name' => 'resolution_time',
+ '@type' => 'T'
+ ]
+ ];
+
+ // Add in the configured fields
+ foreach($attributes as $attr) {
+ $fields[] = [
+ '@name' => $attr->name,
+ // XXX everything is a varchar because we don't have a configuration option for field type
+ '@type' => 'C',
+ '@size' => '80'
+ ];
+ }
+
+ // Configure indexes. id should be auto-generated since it is a primary key.
+ $i = 1;
+ $indexes = [
+ [
+ '@name' => 'matchgrid_i'.$i++,
+ 'col' => 'sor'
+ ],
+ [
+ '@name' => 'matchgrid_i'.$i++,
+ 'col' => 'sorid'
+ ],
+ [
+ '@name' => 'matchgrid_i'.$i++,
+ 'col' => ['sor','sorid'],
+ 'unique' => []
+ ],
+ [
+ '@name' => 'matchgrid_i'.$i++,
+ 'col' => 'referenceid'
+ ],
+ /* The XML Schema can't handle the specification of NULLS FIRST, so we
+ need to create that index manually.
+ [
+ '@name' => 'matchgrid_i'.$i++,
+ 'col' => 'resolution_time',
+// Need custom SQL for this (flag @POSTGRESSPECIFIC ?)
+// 'nulls first' => []
+ ]*/
+ ];
+
+ $dict = NewDataDictionary($dbc);
+ // XXX we could skip the $i++ for index names and just use the attribute names matchgrid_attr_sor
+ // (since these shouldn't get renamed by admins)
+ // createIndexSql also generates a DROP INDEX if we pass REPLACE, however (contrary to
+ // the documentation at http://adodb.org/dokuwiki/doku.php?id=v5:dictionary:createindexsql)
+ // we can't add NULLS FIRST this way.
+ $sql = $dict->createIndexSql('matchgrid_i'.$i++, $tablename, 'resolution_time', ['REPLACE']);
+
+ $sql[1] = rtrim($sql[1], ")") . " NULLS FIRST)";
+
+ // Add in indexes for configured fields
+ foreach($attributes as $attr) {
+ $indexes[] = [
+ // We use the Entity ID to provide some level of reproducibility
+ '@name' => 'matchgrid_attr_id'.$attr->id,
+ 'col' => $attr->name
+ ];
+ }
+
+ // Assemble the schema (ADOdb AXMLS format)
+ $schema = [
+ 'schema' => [
+ '@version' => '0.3',
+ 'table' => [
+ '@name' => $tablename,
+ 'field' => $fields,
+ 'index' => $indexes
+ ],
+ 'sql' => [
+ 'query' => $sql
+ ]
+ ]
+ ];
+
+ // Convert the schema to XML
+ $xobj = \Cake\Utility\Xml::fromArray($schema, array('format' => 'tags'));
+
+ return $xobj->asXML();
+ }
+
+ /**
+ * Connect to the Database.
+ *
+ * @since COmanage Match v1.0.0
+ * @throws RuntimeException
+ */
+
+ protected function connect() {
+ // There's some overlap between here and DatabaseShell.
+
+ // Use the ConnectionManager to get the database config to pass to adodb.
+ $db = ConnectionManager::get('default');
+
+ // $db is a ConnectionInterface object
+ $cfg = $db->config();
+
+ // We only support Postgres (at least for now)
+ if($cfg['driver'] != "Cake\Database\Driver\Postgres") {
+ throw new \RuntimeException(__('match.er.db.driver' , [ $cfg['driver'] ]));
+ }
+
+ // // This really imples postgres8+
+ $dbc = ADONewConnection('postgres9');
+
+ if(!$dbc->Connect($cfg['host'],
+ $cfg['username'],
+ $cfg['password'],
+ $cfg['database'])) {
+ throw new \RuntimeException(__('match.er.db.connect', [$dbc->ErrorMsg()]));
+ }
+
+ return $dbc;
+ }
+
+ /**
+ * Run the specified ADOdb Schema.
+ *
+ * @since COmanage Match v1.0.0
+ * @param ADOConnection $dbc ADOdb Connection Object
+ * @param string $xml XML document, returned by configToSchema
+ */
+
+ protected function runSchema($dbc, string $xml) {
+ $schema = new \adoSchema($dbc);
+
+ // ParseSchema is generating bad SQL for Postgres. eg:
+ // ALTER TABLE cm_cos ALTER COLUMN id SERIAL
+ // which (1) should be ALTER TABLE cm_cos ALTER COLUMN id TYPE SERIAL
+ // and (2) SERIAL isn't usable in an ALTER TABLE statement
+ // So we continue on error
+ // See also CO-1570, etc
+ $schema->ContinueOnError(true);
+
+ // Parse the XML schema we were passed
+ $sql = $schema->ParseSchemaString($xml);
+
+ switch($schema->ExecuteSchema($sql)) {
+ case 2: // !!!
+// $this->out(__('Database schema update successful'));
+ break;
+ default:
+// $this->out(__('Possibly failed to update database schema'));
+ break;
+ }
+
+ // XXX After CO-1570 is addressed we should return true/false (or throw an
+ // exception on error) so an error message can percolate back up the stack.
+ }
+}
diff --git a/app/src/Lib/Match/PostgresService.php b/app/src/Lib/Match/PostgresService.php
new file mode 100644
index 00000000..e3327f50
--- /dev/null
+++ b/app/src/Lib/Match/PostgresService.php
@@ -0,0 +1,87 @@
+config();
+
+ // We only support Postgres (at least for now)
+ if($cfg['driver'] != "Cake\Database\Driver\Postgres") {
+ throw new \RuntimeException(__('match.er.db.driver' , [ $cfg['driver'] ]));
+ }
+
+ // // This really imples postgres8+
+ $this->dbc = ADONewConnection('postgres9');
+
+ if(!$this->dbc->Connect($cfg['host'],
+ $cfg['username'],
+ $cfg['password'],
+ $cfg['database'])) {
+ throw new \RuntimeException(__('match.er.db.connect', [$this->dbc->ErrorMsg()]));
+ }
+
+ // We only want keys based on column names
+ $this->dbc->setFetchMode(ADODB_FETCH_ASSOC);
+
+ return $this->dbc;
+ }
+
+ /**
+ * Disconnect from the database.
+ *
+ * @since COmanage Match v1.0.0
+ */
+
+ public function disconnect() {
+ if($this->dbc) {
+ $this->dbc->Disconnect();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/Lib/Match/ResultManager.php b/app/src/Lib/Match/ResultManager.php
new file mode 100644
index 00000000..4d3272a1
--- /dev/null
+++ b/app/src/Lib/Match/ResultManager.php
@@ -0,0 +1,276 @@
+results[$referenceId][$rowId] = $attributes;
+
+ $referenceId = null;
+ $rowId = null;
+ $parsed = [];
+
+ foreach($attributes as $name => $value) {
+ if($name == 'id') {
+ $rowId = $value;
+ } elseif($name == 'referenceid') {
+ $referenceId = $value;
+ } else {
+ // Store keyed on the API name, not the database name
+ if(!empty($this->attrconfig[$name])) {
+ $parsed[ $this->attrconfig[$name] ] = $value;
+ } else {
+ // eg: sor
+ $parsed[$name] = $value;
+ }
+ }
+ }
+
+// XXX throw error if !$referenceId || $rowid
+ $this->results[$referenceId][$rowId] = $parsed;
+
+ $this->rawResults[$rowId] = $attributes;
+ }
+
+ /**
+ * Obtain the number of results.
+ *
+ * @since COmanage Match v1.0.0
+ * @return int Result count
+ */
+
+ public function count() {
+ return count($this->results);
+ }
+
+ /**
+ * Get the Confidence Mode for this set of results.
+ *
+ * @since COmanage Match v1.0.0
+ * @return ConfidenceModeEnum Confidence Mode
+ */
+
+ public function getConfidenceMode() {
+ return $this->confidenceMode;
+ }
+
+ /**
+ * Obtain the raw array of results.
+ *
+ * @since COmanage Match v1.0.0
+ * @return Array Results
+ */
+
+ public function getRawResults() {
+ return $this->rawResults;
+ }
+
+ /**
+ * Obtain an array of reference IDs in the result set.
+ *
+ * @since COmanage Match v1.0.0
+ * @return Array Reference IDs
+ */
+
+ public function getReferenceIds() {
+ return array_keys($this->results);
+ }
+
+ /**
+ * Obtain the results in an array format suitable for converting to JSON.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $mode "search" (Search Reference ID), "current" (Request Current Values), or "pending" (Request Pending/Resolved Matches)
+ * @return array JSON-ready results
+ */
+
+ public function getResultsForJson($mode="search") {
+// XXX why does this set everything BUT the top level attribute? ("candidates" or "matchRequests")
+ $ret = [];
+
+ foreach($this->results as $referenceId => $sorRow) {
+// XXX note $candidate is NOT used by mode=pending
+ $candidate = ['referenceId' => $referenceId];
+// XXX add confidence?
+
+ foreach($sorRow as $rowId => $attrs) {
+ $parsed = ['matchRequest' => $rowId];
+
+ // We'll sort complex attributes by type before converting them
+ // to the required format
+ $complex = [];
+
+ foreach($attrs as $a => $v) {
+ // Should we skip empty values? We do for now...
+ if(empty($v))
+ continue;
+
+ if(strstr($a, ':')) {
+ // eg: names:given
+ $super = explode(':', $a, 2);
+ // eg: given/official
+ $sub = explode('/', $super[1], 2);
+
+ if(!empty($sub[1])) {
+ $complex[ $super[0] ][ $sub[1] ][ $sub[0] ] = $v;
+ }
+ } elseif($a == 'sorid') {
+// XXX should this be handled via Attribute Manager? or a config entry?
+ // Special case
+ $parsed['identifiers'][] = [
+ "type" => "sor",
+ "identifier" => $v
+ ];
+ } else {
+ // eg: dateOfBirth
+ $parsed[$a] = $v;
+ }
+ }
+
+ // Convert complex attributes into the correct wire format
+ foreach(array_keys($complex) as $attr) {
+ foreach($complex[$attr] as $t => $v) {
+ $v['type'] = $t;
+
+ $parsed[$attr][] = $v;
+ }
+ }
+
+ if($mode == 'current') {
+ // There should only be one entry, so return it directly
+ $candidate['sorAttributes'] = $parsed;
+
+ // Bump up request and resolution times
+ $candidate['requestTime'] = strftime("%FT%TZ",
+ strtotime($candidate['sorAttributes']['request_time']));
+
+ unset($candidate['sorAttributes']['request_time']);
+
+// XXX Note resolutionTime not yet defined in strawman
+ if(!empty($candidate['sorAttributes']['resolution_time'])) {
+ $candidate['resolutionTime'] = strftime("%FT%TZ",
+ strtotime($candidate['sorAttributes']['resolution_time']));
+ }
+ unset($candidate['sorAttributes']['resolution_time']);
+
+ return $candidate;
+ } elseif($mode == 'pending') {
+ $ret[$rowId] = ['attributes' => $parsed];
+
+ // Bump up request and resolution times
+ $ret[$rowId]['requestTime'] = strftime("%FT%TZ", strtotime($parsed['request_time']));
+
+ unset($ret[$rowId]['attributes']['request_time']);
+
+ if(!empty($parsed['resolution_time'])) {
+ $ret[$rowId]['resolutionTime'] = strftime("%FT%TZ", strtotime($parsed['resolution_time']));
+ }
+ unset($ret[$rowId]['attributes']['resolution_time']);
+
+ if(!empty($referenceId)) {
+ $ret[$rowId]['referenceId'] = $referenceId;
+ }
+ } else {
+// XXX request/resolution time are not defined in the strawman for potential match
+// results, should they be?
+ unset($parsed['request_time']);
+ unset($parsed['resolution_time']);
+
+ $candidate['attributes'][] = $parsed;
+
+ $ret[] = $candidate;
+ }
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Set the Confidence Mode for this set of results.
+ *
+ * @since COmanage Match v1.0.0
+ * @param ConfidenceModeEnum $mode Confidence Mode
+ */
+
+ public function setConfidenceMode(string $mode) {
+ $this->confidenceMode = $mode;
+ }
+
+ /**
+ * Set the attribute configuration.
+ *
+ * @since COmanage Match v1.0.0
+ * @param array $config Array of Attributes (and nested AttributeGroups)
+ */
+
+ public function setConfig(array $config) {
+ // Initially we only need the database name -> api name mapping.
+
+ foreach($config as $attr) {
+ $apiname = $attr->api_name;
+
+ if(!empty($attr->attribute_group->name)) {
+ // Append the Attribute Group name to the end of the name, since (for
+ // now, anyway) attribute groups are just types within a complex attribute.
+
+ $apiname .= '/' . $attr->attribute_group->name;
+ }
+
+ $this->attrconfig[ $attr->name ] = $apiname;
+ }
+ }
+}
diff --git a/app/src/Lib/Traits/AssociationTrait.php b/app/src/Lib/Traits/AssociationTrait.php
new file mode 100644
index 00000000..c53729ac
--- /dev/null
+++ b/app/src/Lib/Traits/AssociationTrait.php
@@ -0,0 +1,107 @@
+editContains;
+ }
+
+ /**
+ * Obtain the set of associated models to save during a patch.
+ *
+ * @since COmanage Match v1.0.0
+ * @return array Array of associated models
+ */
+
+ public function getPatchAssociated() {
+ return $this->patchAssociated;
+ }
+
+ /**
+ * Obtain the set of associated models to pull during a view.
+ *
+ * @since COmanage Match v1.0.0
+ * @return array Array of associated models
+ */
+
+ public function getViewContains() {
+ return $this->viewContains;
+ }
+
+ /**
+ * Set the associated models to pull during an edit.
+ *
+ * @since COmanage Match v1.0.0
+ * @param array $c Array of associated models
+ */
+
+ public function setEditContains(array $c) {
+ $this->editContains = $c;
+ }
+
+ /**
+ * Set the associated models to save during a patch.
+ *
+ * @since COmanage Match v1.0.0
+ * @param array $a Array of associated models
+ */
+
+ public function setPatchAssociated(array $a) {
+ $this->patchAssociated = $a;
+ }
+
+ /**
+ * Set the associated models to pull during a view.
+ *
+ * @since COmanage Match v1.0.0
+ * @param array $c Array of associated models
+ */
+
+ public function setViewContains(array $c) {
+ $this->viewContains = $c;
+ }
+}
diff --git a/app/src/Lib/Traits/AutoViewVarsTrait.php b/app/src/Lib/Traits/AutoViewVarsTrait.php
new file mode 100644
index 00000000..d5b59d43
--- /dev/null
+++ b/app/src/Lib/Traits/AutoViewVarsTrait.php
@@ -0,0 +1,57 @@
+autoViewVars;
+ }
+
+ /**
+ * Set the auto view variables.
+ *
+ * @since COmanage Match v1.0.0
+ * @param array $vars Array of auto view variables
+ */
+
+ public function setAutoViewVars($vars) {
+ $this->autoViewVars = $vars;
+ }
+}
diff --git a/app/src/Lib/Traits/MatchgridLinkTrait.php b/app/src/Lib/Traits/MatchgridLinkTrait.php
new file mode 100644
index 00000000..1a0a3ead
--- /dev/null
+++ b/app/src/Lib/Traits/MatchgridLinkTrait.php
@@ -0,0 +1,89 @@
+unkeyedActions, true);
+ }
+
+ /**
+ * Calculate the Matchgrid ID associated with the requested object ID.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Integer $id Matchgrid ID
+ * @return Integer Matchgrid ID
+ * @throws Cake\Datasource\Exception\RecordNotFoundException
+ */
+
+ public function calculateMatchgridId(int $id) {
+ // For now we assume we have a direct foreign key to Matchgrids.
+
+ $obj = $this->findById($id)->firstOrFail();
+
+ return $obj->matchgrid_id;
+ }
+
+ /**
+ * Determine if the associated controller requires a Matchgrid ID.
+ *
+ * @since COmanage Match v1.0.0
+ * @return Boolean True if a Matchgrid ID is required, false otherwise
+ */
+
+ public function requiresMatchgrid() {
+ return $this->requiresMatchgrid;
+ }
+
+ /**
+ * Set if the associated controller requires a Matchgrid ID.
+ *
+ * @since COmanage Match v1.0.0
+ * @param $required Boolean True if a Matchgrid ID is required, false otherwise
+ */
+
+ public function setRequiresMatchgrid(bool $required) {
+ $this->requiresMatchgrid = $required;
+ }
+}
diff --git a/app/src/Lib/Traits/PrimaryLinkTrait.php b/app/src/Lib/Traits/PrimaryLinkTrait.php
new file mode 100644
index 00000000..dda7a28b
--- /dev/null
+++ b/app/src/Lib/Traits/PrimaryLinkTrait.php
@@ -0,0 +1,70 @@
+where([$this->getPrimaryLink() => $options[$this->primaryLink]]);
+ }
+
+ /**
+ * Obtain the primary link.
+ *
+ * @since COmanage Match v1.0.0
+ * @return string Primary link attribute
+ */
+
+ public function getPrimaryLink() {
+ return $this->primaryLink;
+ }
+
+ /**
+ * Set the primary link attribute.
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $field Primary link attribute
+ */
+
+ public function setPrimaryLink($field) {
+ $this->primaryLink = $field;
+ }
+}
diff --git a/app/src/Locale/en_US/default.po b/app/src/Locale/en_US/default.po
new file mode 100644
index 00000000..f80d1080
--- /dev/null
+++ b/app/src/Locale/en_US/default.po
@@ -0,0 +1,353 @@
+# COmanage Match 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 http://www.internet2.edu/comanage COmanage Project
+# @package match
+# @since COmanage Match v1.0.0
+# @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+# This should match the ISO 639-1 two letter language code for the translation
+msgid "match.meta.lang"
+msgstr "en"
+
+msgid "match.meta.match"
+msgstr "COmanage Match"
+
+msgid "match.meta.match.a"
+msgstr "COmanage Match: {0}"
+
+msgid "match.meta.powered"
+msgstr "Powered By"
+
+msgid "match.meta.version"
+msgstr "Version {0}"
+
+### Command Line text
+msgid "match.cmd.db.ok"
+msgstr "Database schema update successful"
+
+msgid "match.cmd.db.schema"
+msgstr "- Loading database schema from {0}"
+
+msgid "match.cmd.opt.admin-username"
+msgstr "Username of initial platform administrator"
+
+msgid "match.cmd.opt.force"
+msgstr "Force a rerun of setup (only if you know what you are doing)""
+
+msgid "match.cmd.se.admin"
+msgstr "- Creating initial administrator permission"
+
+msgid "match.cmd.se.admin.user"
+msgstr "Enter administrator's login username"
+
+msgid "match.cmd.se.already"
+msgstr "Setup appears to have already run"
+
+msgid "match.cmd.se.salt"
+msgstr "- Generating salt file"
+
+### Controllers (Models)
+msgid "match.ct.attribute_groups"
+msgstr "{0,plural,=1{Attribute Group} other{Attribute Groups}}"
+
+msgid "match.ct.attributes"
+msgstr "{0,plural,=1{Attribute} other{Attributes}}"
+
+msgid "match.ct.matchgrids"
+msgstr "{0,plural,=1{Matchgrid} other{Matchgrids}}"
+
+msgid "match.ct.permissions"
+msgstr "{0,plural,=1{Permission} other{Permissions}}"
+
+msgid "match.ct.systems_of_record"
+msgstr "{0,plural,=1{System of Record} other{Systems of Record}}"
+
+# XXX toss?
+# msgid "match.ct.rule_set_attributes"
+# msgstr "{0,plural,=1{Rule Set Attribute} other{Rule Set Attributes}}"
+
+msgid "match.ct.rules"
+msgstr "{0,plural,=1{Rule} other{Rules}}"
+
+### Enumerations
+msgid "match.en.ConfidenceModeEnum.C"
+msgstr "Canonical"
+
+msgid "match.en.ConfidenceModeEnum.P"
+msgstr "Potential"
+
+msgid "match.en.PermissionEnum.A"
+msgstr "Platform Administrator"
+
+msgid "match.en.PermissionEnum.MA"
+msgstr "Matchgrid Administrator"
+
+msgid "match.en.PermissionEnum.RM"
+msgstr "Reconciliation Manager"
+
+msgid "match.en.PermissionEnum.RS"
+msgstr "Reconciliation Support"
+
+msgid "match.en.PermissionEnum.X"
+msgstr "None"
+
+msgid "match.en.ReferenceIdEnum.S"
+msgstr "Sequence"
+
+msgid "match.en.ReferenceIdEnum.U"
+msgstr "UUID (Type 4)"
+
+msgid "match.en.ResolutionModeEnum.E"
+msgstr "External"
+
+msgid "match.en.ResolutionModeEnum.I"
+msgstr "Interactive"
+
+msgid "match.en.SearchTypeEnum.D"
+msgstr "Distance"
+
+msgid "match.en.SearchTypeEnum.E"
+msgstr "Exact"
+
+msgid "match.en.SearchTypeEnum.X"
+msgstr "Skip"
+
+msgid "match.en.SearchTypeEnum.S"
+msgstr "Substring"
+
+msgid "match.en.StatusEnum.A"
+msgstr "Active"
+
+msgid "match.en.StatusEnum.S"
+msgstr "Suspended"
+
+### Error Messages
+msgid "match.er.args"
+msgstr "Incorrect arguments provided to {0}"
+
+msgid "match.er.build"
+msgstr "Error applying matchgrid schema: {0}"
+
+msgid "match.er.db.connect"
+msgstr "Failed to connect to database: {0}"
+
+msgid "match.er.db.driver"
+msgstr "Unsupported database driver: {0}"
+
+msgid "match.er.db.schema"
+msgstr "Possibly failed to update database schema"
+
+msgid "match.er.delete"
+msgstr "Delete Failed"
+
+msgid "match.er.file"
+msgstr "Cannot read file {0}"
+
+msgid "match.er.mgid"
+msgstr "Could not find Matchgrid ID in request"
+
+msgid "match.er.parse.json"
+msgstr "Unknown type {0} for key {1} (parseFromJSON)"
+
+msgid "match.er.primary_link"
+msgstr "Could not find value for Primary Link {0}"
+
+msgid "match.er.reconcile"
+msgstr "Error obtaining pending requests: {0}"
+
+msgid "match.er.reconcile.done"
+msgstr "Request ID {0} already resolved"
+
+msgid "match.er.reconcile.notfound"
+msgstr "Request ID {0} not found"
+
+msgid "match.er.save"
+msgstr "Save Failed ({0})"
+
+# XXX rekey?
+msgid "matchgrid.er.search_type"
+msgstr "Unknown search type '{0}'"
+
+### Fields
+msgid "match.fd.action"
+msgstr "Action"
+
+msgid "match.fd.alphanumeric"
+msgstr "Alphanumeric"
+
+msgid "match.fd.api_name"
+msgstr "API Name"
+
+msgid "match.fd.case_sensitive"
+msgstr "Case Sensitive"
+
+msgid "match.fd.confidence_mode"
+msgstr "Confidence Mode"
+
+msgid "match.fd.description"
+msgstr "Description"
+
+msgid "match.fd.invalidates"
+msgstr "Invalidates"
+
+msgid "match.fd.label"
+msgstr "Label"
+
+msgid "match.fd.name"
+msgstr "Name"
+
+msgid "match.fd.null_equivalents"
+msgstr "Null Equivalents"
+
+msgid "match.fd.ordr"
+msgstr "Order"
+
+msgid "match.fd.permission"
+msgstr "Permission"
+
+msgid "match.fd.referenceid"
+msgstr "Reference ID"
+
+msgid "match.fd.referenceid_method"
+msgstr "Reference ID Assignment Method"
+
+msgid "match.fd.referenceid_start"
+msgstr "Reference ID Initial Value"
+
+msgid "match.fd.referenceid_start.desc"
+msgstr "For sequence based Reference IDs, the first value to assign"
+
+msgid "match.fd.req"
+msgstr "* Denotes Required Field"
+
+msgid "match.fd.request_time"
+msgstr "Request Time"
+
+msgid "match.fd.required"
+msgstr "Required"
+
+msgid "match.fd.resolution_mode"
+msgstr "Resolution Mode"
+
+msgid "match.fd.search_distance"
+msgstr "Search Distance"
+
+msgid "match.fd.search_exact"
+msgstr "Search Exact"
+
+msgid "match.fd.search_substr_from"
+msgstr "Search Substring From"
+
+msgid "match.fd.search_substr_for"
+msgstr "Search Substring For"
+
+msgid "match.fd.search_types"
+msgstr "Search Types"
+
+msgid "match.fd.sor"
+msgstr "System of Record"
+
+msgid "match.fd.sorid"
+msgstr "System of Record ID"
+
+msgid "match.fd.status"
+msgstr "Status"
+
+msgid "match.fd.table_name"
+msgstr "Table Name"
+
+msgid "match.fd.table_name.desc"
+msgstr "Unique, alphanumeric name for matchgrid (will be prefixed mg_ for actual table name)"
+
+msgid "match.fd.username"
+msgstr "Username"
+
+### Informational Messages
+msgid "match.in.matchgrids.none"
+msgstr "There are no matchgrids currently defined."
+
+### Menu Items
+msgid "match.me.platform"
+msgstr "Platform"
+
+### Operations (Commands)
+msgid "match.op.add.a"
+msgstr "Add New {0}"
+
+msgid "match.op.build"
+msgstr "Build"
+
+msgid "match.op.build.confirm"
+msgstr "Are you sure you wish to (re)build this matchgrid?"
+
+msgid "match.op.delete"
+msgstr "Delete"
+
+msgid "match.op.delete.confirm"
+msgstr "Are you sure you wish to delete this record ({0})?"
+
+msgid "match.op.edit"
+msgstr "Edit"
+
+msgid "match.op.edit.a"
+msgstr "Edit {0}"
+
+msgid "match.op.manage"
+msgstr "Manage"
+
+msgid "match.op.manage.a"
+msgstr "Manage {0}"
+
+msgid "match.op.new"
+msgstr "New"
+
+msgid "match.op.reconcile"
+msgstr "Reconcile Unresolved Requests"
+
+msgid "match.op.reconcile.a"
+msgstr "Reconcile Unresolved Requests ({0})"
+
+msgid "match.op.reconcile.request"
+msgstr "Reconcile Unresolved Request {0}/{1}"
+
+msgid "match.op.reconcile.assign"
+msgstr "Assign This Reference ID"
+
+msgid "match.op.save"
+msgstr "Save"
+
+### Results
+msgid "match.rs.build"
+msgstr "Matchgrid schema successfully applied"
+
+msgid "match.rs.deleted"
+msgstr "Deleted"
+
+msgid "match.rs.deleted.a"
+msgstr "{0} Deleted"
+
+msgid "match.rs.pending"
+msgstr "{0,plural,=1{# Pending Match} other{# Pending Matches}}"
+
+msgid "match.rs.refid.assigned"
+msgstr "Assigned Reference ID {0}"
+
+msgid "match.rs.saved"
+msgstr "Saved"
diff --git a/app/src/Model/Behavior/empty b/app/src/Model/Behavior/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/app/src/Model/Entity/Attribute.php b/app/src/Model/Entity/Attribute.php
new file mode 100644
index 00000000..bac47a8f
--- /dev/null
+++ b/app/src/Model/Entity/Attribute.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/AttributeGroup.php b/app/src/Model/Entity/AttributeGroup.php
new file mode 100644
index 00000000..ab988db3
--- /dev/null
+++ b/app/src/Model/Entity/AttributeGroup.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Matchgrid.php b/app/src/Model/Entity/Matchgrid.php
new file mode 100644
index 00000000..c61d9f54
--- /dev/null
+++ b/app/src/Model/Entity/Matchgrid.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Permission.php b/app/src/Model/Entity/Permission.php
new file mode 100644
index 00000000..c84799ed
--- /dev/null
+++ b/app/src/Model/Entity/Permission.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/Rule.php b/app/src/Model/Entity/Rule.php
new file mode 100644
index 00000000..f83b3d74
--- /dev/null
+++ b/app/src/Model/Entity/Rule.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/RuleAttribute.php b/app/src/Model/Entity/RuleAttribute.php
new file mode 100644
index 00000000..854255d8
--- /dev/null
+++ b/app/src/Model/Entity/RuleAttribute.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Entity/SystemOfRecord.php b/app/src/Model/Entity/SystemOfRecord.php
new file mode 100644
index 00000000..fdea2638
--- /dev/null
+++ b/app/src/Model/Entity/SystemOfRecord.php
@@ -0,0 +1,40 @@
+ true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/AttributeGroupsTable.php b/app/src/Model/Table/AttributeGroupsTable.php
new file mode 100644
index 00000000..2687f463
--- /dev/null
+++ b/app/src/Model/Table/AttributeGroupsTable.php
@@ -0,0 +1,83 @@
+addBehavior('Timestamp');
+
+ // Define associations
+ $this->belongsTo('Matchgrids');
+ $this->hasMany('Attributes');
+
+ $this->setDisplayField('name');
+
+ $this->setPrimaryLink('matchgrid_id');
+ $this->setRequiresMatchgrid(true);
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator) {
+ $validator->add(
+ 'name',
+ 'length',
+ [ 'rule' => [ 'maxLength', 32 ] ]
+ );
+ $validator->notEmpty('name');
+
+ $validator->add(
+ 'matchgrid_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('matchgrid_id');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/AttributesTable.php b/app/src/Model/Table/AttributesTable.php
new file mode 100644
index 00000000..ce45bc4f
--- /dev/null
+++ b/app/src/Model/Table/AttributesTable.php
@@ -0,0 +1,178 @@
+addBehavior('Timestamp');
+
+ // Define associations
+ $this->belongsTo('AttributeGroups');
+ $this->belongsTo('Matchgrids');
+ //$this->belongsToMany('Rules', ['through' => 'RuleAttributes']);
+ $this->hasMany('RuleAttributes');
+
+ $this->setDisplayField('name');
+
+ $this->setPrimaryLink('matchgrid_id');
+ $this->setRequiresMatchgrid(true);
+
+ $this->setAutoViewVars([
+ 'attributeGroups' => [
+ 'type' => 'select',
+ 'model' => 'AttributeGroups',
+ 'find' => 'filterPrimaryLink'
+ ]
+ ]);
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator) {
+ $validator->add(
+ 'name',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->notEmpty('username');
+
+ $validator->add(
+ 'matchgrid_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('matchgrid_id');
+
+ $validator->add(
+ 'description',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->allowEmpty('description');
+
+ $validator->add(
+ 'api_name',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->notEmpty('api_name');
+
+ $validator->add(
+ 'attribute_group_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->allowEmpty('attribute_group_id');
+
+ $validator->add(
+ 'alphanumeric',
+ 'toggle',
+ [ 'rule' => [ 'boolean' ] ]
+ );
+ $validator->notEmpty('alphanumeric');
+
+ $validator->add(
+ 'case_sensitive',
+ 'toggle',
+ [ 'rule' => [ 'boolean' ] ]
+ );
+ $validator->notEmpty('case_sensitive');
+
+ $validator->add(
+ 'invalidates',
+ 'toggle',
+ [ 'rule' => [ 'boolean' ] ]
+ );
+ $validator->notEmpty('invalidates');
+
+ $validator->add(
+ 'null_equivalents',
+ 'toggle',
+ [ 'rule' => [ 'boolean' ] ]
+ );
+ $validator->notEmpty('null_equivalents');
+
+ $validator->add(
+ 'required',
+ 'toggle',
+ [ 'rule' => [ 'boolean' ] ]
+ );
+ $validator->notEmpty('required');
+
+ $validator->add(
+ 'search_distance',
+ 'content',
+ [ 'rule' => [ 'range', 1, 9 ] ]
+ );
+ $validator->allowEmpty('search_distance');
+
+ $validator->add(
+ 'search_exact',
+ 'toggle',
+ [ 'rule' => [ 'boolean' ] ]
+ );
+ $validator->notEmpty('search_exact');
+
+ $validator->add(
+ 'search_substr_from',
+ 'content',
+ [ 'rule' => [ 'range', 0, 128 ] ]
+ );
+ $validator->allowEmpty('search_substr_from');
+
+ $validator->add(
+ 'search_substr_for',
+ 'content',
+ [ 'rule' => [ 'range', 1, 128 ] ]
+ );
+ $validator->allowEmpty('search_substr_for');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/MatchgridsTable.php b/app/src/Model/Table/MatchgridsTable.php
new file mode 100644
index 00000000..f7579d9a
--- /dev/null
+++ b/app/src/Model/Table/MatchgridsTable.php
@@ -0,0 +1,208 @@
+addBehavior('Timestamp');
+
+ // Define associations
+ $this->hasMany('Attributes')
+ ->setDependent(true);
+ $this->hasMany('AttributeGroups')
+ ->setDependent(true);
+ $this->hasMany('Permissions')
+ ->setDependent(true);
+ $this->hasMany('Rules')
+ ->setDependent(true);
+ // "Convenience" associations to separate different rule confidence modes
+ $this->hasMany('CanonicalRules', ['className' => 'Rules'])
+ ->setConditions(['confidence_mode' => ConfidenceModeEnum::Canonical]);
+ $this->hasMany('PotentialRules', ['className' => 'Rules'])
+ ->setConditions(['confidence_mode' => ConfidenceModeEnum::Potential]);
+
+ $this->setDisplayField('table_name');
+
+ $this->setAutoViewVars([
+ 'referenceidMethods' => [
+ 'type' => 'enum',
+ 'class' => 'ReferenceIdEnum'
+ ],
+ 'statuses' => [
+ 'type' => 'enum',
+ 'class' => 'StatusEnum'
+ ]
+ ]);
+ }
+
+ /**
+ * Build the specified Matchgrid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param int $id Matchgrid ID
+ * @return bool True on success
+ */
+
+ public function build(int $id) {
+ $matchgrid = $this->getMatchgridConfig($id);
+
+ $Builder = new MatchgridBuilder();
+
+ $Builder->build("mg_" . $matchgrid->table_name, $matchgrid->attributes);
+
+ return true;
+ }
+
+ /**
+ * Calculate the Matchgrid ID associated with the requested object ID.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Integer $id Matchgrid ID
+ * @return Integer Matchgrid ID
+ * @throws Cake\Datasource\Exception\RecordNotFoundException
+ */
+
+ public function calculateMatchgridId(int $id) {
+ // In this case, $id is a matchgrid ID, so just return it. (Other models
+ // will use MatchgridLinkTrait, which this class overrides.)
+
+ // Verify that $id is valid. (Note MatchgridLinkTrait does a similar test.)
+ $mg = $this->findById($id)->firstOrFail();
+
+ return $mg->id;
+ }
+
+ /**
+ * Modify Query for active Matchgrids.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Query $query Cake Query object
+ * @param array $options Cake Query options
+ * @return [type] [description]
+ */
+
+ public function findActiveMatchgrids(Query $query, array $options) {
+ return $query->where(['status' => StatusEnum::Active])->select(['id', 'table_name']);
+ }
+
+ /**
+ * Get the configuration for the specified Matchgrid.
+ *
+ * @since COmanage Match v1.0.0
+ * @param int $id Matchgrid ID
+ * @return Cake\Datasource\EntityInterface Matchgrid object
+ * @throws Cake\Datasource\Exception\InvalidPrimaryKeyException
+ */
+
+ protected function getMatchgridConfig($id) {
+ return $this->get($id,
+ ['contain' => [
+ 'Attributes' => 'AttributeGroups'
+ ]]);
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator) {
+ $validator->add(
+ 'table_name',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->add(
+ 'table_name',
+ 'content',
+ [ 'rule' => [ 'custom', '/^[a-zA-Z0-9_$]+/' ] ]
+ );
+ $validator->notEmpty('table_name');
+
+ $validator->add(
+ 'description',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->allowEmpty('description');
+
+ $validator->add(
+ 'status',
+ 'content',
+ [ 'rule' => [ 'inList', [
+ StatusEnum::Active,
+ StatusEnum::Suspended
+ ] ] ]
+ );
+ $validator->notEmpty('status');
+
+ $validator->add(
+ 'referenceid_method',
+ 'content',
+ [ 'rule' => [ 'inList', [
+ ReferenceIdEnum::Sequence,
+ ReferenceIdEnum::UUID
+ ] ] ]
+ );
+ $validator->notEmpty('referenceid_method');
+
+ $validator->add(
+ 'referenceid_start',
+ 'content',
+ [ 'rule' => [ 'range', 1, null ] ]
+ );
+ $validator->allowEmpty('referenceid_start');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/MetaTable.php b/app/src/Model/Table/MetaTable.php
new file mode 100644
index 00000000..7027b549
--- /dev/null
+++ b/app/src/Model/Table/MetaTable.php
@@ -0,0 +1,78 @@
+newEntity();
+ $meta->id = 1;
+ $meta->upgrade_version = $version;
+
+ if(!$this->save($meta)) {
+ throw new \RuntimeException(__('match.er.save', ['Meta']));
+ }
+
+ return true;
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator) {
+ $validator->add(
+ 'upgrade_version',
+ 'length',
+ [ 'rule' => [ 'maxLength', 16 ] ]
+ );
+ $validator->notEmpty('upgrade_version');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/PermissionsTable.php b/app/src/Model/Table/PermissionsTable.php
new file mode 100644
index 00000000..09abc24a
--- /dev/null
+++ b/app/src/Model/Table/PermissionsTable.php
@@ -0,0 +1,121 @@
+addBehavior('Timestamp');
+
+ // Define associations
+ $this->belongsTo('Matchgrids');
+
+ $this->setDisplayField('username');
+
+ $this->setAutoViewVars([
+ 'matchgrids' => [
+ 'type' => 'select',
+ 'model' => 'Matchgrids',
+ 'find' => 'activeMatchgrids'
+ ],
+ 'permissions' => [
+ 'type' => 'enum',
+ 'class' => 'PermissionEnum'
+ ]
+ ]);
+ }
+
+ /**
+ * Obtain the Permissions for the specified user.
+ *
+ * NOTE: We're using a name that matches Cakes "magic" syntax, but we're not actually following it
+ * (ie: for ->find('forUser'), but we're returning the wrong thing)
+ *
+ * @since COmanage Match v1.0.0
+ * @param string $username Username to obtain Permissions for
+ * @return array Array of user Permissions
+ */
+
+ public function findForUser(string $username) {
+ return $this->find('list', ['keyField' => 'matchgrid_id', 'valueField' => 'permission'])
+ ->where(['username' => $username])
+ ->toArray();
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator) {
+ $validator->add(
+ 'username',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->notEmpty('username');
+
+ $validator->add(
+ 'matchgrid_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->allowEmpty('matchgrid_id');
+
+ $validator->add(
+ 'permission',
+ 'content',
+ [ 'rule' => [ 'inList', [
+ PermissionEnum::MatchgridAdmin,
+ PermissionEnum::PlatformAdmin,
+ PermissionEnum::ReconciliationManager,
+ PermissionEnum::ReconciliationSupport,
+ PermissionEnum::None
+ ] ] ]
+ );
+ $validator->notEmpty('permission');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/RuleAttributesTable.php b/app/src/Model/Table/RuleAttributesTable.php
new file mode 100644
index 00000000..45c5b8c9
--- /dev/null
+++ b/app/src/Model/Table/RuleAttributesTable.php
@@ -0,0 +1,90 @@
+addBehavior('Timestamp');
+
+ // Define associations
+ $this->belongsTo('Attributes');
+ $this->belongsTo('Rules');
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator) {
+ $validator->add(
+ 'rule_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('rule_id');
+
+ $validator->add(
+ 'attribute_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('attribute_id');
+
+ $validator->add(
+ 'search_type',
+ 'content',
+ [ 'rule' => [ 'inList', [
+ SearchTypeEnum::Distance,
+ SearchTypeEnum::Exact,
+ SearchTypeEnum::Skip,
+ SearchTypeEnum::Substring
+ ] ] ]
+ );
+ $validator->notEmpty('search_type');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/RulesTable.php b/app/src/Model/Table/RulesTable.php
new file mode 100644
index 00000000..56f04eaa
--- /dev/null
+++ b/app/src/Model/Table/RulesTable.php
@@ -0,0 +1,132 @@
+addBehavior('Timestamp');
+
+ // Define associations
+ $this->belongsTo('Matchgrids');
+ $this->hasMany('RuleAttributes');
+
+ $this->setDisplayField('name');
+
+ $this->setEditContains(['RuleAttributes']);
+ // During a save, also save RuleAttributes
+ $this->setPatchAssociated(['RuleAttributes']);
+
+ $this->setPrimaryLink('matchgrid_id');
+ $this->setRequiresMatchgrid(true);
+
+ $this->setAutoViewVars([
+ 'attributes' => [
+ 'type' => 'auxiliary',
+ 'model' => 'Attributes',
+ 'find' => 'filterPrimaryLink'
+ ],
+ 'confidenceModes' => [
+ 'type' => 'enum',
+ 'class' => 'ConfidenceModeEnum'
+ ],
+ 'searchTypes' => [
+ 'type' => 'enum',
+ 'class' => 'SearchTypeEnum'
+ ]
+ ]);
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator) {
+ $validator->add(
+ 'name',
+ 'length',
+ [ 'rule' => [ 'maxLength', 32 ] ]
+ );
+ $validator->notEmpty('name');
+
+ $validator->add(
+ 'matchgrid_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('matchgrid_id');
+
+ $validator->add(
+ 'description',
+ 'length',
+ [ 'rule' => [ 'maxLength', 128 ] ]
+ );
+ $validator->allowEmpty('description');
+
+ $validator->add(
+ 'confidence_mode',
+ 'content',
+ [ 'rule' => [ 'inList', [
+ ConfidenceModeEnum::Canonical,
+ ConfidenceModeEnum::Potential
+ ] ] ]
+ );
+ $validator->notEmpty('confidence_mode');
+
+ $validator->add(
+ 'ordr',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->allowEmpty('matchgrid_id');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Model/Table/SystemsOfRecordTable.php b/app/src/Model/Table/SystemsOfRecordTable.php
new file mode 100644
index 00000000..286b75b5
--- /dev/null
+++ b/app/src/Model/Table/SystemsOfRecordTable.php
@@ -0,0 +1,103 @@
+addBehavior('Timestamp');
+
+ // Define associations
+ $this->belongsTo('Matchgrids');
+
+ $this->setDisplayField('label');
+
+ $this->setPrimaryLink('matchgrid_id');
+ $this->setRequiresMatchgrid(true);
+
+ $this->setAutoViewVars([
+ 'resolutionModes' => [
+ 'type' => 'enum',
+ 'class' => 'ResolutionModeEnum'
+ ]
+ ]);
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Match v1.0.0
+ * @param Validator $validator Validator
+ * @return $validator Validator
+ */
+
+ public function validationDefault(Validator $validator) {
+ $validator->add(
+ 'label',
+ 'length',
+ [ 'rule' => [ 'maxLength', 80 ] ]
+ );
+ $validator->notEmpty('label');
+
+ $validator->add(
+ 'matchgrid_id',
+ 'content',
+ [ 'rule' => 'isInteger' ]
+ );
+ $validator->notEmpty('matchgrid_id');
+
+ $validator->add(
+ 'resolution_mode',
+ 'content',
+ [ 'rule' => [ 'inList', [
+ ResolutionModeEnum::External,
+ ResolutionModeEnum::Interactive
+ ] ] ]
+ );
+ $validator->notEmpty('confidence_mode');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Shell/ConsoleShell.php b/app/src/Shell/ConsoleShell.php
new file mode 100644
index 00000000..2eb9395e
--- /dev/null
+++ b/app/src/Shell/ConsoleShell.php
@@ -0,0 +1,81 @@
+err('Unable to load Psy\Shell. ');
+ $this->err('');
+ $this->err('Make sure you have installed psysh as a dependency,');
+ $this->err('and that Psy\Shell is registered in your autoloader.');
+ $this->err('');
+ $this->err('If you are using composer run');
+ $this->err('');
+ $this->err('$ php composer.phar require --dev psy/psysh ');
+ $this->err('');
+
+ return self::CODE_ERROR;
+ }
+
+ $this->out("You can exit with `CTRL-C` or `exit` ");
+ $this->out('');
+
+ Log::drop('debug');
+ Log::drop('error');
+ $this->_io->setLoggers(false);
+ restore_error_handler();
+ restore_exception_handler();
+
+ $psy = new PsyShell();
+ $psy->run();
+ }
+
+ /**
+ * Display help for this console.
+ *
+ * @return \Cake\Console\ConsoleOptionParser
+ */
+ public function getOptionParser()
+ {
+ $parser = new ConsoleOptionParser('console');
+ $parser->setDescription(
+ 'This shell provides a REPL that you can use to interact ' .
+ 'with your application in an interactive fashion. You can use ' .
+ 'it to run adhoc queries with your models, or experiment ' .
+ 'and 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/Template/AttributeGroups/columns.inc b/app/src/Template/AttributeGroups/columns.inc
new file mode 100644
index 00000000..5ad7ec40
--- /dev/null
+++ b/app/src/Template/AttributeGroups/columns.inc
@@ -0,0 +1,32 @@
+ [
+ 'type' => 'link'
+ ]
+];
diff --git a/app/src/Template/AttributeGroups/fields.inc b/app/src/Template/AttributeGroups/fields.inc
new file mode 100644
index 00000000..1a9b7c5e
--- /dev/null
+++ b/app/src/Template/AttributeGroups/fields.inc
@@ -0,0 +1,31 @@
+Field->control('name');
+}
\ No newline at end of file
diff --git a/app/src/Template/Attributes/columns.inc b/app/src/Template/Attributes/columns.inc
new file mode 100644
index 00000000..9f02e671
--- /dev/null
+++ b/app/src/Template/Attributes/columns.inc
@@ -0,0 +1,35 @@
+ [
+ 'type' => 'link'
+ ],
+ 'attribute_group_id' => [
+ 'type' => 'fk'
+ ]
+];
\ No newline at end of file
diff --git a/app/src/Template/Attributes/fields.inc b/app/src/Template/Attributes/fields.inc
new file mode 100644
index 00000000..2b3b0be8
--- /dev/null
+++ b/app/src/Template/Attributes/fields.inc
@@ -0,0 +1,48 @@
+Field->control('name');
+
+ print $this->Field->control('description', [], false);
+
+ print $this->Field->control('api_name');
+
+ print $this->Field->control('alphanumeric', [], false);
+ print $this->Field->control('case_sensitive', [], false);
+ print $this->Field->control('invalidates', [], false);
+ print $this->Field->control('null_equivalents', [], false);
+ print $this->Field->control('required', [], false);
+
+ print $this->Field->control('search_distance', [], false);
+ print $this->Field->control('search_exact', [], false);
+ print $this->Field->control('search_substr_from', [], false);
+ print $this->Field->control('search_substr_for', [], false);
+
+ print $this->Field->control('attribute_group_id', ['empty' => true], false);
+}
diff --git a/app/src/Template/Element/Flash/default.ctp b/app/src/Template/Element/Flash/default.ctp
new file mode 100644
index 00000000..736b27db
--- /dev/null
+++ b/app/src/Template/Element/Flash/default.ctp
@@ -0,0 +1,10 @@
+
+= $message ?>
diff --git a/app/src/Template/Element/Flash/error.ctp b/app/src/Template/Element/Flash/error.ctp
new file mode 100644
index 00000000..e7c4af10
--- /dev/null
+++ b/app/src/Template/Element/Flash/error.ctp
@@ -0,0 +1,6 @@
+
+= $message ?>
diff --git a/app/src/Template/Element/Flash/success.ctp b/app/src/Template/Element/Flash/success.ctp
new file mode 100644
index 00000000..becd5a1f
--- /dev/null
+++ b/app/src/Template/Element/Flash/success.ctp
@@ -0,0 +1,6 @@
+
+= $message ?>
diff --git a/app/src/Template/Element/breadcrumbs.ctp b/app/src/Template/Element/breadcrumbs.ctp
new file mode 100644
index 00000000..29d4d61a
--- /dev/null
+++ b/app/src/Template/Element/breadcrumbs.ctp
@@ -0,0 +1,89 @@
+request->getRequestTarget(false) != '/') {
+ // Don't bother rendering breadcrumbs if we're already at the top page
+
+ $action = $this->template;
+ // $this->name = Models
+ $modelsName = $this->name;
+ // $tablename = models
+ $tableName = \Cake\Utility\Inflector::tableize($this->name);
+
+ $this->Breadcrumbs->setTemplates([
+ 'wrapper' => '{{content}}',
+ 'item' => '{{title}} {{separator}}',
+ 'itemWithoutLink' => '{{title}} {{separator}}',
+ 'separator' => '{{separator}} '
+ ]);
+
+ $this->Breadcrumbs->prepend(
+ __('match.meta.match'),
+ '/'
+ );
+
+ if(!empty($vv_cur_mg)
+ && ($modelsName != 'Matchgrids' || $action != 'manage')) {
+ // Link to matchgrid if set
+ $this->Breadcrumbs->add(
+ $vv_cur_mg->table_name,
+ ['controller' => 'matchgrids',
+ 'action' => 'manage',
+ $vv_cur_mg->id ]
+ );
+ }
+
+ if($action != 'index'
+ && ($modelsName != 'Matchgrids' && $action != 'pending')) {
+ // Default parent is index, to which we might need to append the Matchgrid ID
+
+ $target = [
+ 'controller' => $tableName,
+ 'action' => 'index'
+ ];
+
+ if(!empty($vv_cur_mg)) {
+ $target['matchgrid_id'] = $vv_cur_mg->id;
+ }
+
+ $this->Breadcrumbs->add(
+ __('match.ct.'.$tableName, [99]),
+ $target
+ );
+ }
+
+ if(!empty($vv_title)) {
+ $this->Breadcrumbs->add(
+ $vv_title
+ );
+ }
+
+ print $this->Breadcrumbs->render(
+ [],
+ ['separator' => ' > ']
+ );
+}
\ No newline at end of file
diff --git a/app/src/Template/Element/footer.ctp b/app/src/Template/Element/footer.ctp
new file mode 100644
index 00000000..ae8ce6f4
--- /dev/null
+++ b/app/src/Template/Element/footer.ctp
@@ -0,0 +1,34 @@
+
+
+
diff --git a/app/src/Template/Element/javascript.ctp b/app/src/Template/Element/javascript.ctp
new file mode 100644
index 00000000..03ca6a3a
--- /dev/null
+++ b/app/src/Template/Element/javascript.ctp
@@ -0,0 +1,191 @@
+
+
+
diff --git a/app/src/Template/Element/menuMain.ctp b/app/src/Template/Element/menuMain.ctp
new file mode 100644
index 00000000..b1acde7d
--- /dev/null
+++ b/app/src/Template/Element/menuMain.ctp
@@ -0,0 +1,110 @@
+
+XXX PLACEHOLDER BECAUSE
+MENU DOESNT ALIGN
+
+ 'edit',
+ 'attribute_groups' => 'storage',
+ 'rules' => 'assignment',
+ 'systems_of_record' => 'gavel',
+ ];
+
+ foreach($models as $model => $icon) {
+ if($vv_menu_permissions[$model]) {
+ print '";
+ }
+ }
+ } else {
+ // Only render platform level configuration when not in the context of a matchgrid
+
+ // Matchgrids
+ if($vv_menu_permissions['matchgrids']) {
+ print '";
+ }
+
+ // Permissions
+ if($vv_menu_permissions['permissions']) {
+ print '";
+ }
+ }
+ ?>
+
diff --git a/app/src/Template/Element/menuUser.ctp b/app/src/Template/Element/menuUser.ctp
new file mode 100644
index 00000000..2a45f670
--- /dev/null
+++ b/app/src/Template/Element/menuUser.ctp
@@ -0,0 +1,67 @@
+
+Hello, World
+
+
+
+
+
+
+
+
+ 'auth',
+ 'action' => 'login',
+ 'plugin' => false
+ );
+ print $this->Html->link("XXX LOGIN" . ' ',
+ $args, array('escape'=>false, 'id' => 'login', 'class' => ''));
+ }
+ ?>
+
+
+
diff --git a/app/src/Template/Email/html/default.ctp b/app/src/Template/Email/html/default.ctp
new file mode 100644
index 00000000..ac3daa7f
--- /dev/null
+++ b/app/src/Template/Email/html/default.ctp
@@ -0,0 +1,20 @@
+ ' . $line . "
\n";
+endforeach;
diff --git a/app/src/Template/Email/text/default.ctp b/app/src/Template/Email/text/default.ctp
new file mode 100644
index 00000000..862cd9f7
--- /dev/null
+++ b/app/src/Template/Email/text/default.ctp
@@ -0,0 +1,16 @@
+layout = 'error';
+
+if (Configure::read('debug')) :
+ $this->layout = 'dev_error';
+
+ $this->assign('title', $message);
+ $this->assign('templateName', 'error400.ctp');
+
+ $this->start('file');
+?>
+queryString)) : ?>
+
+ SQL Query:
+ = h($error->queryString) ?>
+
+
+params)) : ?>
+ SQL Query Params:
+ params) ?>
+
+= $this->element('auto_table_warning') ?>
+end();
+endif;
+?>
+= h($message) ?>
+
+ = __d('cake', 'Error') ?>:
+ = __d('cake', 'The requested address {0} was not found on this server.', "'{$url}' ") ?>
+
diff --git a/app/src/Template/Error/error500.ctp b/app/src/Template/Error/error500.ctp
new file mode 100644
index 00000000..3328cc52
--- /dev/null
+++ b/app/src/Template/Error/error500.ctp
@@ -0,0 +1,43 @@
+layout = 'error';
+
+if (Configure::read('debug')) :
+ $this->layout = 'dev_error';
+
+ $this->assign('title', $message);
+ $this->assign('templateName', 'error500.ctp');
+
+ $this->start('file');
+?>
+queryString)) : ?>
+
+ SQL Query:
+ = h($error->queryString) ?>
+
+
+params)) : ?>
+ SQL Query Params:
+ params) ?>
+
+
+ Error in:
+ = sprintf('%s, line %s', str_replace(ROOT, 'ROOT', $error->getFile()), $error->getLine()) ?>
+
+element('auto_table_warning');
+
+ if (extension_loaded('xdebug')) :
+ xdebug_print_function_stack();
+ endif;
+
+ $this->end();
+endif;
+?>
+= __d('cake', 'An Internal Error Has Occurred') ?>
+
+ = __d('cake', 'Error') ?>:
+ = h($message) ?>
+
diff --git a/app/src/Template/Layout/Email/html/default.ctp b/app/src/Template/Layout/Email/html/default.ctp
new file mode 100644
index 00000000..3ff87ff8
--- /dev/null
+++ b/app/src/Template/Layout/Email/html/default.ctp
@@ -0,0 +1,24 @@
+
+
+
+
+ = $this->fetch('title') ?>
+
+
+ = $this->fetch('content') ?>
+
+
diff --git a/app/src/Template/Layout/Email/text/default.ctp b/app/src/Template/Layout/Email/text/default.ctp
new file mode 100644
index 00000000..29b439cc
--- /dev/null
+++ b/app/src/Template/Layout/Email/text/default.ctp
@@ -0,0 +1,16 @@
+fetch('content');
diff --git a/app/src/Template/Layout/ajax.ctp b/app/src/Template/Layout/ajax.ctp
new file mode 100644
index 00000000..29b439cc
--- /dev/null
+++ b/app/src/Template/Layout/ajax.ctp
@@ -0,0 +1,16 @@
+fetch('content');
diff --git a/app/src/Template/Layout/default.ctp b/app/src/Template/Layout/default.ctp
new file mode 100644
index 00000000..a91cd008
--- /dev/null
+++ b/app/src/Template/Layout/default.ctp
@@ -0,0 +1,167 @@
+
+
+
+
+ = $this->Html->meta('viewport', 'width=device-width, initial-scale=1.0') . "\n"; ?>
+ = $this->Html->charset(); ?>
+
+ = (!empty($vv_title) ? $vv_title : __('match.meta.match')); ?>
+
+
+
+ = $this->Html->meta('favicon.ico', '/favicon.ico', array('type' => 'icon')) . "\n"; ?>
+
+
+ = $this->Html->css([
+ 'jquery/jquery-ui-1.12.1.custom/jquery-ui.min',
+ 'mdl/mdl-1.3.0/material.min.css',
+ 'jquery/metisMenu/metisMenu.min.css',
+ 'fonts/Font-Awesome-4.6.3/css/font-awesome.min',
+ 'co-base',
+ 'co-responsive'
+ ]) . "\n"; ?>
+
+
+ = $this->Html->script([
+ 'jquery/jquery-3.2.1.min.js',
+ 'jquery/jquery-ui-1.12.1.custom/jquery-ui.min.js'
+ ]) . "\n"; ?>
+
+
+ = $this->fetch('meta') ?>
+ = $this->fetch('css') ?>
+ = $this->fetch('script') ?>
+
+
+
+
+ Skip to main content.
+
+
+
+
+
+
+
+
+
+
+
+ = $this->element('menuMain'); ?>
+
+
+
+
+
+
+
+
+
+ = $this->element('breadcrumbs'); ?>
+
+
+
+
+
+
+ = $this->fetch('content'); ?>
+
+
+
+
+
+
+
+
+ = $this->Html->script([
+ 'mdl/mdl-1.3.0/material.min.js',
+ 'jquery/metisMenu/metisMenu.min.js',
+ 'js-cookie/js.cookie-2.1.3.min.js',
+ 'jquery/spin.min.js',
+ 'comanage.js'
+ ]) . "\n"; ?>
+
+
+ element('javascript'); ?>
+
+
+ = $this->Flash->render() ?>
+
+
diff --git a/app/src/Template/Layout/error.ctp b/app/src/Template/Layout/error.ctp
new file mode 100644
index 00000000..80be38d4
--- /dev/null
+++ b/app/src/Template/Layout/error.ctp
@@ -0,0 +1,47 @@
+
+
+
+
+ = $this->Html->charset() ?>
+
+ = $this->fetch('title') ?>
+
+ = $this->Html->meta('icon') ?>
+
+ = $this->Html->css('base.css') ?>
+ = $this->Html->css('cake.css') ?>
+
+ = $this->fetch('meta') ?>
+ = $this->fetch('css') ?>
+ = $this->fetch('script') ?>
+
+
+
+
+
+ = $this->Flash->render() ?>
+
+ = $this->fetch('content') ?>
+
+
+
+
+
diff --git a/app/src/Template/Layout/rest.ctp b/app/src/Template/Layout/rest.ctp
new file mode 100644
index 00000000..f182b137
--- /dev/null
+++ b/app/src/Template/Layout/rest.ctp
@@ -0,0 +1,4 @@
+fetch('content');
diff --git a/app/src/Template/Layout/rss/default.ctp b/app/src/Template/Layout/rss/default.ctp
new file mode 100644
index 00000000..8269be21
--- /dev/null
+++ b/app/src/Template/Layout/rss/default.ctp
@@ -0,0 +1,11 @@
+fetch('title');
+endif;
+
+echo $this->Rss->document(
+ $this->Rss->channel([], $channel, $this->fetch('content'))
+);
diff --git a/app/src/Template/Matchgrids/columns.inc b/app/src/Template/Matchgrids/columns.inc
new file mode 100644
index 00000000..97acee90
--- /dev/null
+++ b/app/src/Template/Matchgrids/columns.inc
@@ -0,0 +1,50 @@
+ [
+ 'type' => 'link'
+ ],
+ 'status' => [
+ 'type' => 'enum',
+ 'class' => 'StatusEnum'
+ ]
+];
+
+$indexActions = [
+ [
+ 'action' => 'manage',
+ 'class' => 'configurebutton'
+ ],
+ [
+ 'action' => 'build',
+ 'class' => 'buildbutton'
+ ]
+];
diff --git a/app/src/Template/Matchgrids/fields.inc b/app/src/Template/Matchgrids/fields.inc
new file mode 100644
index 00000000..4e64be26
--- /dev/null
+++ b/app/src/Template/Matchgrids/fields.inc
@@ -0,0 +1,66 @@
+
+
+Field->control('table_name');
+
+ print $this->Field->control('description', [], false);
+
+ print $this->Field->control('status',
+ ['empty' => false]);
+
+ print $this->Field->control('referenceid_method',
+ ['empty' => true,
+ 'onChange' => 'fields_update_gadgets();']);
+
+ print $this->Field->control('referenceid_start',
+ ['default' => 1001]);
+}
diff --git a/app/src/Template/Matchgrids/manage.ctp b/app/src/Template/Matchgrids/manage.ctp
new file mode 100644
index 00000000..a6c64f14
--- /dev/null
+++ b/app/src/Template/Matchgrids/manage.ctp
@@ -0,0 +1,34 @@
+Html->link(__('match.op.reconcile'),
+ ['controller' => 'Matchgrids',
+ 'action' => 'pending',
+ $vv_cur_mg->id],
+ ['class' => 'reconcilebutton']);
diff --git a/app/src/Template/Matchgrids/pending.ctp b/app/src/Template/Matchgrids/pending.ctp
new file mode 100644
index 00000000..11eb68dc
--- /dev/null
+++ b/app/src/Template/Matchgrids/pending.ctp
@@ -0,0 +1,54 @@
+
+
+= $vv_title; ?>
+
+= __('match.rs.pending', [count($vv_pending)]); ?>
+
+
+
+ = __('match.fd.sor'); ?>
+ = __('match.fd.sorid'); ?>
+ = __('match.fd.request_time'); ?>
+
+
+
+
+ = $p['sor']; ?>
+
+
+ = $this->Html->link($p['sorid'], ['action' => 'reconcile', $vv_matchgrid_id, 'rowid' => $p['id']]); ?>
+
+
+ = $p['request_time']; ?>
+
+
+
+
diff --git a/app/src/Template/Matchgrids/reconcile.ctp b/app/src/Template/Matchgrids/reconcile.ctp
new file mode 100644
index 00000000..21ca011a
--- /dev/null
+++ b/app/src/Template/Matchgrids/reconcile.ctp
@@ -0,0 +1,68 @@
+
+= __('match.op.reconcile'); ?>
+
+
+
+ = __('match.fd.referenceid'); ?>
+ = __('match.ct.attributes', [99]); ?>
+ = __('match.fd.action'); ?>
+
+
+
+
+ = (!empty($c['referenceid']) ? $c['referenceid'] : __('match.op.new')); ?>
+
+
+
+ $v) {
+ print "" . $k . " : " . $v . " ";
+ }
+ ?>
+
+
+
+ =
+ $this->Form->postLink(__('match.op.reconcile.assign'),
+ ['action' => 'reconcile',
+ $vv_cur_mg->id],
+ ['data' => [
+ 'rowid' => $vv_request['id'],
+ // Default value needs to be the literal string "new" and not a localized text string
+ 'referenceid' => (!empty($c['referenceid']) ? $c['referenceid'] : 'new')
+ ],
+ 'confirm' => 'Are you sure?', // XXX better text
+ 'class' => 'linkbutton']);
+ ?>
+
+
+
+
diff --git a/app/src/Template/Pages/home.ctp b/app/src/Template/Pages/home.ctp
new file mode 100644
index 00000000..3a40f37f
--- /dev/null
+++ b/app/src/Template/Pages/home.ctp
@@ -0,0 +1,83 @@
+
+
+
+ info
+ = __('match.in.matchgrids.none'); ?>
+
+
+
+
+ $name): ?>
+
+
+
+
+ = filter_var($name, FILTER_SANITIZE_SPECIAL_CHARS); ?>
+
+
+ =
+ $this->Html->link($label,
+ ['controller' => 'Matchgrids',
+ 'action' => $action,
+ $id],
+ ['class' => 'reconcilebutton']);
+ ?>
+
+
+
+
+
+
diff --git a/app/src/Template/Permissions/columns.inc b/app/src/Template/Permissions/columns.inc
new file mode 100644
index 00000000..9be86bcc
--- /dev/null
+++ b/app/src/Template/Permissions/columns.inc
@@ -0,0 +1,37 @@
+ [
+ 'type' => 'link'
+ ],
+ 'matchgrid_id' => [ 'type' => 'fk' ],
+ 'permission' => [
+ 'type' => 'enum',
+ 'class' => 'PermissionEnum'
+ ]
+];
\ No newline at end of file
diff --git a/app/src/Template/Permissions/fields.inc b/app/src/Template/Permissions/fields.inc
new file mode 100644
index 00000000..479519fa
--- /dev/null
+++ b/app/src/Template/Permissions/fields.inc
@@ -0,0 +1,60 @@
+
+
+Field->control('username');
+
+ print $this->Field->control('permission',
+ ['empty' => true,
+ 'onChange' => 'fields_update_gadgets();']);
+
+ print $this->Field->control('matchgrid_id');
+}
\ No newline at end of file
diff --git a/app/src/Template/Rules/columns.inc b/app/src/Template/Rules/columns.inc
new file mode 100644
index 00000000..617a13cd
--- /dev/null
+++ b/app/src/Template/Rules/columns.inc
@@ -0,0 +1,39 @@
+ [
+ 'type' => 'link'
+ ],
+ 'confidence_mode' => [
+ 'type' => 'enum',
+ 'class' => 'ConfidenceModeEnum'
+ ],
+ 'ordr' => [
+ 'type' => 'echo'
+ ],
+];
\ No newline at end of file
diff --git a/app/src/Template/Rules/fields.inc b/app/src/Template/Rules/fields.inc
new file mode 100644
index 00000000..545c8422
--- /dev/null
+++ b/app/src/Template/Rules/fields.inc
@@ -0,0 +1,95 @@
+
+
+Field->control('name');
+ print $this->Field->control('description', [], false);
+ print $this->Field->control('confidence_mode', ['empty' => true]);
+ print $this->Field->control('ordr');
+
+// XXX only list attribute that make sense for canonical vs potential
+// eg if "Search Exact" is not ticked (and/or maybe subtring), attribute should not be available for
+// canonical rules
+// XXX we need $attributes passed directly from the controller (where matchgrid_id=cur_id)
+
+ print "" . __('match.fd.search_types') . " \n";
+
+ $i = 0;
+
+ foreach($attributes as $a) {
+ // Calculate the current value, since cake automagic doesn't seem to get it
+ $id = null;
+ $val = SearchTypeEnum::Skip;
+
+ if(!empty($vv_obj->rule_attributes)) {
+ // This will return an array even though we expect one value
+ $curvals = \Cake\Utility\Hash::extract($vv_obj->rule_attributes, '{n}[attribute_id='.$a->id.']');
+
+ if(!empty($curvals[0])) {
+ $id = $curvals[0]->id;
+ $val = $curvals[0]->search_type;
+ }
+ }
+
+ if($id) {
+ print $this->Form->hidden('rule_attributes.'.$i.'.id', ['value' => $id]);
+ }
+ print $this->Form->hidden('rule_attributes.'.$i.'.attribute_id', ['value' => $a->id]);
+ // XXX don't allow Distance or Substring if Canonical (though substring could be ok for canonical?)
+ print $this->Field->control('rule_attributes.'.$i.'.search_type', ['value' => $val], true, $a->name);
+ $i++;
+ }
+}
\ No newline at end of file
diff --git a/app/src/Template/Standard/add-edit-view.ctp b/app/src/Template/Standard/add-edit-view.ctp
new file mode 100644
index 00000000..45e91ce1
--- /dev/null
+++ b/app/src/Template/Standard/add-edit-view.ctp
@@ -0,0 +1,69 @@
+template;
+// $this->name = Models
+$modelsName = $this->name;
+// $tablename = models
+$tableName = \Cake\Utility\Inflector::tableize($this->name);
+?>
+= $vv_title; ?>
+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, $action, ($action == 'add' || $action == 'edit'));
+
+ include(APP . "Template/" . $modelsName . "/fields.inc");
+
+ if($action == 'add' || $action == 'edit') {
+ 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(__('match.op.save'));
+ print $this->Form->end();
+ }
+
+ print $this->Field->endControlSet();
diff --git a/app/src/Template/Standard/index.ctp b/app/src/Template/Standard/index.ctp
new file mode 100644
index 00000000..b1d82897
--- /dev/null
+++ b/app/src/Template/Standard/index.ctp
@@ -0,0 +1,155 @@
+name = Models
+$modelsName = $this->name;
+// $tablename = models
+$tableName = \Cake\Utility\Inflector::tableize($this->name);
+
+// Our default link action is edit, unless the model config overrides it
+$primaryAction = 'edit';
+
+// Read the index configuration ($indexColumns) for this model
+include(APP . "Template/" . $modelsName . "/columns.inc");
+
+// $linkFilter is used for models that belong to a specific parent model (eg: matchgrid_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($c) {
+ if(strpos($c, "_id", strlen($c)-3)) {
+ // Key is of the form field_id, use .ct label instead
+ $k = \Cake\Utility\Inflector::pluralize(substr($c, 0, strlen($c)-3));
+
+ return __('match.ct.'.$k, [1]);
+ }
+
+ return __('match.fd.'.$c);
+}
+?>
+= $vv_title; ?>
+Html->link(__('match.op.add.a', __('match.ct.'.$tableName, [1])),
+ array_merge($linkFilter, ['action' => 'add']),
+ ['class' => 'addbutton']);
+}
+?>
+
+
+ $cfg): ?>
+ = _column_key($col); ?>
+
+ = __('match.fd.action'); ?>
+
+
+
+ $cfg): ?>
+
+ $col);
+ break;
+ case 'link':
+ print $this->Html->link($entity->$col, ['action' => $primaryAction, $entity->id]);
+ break;
+ case 'echo':
+ default:
+ // Just echo the value
+ print $entity->$col;
+ break;
+// XXX dates can be rendered as eg $entity->created->format(DATE_RFC850);
+ }
+ ?>
+
+
+
+ Html->link(
+ __('match.op.edit'),
+ ['action' => 'edit', $entity->id],
+ ['class' => 'editbutton']
+ );
+ }
+
+ if($vv_permissions['delete']) {
+ print $this->Form->postLink(
+ __('match.op.delete'),
+ ['action' => 'delete', $entity->id],
+// XXX should be configurable which field we put in, maybe displayField?
+ ['confirm' => __('match.op.delete.confirm', [$entity->id]),
+ 'class' => 'deletebutton']
+ );
+ }
+
+ if(!empty($indexActions)) {
+ // Insert additional actions as per the .inc file
+
+ if(isset($entity->status) && $entity->status == StatusEnum::Active) {
+ foreach($indexActions as $a) {
+ if($vv_permissions[ $a['action'] ]) {
+ // If we have a .confirm text, use postLink instead
+
+ $confirmKey = 'match.op.'.$a['action'].'.confirm';
+ $confirmTxt = __($confirmKey);
+
+ if($confirmTxt != $confirmKey) {
+ // We found the localized string
+
+ print $this->Form->postLink(
+ __('match.op.' . $a['action']),
+ ['action' => $a['action'], $entity->id],
+ // XXX should be configurable which field we put in, maybe displayField?
+ ['confirm' => __($confirmKey, [$entity->id]),
+ 'class' => $a['class']]
+ );
+ } else {
+ print $this->Html->link(
+ __('match.op.' . $a['action']),
+ ['action' => $a['action'], $entity->id],
+ ['class' => $a['class']]
+ );
+ }
+ }
+ }
+ }
+ }
+ ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/src/Template/SystemsOfRecord/columns.inc b/app/src/Template/SystemsOfRecord/columns.inc
new file mode 100644
index 00000000..51742021
--- /dev/null
+++ b/app/src/Template/SystemsOfRecord/columns.inc
@@ -0,0 +1,36 @@
+ [
+ 'type' => 'link'
+ ],
+ 'resolution_mode' => [
+ 'type' => 'enum',
+ 'class' => 'ResolutionModeEnum'
+ ]
+];
\ No newline at end of file
diff --git a/app/src/Template/SystemsOfRecord/fields.inc b/app/src/Template/SystemsOfRecord/fields.inc
new file mode 100644
index 00000000..bb347804
--- /dev/null
+++ b/app/src/Template/SystemsOfRecord/fields.inc
@@ -0,0 +1,32 @@
+Field->control('label');
+ print $this->Field->control('resolution_mode', ['empty' => true]);
+}
diff --git a/app/src/Template/TierApi/response.ctp b/app/src/Template/TierApi/response.ctp
new file mode 100644
index 00000000..be432e42
--- /dev/null
+++ b/app/src/Template/TierApi/response.ctp
@@ -0,0 +1,32 @@
+response = $this->response->withType('ajax');
+ }
+}
diff --git a/app/src/View/AppView.php b/app/src/View/AppView.php
new file mode 100644
index 00000000..d35ffc67
--- /dev/null
+++ b/app/src/View/AppView.php
@@ -0,0 +1,45 @@
+loadHelper('Field');
+ }
+}
diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php
new file mode 100644
index 00000000..fb69c6bc
--- /dev/null
+++ b/app/src/View/Helper/FieldHelper.php
@@ -0,0 +1,148 @@
+
+
+
+ ' . ($this->editable
+ ? $this->Form->label($fieldName, $label)
+ : $label)
+ . ($required ? ' * ' : '') . '
+
+ ' . ($desc ? '
' . $desc . ' ' : "") .'
+
+
+ ' . $this->Form->control($fieldName, $coptions) . '
+
+
+ ';
+ }
+
+ /**
+ * Emit a submit control.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $label Text for submit button
+ * @return String
+ */
+
+ public function submit($label) {
+ return '
+
+ ' . __('match.fd.req') . '
+
+
+ ' . $this->Form->button($label) . '
+
+ ';
+ }
+
+ /**
+ * End a set of form controls.
+ *
+ * @since COmanage Match v1.0.0
+ * @return String
+ */
+
+ public function endControlSet() {
+ return "\n";
+ }
+
+ /**
+ * Start a set of form controls.
+ *
+ * @since COmanage Match v1.0.0
+ * @param String $modelName Model name for form
+ * @param String $action Current action
+ * @param String $editable True if controls are read/write, false for read only
+ * @return String
+ */
+
+ public function startControlSet($modelName, $action, $editable=true) {
+ $this->editable = $editable;
+
+ return '