diff --git a/.github/workflows/productpage-deploy b/.github/workflows/productpage-deploy new file mode 100644 index 000000000..4f4090e31 --- /dev/null +++ b/.github/workflows/productpage-deploy @@ -0,0 +1,11 @@ +name: My Product Page Workflow + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Deploy Product Github Page + uses: OpenCatalogi/productpage-action@0.0.1-main.6.3c445a5 diff --git a/.readthedocs.yml b/.readthedocs.yml index 57d39b31f..f91650cbb 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,10 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # Build all formats formats: all diff --git a/api/config/packages/framework.yaml b/api/config/packages/framework.yaml index 23c913ea2..7be339232 100644 --- a/api/config/packages/framework.yaml +++ b/api/config/packages/framework.yaml @@ -22,6 +22,12 @@ framework: app: cache.adapter.redis system: cache.adapter.redis default_redis_provider: "redis://%env(REDIS_HOST)%:%env(int:REDIS_PORT)%" + + + trusted_hosts: "%env(TRUSTED_HOSTS)%" + trusted_proxies: "%env(TRUSTED_PROXIES)%" + trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix'] + parameters: samesite: none when@test: diff --git a/api/docker/php/docker-entrypoint.sh b/api/docker/php/docker-entrypoint.sh index 7050456b6..a7a99f934 100644 --- a/api/docker/php/docker-entrypoint.sh +++ b/api/docker/php/docker-entrypoint.sh @@ -34,6 +34,14 @@ if [ "$1" = 'php-fpm' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then echo "Creating the database" bin/console doctrine:database:create --if-not-exists --no-interaction + # Make sure we clear query,result & metadata doctrine cache before migrations during init + if [ "$APP_INIT" = 'true' ]; then + echo "Clearing query,result & metadata doctrine cache" + bin/console doctrine:cache:clear-query + bin/console doctrine:cache:clear-result + bin/console doctrine:cache:clear-metadata + fi + # Get the database inline with the newest version. echo "Migrating the database to the currently used version" bin/console doctrine:migrations:migrate --no-interaction diff --git a/api/helm/commonground-gateway-1.5.4.tgz b/api/helm/commonground-gateway-1.5.4.tgz new file mode 100644 index 000000000..3471833d6 Binary files /dev/null and b/api/helm/commonground-gateway-1.5.4.tgz differ diff --git a/api/helm/commonground-gateway/Chart.yaml b/api/helm/commonground-gateway/Chart.yaml index a080d51ca..df3821ec3 100644 --- a/api/helm/commonground-gateway/Chart.yaml +++ b/api/helm/commonground-gateway/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.5.3 +version: 1.5.4 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/api/helm/commonground-gateway/values.yaml b/api/helm/commonground-gateway/values.yaml index fae76fdcc..baa9eed47 100644 --- a/api/helm/commonground-gateway/values.yaml +++ b/api/helm/commonground-gateway/values.yaml @@ -248,12 +248,6 @@ securityContextNginx: capabilities: drop: - all - add: - - chown - - dac_override - - setgid - - setuid - - net_bind_service runAsUser: 101 # # Warning: the Nginx image is at this moment not able to run on a read-only filesystem. # readOnlyRootFilesystem: false diff --git a/api/helm/index.yaml b/api/helm/index.yaml index 7ef367f0a..d830bdf74 100644 --- a/api/helm/index.yaml +++ b/api/helm/index.yaml @@ -3,7 +3,37 @@ entries: commonground-gateway: - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.564702861+02:00" + created: "2023-10-18T16:45:04.057481884+02:00" + dependencies: + - condition: postgresql.enabled + name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 12.1.2 + - condition: redis.enabled + name: redis + repository: https://charts.bitnami.com/bitnami + version: 17.3.11 + - name: rabbitmq + repository: https://charts.bitnami.com/bitnami + version: 11.9.1 + - condition: mongodb.enabled + name: mongodb + repository: https://charts.bitnami.com/bitnami + version: 13.4.4 + - condition: gateway-ui.enabled + name: gateway-ui + repository: https://raw.githubusercontent.com/ConductionNL/gateway-ui/development/helm/ + version: 0.1.7 + description: A Helm chart for Kubernetes + digest: 0c49e55f583b242b21ce9540eb9b0a9c5bd04ce654ef89fe54742bb6b75be574 + name: commonground-gateway + type: application + urls: + - commonground-gateway-1.5.4.tgz + version: 1.5.4 + - apiVersion: v2 + appVersion: "2.2" + created: "2023-10-18T16:45:04.038953722+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -33,7 +63,7 @@ entries: version: 1.5.3 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.526883527+02:00" + created: "2023-10-18T16:45:04.018785986+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -63,7 +93,7 @@ entries: version: 1.5.2 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.477866486+02:00" + created: "2023-10-18T16:45:03.999122238+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -93,7 +123,7 @@ entries: version: 1.5.1 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.428641923+02:00" + created: "2023-10-18T16:45:03.980292881+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -123,7 +153,7 @@ entries: version: 1.5.0 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.38472829+02:00" + created: "2023-10-18T16:45:03.959404358+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -153,7 +183,7 @@ entries: version: 1.4.7 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.338608404+02:00" + created: "2023-10-18T16:45:03.940477939+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -183,7 +213,7 @@ entries: version: 1.4.6 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.292268968+02:00" + created: "2023-10-18T16:45:03.921531791+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -213,7 +243,7 @@ entries: version: 1.4.5 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.24879378+02:00" + created: "2023-10-18T16:45:03.901356898+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -243,7 +273,7 @@ entries: version: 1.4.4 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.207104579+02:00" + created: "2023-10-18T16:45:03.881647042+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -273,7 +303,7 @@ entries: version: 1.4.3 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.167023862+02:00" + created: "2023-10-18T16:45:03.86265352+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -303,7 +333,7 @@ entries: version: 1.4.2 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.126112881+02:00" + created: "2023-10-18T16:45:03.842650213+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -333,7 +363,7 @@ entries: version: 1.4.1 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.082395521+02:00" + created: "2023-10-18T16:45:03.822713024+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -363,7 +393,7 @@ entries: version: 1.4.0 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:20.037485987+02:00" + created: "2023-10-18T16:45:03.802685235+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -393,7 +423,7 @@ entries: version: 1.3.1 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.983823358+02:00" + created: "2023-10-18T16:45:03.782985437+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -423,7 +453,7 @@ entries: version: 1.3.0 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.934141382+02:00" + created: "2023-10-18T16:45:03.761441521+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -453,7 +483,7 @@ entries: version: 1.2.9 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.890666497+02:00" + created: "2023-10-18T16:45:03.742357684+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -479,7 +509,7 @@ entries: version: 1.2.8 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.847904198+02:00" + created: "2023-10-18T16:45:03.723257181+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -505,7 +535,7 @@ entries: version: 1.2.7 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.807284609+02:00" + created: "2023-10-18T16:45:03.701902186+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -531,7 +561,7 @@ entries: version: 1.2.6 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.765666853+02:00" + created: "2023-10-18T16:45:03.683104297+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -557,7 +587,7 @@ entries: version: 1.2.5 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.724589415+02:00" + created: "2023-10-18T16:45:03.663426416+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -583,7 +613,7 @@ entries: version: 1.2.4 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.680767979+02:00" + created: "2023-10-18T16:45:03.644969076+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -609,7 +639,7 @@ entries: version: 1.2.3 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.635806505+02:00" + created: "2023-10-18T16:45:03.623540997+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -635,7 +665,7 @@ entries: version: 1.2.2 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.591555816+02:00" + created: "2023-10-18T16:45:03.604204858+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -661,7 +691,7 @@ entries: version: 1.2.1 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.548281615+02:00" + created: "2023-10-18T16:45:03.585485+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -687,7 +717,7 @@ entries: version: 1.2.0 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.503228126+02:00" + created: "2023-10-18T16:45:03.566753846+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -713,7 +743,7 @@ entries: version: 1.1.4 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.464211318+02:00" + created: "2023-10-18T16:45:03.548107215+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -739,7 +769,7 @@ entries: version: 1.1.3 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.425360525+02:00" + created: "2023-10-18T16:45:03.529070757+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -765,7 +795,7 @@ entries: version: 1.1.2 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.386759288+02:00" + created: "2023-10-18T16:45:03.510619228+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -791,7 +821,7 @@ entries: version: 1.1.1 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.345205171+02:00" + created: "2023-10-18T16:45:03.490137481+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -817,7 +847,7 @@ entries: version: 1.1.0 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.289576043+02:00" + created: "2023-10-18T16:45:03.468728137+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -843,7 +873,7 @@ entries: version: 1.0.7 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.245645948+02:00" + created: "2023-10-18T16:45:03.450250564+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -869,7 +899,7 @@ entries: version: 1.0.6 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.200996043+02:00" + created: "2023-10-18T16:45:03.433485833+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -895,7 +925,7 @@ entries: version: 1.0.5 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.162515946+02:00" + created: "2023-10-18T16:45:03.413885966+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -921,7 +951,7 @@ entries: version: 1.0.4 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.120622703+02:00" + created: "2023-10-18T16:45:03.392084738+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -947,7 +977,7 @@ entries: version: 1.0.3 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.072923814+02:00" + created: "2023-10-18T16:45:03.372124651+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -973,7 +1003,7 @@ entries: version: 1.0.2 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:19.021397777+02:00" + created: "2023-10-18T16:45:03.353057129+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -999,7 +1029,7 @@ entries: version: 1.0.1 - apiVersion: v2 appVersion: "2.2" - created: "2023-08-18T11:12:18.964538525+02:00" + created: "2023-10-18T16:45:03.333514992+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1025,7 +1055,7 @@ entries: version: 1.0.0 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.673396208+02:00" + created: "2023-10-18T16:45:03.212457732+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1047,7 +1077,7 @@ entries: version: 0.1.12 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.643866865+02:00" + created: "2023-10-18T16:45:03.199088584+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1069,7 +1099,7 @@ entries: version: 0.1.11 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.612722949+02:00" + created: "2023-10-18T16:45:03.184067849+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1091,7 +1121,7 @@ entries: version: 0.1.10 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.914334495+02:00" + created: "2023-10-18T16:45:03.314812842+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1113,7 +1143,7 @@ entries: version: 0.1.9 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.884052708+02:00" + created: "2023-10-18T16:45:03.299412479+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1135,7 +1165,7 @@ entries: version: 0.1.8 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.85038335+02:00" + created: "2023-10-18T16:45:03.287541405+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1157,7 +1187,7 @@ entries: version: 0.1.7 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.811566491+02:00" + created: "2023-10-18T16:45:03.271231516+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1176,7 +1206,7 @@ entries: version: 0.1.6 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.787039221+02:00" + created: "2023-10-18T16:45:03.260098809+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1195,7 +1225,7 @@ entries: version: 0.1.5 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.762744622+02:00" + created: "2023-10-18T16:45:03.249620454+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1214,7 +1244,7 @@ entries: version: 0.1.4 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.739024135+02:00" + created: "2023-10-18T16:45:03.239896838+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1233,7 +1263,7 @@ entries: version: 0.1.3 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.716597385+02:00" + created: "2023-10-18T16:45:03.229631242+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1252,7 +1282,7 @@ entries: version: 0.1.2 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.577499056+02:00" + created: "2023-10-18T16:45:03.168916997+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1271,7 +1301,7 @@ entries: version: 0.1.1 - apiVersion: v2 appVersion: 1.16.0 - created: "2023-08-18T11:12:18.546669967+02:00" + created: "2023-10-18T16:45:03.154838459+02:00" dependencies: - condition: postgresql.enabled name: postgresql @@ -1288,4 +1318,4 @@ entries: urls: - commonground-gateway-0.1.0.tgz version: 0.1.0 -generated: "2023-08-18T11:12:18.515094066+02:00" +generated: "2023-10-18T16:45:03.143413979+02:00" diff --git a/api/migrations/Version20230228084820.php b/api/migrations/Version20230228084820.php index 7255ac41a..51a9a8365 100644 --- a/api/migrations/Version20230228084820.php +++ b/api/migrations/Version20230228084820.php @@ -31,7 +31,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); $this->addSql('ALTER TABLE cronjob DROP reference'); $this->addSql('ALTER TABLE cronjob DROP version'); $this->addSql('ALTER TABLE collection_entity DROP reference'); diff --git a/api/migrations/Version20230228095524.php b/api/migrations/Version20230228095524.php index 31c68443d..b81793b5c 100644 --- a/api/migrations/Version20230228095524.php +++ b/api/migrations/Version20230228095524.php @@ -27,7 +27,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); $this->addSql('ALTER TABLE endpoint DROP reference'); $this->addSql('ALTER TABLE endpoint DROP version'); } diff --git a/api/migrations/Version20230303141510.php b/api/migrations/Version20230303141510.php index 822b540f6..2c2afd818 100644 --- a/api/migrations/Version20230303141510.php +++ b/api/migrations/Version20230303141510.php @@ -27,7 +27,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); $this->addSql('ALTER TABLE gateway DROP endpoints_config'); } } diff --git a/api/migrations/Version20230309160743.php b/api/migrations/Version20230309160743.php index d3ee7bebe..3da4c568c 100644 --- a/api/migrations/Version20230309160743.php +++ b/api/migrations/Version20230309160743.php @@ -27,7 +27,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); $this->addSql('ALTER TABLE application DROP configuration'); } } diff --git a/api/migrations/Version20230328151236.php b/api/migrations/Version20230328151236.php index 3595cc2e6..aecf9f48b 100644 --- a/api/migrations/Version20230328151236.php +++ b/api/migrations/Version20230328151236.php @@ -52,7 +52,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // This down() migration is auto-generated, please modify it to your needs. - $this->addSql('CREATE SCHEMA public'); $this->addSql('ALTER TABLE security_group DROP reference'); $this->addSql('ALTER TABLE security_group DROP version'); $this->addSql('ALTER TABLE "user" DROP reference'); diff --git a/api/migrations/Version20230504111926.php b/api/migrations/Version20230504111926.php index 45bcf9466..9cb7cad35 100644 --- a/api/migrations/Version20230504111926.php +++ b/api/migrations/Version20230504111926.php @@ -29,7 +29,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); $this->addSql('DROP TABLE "gateway_audit_trail"'); $this->addSql('ALTER TABLE audit_trail ALTER id TYPE UUID'); $this->addSql('ALTER TABLE audit_trail ALTER id DROP DEFAULT'); diff --git a/api/migrations/Version20230602151620.php b/api/migrations/Version20230602151620.php index e8b03f5d0..a6a6a49cd 100644 --- a/api/migrations/Version20230602151620.php +++ b/api/migrations/Version20230602151620.php @@ -26,7 +26,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); $this->addSql('DROP INDEX entity_attribute_unique'); } } diff --git a/api/migrations/Version20230626125323.php b/api/migrations/Version20230626125323.php index 2f4f71ec3..dfb936b4b 100644 --- a/api/migrations/Version20230626125323.php +++ b/api/migrations/Version20230626125323.php @@ -26,7 +26,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); $this->addSql('ALTER TABLE attribute ALTER allow_cascade DROP DEFAULT'); } } diff --git a/api/migrations/Version20230922133622.php b/api/migrations/Version20230922133622.php index 03fe88ae2..12ef7ca0e 100644 --- a/api/migrations/Version20230922133622.php +++ b/api/migrations/Version20230922133622.php @@ -27,7 +27,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); $this->addSql('ALTER TABLE action DROP user_id'); $this->addSql('ALTER TABLE cronjob DROP user_id'); } diff --git a/api/migrations/Version20230926112500.php b/api/migrations/Version20230926112500.php index cbbde0146..8456f62ce 100644 --- a/api/migrations/Version20230926112500.php +++ b/api/migrations/Version20230926112500.php @@ -26,7 +26,6 @@ public function up(Schema $schema): void public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); - $this->addSql('ALTER TABLE "user" DROP organisation_id'); + $this->addSql('ALTER TABLE "user" RENAME COLUMN organization_id TO organisation_id'); } } diff --git a/api/migrations/Version20231110140316.php b/api/migrations/Version20231110140316.php new file mode 100644 index 000000000..72fb31735 --- /dev/null +++ b/api/migrations/Version20231110140316.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE synchronization ADD sha VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE synchronization DROP sha'); + } +} diff --git a/api/migrations/Version20231123112218.php b/api/migrations/Version20231123112218.php new file mode 100644 index 000000000..d9f5e1d82 --- /dev/null +++ b/api/migrations/Version20231123112218.php @@ -0,0 +1,60 @@ +logging boolean to Gateway->configLogging array'; + + }//end getDescription() + + + /** + * Migrate up. + * + * @param Schema $schema Schema. + * @return void + */ + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE gateway ALTER logging TYPE TEXT'); + $this->addSql('ALTER TABLE gateway ALTER logging SET DEFAULT \'a:10:{s:10:"callMethod";b:1;s:7:"callUrl";b:1;s:9:"callQuery";b:1;s:15:"callContentType";b:1;s:8:"callBody";b:1;s:18:"responseStatusCode";b:1;s:19:"responseContentType";b:1;s:12:"responseBody";b:1;s:16:"maxCharCountBody";i:500;s:21:"maxCharCountErrorBody";i:2000;}\''); + $this->addSql('UPDATE gateway SET logging = \'a:10:{s:10:"callMethod";b:1;s:7:"callUrl";b:1;s:9:"callQuery";b:1;s:15:"callContentType";b:1;s:8:"callBody";b:1;s:18:"responseStatusCode";b:1;s:19:"responseContentType";b:1;s:12:"responseBody";b:1;s:16:"maxCharCountBody";i:500;s:21:"maxCharCountErrorBody";i:2000;}\''); + $this->addSql('ALTER TABLE gateway ALTER logging SET NOT NULL'); + $this->addSql('COMMENT ON COLUMN gateway.logging IS \'(DC2Type:array)\''); + $this->addSql('ALTER TABLE gateway RENAME COLUMN logging TO logging_config'); + }//end up() + + /** + * Migrate down. + * + * @param Schema $schema Schema. + * @return void + */ + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE gateway RENAME COLUMN logging_config TO logging'); + $this->addSql('ALTER TABLE gateway ALTER logging DROP NOT NULL'); + $this->addSql('ALTER TABLE gateway ALTER logging DROP DEFAULT'); + $this->addSql('UPDATE gateway SET logging = NULL'); + $this->addSql('ALTER TABLE gateway ALTER logging TYPE BOOLEAN USING logging::boolean'); + $this->addSql('COMMENT ON COLUMN gateway.logging IS NULL'); + }//end down() +}//end class diff --git a/api/migrations/Version20231219151046.php b/api/migrations/Version20231219151046.php new file mode 100644 index 000000000..464b3d7e2 --- /dev/null +++ b/api/migrations/Version20231219151046.php @@ -0,0 +1,51 @@ +addSql('ALTER TABLE action ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE application ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE collection_entity ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE cronjob ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE endpoint ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE entity ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE gateway ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE mapping ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE organization ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE security_group ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE template ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE "user" ALTER version SET DEFAULT \'0.0.1\''); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE action ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE application ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE collection_entity ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE cronjob ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE endpoint ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE entity ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE gateway ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE mapping ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE organization ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE security_group ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE template ALTER version SET DEFAULT NULL'); + $this->addSql('ALTER TABLE "user" ALTER version SET DEFAULT NULL'); + } +} diff --git a/api/migrations/Version20240109135300.php b/api/migrations/Version20240109135300.php new file mode 100644 index 000000000..60a5facc6 --- /dev/null +++ b/api/migrations/Version20240109135300.php @@ -0,0 +1,51 @@ +addSql('ALTER TABLE action ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE application ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE collection_entity ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE cronjob ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE endpoint ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE entity ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE gateway ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE mapping ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE organization ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE security_group ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE template ALTER version SET DEFAULT \'0.0.0\''); + $this->addSql('ALTER TABLE "user" ALTER version SET DEFAULT \'0.0.0\''); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE action ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE application ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE collection_entity ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE cronjob ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE endpoint ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE entity ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE gateway ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE mapping ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE organization ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE security_group ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE template ALTER version SET DEFAULT \'0.0.1\''); + $this->addSql('ALTER TABLE "user" ALTER version SET DEFAULT \'0.0.1\''); + } +} diff --git a/api/src/ActionHandler/EmailHandler.php b/api/src/ActionHandler/EmailHandler.php deleted file mode 100644 index f21e338e5..000000000 --- a/api/src/ActionHandler/EmailHandler.php +++ /dev/null @@ -1,109 +0,0 @@ -emailService = $emailService; - } - - /** - * This function returns the requered configuration as a [json-schema](https://json-schema.org/) array. - * - * @throws array a [json-schema](https://json-schema.org/) that this action should comply to - */ - public function getConfiguration(): array - { - return [ - '$id' => 'https://commongateway.nl/ActionHandler/EmailHandler.ActionHandler.json', - '$schema' => 'https://docs.commongateway.nl/schemas/ActionHandler.schema.json', - 'title' => 'EmailHandler', - 'required' => ['ServiceDNS', 'template', 'sender', 'receiver', 'subject'], - 'properties' => [ - 'serviceDNS' => [ - 'type' => 'string', - 'description' => 'The DNS of the mail provider, see https://symfony.com/doc/6.2/mailer.html for details', - 'example' => 'native://default', - 'required' => true, - ], - 'template' => [ - 'type' => 'string', - 'description' => 'The actual email template, should be a base64 encoded twig template', - 'example' => 'eyMgdG9kbzogbW92ZSB0aGlzIHRvIGFuIGVtYWlsIHBsdWdpbiAoc2VlIEVtYWlsU2VydmljZS5waHApICN9CjwhRE9DVFlQRSBodG1sIFBVQkxJQyAiLS8vVzNDLy9EVEQgWEhUTUwgMS4wIFRyYW5zaXRpb25hbC8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9UUi94aHRtbDEvRFREL3hodG1sMS10cmFuc2l0aW9uYWwuZHRkIj4KPGh0bWwgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiPgo8aGVhZD4KICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCIgLz4KICA8bWV0YSBodHRwLWVxdWl2PSJDb250ZW50LVR5cGUiIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD1VVEYtOCIgLz4KICA8dGl0bGU+e3sgc3ViamVjdCB9fTwvdGl0bGU+CgogIDxsaW5rIHJlbD0icHJlY29ubmVjdCIgaHJlZj0iaHR0cHM6Ly9mb250cy5nc3RhdGljLmNvbSIgLz4KICA8bGluawogICAgICAgICAgaHJlZj0iaHR0cHM6Ly9mb250cy5nb29nbGVhcGlzLmNvbS9jc3MyP2ZhbWlseT1GYXVzdGluYTp3Z2h0QDYwMCZkaXNwbGF5PXN3YXAiCiAgICAgICAgICByZWw9InN0eWxlc2hlZXQiCiAgLz4KICA8bGluawogICAgICAgICAgaHJlZj0iaHR0cHM6Ly9mb250cy5nb29nbGVhcGlzLmNvbS9jc3MyP2ZhbWlseT1Tb3VyY2UrU2FucytQcm8mZGlzcGxheT1zd2FwIgogICAgICAgICAgcmVsPSJzdHlsZXNoZWV0IgogIC8+CgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyIgcmVsPSJzdHlsZXNoZWV0IiBtZWRpYT0iYWxsIj4KICAgIC8qIEJhc2UgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICovCgogICAgYm9keSB7CiAgICAgIHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7CiAgICAgIGhlaWdodDogMTAwJTsKICAgICAgbWFyZ2luOiAwOwogICAgICBtc28tbGluZS1oZWlnaHQtcnVsZTogZXhhY3RseTsKICAgICAgbGluZS1oZWlnaHQ6IDEuNDsKICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2ZmZmZmZjsKICAgICAgY29sb3I6ICM3NDc4N2U7CiAgICAgIC13ZWJraXQtdGV4dC1zaXplLWFkanVzdDogbm9uZTsKICAgIH0KCiAgICBwLAogICAgdWwsCiAgICBvbCwKICAgIGJsb2NrcXVvdGUgewogICAgICBtc28tbGluZS1oZWlnaHQtcnVsZTogZXhhY3RseTsKICAgICAgbGluZS1oZWlnaHQ6IDEuNDsKICAgICAgdGV4dC1hbGlnbjogbGVmdDsKICAgIH0KCiAgICBhIHsKICAgICAgY29sb3I6ICMxZDU1ZmY7CiAgICAgIHRleHQtZGVjb3JhdGlvbjogbm9uZTsKICAgIH0KCiAgICBhOmhvdmVyIHsKICAgICAgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmU7CiAgICB9CgogICAgcCBhIHsKICAgICAgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmU7CiAgICB9CgogICAgYSBpbWcgewogICAgICBib3JkZXI6IG5vbmU7CiAgICB9CgogICAgdGQgewogICAgICB3b3JkLWJyZWFrOiBicmVhay13b3JkOwogICAgfQogICAgLyogTGF5b3V0IC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAqLwoKICAgIC5oZWFkZXIgewogICAgICBiYWNrZ3JvdW5kOiAjMWQ1NWZmOwogICAgICB3aWR0aDogMTAwJTsKICAgICAgaGVpZ2h0OiAyMzZweDsKICAgICAgYmFja2dyb3VuZC1yZXBlYXQ6IG5vLXJlcGVhdDsKICAgICAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyOwogICAgfQoKICAgIC5oZWFkZXItY2VsbCB7CiAgICAgIHBhZGRpbmc6IDE2cHggMjRweDsKICAgIH0KCiAgICAuZW1haWwtd3JhcHBlciB7CiAgICAgIHdpZHRoOiAxMDAlOwogICAgICBtYXJnaW46IDA7CiAgICAgIHBhZGRpbmc6IDA7CiAgICAgIC1wcmVtYWlsZXItd2lkdGg6IDEwMCU7CiAgICAgIC1wcmVtYWlsZXItY2VsbHBhZGRpbmc6IDA7CiAgICAgIC1wcmVtYWlsZXItY2VsbHNwYWNpbmc6IDA7CiAgICAgIGJhY2tncm91bmQtY29sb3I6ICNmZmZmZmY7CiAgICB9CgogICAgLmVtYWlsLWNvbnRlbnQgewogICAgICB3aWR0aDogMTAwJTsKICAgICAgbWFyZ2luOiAwOwogICAgICBwYWRkaW5nOiAwOwogICAgICAtcHJlbWFpbGVyLXdpZHRoOiAxMDAlOwogICAgICAtcHJlbWFpbGVyLWNlbGxwYWRkaW5nOiAwOwogICAgICAtcHJlbWFpbGVyLWNlbGxzcGFjaW5nOiAwOwogICAgfQogICAgLyogTWFzdGhlYWQgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gKi8KCiAgICAuZW1haWwtbWFzdGhlYWQgewogICAgICBwYWRkaW5nOiAyNXB4IDA7CiAgICAgIHRleHQtYWxpZ246IGNlbnRlcjsKICAgIH0KCiAgICAuZW1haWwtbWFzdGhlYWRfbG9nbyB7CiAgICAgIHdpZHRoOiA5NHB4OwogICAgfQoKICAgIC5lbWFpbC1tYXN0aGVhZF9uYW1lIHsKICAgICAgZm9udC1zaXplOiAxNnB4OwogICAgICBmb250LXdlaWdodDogNjAwOwogICAgICBjb2xvcjogI2JiYmZjMzsKICAgICAgdGV4dC1kZWNvcmF0aW9uOiBub25lOwogICAgICB0ZXh0LXNoYWRvdzogMCAxcHggMCB3aGl0ZTsKICAgIH0KICAgIC8qIEJvZHkgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICovCgogICAgLmVtYWlsLWJvZHkgewogICAgICB3aWR0aDogMTAwJTsKICAgICAgbWFyZ2luOiAwOwogICAgICBwYWRkaW5nOiAwOwogICAgICAtcHJlbWFpbGVyLXdpZHRoOiAxMDAlOwogICAgICAtcHJlbWFpbGVyLWNlbGxwYWRkaW5nOiAwOwogICAgICAtcHJlbWFpbGVyLWNlbGxzcGFjaW5nOiAwOwogICAgICBiYWNrZ3JvdW5kOiBub25lOwogICAgfQoKICAgIC5lbWFpbC1ib2R5X2lubmVyIHsKICAgICAgd2lkdGg6IDY0MHB4OwogICAgICBtYXJnaW46IDAgYXV0bzsKICAgICAgcGFkZGluZzogMDsKICAgICAgLXByZW1haWxlci13aWR0aDogNTcwcHg7CiAgICAgIC1wcmVtYWlsZXItY2VsbHBhZGRpbmc6IDA7CiAgICAgIC1wcmVtYWlsZXItY2VsbHNwYWNpbmc6IDA7CiAgICAgIGJhY2tncm91bmQtY29sb3I6ICNmZmZmZmY7CiAgICB9CgogICAgLmVtYWlsLWZvb3RlciB7CiAgICAgIHdpZHRoOiA2NDBweDsKICAgICAgbWFyZ2luOiAwIGF1dG87CiAgICAgIHBhZGRpbmc6IDA7CiAgICAgIC1wcmVtYWlsZXItd2lkdGg6IDU3MHB4OwogICAgICAtcHJlbWFpbGVyLWNlbGxwYWRkaW5nOiAwOwogICAgICAtcHJlbWFpbGVyLWNlbGxzcGFjaW5nOiAwOwogICAgICB0ZXh0LWFsaWduOiBjZW50ZXI7CiAgICB9CgogICAgLmVtYWlsLWZvb3RlciBwIHsKICAgICAgY29sb3I6ICNhZWFlYWU7CiAgICB9CgogICAgLmJvZHktYWN0aW9uIHsKICAgICAgd2lkdGg6IDEwMCU7CiAgICAgIG1hcmdpbjogNDBweCBhdXRvOwogICAgICBwYWRkaW5nOiAwOwogICAgICAtcHJlbWFpbGVyLXdpZHRoOiAxMDAlOwogICAgICAtcHJlbWFpbGVyLWNlbGxwYWRkaW5nOiAwOwogICAgICAtcHJlbWFpbGVyLWNlbGxzcGFjaW5nOiAwOwogICAgICB0ZXh0LWFsaWduOiBjZW50ZXI7CiAgICB9CgogICAgLmJvZHktc3ViIHsKICAgICAgbWFyZ2luLXRvcDogMjVweDsKICAgICAgcGFkZGluZy10b3A6IDI1cHg7CiAgICAgIGJvcmRlci10b3A6IDFweCBzb2xpZCAjZWRlZmYyOwogICAgfQoKICAgIC5jb250ZW50LWNlbGwgewogICAgICBwYWRkaW5nOiAzNnB4IDE2cHg7CiAgICB9CgogICAgLnByZWhlYWRlciB7CiAgICAgIGRpc3BsYXk6IG5vbmUgIWltcG9ydGFudDsKICAgICAgdmlzaWJpbGl0eTogaGlkZGVuOwogICAgICBtc28taGlkZTogYWxsOwogICAgICBmb250LXNpemU6IDFweDsKICAgICAgbXNvLWxpbmUtaGVpZ2h0LXJ1bGU6IGV4YWN0bHk7CiAgICAgIGxpbmUtaGVpZ2h0OiAxcHg7CiAgICAgIG1heC1oZWlnaHQ6IDA7CiAgICAgIG1heC13aWR0aDogMDsKICAgICAgb3BhY2l0eTogMDsKICAgICAgb3ZlcmZsb3c6IGhpZGRlbjsKICAgIH0KICAgIC8qIEF0dHJpYnV0ZSBsaXN0IC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAqLwoKICAgIC5hdHRyaWJ1dGVzIHsKICAgICAgbWFyZ2luOiAwIDAgMjFweDsKICAgIH0KCiAgICAuYXR0cmlidXRlc19jb250ZW50IHsKICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2VkZWZmMjsKICAgICAgcGFkZGluZzogMTZweDsKICAgIH0KCiAgICAuYXR0cmlidXRlc19pdGVtIHsKICAgICAgcGFkZGluZzogMDsKICAgIH0KICAgIC8qIFJlbGF0ZWQgSXRlbXMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICovCgogICAgLnJlbGF0ZWQgewogICAgICB3aWR0aDogMTAwJTsKICAgICAgbWFyZ2luOiAwOwogICAgICBwYWRkaW5nOiAyNXB4IDAgMCAwOwogICAgICAtcHJlbWFpbGVyLXdpZHRoOiAxMDAlOwogICAgICAtcHJlbWFpbGVyLWNlbGxwYWRkaW5nOiAwOwogICAgICAtcHJlbWFpbGVyLWNlbGxzcGFjaW5nOiAwOwogICAgfQoKICAgIC5yZWxhdGVkX2l0ZW0gewogICAgICBwYWRkaW5nOiAxMHB4IDA7CiAgICAgIGNvbG9yOiAjNzQ3ODdlOwogICAgICBmb250LXNpemU6IDE1cHg7CiAgICAgIG1zby1saW5lLWhlaWdodC1ydWxlOiBleGFjdGx5OwogICAgICBsaW5lLWhlaWdodDogMThweDsKICAgIH0KCiAgICAucmVsYXRlZF9pdGVtLXRpdGxlIHsKICAgICAgZGlzcGxheTogYmxvY2s7CiAgICAgIG1hcmdpbjogMC41ZW0gMCAwOwogICAgfQoKICAgIC5yZWxhdGVkX2l0ZW0tdGh1bWIgewogICAgICBkaXNwbGF5OiBibG9jazsKICAgICAgcGFkZGluZy1ib3R0b206IDEwcHg7CiAgICB9CgogICAgLnJlbGF0ZWRfaGVhZGluZyB7CiAgICAgIGJvcmRlci10b3A6IDFweCBzb2xpZCAjZWRlZmYyOwogICAgICB0ZXh0LWFsaWduOiBjZW50ZXI7CiAgICAgIHBhZGRpbmc6IDI1cHggMCAxMHB4OwogICAgfQoKICAgIC8qIFV0aWxpdGllcyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gKi8KCiAgICAubm8tbWFyZ2luIHsKICAgICAgbWFyZ2luOiAwOwogICAgfQoKICAgIC5tYXJnaW4tdG9wIHsKICAgICAgbWFyZ2luLXRvcDogOHB4OwogICAgfQoKICAgIC5hbGlnbi1yaWdodCB7CiAgICAgIHRleHQtYWxpZ246IHJpZ2h0OwogICAgfQoKICAgIC5hbGlnbi1sZWZ0IHsKICAgICAgdGV4dC1hbGlnbjogbGVmdDsKICAgIH0KCiAgICAuYWxpZ24tY2VudGVyIHsKICAgICAgdGV4dC1hbGlnbjogY2VudGVyOwogICAgfQogICAgLypNZWRpYSBRdWVyaWVzIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAqLwoKICAgIEBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKG1heC13aWR0aDogNjAwcHgpIHsKICAgICAgLmVtYWlsLWJvZHlfaW5uZXIsCiAgICAgIC5lbWFpbC1mb290ZXIgewogICAgICAgIHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7CiAgICAgIH0KICAgIH0KCiAgICBAbWVkaWEgb25seSBzY3JlZW4gYW5kIChtYXgtd2lkdGg6IDUwMHB4KSB7CiAgICAgIC5idXR0b24gewogICAgICAgIHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7CiAgICAgIH0KICAgIH0KCiAgICAvKiBDYXJkcyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gKi8KICAgIC5jYXJkIHsKICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2ZmZjsKICAgICAgYm9yZGVyLXRvcDogMXB4IHNvbGlkICNlMGUxZTU7CiAgICAgIGJvcmRlci1yaWdodDogMXB4IHNvbGlkICNlMGUxZTU7CiAgICAgIGJvcmRlci1ib3R0b206IDFweCBzb2xpZCAjZTBlMWU1OwogICAgICBib3JkZXItbGVmdDogMXB4IHNvbGlkICNlMGUxZTU7CiAgICAgIHBhZGRpbmc6IDI0cHg7CiAgICAgIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICAgICAgY29sb3I6ICMzOTM5M2E7CiAgICAgIHRleHQtZGVjb3JhdGlvbjogbm9uZTsKICAgICAgd2lkdGg6IDEwMCU7CiAgICAgIGJvcmRlci1yYWRpdXM6IDNweDsKICAgICAgYm94LXNoYWRvdzogMCA0cHggM3B4IC0zcHggcmdiYSgwLCAwLCAwLCAwLjA4KTsKICAgICAgLXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OiBub25lOwogICAgICBtc28tbGluZS1oZWlnaHQtcnVsZTogZXhhY3RseTsKICAgICAgbGluZS1oZWlnaHQ6IDEuNzU7CiAgICAgIGxldHRlci1zcGFjaW5nOiAwLjhweDsKICAgIH0KCiAgICAvKiBCdXR0b25zIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAqLwoKICAgIC5idXR0b24gewogICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMWRiNGVkOwogICAgICBib3JkZXItdG9wOiAxMHB4IHNvbGlkICMxZGI0ZWQ7CiAgICAgIGJvcmRlci1yaWdodDogMThweCBzb2xpZCAjMWRiNGVkOwogICAgICBib3JkZXItYm90dG9tOiAxMHB4IHNvbGlkICMxZGI0ZWQ7CiAgICAgIGJvcmRlci1sZWZ0OiAxOHB4IHNvbGlkICMxZGI0ZWQ7CiAgICAgIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICAgICAgY29sb3I6ICNmZmY7CiAgICAgIHRleHQtZGVjb3JhdGlvbjogbm9uZTsKICAgICAgYm9yZGVyLXJhZGl1czogNHB4OwogICAgICBib3gtc2hhZG93OiAwIDJweCAzcHggcmdiYSgwLCAwLCAwLCAwLjE2KTsKICAgICAgLXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OiBub25lOwogICAgICBtc28tbGluZS1oZWlnaHQtcnVsZTogZXhhY3RseTsKICAgICAgd2lkdGg6IDEwMCU7CiAgICAgIHRleHQtYWxpZ246IGNlbnRlcjsKICAgICAgZm9udC1zaXplOiAxNHB4OwogICAgICBmb250LXdlaWdodDogNjAwOwogICAgfQoKICAgIC5zbWFsbC1sb2dvIHsKICAgICAgd2lkdGg6IDI0cHg7CiAgICAgIGhlaWdodDogMjRweDsKICAgIH0KCiAgICAuaW5saW5lIHsKICAgICAgZGlzcGxheTogaW5saW5lOwogICAgfQogICAgLyogVHlwZSAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gKi8KCiAgICBwIHsKICAgICAgbWFyZ2luOiAwOwogICAgICBjb2xvcjogIzM5MzkzYTsKICAgICAgZm9udC1zaXplOiAxNXB4OwogICAgICBtc28tbGluZS1oZWlnaHQtcnVsZTogZXhhY3RseTsKICAgICAgbGV0dGVyLXNwYWNpbmc6IG5vcm1hbDsKICAgICAgdGV4dC1hbGlnbjogbGVmdDsKICAgICAgbGluZS1oZWlnaHQ6IDIwcHg7CiAgICB9CgogICAgcCArIHAgewogICAgICBtYXJnaW4tdG9wOiAyMHB4OwogICAgfQoKICAgIHAuc3VmZml4IHsKICAgICAgZm9udC1zaXplOiAxNHB4OwogICAgfQoKICAgIHAuc3ViIHsKICAgICAgZm9udC1zaXplOiAxMnB4OwogICAgfQoKICAgIHAuY2VudGVyIHsKICAgICAgdGV4dC1hbGlnbjogY2VudGVyOwogICAgfQoKICAgIC5zdWJ0bGUgewogICAgICBjb2xvcjogI2IxYjFiMTsKICAgIH0KCiAgICAvKiBGb290ZXIgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICovCgogICAgLmxvZ28tbGFiZWwgewogICAgICB2ZXJ0aWNhbC1hbGlnbjogdG9wOwogICAgICBmb250LXNpemU6IDE0cHg7CiAgICAgIG1hcmdpbi1sZWZ0OiA0cHg7CiAgICB9CgogICAgLmZvb3Rlci1jZWxsIHsKICAgICAgcGFkZGluZzogOHB4IDI0cHg7CiAgICB9CgogICAgLmZvb3Rlci1uYXYgewogICAgICBtYXJnaW4tbGVmdDogOHB4OwogICAgICBmb250LXNpemU6IDE0cHg7CiAgICAgIGNvbG9yOiAjMzkzOTNhOwogICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7CiAgICB9CgogICAgLmhlYWRlci1saW5rIHsKICAgICAgdGV4dC1kZWNvcmF0aW9uOiBub25lOwogICAgICBmb250LXNpemU6IDE0cHg7CiAgICAgIGNvbG9yOiAjMWQ1NWZmOwogICAgICBmb250LXdlaWdodDogNTAwOwogICAgfQoKICAgIC5tYXJnaW4tdG9wIHsKICAgICAgbWFyZ2luLXRvcDogMTZweDsKICAgIH0KCiAgICAubG9nby1jb250YWluZXIgewogICAgICB3aWR0aDogMTAwJTsKICAgICAgbWFyZ2luLWJvdHRvbTogNTZweDsKICAgIH0KCiAgICAubG9nbyB7CiAgICAgIGRpc3BsYXk6IGJsb2NrOwogICAgfQoKICAgIC8qIEN1c3RvbSBzdHlsZXMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICovCiAgICBociB7CiAgICAgIGJvcmRlci10b3A6IDFweCBzb2xpZCAjZDlkOWRlOwogICAgICBjb2xvcjogI2Q5ZDlkZTsKICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2Q5ZDlkZTsKICAgICAgbWFyZ2luLXRvcDogMzJweDsKICAgICAgbWFyZ2luLWJvdHRvbTogNDBweDsKICAgIH0KCiAgICBoMSB7CiAgICAgIGZvbnQtZmFtaWx5OiAiRmF1c3RpbmEiLCBzZXJpZjsKICAgICAgZm9udC1zaXplOiAzMnB4OwogICAgICBmb250LXdlaWdodDogNjAwOwogICAgICBjb2xvcjogIzIzMjMyNjsKICAgICAgbWFyZ2luLWJvdHRvbTogMjJweDsKICAgIH0KCiAgICBwIHsKICAgICAgZm9udC1mYW1pbHk6ICJTb3VyY2UgU2FucyBQcm8iLCBzYW5zLXNlcmlmOwogICAgICBmb250LXNpemU6IDE4cHg7CiAgICAgIGxpbmUtaGVpZ2h0OiAxLjY7CiAgICAgIGNvbG9yOiAjMjMyMzI2OwogICAgfQoKICAgIC5idXR0b24gewogICAgICBmb250LWZhbWlseTogIlNvdXJjZSBTYW5zIFBybyIsIHNhbnMtc2VyaWY7CiAgICB9CgogICAgLmNvbnRlbnQtY2VsbCB7CiAgICAgIHBhZGRpbmc6IDQwcHggNDBweDsKICAgIH0KCiAgICAuYnV0dG9uIHsKICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2ZmNWEyNjsKICAgICAgYm9yZGVyLXRvcDogMTBweCBzb2xpZCAjZmY1YTI2OwogICAgICBib3JkZXItcmlnaHQ6IDE4cHggc29saWQgI2ZmNWEyNjsKICAgICAgYm9yZGVyLWJvdHRvbTogMTBweCBzb2xpZCAjZmY1YTI2OwogICAgICBib3JkZXItbGVmdDogMThweCBzb2xpZCAjZmY1YTI2OwogICAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7CiAgICAgIGNvbG9yOiAjZmZmOwogICAgICB3aWR0aDogYXV0bzsKICAgICAgYm94LXNoYWRvdzogbm9uZTsKICAgICAgdGV4dC1kZWNvcmF0aW9uOiBub25lOwogICAgICBib3JkZXItcmFkaXVzOiA4cHg7CiAgICAgIC13ZWJraXQtdGV4dC1zaXplLWFkanVzdDogbm9uZTsKICAgICAgbXNvLWxpbmUtaGVpZ2h0LXJ1bGU6IGV4YWN0bHk7CiAgICAgIHRleHQtYWxpZ246IGNlbnRlcjsKICAgICAgZm9udC1zaXplOiAxNHB4OwogICAgICBmb250LXdlaWdodDogNjAwOwogICAgfQogIDwvc3R5bGU+CjwvaGVhZD4KPGJvZHk+Cjx0YWJsZSBjbGFzcz0iZW1haWwtd3JhcHBlciIgd2lkdGg9IjEwMCUiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+CiAgPHRyPgogICAgPHRkIGFsaWduPSJjZW50ZXIiPgogICAgICA8dGFibGUKICAgICAgICAgICAgICBjbGFzcz0iZW1haWwtY29udGVudCIKICAgICAgICAgICAgICB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICBjZWxscGFkZGluZz0iMCIKICAgICAgICAgICAgICBjZWxsc3BhY2luZz0iMCIKICAgICAgPgogICAgICAgIDx0cj4KICAgICAgICAgIDx0ZCBjbGFzcz0iZW1haWwtbWFzdGhlYWQiPjwvdGQ+CiAgICAgICAgPC90cj4KICAgICAgICA8IS0tIEVtYWlsIEJvZHkgLS0+CiAgICAgICAgPHRyPgogICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgIGNsYXNzPSJlbWFpbC1ib2R5IgogICAgICAgICAgICAgICAgICB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgY2VsbHBhZGRpbmc9IjAiCiAgICAgICAgICAgICAgICAgIGNlbGxzcGFjaW5nPSIwIgogICAgICAgICAgPgogICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICBjbGFzcz0iZW1haWwtYm9keV9pbm5lciIKICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIgogICAgICAgICAgICAgICAgICAgIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgIGJhY2tncm91bmQtY29sb3I9IiNlZGVmZjIiCiAgICAgICAgICAgICAgICAgICAgY2VsbHBhZGRpbmc9IjAiCiAgICAgICAgICAgICAgICAgICAgY2VsbHNwYWNpbmc9IjAiCiAgICAgICAgICAgID4KICAgICAgICAgICAgICA8IS0tIEJvZHkgY29udGVudCAtLT4KICAgICAgICAgICAgICA8dHI+PC90cj4KICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICA8dGQgY2xhc3M9ImNvbnRlbnQtY2VsbCIgd2lkdGg9IjEwMCUiPgogICAgICAgICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgICAgICAgIHt7IGF1dGhvciB9fSBoZWVmdCBmZWVkYmFjayBnZWdldmVuIG9wOiB7eyB0b3BpYyB9fQogICAgICAgICAgICAgICAgICA8L3A+CiAgICAgICAgICAgICAgICAgIDxici8+CiAgICAgICAgICAgICAgICAgIDxwPgogICAgICAgICAgICAgICAgICAgICAge3sgZGVzY3JpcHRpb24gfCBubDJiciB9fQogICAgICAgICAgICAgICAgICA8L3A+CiAgICAgICAgICAgICAgICAgIDxociAvPgogICAgICAgICAgICAgICAgICA8cD5NZXQgdnJpZW5kZWxpamtlIGdyb2V0LDwvcD4KICAgICAgICAgICAgICAgICAgPHA+S0lTUzwvcD4KICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICA8dHI+PC90cj4KICAgICAgICAgICAgPC90YWJsZT4KICAgICAgICAgIDwvdGQ+CiAgICAgICAgPC90cj4KICAgICAgPC90YWJsZT4KICAgIDwvdGQ+CiAgPC90cj4KPC90YWJsZT4KPC9ib2R5Pgo8L2h0bWw+Cg==', - 'required' => true, - ], - 'variables' => [ - 'type' => 'array', - 'description' => 'The variables supported by this template (might contain default vallues)', - 'nullable' => true, - ], - 'sender' => [ - 'type' => 'string', - 'description' => 'The sender of the email', - 'example' => 'info@conduction.nl', - 'required' => true, - ], - 'receiver' => [ - 'type' => 'string', - 'description' => 'The receiver of the email', - 'example' => 'j.do@conduction.nl', - 'required' => true, - ], - 'subject' => [ - 'type' => 'string', - 'description' => 'The subject of the email', - 'example' => 'Your weekly update', - 'required' => true, - ], - 'cc' => [ - 'type' => 'string', - 'description' => 'Carbon copy, email boxes that should receive a copy of this mail', - 'example' => 'archive@conduction.nl', - 'nullable' => true, - ], - 'bcc' => [ - 'type' => 'string', - 'description' => 'Blind carbon copy, people that should receive a copy without other recipient knowing', - 'example' => 'b.brother@conduction.nl', - 'nullable' => true, - ], - 'replyTo' => [ - 'type' => 'string', - 'description' => 'The address the receiver should reply to, only provide this if it differs from the sender address', - 'example' => 'no-reply@conduction.nl', - 'nullable' => true, - ], - 'priority' => [ - 'type' => 'int', - 'description' => 'An optional priority for the email', - 'nullable' => true, - ], - ], - ]; - } - - /** - * This function runs the email service plugin. - * - * @param array $data The data from the call - * @param array $configuration The configuration of the action - * - * @throws TransportExceptionInterface|LoaderError|RuntimeError|SyntaxError - * - * @return array - */ - public function run(array $data, array $configuration): array - { - return $this->emailService->emailHandler($data, $configuration); - } -} diff --git a/api/src/Command/InitializationCommand.php b/api/src/Command/InitializationCommand.php index 6d4ac71ba..f3245572c 100644 --- a/api/src/Command/InitializationCommand.php +++ b/api/src/Command/InitializationCommand.php @@ -249,7 +249,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Handling users $io->section('Looking for an user'); if (!$user = $this->entityManager->getRepository('App:User')->findOneBy([])) { - $io->info('No User found, creating a new one'); + $io->info('No User found, creating a default and APIKEY one'); + $user = new User(); $user->setName('Default User'); $user->setReference('https://docs.commongateway.nl/user/default.user.json'); @@ -261,6 +262,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int $user->setOrganization($organization); $this->entityManager->persist($user); + + $apikeyUser = new User(); + $apikeyUser->setName('APIKEY_USER'); + $apikeyUser->setReference('https://docs.commongateway.nl/user/default.apikey.user.json'); + $apikeyUser->setDescription('Created during auto configuration'); + $apikeyUser->setEmail('apikey@test.com'); + $apikeyUser->setPassword($this->hasher->hashPassword($apikeyUser, '!ChangeMe!')); + $apikeyUser->addSecurityGroup($securityGroupAdmin); + $apikeyUser->addApplication($application); + $apikeyUser->setOrganization($organization); + + $this->entityManager->persist($apikeyUser); } else { $io->info('User found, continuing....'); } diff --git a/api/src/Controller/ConvenienceController.php b/api/src/Controller/ConvenienceController.php index 32f7f46c6..f4d642310 100755 --- a/api/src/Controller/ConvenienceController.php +++ b/api/src/Controller/ConvenienceController.php @@ -40,7 +40,6 @@ class ConvenienceController extends AbstractController private ActionSubscriber $actionSubscriber; private ObjectEntityService $objectEntityService; private MappingService $mappingService; - private Environment $twig; public function __construct( EntityManagerInterface $entityManager, @@ -50,7 +49,7 @@ public function __construct( HandlerService $handlerService, ActionSubscriber $actionSubscriber, ObjectEntityService $objectEntityService, - Environment $twig + MappingService $mappingService ) { $this->entityManager = $entityManager; $this->serializer = $serializer; @@ -60,8 +59,7 @@ public function __construct( $this->handlerService = $handlerService; $this->actionSubscriber = $actionSubscriber; $this->objectEntityService = $objectEntityService; - $this->twig = $twig; - $this->mappingService = new MappingService($twig); + $this->mappingService = $mappingService; } /** diff --git a/api/src/Controller/LoginController.php b/api/src/Controller/LoginController.php index 70e6aa1cb..25ab685c7 100644 --- a/api/src/Controller/LoginController.php +++ b/api/src/Controller/LoginController.php @@ -48,6 +48,8 @@ public function MeAction(Request $request) 'last_name' => $this->getUser()->getLastName(), 'name' => $this->getUser()->getName(), 'email' => $this->getUser()->getEmail(), + 'person' => $userService->getPersonForUser($this->getUser()), // Get person ObjectEntity (->Entity with function = person) by id + 'organization' => $userService->getOrganizationForUser($this->getUser()), // Get organization ObjectEntity (->Entity with function = organization) by id ]; $result = json_encode($result); } else { diff --git a/api/src/Controller/UserController.php b/api/src/Controller/UserController.php index 71cdc0eb4..cdc9ba715 100644 --- a/api/src/Controller/UserController.php +++ b/api/src/Controller/UserController.php @@ -64,7 +64,7 @@ public function resetTokenAction(SerializerInterface $serializer, \CommonGateway $accessToken = $this->authenticationService->refreshAccessToken($session->get('refresh_token'), $session->get('authenticator')); $user = $this->getUser(); if ($user instanceof AuthenticationUser === false) { - return new Response('User not found', 401); + return new Response(json_encode(["Message" => 'User not found.']), 401, ['Content-type' => 'application/json']); } $serializeUser = new User(); @@ -95,30 +95,28 @@ public function resetTokenAction(SerializerInterface $serializer, \CommonGateway $status = 200; $user = $this->getUser(); if ($user instanceof AuthenticationUser === false) { - return new Response('User not found', 401); + return new Response(json_encode(["Message" => 'User not found.']), 401, ['Content-type' => 'application/json']); } $user = $this->entityManager->getRepository('App:User')->find($user->getUserIdentifier()); - if ($user->getOrganization() !== null) { - $organizations[] = $user->getOrganization(); - } - foreach ($user->getApplications() as $application) { - if ($application->getOrganization() !== null) { - $organizations[] = $application->getOrganization(); - } - } + // Set organization id and user id in session + $this->session->set('user', $user->getId()->toString()); + $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); - // If user has no organization, we default activeOrganization to an organization of a userGroup this user has and else the application organization; - $this->session->set('activeOrganization', $user->getOrganization()->getId()->toString()); + $response = $this->validateUserApp($user); + if ($response !== null) + return $response; + // TODO: maybe do not just get the first Application here, but get application using ApplicationService->getApplication() and ... + // todo... if this returns an application check if the user is part of this application or one of the organizations of this application? $user->setJwtToken($authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session))); return new Response($serializer->serialize($user, 'json'), $status, ['Content-type' => 'application/json']); } /** - * Create an authentication user from a entity user. + * Create an authentication user from an entity user. * * @param User $user The user to log in. * @@ -166,10 +164,11 @@ public function createAuthenticationUser(User $user): AuthenticationUser } /** - * Add the logged in user to session. + * Add the logged-in user to session. * - * @param User $user The user to log in. + * @param User $user The user to log in. * @param EventDispatcherInterface $eventDispatcher The event dispatcher. + * @param Request $request * * @return void */ @@ -203,18 +202,16 @@ public function apiLoginAction(Request $request, UserPasswordHasherInterface $ha return new Response(json_encode($response), 401, ['Content-type' => 'application/json']); } - if ($user->getOrganization() !== null) { - $organizations[] = $user->getOrganization(); - } - foreach ($user->getApplications() as $application) { - if ($application->getOrganization() !== null) { - $organizations[] = $application->getOrganization(); - } - } + // Set organization id and user id in session + $this->session->set('user', $user->getId()->toString()); + $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); - // If user has no organization, we default activeOrganization to an organization of a userGroup this user has and else the application organization; - $this->session->set('activeOrganization', $user->getOrganization()->getId()->toString()); + $response = $this->validateUserApp($user); + if ($response !== null) + return $response; + // TODO: maybe do not just get the first Application here, but get application using ApplicationService->getApplication() and ... + // todo... if this returns an application check if the user is part of this application or one of the organizations of this application? $token = $authenticationService->createJwtToken($user->getApplications()[0]->getPrivateKey(), $authenticationService->serializeUser($user, $this->session)); $user->setJwtToken($token); @@ -238,6 +235,34 @@ public function apiLoginAction(Request $request, UserPasswordHasherInterface $ha return new Response(json_encode($userArray), $status, ['Content-type' => 'application/json']); } + /** + * Checks if $user has an application and if that application has a PrivateKey set. If not return error Response. + * + * @param User $user A user to check. + * + * @return Response|null Error Response or null. + */ + private function validateUserApp(User $user): ?Response + { + if (empty($user->getApplications()) === true) { + return new Response( + json_encode(["Message" => 'This user is not yet connected to any application.']), + 409, + ['Content-type' => 'application/json'] + ); + } + + if (empty($user->getApplications()[0]->getPrivateKey()) === true) { + return new Response( + json_encode(["Message" => "Can't create a token because application ({$user->getApplications()[0]->getId()->toString()}) doesn't have a PrivateKey."]), + 409, + ['Content-type' => 'application/json'] + ); + } + + return null; + } + /** * Removes some sensitive data from the login response. * diff --git a/api/src/Controller/ZZController.php b/api/src/Controller/ZZController.php index 632d5fad8..8806b7fed 100644 --- a/api/src/Controller/ZZController.php +++ b/api/src/Controller/ZZController.php @@ -58,12 +58,12 @@ public function objectAction( } /** - * @TODO This function needs to be more dynamic: /{item}/api/{path}. * This function dynamically handles the custom endpoints. * - * @Route("/klanten/api/{path}", name="dynamic_route_second", requirements={"path" = ".+"}) + * @Route("/{prefix}/api/{path}", name="dynamic_route_second", requirements={"path" = ".+"}) * * @param string|null $path + * @param string|null $bundle * @param Request $request * @param EndpointService $endpointService * @return Response @@ -71,6 +71,7 @@ public function objectAction( */ public function dynamicCustomAction( ?string $path, + ?string $bundle, Request $request, EndpointService $endpointService ): Response { @@ -181,6 +182,8 @@ private function getAcceptType(Request $request): string return 'html'; case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': return 'docx'; + case 'application/json+aggregations': + return 'aggregations'; }//end switch throw new BadRequestHttpException('No proper accept could be determined'); diff --git a/api/src/Entity/Action.php b/api/src/Entity/Action.php index 17c9e6cb4..64f8c0a41 100644 --- a/api/src/Entity/Action.php +++ b/api/src/Entity/Action.php @@ -15,6 +15,7 @@ use Gedmo\Mapping\Annotation as Gedmo; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; @@ -32,7 +33,7 @@ * collectionOperations={ * "get"={"path"="/admin/actions"}, * "post"={"path"="/admin/actions"} - * }) + * } * ) * * @ORM\HasLifecycleCallbacks @@ -48,6 +49,8 @@ * "name": "exact", * "reference": "exact" * }) + * + * @UniqueEntity("reference") */ class Action { @@ -73,16 +76,20 @@ class Action /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true) */ - private $reference; + private ?string $reference; /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private $version; + private string $version = '0.0.0'; /** * @var string The name of the action diff --git a/api/src/Entity/ActionHandler.php b/api/src/Entity/ActionHandler.php index 7c531ba14..50d3a301e 100644 --- a/api/src/Entity/ActionHandler.php +++ b/api/src/Entity/ActionHandler.php @@ -28,7 +28,7 @@ * collectionOperations={ * "get"={"path"="/admin/actionHandlers"}, * "post"={"path"="/admin/actionHandlers"} - * }) + * } * ) * * @ORM\Entity(repositoryClass=ActionHandlerRepository::class) diff --git a/api/src/Entity/Application.php b/api/src/Entity/Application.php index 2afb2f2fd..3d3a52989 100644 --- a/api/src/Entity/Application.php +++ b/api/src/Entity/Application.php @@ -16,6 +16,7 @@ use Gedmo\Mapping\Annotation as Gedmo; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\MaxDepth; use Symfony\Component\Validator\Constraints as Assert; @@ -34,7 +35,7 @@ * collectionOperations={ * "get"={"path"="/admin/applications"}, * "post"={"path"="/admin/applications"} - * }) + * } * ) * * @ORM\HasLifecycleCallbacks @@ -50,6 +51,8 @@ * "name": "exact", * "reference": "exact" * }) + * + * @UniqueEntity("reference") */ class Application { @@ -101,6 +104,8 @@ class Application /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) */ private ?string $reference = null; @@ -108,9 +113,11 @@ class Application /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * The hosts that this applications uses, keep in ind that a host is exluding a trailing slach / and https:// ot http://. diff --git a/api/src/Entity/Attribute.php b/api/src/Entity/Attribute.php index 1bcd2d559..8d899ac4d 100644 --- a/api/src/Entity/Attribute.php +++ b/api/src/Entity/Attribute.php @@ -24,7 +24,7 @@ use Symfony\Component\Validator\Constraints as Assert; /** - * An possible attribute on an Entity. + * A possible attribute on an Entity. * * @category Entity * @@ -39,7 +39,8 @@ * collectionOperations={ * "get"={"path"="/admin/attributes"}, * "post"={"path"="/admin/attributes"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass=AttributeRepository::class) * diff --git a/api/src/Entity/AuditTrail.php b/api/src/Entity/AuditTrail.php index 17e0f1517..313136ae5 100644 --- a/api/src/Entity/AuditTrail.php +++ b/api/src/Entity/AuditTrail.php @@ -33,7 +33,8 @@ * "get"={"path"="/admin/audit_trails"}, * "post"={"path"="/admin/audit_trails"} * }, - * attributes={"order"={"creationDate": "DESC"}}) + * attributes={"order"={"creationDate": "DESC"}} + * ) * * @ORM\Entity(repositoryClass=AuditTrailRepository::class) * diff --git a/api/src/Entity/CollectionEntity.php b/api/src/Entity/CollectionEntity.php index 97823f723..c34e1a7eb 100644 --- a/api/src/Entity/CollectionEntity.php +++ b/api/src/Entity/CollectionEntity.php @@ -36,7 +36,8 @@ * collectionOperations={ * "get"={"path"="/admin/collections"}, * "post"={"path"="/admin/collections"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass=CollectionEntityRepository::class) * @@ -51,6 +52,7 @@ * }) * * @UniqueEntity("name") + * @UniqueEntity("reference") */ class CollectionEntity { @@ -100,6 +102,8 @@ class CollectionEntity /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) */ private ?string $reference = null; @@ -107,9 +111,11 @@ class CollectionEntity /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * @var ?string The location where the OAS can be loaded from diff --git a/api/src/Entity/Contract.php b/api/src/Entity/Contract.php index 5184262c4..f32d81283 100644 --- a/api/src/Entity/Contract.php +++ b/api/src/Entity/Contract.php @@ -33,7 +33,8 @@ * collectionOperations={ * "get"={"path"="/admin/contracts"}, * "post"={"path"="/admin/contracts"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass=ContractRepository::class) * diff --git a/api/src/Entity/Cronjob.php b/api/src/Entity/Cronjob.php index 862ba6b03..cc220c227 100644 --- a/api/src/Entity/Cronjob.php +++ b/api/src/Entity/Cronjob.php @@ -13,6 +13,7 @@ use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; @@ -30,7 +31,8 @@ * collectionOperations={ * "get"={"path"="/admin/cronjobs"}, * "post"={"path"="/admin/cronjobs"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass=CronjobRepository::class) * @@ -43,6 +45,8 @@ * "name": "exact", * "reference": "exact" * }) + * + * @UniqueEntity("reference") */ class Cronjob { @@ -94,6 +98,8 @@ class Cronjob /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) */ private ?string $reference = null; @@ -101,9 +107,11 @@ class Cronjob /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * @var string The crontab that determines the interval https://crontab.guru/ diff --git a/api/src/Entity/DashboardCard.php b/api/src/Entity/DashboardCard.php index 777cb6c4e..8e78148ce 100644 --- a/api/src/Entity/DashboardCard.php +++ b/api/src/Entity/DashboardCard.php @@ -27,7 +27,8 @@ * collectionOperations={ * "get"={"path"="/admin/dashboardCards"}, * "post"={"path"="/admin/dashboardCards"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass=DashboardCardRepository::class) * diff --git a/api/src/Entity/Endpoint.php b/api/src/Entity/Endpoint.php index 567c865c4..1eb7c2772 100644 --- a/api/src/Entity/Endpoint.php +++ b/api/src/Entity/Endpoint.php @@ -18,6 +18,7 @@ use Gedmo\Mapping\Annotation as Gedmo; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\MaxDepth; use Symfony\Component\Validator\Constraints as Assert; @@ -36,7 +37,8 @@ * collectionOperations={ * "get"={"path"="/admin/endpoints"}, * "post"={"path"="/admin/endpoints"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass="App\Repository\EndpointRepository") * @@ -57,6 +59,8 @@ * "entities", * "proxy" * }) + * + * @UniqueEntity("reference") */ class Endpoint { @@ -321,6 +325,8 @@ class Endpoint /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true) */ private ?string $reference = null; @@ -328,9 +334,11 @@ class Endpoint /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * Constructor for creating an Endpoint. Use $entity to create an Endpoint for an Entity or diff --git a/api/src/Entity/Entity.php b/api/src/Entity/Entity.php index a3091daed..2185064a3 100644 --- a/api/src/Entity/Entity.php +++ b/api/src/Entity/Entity.php @@ -19,6 +19,7 @@ use Gedmo\Mapping\Annotation as Gedmo; use phpDocumentor\Reflection\Types\This; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\MaxDepth; use Symfony\Component\Validator\Constraints as Assert; @@ -50,7 +51,8 @@ * "description"="Deletes all objects that belong to this schema" * } * }, - * }) + * } + * ) * * @ORM\Entity(repositoryClass="App\Repository\EntityRepository") * @@ -63,6 +65,8 @@ * "name": "exact", * "reference": "exact" * }) + * + * @UniqueEntity("reference") */ class Entity { @@ -411,6 +415,8 @@ class Entity /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) */ private ?string $reference = null; @@ -418,9 +424,11 @@ class Entity /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; //todo: do we want read/write groups here? /** diff --git a/api/src/Entity/Gateway.php b/api/src/Entity/Gateway.php index fc0e8c491..e3251a52c 100644 --- a/api/src/Entity/Gateway.php +++ b/api/src/Entity/Gateway.php @@ -149,6 +149,7 @@ * }) * * @UniqueEntity("name") + * @UniqueEntity("reference") */ class Gateway { @@ -216,6 +217,8 @@ class Gateway /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) */ private ?string $reference = null; @@ -223,9 +226,11 @@ class Gateway /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * @var string The location where the Gateway needs to be accessed @@ -559,11 +564,26 @@ class Gateway private ?string $documentation = null; /** - * Setting logging to true will couse ALL responses to be logged (normaly we only log errors). Doing so wil dramaticly slow down the gateway and couse an increase in database size. This is not recomended outside of development purposes. + * @var array Configuration for logging, when an api call is made on the source we can log some information for this call. With this array you can enable/disable what will be logged. * - * @ORM\Column(type="boolean", nullable=true) + * @Assert\NotNull + * + * @Groups({"read","write"}) + * + * @ORM\Column(type="array") */ - private $logging; + private array $loggingConfig = [ + 'callMethod' => true, + 'callUrl' => true, + 'callQuery' => true, + 'callContentType' => true, + 'callBody' => true, + 'responseStatusCode' => true, + 'responseContentType' => true, + 'responseBody' => true, + 'maxCharCountBody' => 500, + 'maxCharCountErrorBody' => 2000, + ]; /** * @var array ... @@ -617,7 +637,9 @@ class Gateway * * @ORM\Column(type="array", nullable=true) */ - private ?array $configuration = []; + private ?array $configuration = [ + "verify" => true + ]; /** * @var array|null The configuration for endpoints on this source, mostly mapping for now. @@ -792,10 +814,16 @@ public function fromSchema(array $schema): self array_key_exists('jwtId', $schema) ? $this->setJwtId($schema['jwtId']) : ''; array_key_exists('username', $schema) ? $this->setUsername($schema['username']) : ''; array_key_exists('documentation', $schema) ? $this->setDocumentation($schema['documentation']) : ''; + array_key_exists('loggingConfig', $schema) ? $this->setLoggingConfig($schema['loggingConfig']) : ''; array_key_exists('headers', $schema) ? $this->setHeaders($schema['headers']) : ''; array_key_exists('translationConfig', $schema) ? $this->setTranslationConfig($schema['translationConfig']) : ''; array_key_exists('type', $schema) ? $this->setType($schema['type']) : ''; - array_key_exists('configuration', $schema) ? $this->setConfiguration($schema['configuration']) : ''; + if (isset($schema['configuration']) === true) { + if (isset($schema['configuration']['verify']) === false) { + $schema['configuration']['verify'] = true; + } + $this->setConfiguration($schema['configuration']); + } array_key_exists('endpointsConfig', $schema) ? $this->setEndpointsConfig($schema['endpointsConfig']) : ''; array_key_exists('isEnabled', $schema) ? $this->setIsEnabled($schema['isEnabled']) : ''; @@ -826,6 +854,7 @@ public function toSchema(): array 'jwtId' => $this->getJwtId(), 'username' => $this->getUsername(), 'documentation' => $this->getDocumentation(), + 'loggingConfig' => $this->getLoggingConfig(), 'headers' => $this->getHeaders(), 'translationConfig' => $this->getTranslationConfig(), 'type' => $this->getType(), @@ -857,6 +886,7 @@ public function export(): ?array 'password' => $this->getPassword(), 'apikey' => $this->getApikey(), 'documentation' => $this->getDocumentation(), + 'loggingConfig' => $this->getLoggingConfig(), 'headers' => $this->getHeaders(), 'translationConfig' => $this->getTranslationConfig(), 'type' => $this->getType(), @@ -1129,14 +1159,14 @@ public function setDocumentation(?string $documentation): self return $this; } - public function getLogging(): ?bool + public function getLoggingConfig(): ?array { - return $this->logging; + return $this->loggingConfig; } - public function setLogging(?bool $logging): self + public function setLoggingConfig(array $loggingConfig): self { - $this->logging = $logging; + $this->loggingConfig = array_merge($loggingConfig); return $this; } diff --git a/api/src/Entity/Mapping.php b/api/src/Entity/Mapping.php index 6187d7fd4..5e7a2a952 100644 --- a/api/src/Entity/Mapping.php +++ b/api/src/Entity/Mapping.php @@ -15,6 +15,7 @@ use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; @@ -30,7 +31,7 @@ * collectionOperations={ * "get"={"path"="/admin/mappings"}, * "post"={"path"="/admin/mappings"} - * }) + * } * ) * * @ORM\Entity(repositoryClass=MappingRepository::class) @@ -44,6 +45,8 @@ * "name": "exact", * "reference": "exact" * }) + * + * @UniqueEntity("reference") */ class Mapping { @@ -69,6 +72,8 @@ class Mapping /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) */ private ?string $reference = null; @@ -76,9 +81,11 @@ class Mapping /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * @var string The name of the mapping diff --git a/api/src/Entity/ObjectEntity.php b/api/src/Entity/ObjectEntity.php index fb6eb4e43..8f9095279 100644 --- a/api/src/Entity/ObjectEntity.php +++ b/api/src/Entity/ObjectEntity.php @@ -19,7 +19,7 @@ use function Symfony\Component\Translation\t; /** - * An (data) object that resides within the datalayer of the gateway. + * A (data) object that resides within the datalayer of the gateway. * * @category Entity * @@ -1184,6 +1184,12 @@ public function toArray(array $configuration = []): array $config['maxDepth'] = $attribute->getObject()->getMaxDepth() + $config['level']; } $config['level'] = $config['level'] + 1; + + //TODO: This is a very hacky solution that has to be changed back ASAP + if ($attribute->getObject() === $this->getEntity()) { + $config['maxDepth'] = $config['level']; + } + $objectToArray = $object->toArray($config); // Check if we want an embedded array @@ -1251,6 +1257,11 @@ public function toArray(array $configuration = []): array $config['maxDepth'] = $attribute->getObject()->getMaxDepth() + $config['level']; } $config['level'] = $config['level'] + 1; + + //TODO: This is a very hacky solution that has to be changed back ASAP + if ($attribute->getObject() === $this->getEntity()) { + $config['maxDepth'] = $config['level']; + } $objectToArray = $object->toArray($config); // Check if we want an embedded array diff --git a/api/src/Entity/Organization.php b/api/src/Entity/Organization.php index cd231688b..7043caac4 100644 --- a/api/src/Entity/Organization.php +++ b/api/src/Entity/Organization.php @@ -16,6 +16,7 @@ use Gedmo\Mapping\Annotation as Gedmo; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\MaxDepth; use Symfony\Component\Validator\Constraints as Assert; @@ -34,7 +35,8 @@ * collectionOperations={ * "get"={"path"="/admin/organizations"}, * "post"={"path"="/admin/organizations"} - * }) + * } + * ) * * @ORM\HasLifecycleCallbacks * @@ -49,6 +51,8 @@ * "name": "exact", * "reference": "exact" * }) + * + * @UniqueEntity("reference") */ class Organization { @@ -100,6 +104,8 @@ class Organization /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) */ private ?string $reference = null; @@ -107,9 +113,11 @@ class Organization /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * @Groups({"read", "write"}) diff --git a/api/src/Entity/Property.php b/api/src/Entity/Property.php index f50b8da07..eaeadb576 100644 --- a/api/src/Entity/Property.php +++ b/api/src/Entity/Property.php @@ -28,7 +28,7 @@ * collectionOperations={ * "get"={"path"="/admin/properties"}, * "post"={"path"="/admin/properties"} - * }) + * } * ) * * @ORM\Entity(repositoryClass=PropertyRepository::class) diff --git a/api/src/Entity/Purpose.php b/api/src/Entity/Purpose.php index 23c26c033..71af172b7 100644 --- a/api/src/Entity/Purpose.php +++ b/api/src/Entity/Purpose.php @@ -28,7 +28,8 @@ * collectionOperations={ * "get"={"path"="/admin/purposes"}, * "post"={"path"="/admin/purposes"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass=PurposeRepository::class) * diff --git a/api/src/Entity/SecurityGroup.php b/api/src/Entity/SecurityGroup.php index ded4d2b26..0203a7cab 100644 --- a/api/src/Entity/SecurityGroup.php +++ b/api/src/Entity/SecurityGroup.php @@ -16,6 +16,7 @@ use Gedmo\Mapping\Annotation as Gedmo; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\MaxDepth; use Symfony\Component\Validator\Constraints as Assert; @@ -34,7 +35,7 @@ * collectionOperations={ * "get"={"path"="/admin/user_groups"}, * "post"={"path"="/admin/user_groups"} - * }) + * } * ) * * @ORM\HasLifecycleCallbacks @@ -50,6 +51,8 @@ * "name": "exact", * "reference": "exact" * }) + * + * @UniqueEntity("reference") */ class SecurityGroup { @@ -101,6 +104,8 @@ class SecurityGroup /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) */ private ?string $reference = null; @@ -108,9 +113,11 @@ class SecurityGroup /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * @Groups({"read", "write"}) diff --git a/api/src/Entity/Synchronization.php b/api/src/Entity/Synchronization.php index 4bd2025e7..6fc019997 100644 --- a/api/src/Entity/Synchronization.php +++ b/api/src/Entity/Synchronization.php @@ -32,7 +32,8 @@ * collectionOperations={ * "get"={"path"="/admin/synchronizations"}, * "post"={"path"="/admin/synchronizations"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass=SynchronizationRepository::class) * @@ -154,7 +155,16 @@ class Synchronization private ?string $hash = ''; /** - * @var bool Whether or not the synchronization is blocked + * @var ?string The sha(256) used to check if a Sync should be triggered cause the object has changed + * + * @Groups({"read","write"}) + * + * @ORM\Column(type="string", nullable=true) + */ + private ?string $sha = null; + + /** + * @var bool Whether the synchronization is blocked * * @Groups({"read", "write"}) * @@ -361,6 +371,18 @@ public function setHash(?string $hash): self return $this; } + public function getSha(): ?string + { + return $this->sha; + } + + public function setSha(?string $sha): self + { + $this->sha = $sha; + + return $this; + } + public function getSourceLastChanged(): ?\DateTimeInterface { return $this->sourceLastChanged; diff --git a/api/src/Entity/Template.php b/api/src/Entity/Template.php index a400d7a1a..4fa1edd19 100644 --- a/api/src/Entity/Template.php +++ b/api/src/Entity/Template.php @@ -13,6 +13,7 @@ use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; @@ -30,7 +31,8 @@ * collectionOperations={ * "get"={"path"="/admin/templates"}, * "post"={"path"="/admin/templates"} - * })) + * } + * ) * * @ORM\Entity(repositoryClass=TemplateRepository::class) * @@ -41,8 +43,11 @@ * @ApiFilter(DateFilter::class, strategy=DateFilter::EXCLUDE_NULL) * @ApiFilter(SearchFilter::class, properties={ * "supportedSchemas": "exact", - * "name": "exact" + * "name": "exact", + * "reference": "exact" * }) + * + * @UniqueEntity("reference") */ class Template { @@ -127,6 +132,8 @@ class Template /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true) */ private ?string $reference = null; @@ -134,9 +141,11 @@ class Template /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * @var Datetime|null The moment this resource was created diff --git a/api/src/Entity/Translation.php b/api/src/Entity/Translation.php index c5f7561a0..731ade771 100644 --- a/api/src/Entity/Translation.php +++ b/api/src/Entity/Translation.php @@ -35,7 +35,8 @@ * "path"="/admin/table_names" * }, * "post"={"path"="/admin/translations"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass=TranslationRepository::class) * diff --git a/api/src/Entity/Unread.php b/api/src/Entity/Unread.php index 836f1a822..2f96e9a2b 100644 --- a/api/src/Entity/Unread.php +++ b/api/src/Entity/Unread.php @@ -30,7 +30,7 @@ * collectionOperations={ * "get"={"path"="/admin/unreads"}, * "post"={"path"="/admin/unreads"} - * }) + * } * ) * * @ORM\Entity(repositoryClass="App\Repository\UnreadRepository") diff --git a/api/src/Entity/User.php b/api/src/Entity/User.php index 845610de9..7d2797e63 100644 --- a/api/src/Entity/User.php +++ b/api/src/Entity/User.php @@ -16,6 +16,7 @@ use Gedmo\Mapping\Annotation as Gedmo; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\MaxDepth; @@ -35,7 +36,8 @@ * collectionOperations={ * "get"={"path"="/admin/users"}, * "post"={"path"="/admin/users"} - * }) + * } + * ) * * @ORM\HasLifecycleCallbacks * @@ -50,6 +52,8 @@ * "reference": "exact" * }) * + * @UniqueEntity("reference") + * * @ORM\Table(name="`user`") */ class User implements PasswordAuthenticatedUserInterface @@ -102,6 +106,8 @@ class User implements PasswordAuthenticatedUserInterface /** * @Groups({"read", "write"}) * + * @Assert\NotNull + * * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) */ private ?string $reference = null; @@ -109,9 +115,11 @@ class User implements PasswordAuthenticatedUserInterface /** * @Groups({"read", "write"}) * - * @ORM\Column(type="string", length=255, nullable=true, options={"default": null}) + * @Assert\NotNull + * + * @ORM\Column(type="string", length=255, options={"default": "0.0.0"}) */ - private ?string $version = null; + private string $version = '0.0.0'; /** * @Groups({"write"}) diff --git a/api/src/Entity/Value.php b/api/src/Entity/Value.php index e83e0f249..89a6c4cae 100644 --- a/api/src/Entity/Value.php +++ b/api/src/Entity/Value.php @@ -40,7 +40,8 @@ * collectionOperations={ * "get"={"path"="/admin/values"}, * "post"={"path"="/admin/values"} - * }) + * } + * ) * * @ORM\Entity(repositoryClass="App\Repository\ValueRepository") * @@ -659,21 +660,38 @@ public function setValue($value, bool $unsafe = false, ?DateTimeInterface $dateM foreach ($valueArray as $value) { // Catch Array input (for hydrator) if (is_array($value)) { - $object = new ObjectEntity($this->getAttribute()->getObject()); + $object = null; + + // Make sure to not create new objects if we don't have to (_id in testdata)... + if (isset($value['_id'])) { + $objects = $this->objects->filter(function ($item) use ($value) { + return $item->getId() !== null && $item->getId()->toString() === $value['_id']; + }); + + if (count($objects) > 0) { + $object = $objects[0]; + } + } + if ($object instanceof ObjectEntity === false) { + // failsafe to not create duplicate sub objects. In some weird cases $objects[0] doesn't return an ObjectEntity. + if (isset($objects) === true && count($objects) > 0) { + continue; + } + $object = new ObjectEntity($this->getAttribute()->getObject()); + } $object->setOwner($this->getObjectEntity()->getOwner()); $object->setApplication($this->getObjectEntity()->getApplication()); $object->setOrganization($this->getObjectEntity()->getOrganization()); $object->hydrate($value, $unsafe, $dateModified); $value = $object; - $this->hydratedObjects[] = $object; } if (is_string($value)) { $idArray[] = $value; } elseif (!$value) { continue; - } elseif ($value instanceof ObjectEntity) { + } elseif ($value instanceof ObjectEntity && $this->objects->contains($value) === false) { $this->addObject($value); } } diff --git a/api/src/Repository/ActionRepository.php b/api/src/Repository/ActionRepository.php index 2a883d429..7d7e5746a 100644 --- a/api/src/Repository/ActionRepository.php +++ b/api/src/Repository/ActionRepository.php @@ -26,6 +26,8 @@ public function __construct(ManagerRegistry $registry) */ public function findByListens(string $listen): array { + // Todo: maybe add actions to MongoDB as well, so we can use better queries for this: + // Todo: %$listen% will sometimes find actions it shouldn't find, example: action listening to test.test.123 will be found if action test.test is thrown. $query = $this->createQueryBuilder('a') ->andWhere('a.listens LIKE :listen') ->setParameter('listen', "%$listen%") diff --git a/api/src/Repository/ApplicationRepository.php b/api/src/Repository/ApplicationRepository.php index 15765f22b..53752d1c2 100644 --- a/api/src/Repository/ApplicationRepository.php +++ b/api/src/Repository/ApplicationRepository.php @@ -21,24 +21,21 @@ public function __construct(ManagerRegistry $registry) } /** - * @param string $domain + * Find all applications that have the given $domain in there list of domains. * - * @throws NonUniqueResultException + * @param string $domain A domain to search with. * - * @return Application|null + * @return array|null */ - public function findByDomain(string $domain): ?Application + public function findByDomain(string $domain): ?array { - // TODO: something like this $query = $this->createQueryBuilder('a') - ->andWhere(':domain IN (a.domains)') - ->setParameters(['domain' => $domain]); - -// var_dump($query->getDQL()); + ->andWhere('a.domains LIKE :domain') + ->setParameters(['domain' => "%$domain%"]); return $query ->getQuery() - ->getOneOrNullResult(); + ->getResult(); } // /** diff --git a/api/src/Repository/ObjectEntityRepository.php b/api/src/Repository/ObjectEntityRepository.php index 6823c370f..9f3b68051 100644 --- a/api/src/Repository/ObjectEntityRepository.php +++ b/api/src/Repository/ObjectEntityRepository.php @@ -35,6 +35,105 @@ public function __construct(ManagerRegistry $registry, SessionInterface $session parent::__construct($registry, ObjectEntity::class); } + /** + * Gets and returns an array with the allowed filters on an Entity (including its subEntities / sub-filters). + * + * @param Entity $Entity The Entity we are currently doing a get collection on. + * @param string $prefix + * @param int $level + * @param bool $embedded + * + * @return array The array with allowed filters. + */ + public function getFilterParameters(Entity $Entity, string $prefix = '', int $level = 1, bool $embedded = false): array + { + $prefix = $embedded && $level === 2 ? "embedded.$prefix" : $prefix; + + //todo: we only check for the allowed keys/attributes to filter on, if this attribute is a dateTime (or date), we should also check if the value is a valid dateTime string? + // NOTE: + // Filter id looks for ObjectEntity id and externalId + // Filter _id looks specifically/only for ObjectEntity id + // Filter _externalId looks specifically/only for ObjectEntity externalId + + // defaults + $filters = [ + $prefix.'id', $prefix.'_id', $prefix.'_externalId', $prefix.'_uri', $prefix.'_self', $prefix.'_organization', + $prefix.'_application', $prefix.'_dateCreated', $prefix.'_dateModified', $prefix.'_mapping', + ]; + + foreach ($Entity->getAttributes() as $attribute) { + if (in_array($attribute->getType(), ['string', 'date', 'datetime', 'integer', 'float', 'number', 'boolean']) && $attribute->getSearchable()) { + $filters[] = $prefix.$attribute->getName(); + } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { + $attribute->getSearchable() && $filters[] = $prefix.$attribute->getName(); + $embeddedString = ''; + if ($embedded && $level > 1) { + $embeddedString = 'embedded.'; + } + $filters = array_merge($filters, $this->getFilterParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1, $embedded)); + } + } + + return $filters; + } + + /** + * Gets and returns an array with the allowed sortable attributes on an Entity (including its subEntities). + * + * @param Entity $Entity The Entity we are currently doing a get collection on. + * @param string $prefix + * @param int $level + * @param bool $embedded + * + * @return array The array with allowed attributes to sort by. + */ + public function getOrderParameters(Entity $Entity, string $prefix = '', int $level = 1, bool $embedded = false): array + { + $prefix = $prefix; + if ($embedded && $level === 2) { + $prefix = "embedded.$prefix"; + } + // defaults + $sortable = [$prefix.'_dateCreated', $prefix.'_dateModified']; + + foreach ($Entity->getAttributes() as $attribute) { + if (in_array($attribute->getType(), ['string', 'date', 'datetime', 'integer', 'float', 'number']) && $attribute->getSortable()) { + $sortable[] = $prefix.$attribute->getName(); + } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { + $embeddedString = ''; + if ($embedded && $level > 1) { + $embeddedString = 'embedded.'; + } + $sortable = array_merge($sortable, $this->getOrderParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1)); + } + } + + return $sortable; + } + + /** + * Finds object entities on their id or a sourceId of a synchronization this ObjectEntity has. + * + * @param string $identifier + * + * @throws NonUniqueResultException + * + * @return ObjectEntity The found object entity + */ + public function findByAnyId(string $identifier): ?ObjectEntity + { + $query = $this->createQueryBuilder('o') + ->leftJoin('o.synchronizations', 's') + ->where('s.sourceId = :identifier') + ->setParameter('identifier', $identifier); + + if (Uuid::isValid($identifier)) { + $query->orWhere('o.id = :identifier'); + } + + return $query->getQuery()->getOneOrNullResult(); + } + /** * Does the same as findByEntity(), but also returns an integer representing the total amount of results using the input to create a sql statement. $entity is required. * @@ -47,6 +146,7 @@ public function __construct(ManagerRegistry $registry, SessionInterface $session * @throws NoResultException|NonUniqueResultException * * @return array With a key 'objects' containing the actual objects found and a key 'total' with an integer representing the total amount of results found. + * @deprecated */ public function findAndCountByEntity(Entity $entity, array $filters = [], array $order = [], int $offset = 0, int $limit = 25): array { @@ -81,6 +181,7 @@ public function findAndCountByEntity(Entity $entity, array $filters = [], array * @throws Exception * * @return array Returns an array of ObjectEntity objects + * @deprecated */ public function findByEntity(Entity $entity, array $filters = [], array $order = [], int $offset = 0, int $limit = 25, QueryBuilder $query = null): array { @@ -103,6 +204,7 @@ public function findByEntity(Entity $entity, array $filters = [], array $order = * @throws NoResultException|NonUniqueResultException * * @return int Returns an integer, for the total ObjectEntities found with this Entity and with the given filters. + * @deprecated */ public function countByEntity(Entity $entity, array $filters = [], QueryBuilder $query = null): int { @@ -155,11 +257,11 @@ private function createQuery(Entity $entity, array $filters = [], array $order = // Multitenancy, only show objects this user is allowed to see. // Only show objects this user owns or object that have an organization this user is part of or that are inhereted down the line - $organizations = $this->session->get('organizations', []); + $organizations = []; $parentOrganizations = []; // Make sure we only check for parentOrganizations if inherited is true in the (ObjectEntity)->entity->inherited if ($entity->getInherited()) { - $parentOrganizations = $this->session->get('parentOrganizations', []); + $parentOrganizations = []; } // $query->andWhere('o.organization IN (:organizations) OR o.organization IN (:parentOrganizations) OR o.organization = :defaultOrganization OR o.owner = :userId') @@ -872,92 +974,4 @@ private function makeKeySqlFriendly(string $key): string // todo, probably add more special characters to replace... return str_replace('-', 'Dash', $key); } - - /** - * Gets and returns an array with the allowed filters on an Entity (including its subEntities / sub-filters). - * - * @param Entity $Entity The Entity we are currently doing a get collection on. - * @param string $prefix - * @param int $level - * - * @return array The array with allowed filters. - */ - public function getFilterParameters(Entity $Entity, string $prefix = '', int $level = 1, bool $embedded = false): array - { - $prefix = $embedded && $level === 2 ? "embedded.$prefix" : $prefix; - - //todo: we only check for the allowed keys/attributes to filter on, if this attribute is a dateTime (or date), we should also check if the value is a valid dateTime string? - // NOTE: - // Filter id looks for ObjectEntity id and externalId - // Filter _id looks specifically/only for ObjectEntity id - // Filter _externalId looks specifically/only for ObjectEntity externalId - - // defaults - $filters = [ - $prefix.'id', $prefix.'_id', $prefix.'_externalId', $prefix.'_uri', $prefix.'_self', $prefix.'_organization', - $prefix.'_application', $prefix.'_dateCreated', $prefix.'_dateModified', $prefix.'_mapping', - ]; - - foreach ($Entity->getAttributes() as $attribute) { - if (in_array($attribute->getType(), ['string', 'date', 'datetime', 'integer', 'float', 'number', 'boolean']) && $attribute->getSearchable()) { - $filters[] = $prefix.$attribute->getName(); - } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { - $attribute->getSearchable() && $filters[] = $prefix.$attribute->getName(); - $embeddedString = $embedded && $level > 1 ? 'embedded.' : ''; - $filters = array_merge($filters, $this->getFilterParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1, $embedded)); - } - } - - return $filters; - } - - /** - * Gets and returns an array with the allowed sortable attributes on an Entity (including its subEntities). - * - * @param Entity $Entity The Entity we are currently doing a get collection on. - * @param string $prefix - * @param int $level - * - * @return array The array with allowed attributes to sort by. - */ - public function getOrderParameters(Entity $Entity, string $prefix = '', int $level = 1, bool $embedded = false): array - { - $prefix = $embedded && $level === 2 ? "embedded.$prefix" : $prefix; - // defaults - $sortable = [$prefix.'_dateCreated', $prefix.'_dateModified']; - - foreach ($Entity->getAttributes() as $attribute) { - if (in_array($attribute->getType(), ['string', 'date', 'datetime', 'integer', 'float', 'number']) && $attribute->getSortable()) { - $sortable[] = $prefix.$attribute->getName(); - } elseif ($attribute->getObject() && $level < 3 && !str_contains($prefix, $attribute->getName().'.')) { - $embeddedString = $embedded && $level > 1 ? 'embedded.' : ''; - $sortable = array_merge($sortable, $this->getOrderParameters($attribute->getObject(), $prefix.$embeddedString.$attribute->getName().'.', $level + 1)); - } - } - - return $sortable; - } - - /** - * Finds object entities on their id or a sourceId of a synchronization this ObjectEntity has. - * - * @param string $identifier - * - * @throws NonUniqueResultException - * - * @return ObjectEntity The found object entity - */ - public function findByAnyId(string $identifier): ?ObjectEntity - { - $query = $this->createQueryBuilder('o') - ->leftJoin('o.synchronizations', 's') - ->where('s.sourceId = :identifier') - ->setParameter('identifier', $identifier); - - if (Uuid::isValid($identifier)) { - $query->orWhere('o.id = :identifier'); - } - - return $query->getQuery()->getOneOrNullResult(); - } } diff --git a/api/src/Security/ApiKeyAuthenticator.php b/api/src/Security/ApiKeyAuthenticator.php index 9bb517b62..a5c04a7f0 100644 --- a/api/src/Security/ApiKeyAuthenticator.php +++ b/api/src/Security/ApiKeyAuthenticator.php @@ -2,6 +2,7 @@ namespace App\Security; +use App\Entity\Application; use App\Entity\User; use App\Security\User\AuthenticationUser; use Doctrine\ORM\EntityManagerInterface; @@ -76,20 +77,36 @@ public function authenticate(Request $request): PassportInterface { $key = $request->headers->get('Authorization'); $application = $this->entityManager->getRepository('App:Application')->findOneBy(['secret' => $key]); - if (!$application) { + if ($application === null) { throw new AuthenticationException('Invalid ApiKey'); } try { - $user = $application->getOrganization()->getUsers()[0]; + $user = $application->getOrganization()->getUsers()->first(); + + $userCollection = $application->getOrganization()->getUsers(); + $users = $userCollection->filter(function (User $user) { + return $user->getName() === 'APIKEY_USER'; + }); + + if (count($users) > 0) { + $user = $users->first(); + } } catch (\Exception $exception) { - throw new AuthenticationException('Invalid User'); + throw new AuthenticationException('An invalid User (or no user) is configured for this ApiKey'); } - $this->session->set('apiKeyApplication', $application->getId()->toString()); - if (!$user || !($user instanceof User)) { - throw new AuthenticationException('The provided token does not match the user it refers to'); + if ($user instanceof User === false) { + throw new AuthenticationException('An invalid User (or no user) is configured for this ApiKey'); } + + // Set apiKey Application id in session + $this->session->set('apiKeyApplication', $application->getId()->toString()); + + // Set organization id and user id in session + $this->session->set('user', $user->getId()->toString()); + $this->session->set('organization', $user->getOrganization() !== null ? $user->getOrganization()->getId()->toString() : null); + $roleArray = []; foreach ($user->getSecurityGroups() as $securityGroup) { $roleArray['roles'][] = "Role_{$securityGroup->getName()}"; @@ -105,16 +122,6 @@ public function authenticate(Request $request): PassportInterface } } - $organizations = []; - if ($user->getOrganization()) { - $organizations[] = $user->getOrganization(); - } - - $organizations[] = 'localhostOrganization'; - $this->session->set('organizations', $organizations); - // If user has no organization, we default activeOrganization to an organization of a userGroup this user has and else the application organization; - $this->session->set('activeOrganization', $user->getOrganization()); - $userArray = [ 'id' => $user->getId()->toString(), 'email' => $user->getEmail(), diff --git a/api/src/Security/OIDCAuthenticator.php b/api/src/Security/OIDCAuthenticator.php index a55b40e90..c7a000716 100644 --- a/api/src/Security/OIDCAuthenticator.php +++ b/api/src/Security/OIDCAuthenticator.php @@ -2,7 +2,11 @@ namespace App\Security; +use App\Entity\SecurityGroup; +use App\Entity\User; +use App\Exception\GatewayException; use App\Security\User\AuthenticationUser; +use App\Service\ApplicationService; use App\Service\AuthenticationService; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; @@ -32,6 +36,16 @@ class OIDCAuthenticator extends AbstractAuthenticator */ private LoggerInterface $logger; + /** + * @var \CommonGateway\CoreBundle\Service\AuthenticationService The new authenticationService + */ + private \CommonGateway\CoreBundle\Service\AuthenticationService $coreAuthenticationService; + + /** + * @var ApplicationService The application service + */ + private ApplicationService $applicationService; + /** * Constructor @@ -41,13 +55,17 @@ class OIDCAuthenticator extends AbstractAuthenticator * @param EntityManagerInterface $entityManager The entity manager * @param ParameterBagInterface $parameterBag The Parameter Bag * @param LoggerInterface $callLogger The call logger + * @param \CommonGateway\CoreBundle\Service\AuthenticationService $coreAuthenticationService The new auth service + * @param ApplicationService $applicationService $the application service */ public function __construct( AuthenticationService $authenticationService, SessionInterface $session, EntityManagerInterface $entityManager, ParameterBagInterface $parameterBag, - LoggerInterface $callLogger + LoggerInterface $callLogger, + \CommonGateway\CoreBundle\Service\AuthenticationService $coreAuthenticationService, + ApplicationService $applicationService ) { $this->authenticationService = $authenticationService; @@ -55,6 +73,8 @@ public function __construct( $this->entityManager = $entityManager; $this->parameterBag = $parameterBag; $this->logger = $callLogger; + $this->coreAuthenticationService = $coreAuthenticationService; + $this->applicationService = $applicationService; } public function supports(Request $request): ?bool @@ -82,28 +102,64 @@ public function authenticate(Request $request): PassportInterface $accessToken = $this->authenticationService->authenticate($method, $identifier, $code); $result = json_decode(base64_decode(explode('.', $accessToken['access_token'])[1]), true); - $this->logger->notice('Received result from OIDC connector', ['authResult' => $result]); + $this->logger->notice('Received result from OIDC connector', ['authResult' => $accessToken]); // Make sure groups is always an array, even if there are no groups. - if (is_array($result['groups']) === false && $result['groups'] !== null) { + if (isset($result['groups']) !== false && (is_array($result['groups']) === false && $result['groups'] !== null)) { $result['groups'] = [$result['groups']]; - } else if (is_array($result['groups']) === false) { + } else if (isset($result['groups']) === false || is_array($result['groups']) === false) { $result['groups'] = []; + if (isset($result['group']) === true && is_array($result['group']) === true) { + $result['groups'] = $result['group']; + } } - // Set default organization in session for multitenancy (see how this is done in other Authenticators, this can be different for each one!) - $defaultOrganization = $this->getDefaultOrganization(); - $organizations = [$defaultOrganization, 'localhostOrganization']; - $parentOrganizations[] = 'localhostOrganization'; - $this->session->set('organizations', $organizations); - $this->session->set('parentOrganizations', $parentOrganizations); - $this->session->set('activeOrganization', $defaultOrganization); - if (isset($accessToken['refresh_token'])) { - $this->session->set('refresh_token', $accessToken['refresh_token']); - } +// if (isset($accessToken['refresh_token'])) { +// $this->session->set('refresh_token', $accessToken['refresh_token']); +// $userIdentifier = $result['email']; +// } else { + $doctrineUser = $this->entityManager->getRepository('App:User')->findOneBy(['email' => $result['email']]); + if($doctrineUser instanceof User === false) { + $doctrineUser = new User(); + } + $doctrineUser->setName($result['name'] ?? $result['sub']); + $doctrineUser->setEmail($result['email']); + $doctrineUser->setPassword(''); + $doctrineUser->addApplication($this->applicationService->getApplication()); + $doctrineUser->setOrganization($doctrineUser->getApplications()->first()->getOrganization()); + $this->session->set('organization', $doctrineUser->getApplications()->first()->getOrganization()); + + foreach ($result['groups'] as $group) { + $securityGroup = $this->entityManager->getRepository('App:SecurityGroup')->findOneBy(['name' => $group]); + if ($securityGroup instanceof SecurityGroup === true) { + $doctrineUser->addSecurityGroup($securityGroup); + } + } + + $this->entityManager->persist($doctrineUser); + $this->entityManager->flush(); + + $userIdentifier = $doctrineUser->getId()->toString(); + + if (empty($doctrineUser->getApplications()[0]->getPrivateKey()) === true) { + throw new GatewayException("Can't create a token because application doesn't have a PrivateKey." ?? null, 409, null, [ + 'data' => ['application_id' => $doctrineUser->getApplications()[0]->getId()->toString()], 'path' => '', 'responseType' => Response::HTTP_CONFLICT, + ]); + } + + // TODO: maybe do not just get the first Application here, but get application using ApplicationService->getApplication() and ... + // todo... if this returns an application check if the user is part of this application or one of the organizations of this application? + $token = $this->coreAuthenticationService->createJwtToken($doctrineUser->getApplications()[0]->getPrivateKey(), $this->coreAuthenticationService->serializeUser($doctrineUser, $this->session)); + + $doctrineUser->setJwtToken($token); + $this->session->set('jwtToken', $token); + + $this->entityManager->persist($doctrineUser); + $this->entityManager->flush(); +// } return new Passport( - new UserBadge($result['email'], function ($userIdentifier) use ($result) { + new UserBadge($userIdentifier, function ($userIdentifier) use ($result) { return new AuthenticationUser( $userIdentifier, $result['email'], @@ -125,24 +181,6 @@ function ($credentials, $user) { ); } - private function getDefaultOrganization(): string - { - // Find application->organization - if ($this->session->get('application')) { - $application = $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); - if (!empty($application) && $application->getOrganization()) { - return $application->getOrganization(); - } - } - // Else find and return 'the' default organization - $organization = $this->entityManager->getRepository('App:ObjectEntity')->findOneBy(['id' => 'a1c8e0b6-2f78-480d-a9fb-9792142f4761']); - if (!empty($organization) && $organization->getOrganization()) { - return $organization->getOrganization(); - } - - return 'http://api/admin/organizations/a1c8e0b6-2f78-480d-a9fb-9792142f4761'; - } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return new RedirectResponse($this->session->get('backUrl', $this->parameterBag->get('defaultBackUrl')) ?? $request->headers->get('referer') ?? $request->getSchemeAndHttpHost()); diff --git a/api/src/Security/TokenAuthenticator.php b/api/src/Security/TokenAuthenticator.php index 10b2d63ac..f12845b68 100644 --- a/api/src/Security/TokenAuthenticator.php +++ b/api/src/Security/TokenAuthenticator.php @@ -99,6 +99,7 @@ public function getPublicKey(string $token): string * @param string $token The token provided by the user * * @return array The payload of the token + * @throws GatewayException */ public function validateToken(string $token): array { @@ -139,9 +140,7 @@ private function prefixRoles(array $roles): array * * @param Request $request * - * @throws CacheException * @throws GatewayException - * @throws InvalidArgumentException * * @return PassportInterface */ diff --git a/api/src/Service/ApplicationService.php b/api/src/Service/ApplicationService.php index 119049407..6536ff90c 100644 --- a/api/src/Service/ApplicationService.php +++ b/api/src/Service/ApplicationService.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\Entity\Application; use App\Exception\GatewayException; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\EntityManagerInterface; @@ -29,67 +30,64 @@ public function __construct( } /** - * A function that finds an application or creates one. + * A function that finds an application. * * @throws GatewayException */ - public function getApplication() + public function getApplication(): Application { - if ($application = $this->session->get('application')) { + // If application is already in the session + if ($this->session->has('application')) { $application = $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); - if (!empty($application)) { + if ($application !== null) { return $application; } - } elseif ($this->session->get('apiKeyApplication')) { - // If an api-key is used for authentication we already know which application is used - return $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('apiKeyApplication')]); } - // get publickey + // If an api-key is used for authentication we already know which application is used + if ($this->session->has('apiKeyApplication')) { + $application = $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('apiKeyApplication')]); + if ($application !== null) { + $this->session->set('application', $application->getId()->toString()); + return $application; + } + } + + // Find application using the publicKey $public = ($this->request->headers->get('public') ?? $this->request->query->get('public')); + if (empty($public) === false) { + $application = $this->entityManager->getRepository('App:Application')->findOneBy(['public' => $public]); + if ($application !== null) { + $this->session->set('application', $application->getId()->toString()); + return $application; + } + } - // get host/domain + // Find application using the host/domain $host = ($this->request->headers->get('host') ?? $this->request->query->get('host')); -// $host = 'api.buren.commonground.nu'; - ($application = $this->entityManager->getRepository('App:Application')->findOneBy(['public' => $public])) && !empty($application) && $this->session->set('application', $application->getId()->toString()); - - if (!$application) { - // @todo Create and use query in ApplicationRepository - - $criteria = new Criteria(); + if (empty($host) === false) { + $applications = $this->entityManager->getRepository('App:Application')->findByDomain($host); + if (count($applications) > 0) { + $this->session->set('application', $applications[0]->getId()->toString()); - // $application = $this->entityManager->getRepository('App:Application')->findAll()-> - $applications = $this->entityManager->getRepository('App:Application')->findAll(); - foreach ($applications as $app) { - $app->getDomains() !== null && in_array($host, $app->getDomains()) && $application = $app; - if (isset($application)) { - break; - } + return $applications[0]; } -// if(count($applications) > 0) { -// $application = $applications[0]; -// } } - if (!$application) { - $this->session->set('application', null); + // No application was found + $this->session->set('application', null); - // Set message - $public && $message = 'No application found with public '.$public; - $host && $message = 'No application found with host '.$host; - !$public && !$host && $message = 'No host or application given'; - - // Set data - $public && $data = ['public' => $public]; - $host && $data = ['host' => $host]; - - throw new GatewayException($message ?? null, null, null, [ - 'data' => $data ?? null, 'path' => $public ?? $host ?? 'Header', 'responseType' => Response::HTTP_FORBIDDEN, - ]); - } + // Set message + $public && $message = 'No application found with public '.$public; + $host && $message = 'No application found with host '.$host; + !$public && !$host && $message = 'No host or application given'; - $this->session->set('application', $application->getId()->toString()); + // Set data + $public && $data = ['public' => $public]; + $host && $data = ['host' => $host]; - return $application; + throw new GatewayException($message ?? null, null, null, [ + 'data' => $data ?? null, 'path' => $public ?? $host ?? 'Header', 'responseType' => Response::HTTP_FORBIDDEN, + ]); } } diff --git a/api/src/Service/AuthorizationService.php b/api/src/Service/AuthorizationService.php index 9f4158cdd..ecee30606 100644 --- a/api/src/Service/AuthorizationService.php +++ b/api/src/Service/AuthorizationService.php @@ -226,8 +226,7 @@ public function getScopesForAnonymous(): array $item = $this->cache->getItem('anonymousScopes'); $itemOrg = $this->cache->getItem('anonymousOrg'); if ($item->isHit() && $itemOrg->isHit()) { - $this->session->set('organizations', [$itemOrg->get()]); - $this->session->set('activeOrganization', $itemOrg->get()); + $this->session->set('organization', $itemOrg->get()); return $item->get(); } @@ -239,8 +238,7 @@ public function getScopesForAnonymous(): array foreach ($groups[0]['scopes'] as $scope) { $scopes[] = strtolower($scope['code']); } - $this->session->set('organizations', [$groups[0]['organization']]); - $this->session->set('activeOrganization', $groups[0]['organization']); + $this->session->set('organization', $groups[0]['organization']); $itemOrg->set($groups[0]['organization']); $itemOrg->tag('anonymousOrg'); $this->cache->save($itemOrg); diff --git a/api/src/Service/EavService.php b/api/src/Service/EavService.php index 78c51f228..7c283a091 100644 --- a/api/src/Service/EavService.php +++ b/api/src/Service/EavService.php @@ -149,7 +149,7 @@ public function getObject(?string $id, string $method, Entity $entity) $object = new ObjectEntity(); $object->setEntity($entity); // if entity->function == 'organization', organization for this ObjectEntity will be changed later in handleMutation - $this->session->get('activeOrganization') ? $object->setOrganization($this->session->get('activeOrganization')) : $object->setOrganization('http://testdata-organization'); + $this->session->get('organization') ? $object->setOrganization($this->session->get('organization')) : $object->setOrganization('http://testdata-organization'); $application = $this->em->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); $object->setApplication(!empty($application) ? $application : null); @@ -159,6 +159,1037 @@ public function getObject(?string $id, string $method, Entity $entity) return null; } + /** + * Handles an api request. + * + * @param Request $request + * + * @throws Exception + * + * @return Response + * @deprecated + */ + public function handleRequest(Request $request): Response + { + $this->cache->invalidateTags(['grantedScopes']); + + // Lets get our base stuff + $requestBase = $this->getRequestBase($request); + $contentType = $this->getRequestContentType($request, $requestBase['extension']); + $entity = $this->getEntity($requestBase['path']); + $body = []; // Lets default + + // What if we canot find an entity? + if (is_array($entity)) { + $resultConfig['responseType'] = Response::HTTP_BAD_REQUEST; + $resultConfig['result'] = $entity; + $entity = null; + } + + // Get a body + if ($request->getContent()) { + //@todo support xml messages + $body = json_decode($request->getContent(), true); + $body = filter_var_array($body, FILTER_SANITIZE_ENCODED); + } + // // If we have no body but are using form-data with a POST or PUT call instead: //TODO find a better way to deal with form-data? + // elseif ($request->getMethod() == 'POST' || $request->getMethod() == 'PUT') { + // // get other input values from form-data and put it in $body ($request->get('name')) + // $body = $this->handleFormDataBody($request, $entity); + // + // $formDataResult = $this->handleFormDataFiles($request, $entity, $object); + // if (array_key_exists('result', $formDataResult)) { + // $result = $formDataResult['result']; + // $responseType = Response::HTTP_BAD_REQUEST; + // } else { + // $object = $formDataResult; + // } + // } + + if (!isset($resultConfig['result'])) { + $resultConfig = $this->generateResult($request, $entity, $requestBase, $body); + } + + $options = []; + switch ($contentType) { + case 'text/csv': + $options = [ + CsvEncoder::ENCLOSURE_KEY => '"', + CsvEncoder::ESCAPE_CHAR_KEY => '+', + ]; + + // Lets allow _mapping tot take place + /* @todo remove the old fields support */ + /* @todo make this universal */ + if ($mapping = $request->query->get('_mapping')) { + foreach ($resultConfig['result'] as $key => $result) { + $resultConfig['result'][$key] = $this->translationService->dotHydrator([], $result, $mapping); + } + } + } + + // Lets seriliaze the shizle + $result = $this->serializerService->serialize(new ArrayCollection($resultConfig['result']), $requestBase['renderType'], $options); + + // Afther that we transale the shizle out of it + + /*@todo this is an ugly catch to make sure it only applies to bisc */ + /*@todo this should DEFINTLY be configuration */ + if ($contentType === 'text/csv') { + $translationVariables = [ + 'OTHER' => 'Anders', + 'YES_OTHER' => '"Ja, Anders"', + ]; + + $result = $this->translationService->parse($result, true, $translationVariables); + } else { + $translationVariables = []; + } + + /* + if ($contentType === 'text/csv') { + $replacements = [ + '/student\.person.givenName/' => 'Voornaam', + '/student\.person.additionalName/' => 'Tussenvoegsel', + '/student\.person.familyName/' => 'Achternaam', + '/student\.person.emails\..\.email/' => 'E-mail adres', + '/student.person.telephones\..\.telephone/' => 'Telefoonnummer', + '/student\.intake\.dutchNTLevel/' => 'NT1/NT2', + '/participations\.provider\.id/' => 'ID aanbieder', + '/participations\.provider\.name/' => 'Aanbieder', + '/participations/' => 'Deelnames', + '/learningResults\..\.id/' => 'ID leervraag', + '/learningResults\..\.verb/' => 'Werkwoord', + '/learningResults\..\.subjectOther/' => 'Onderwerp (anders)', + '/learningResults\..\.subject/' => 'Onderwerp', + '/learningResults\..\.applicationOther/' => 'Toepasing (anders)', + '/learningResults\..\.application/' => 'Toepassing', + '/learningResults\..\.levelOther/' => 'Niveau (anders)', + '/learningResults\..\.level/' => 'Niveau', + '/learningResults\..\.participation/' => 'Deelname', + '/learningResults\..\.testResult/' => 'Test Resultaat', + '/agreements/' => 'Overeenkomsten', + '/desiredOffer/' => 'Gewenst aanbod', + '/advisedOffer/' => 'Geadviseerd aanbod', + '/offerDifference/' => 'Aanbod verschil', + '/person\.givenName/' => 'Voornaam', + '/person\.additionalName/' => 'Tussenvoegsel', + '/person\.familyName/' => 'Achternaam', + '/person\.emails\..\.email/' => 'E-mail adres', + '/person.telephones\..\.telephone/' => 'Telefoonnummer', + '/intake\.date/' => 'Aanmaakdatum', + '/intake\.referringOrganizationEmail/' => 'Verwijzer Email', + '/intake\.referringOrganizationOther/' => 'Verwijzer Telefoon', + '/intake\.referringOrganization/' => 'Verwijzer', + '/intake\.foundViaOther/' => 'Via (anders)', + '/intake\.foundVia/' => 'Via', + '/roles/' => 'Rollen', + '/student\.id/' => 'ID deelnemer', + '/description/' => 'Beschrijving', + '/motivation/' => 'Leervraag', + '/languageHouse\.name/' => 'Naam taalhuis', + ]; + + foreach ($replacements as $key => $value) { + $result = preg_replace($key, $value, $result); + } + } + */ + + // Let return the shizle + $response = new Response( + $result, + $resultConfig['responseType'], + ['content-type' => $contentType] + ); + + // Let intervene if it is a known file extension + $supportedExtensions = ['json', 'jsonld', 'jsonhal', 'xml', 'csv', 'yaml']; + if ($entity && in_array($requestBase['extension'], $supportedExtensions)) { + $date = new \DateTime(); + $date = $date->format('Ymd_His'); + $disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, "{$entity->getName()}_{$date}.{$requestBase['extension']}"); + $response->headers->set('Content-Disposition', $disposition); + } + + return $response; + } + + /** + * Handles an api request. + * + * @param Request $request + * + * @throws Exception + * + * @return Response + * @deprecated + */ + public function generateResult(Request $request, Entity $entity, array $requestBase, ?array $body = []): array + { + // Lets get our base stuff + $result = $requestBase['result']; + + // Set default responseType + $responseType = Response::HTTP_OK; + + // Get the application by searching for an application with a domain that matches the host of this request + $host = $request->headers->get('host'); + // TODO: use a sql query instead of array_filter for finding the correct application + // $application = $this->em->getRepository('App:Application')->findByDomain($host); + // if (!empty($application)) { + // $this->session->set('application', $application->getId()->toString()); + // } + $applications = $this->em->getRepository('App:Application')->findAll(); + $applications = array_values(array_filter($applications, function (Application $application) use ($host) { + return in_array($host, $application->getDomains()); + })); + if (count($applications) > 0) { + $this->session->set('application', $applications[0]->getId()->toString()); + } elseif ($this->session->get('apiKeyApplication')) { + // If an api-key is used for authentication we already know which application is used + $this->session->set('application', $this->session->get('apiKeyApplication')); + } else { + // var_dump('no application found'); + if ($host == 'localhost') { + $localhostApplication = new Application(); + $localhostApplication->setName('localhost'); + $localhostApplication->setDescription('localhost application'); + $localhostApplication->setDomains(['localhost']); + $localhostApplication->setPublic(''); + $localhostApplication->setSecret(''); + $localhostApplication->setOrganization('localhostOrganization'); + $this->em->persist($localhostApplication); + $this->em->flush(); + $this->session->set('application', $localhostApplication->getId()->toString()); + // var_dump('Created Localhost Application'); + } else { + $this->session->set('application', null); + $responseType = Response::HTTP_FORBIDDEN; + $result = [ + 'message' => 'No application found with domain '.$host, + 'type' => 'Forbidden', + 'path' => $host, + 'data' => ['host' => $host], + ]; + } + } + + if (!$this->session->get('organization') && $this->session->get('application')) { + $application = $this->em->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); + $this->session->set('organization', !empty($application) ? $application->getOrganization() : null); + } + + // Lets create an object + if (($requestBase['id'] || $request->getMethod() == 'POST') && $responseType == Response::HTTP_OK) { + $object = $this->getObject($requestBase['id'], $request->getMethod(), $entity); + if (array_key_exists('type', $object) && $object['type'] == 'Bad Request') { + $responseType = Response::HTTP_BAD_REQUEST; + $result = $object; + $object = null; + } // Lets check if the user is allowed to view/edit this resource. +// elseif (!$this->objectEntityService->checkOwner($object)) { +// // TODO: do we want to throw a different error if there are nog organizations in the session? (because of logging out for example) +// if ($object->getOrganization() && !in_array($object->getOrganization(), [])) { +// $object = null; // Needed so we return the error and not the object! +// $responseType = Response::HTTP_FORBIDDEN; +// $result = [ +// 'message' => 'You are forbidden to view or edit this resource.', +// 'type' => 'Forbidden', +// 'path' => $entity->getName(), +// 'data' => ['id' => $requestBase['id']], +// ]; +// } +// } + } + + // Check for scopes, if forbidden to view/edit overwrite result so far to this forbidden error + if ((!isset($object) || !$object->getUri()) || !$this->objectEntityService->checkOwner($object)) { + try { + //TODO what to do if we do a get collection and want to show objects this user is the owner of, but not any other objects? + $this->authorizationService->checkAuthorization([ + 'method' => $request->getMethod(), + 'entity' => $entity, + 'object' => $object ?? null, + ]); + } catch (AccessDeniedException $e) { + $result = [ + 'message' => $e->getMessage(), + 'type' => 'Forbidden', + 'path' => $entity->getName(), + 'data' => [], + ]; + + return [ + 'result' => $result, + 'responseType' => Response::HTTP_FORBIDDEN, + 'object' => $object ?? null, + ]; + } + } + + // Lets allow for filtering specific fields + $fields = $this->getRequestFields($request); + + // Lets setup a switchy kinda thingy to handle the input (in handle functions) + // Its a enity endpoint + if ($requestBase['id'] && isset($object) && $object instanceof ObjectEntity) { + // Lets handle all different type of endpoints + $endpointResult = $this->handleEntityEndpoint($request, [ + 'object' => $object ?? null, 'body' => $body ?? null, 'fields' => $fields, 'path' => $requestBase['path'], + ]); + } + // its an collection endpoind + elseif ($responseType == Response::HTTP_OK) { + $endpointResult = $this->handleCollectionEndpoint($request, [ + 'object' => $object ?? null, 'body' => $body ?? null, 'fields' => $fields, 'path' => $requestBase['path'], + 'entity' => $entity, 'extension' => $requestBase['extension'], + ]); + } + if (isset($endpointResult)) { + $result = $endpointResult['result']; + $responseType = $endpointResult['responseType']; + } + + // If we have an error we want to set the responce type to error + if (isset($result) && array_key_exists('type', $result) && $result['type'] == 'error') { + $responseType = Response::HTTP_BAD_REQUEST; + } + + return [ + 'result' => $result, + 'responseType' => $responseType, + 'object' => $object ?? null, + ]; + } + + /** + * Gets the path, id, extension & renderType from the Request. + * + * @param Request $request + * + * @return array + * @deprecated + */ + private function getRequestBase(Request $request): array + { + // Lets get our base stuff + $path = $request->attributes->get('entity'); + $id = $request->attributes->get('id'); + + $extension = false; + + // Lets pull a render type form the extension if we have any + if (strpos($path, '.') && $renderType = explode('.', $path)) { + $path = $renderType[0]; + $renderType = end($renderType); + $extension = $renderType; + } elseif (strpos($id, '.') && $renderType = explode('.', $id)) { + $id = $renderType[0]; + $renderType = end($renderType); + $extension = $renderType; + } else { + $renderType = 'json'; + } + + return [ + 'path' => $path, + 'id' => $id, + 'extension' => $extension, + 'renderType' => $renderType, + 'result' => $this->checkAllowedRenderTypes($renderType, $path), + ]; + } + + /** + * Let do a backup to default to an allowed render type. + * + * @param string $renderType + * @param string $path + * + * @return array|null + * @deprecated + */ + private function checkAllowedRenderTypes(string $renderType, string $path): ?array + { + // Let do a backup to defeault to an allowed render type + $renderTypes = ['json', 'jsonld', 'jsonhal', 'xml', 'csv', 'yaml']; + if ($renderType && !in_array($renderType, $renderTypes)) { + return [ + 'message' => 'The rendering of this type is not suported, suported types are '.implode(',', $renderTypes), + 'type' => 'Bad Request', + 'path' => $path, + 'data' => ['rendertype' => $renderType], + ]; + } + + return null; + } + + /** + * @param Request $request + * @param string $extension + * + * @return string + * @deprecated + */ + private function getRequestContentType(Request $request, string $extension): string + { + // This should be moved to the commonground service and callded true $this->serializerService->getRenderType($contentType); + $acceptHeaderToSerialiazation = [ + 'application/json' => 'json', + 'application/ld+json' => 'jsonld', + 'application/json+ld' => 'jsonld', + 'application/hal+json' => 'jsonhal', + 'application/json+hal' => 'jsonhal', + 'application/xml' => 'xml', + 'text/csv' => 'csv', + 'text/yaml' => 'yaml', + ]; + + $contentType = $request->headers->get('accept'); + // If we overrule the content type then we must adjust the return header acordingly + if ($extension) { + $contentType = array_search($extension, $acceptHeaderToSerialiazation); + } elseif (!array_key_exists($contentType, $acceptHeaderToSerialiazation)) { + $contentType = 'application/json'; + } + + return $contentType; + } + + /** + * Creates a body array from the given key+values when using form-data for an POST or PUT (excl. attribute of type file). + * + * @param Request $request + * @param Entity $entity + * + * @return array + * @deprecated + */ + private function handleFormDataBody(Request $request, Entity $entity): array + { + // get other input values from form-data and put it in $body ($request->get('name')) + // TODO: Maybe use $request->request->all() and filter out attributes with type = file after that? ... + // todo... (so that we can check for input key+values that are not allowed and throw an error/warning instead of just ignoring them) + $body = []; + foreach ($entity->getAttributes() as $attribute) { + if ($attribute->getType() != 'file' && $request->get($attribute->getName())) { + $body[$attribute->getName()] = $request->get($attribute->getName()); + } + } + + return $body; + } + + /** + * Handles file validation and mutations for form-data. + * + * @param Request $request + * @param Entity $entity + * @param ObjectEntity $objectEntity + * + * @throws Exception + * @deprecated + */ + private function handleFormDataFiles(Request $request, Entity $entity, ObjectEntity $objectEntity) + { + if (count($request->files) > 0) { + // Check if this entity has an attribute with type file + $criteria = Criteria::create()->andWhere(Criteria::expr()->eq('type', 'file'))->setMaxResults(1); + $attributes = $entity->getAttributes()->matching($criteria); + + // If no attribute with type file found, throw an error + if ($attributes->isEmpty()) { + $result = [ + 'message' => 'No attribute with type file found for this entity', + 'type' => 'Bad Request', + 'path' => $entity->getName(), + 'data' => [], + ]; + + return ['result' => $result]; + } else { + // Else set attribute to the attribute with type = file + $attribute = $attributes->first(); + // Get the value (file(s)) for this attribute + $value = $request->files->get($attribute->getName()); + + if ($attribute->getMultiple()) { + // When using form-data with multiple=true for files the form-data key should have [] after the name (to make it an array, example key: files[], and support multiple file uploads with one key+multiple files in a single value) + if (!is_array($value)) { + $objectEntity->addError($attribute->getName(), 'Multiple is set for this attribute. Expecting an array of files. (Use array in form-data with the following key: '.$attribute->getName().'[])'); + } else { + // Loop through all files, validate them and store them in the files ArrayCollection + foreach ($value as $file) { + } + } + } else { + // Validate (and create/update) this file + } + + return $objectEntity; + } + } + } + + /** + * Gets fields from the request to use for filtering specific fields. + * + * @param Request $request + * + * @return array + * @deprecated + */ + public function getRequestFields(Request $request): ?array + { + $fields = $request->query->has('fields') ? $request->query->get('fields') : $request->query->get('_fields'); + + if ($fields) { + // Lets deal with a comma seperated list + if (!is_array($fields)) { + $fields = explode(',', $fields); + } + + $dot = new Dot(); + // Lets turn the from dor attat into an propper array + foreach ($fields as $key => $value) { + $dot->add($value, true); + } + + $fields = $dot->all(); + } + + return $fields; + } + + /** + * Gets extend from the request to use for extending. + * + * @param Request $request + * + * @return array + * @deprecated + */ + public function getRequestExtend(Request $request): ?array + { + $extend = $request->query->has('extend') ? $request->query->get('extend') : $request->query->get('_extend'); + + if ($extend) { + // Lets deal with a comma seperated list + if (!is_array($extend)) { + $extend = explode(',', $extend); + } + + $dot = new Dot(); + // Lets turn the from dor attat into an propper array + foreach ($extend as $key => $value) { + $dot->add($value, true); + } + + $extend = $dot->all(); + } + + return $extend; + } + + /** + * Handles entity endpoints. + * + * @param Request $request + * @param array $info Array with some required info, must contain the following keys: object, body, fields & path. + * + * @throws Exception + * + * @return array + * @deprecated + */ + public function handleEntityEndpoint(Request $request, array $info): array + { + // Lets setup a switchy kinda thingy to handle the input + // Its an enity endpoint + switch ($request->getMethod()) { + case 'GET': + $result = $this->handleGet($info['object'], $info['fields'], null); + $responseType = Response::HTTP_OK; + break; + case 'PUT': + // Transfer the variable to the service + $result = $this->handleMutation($info['object'], $info['body'], $info['fields'], $request); + $responseType = Response::HTTP_OK; + if (isset($result) && array_key_exists('type', $result) && $result['type'] == 'Forbidden') { + $responseType = Response::HTTP_FORBIDDEN; + } + break; + case 'DELETE': + $result = $this->handleDelete($info['object']); + $responseType = Response::HTTP_NO_CONTENT; + if (isset($result) && array_key_exists('type', $result) && $result['type'] == 'Forbidden') { + $responseType = Response::HTTP_FORBIDDEN; + } + break; + default: + $result = [ + 'message' => 'This method is not allowed on this endpoint, allowed methods are GET, PUT and DELETE', + 'type' => 'Bad Request', + 'path' => $info['path'], + 'data' => ['method' => $request->getMethod()], + ]; + $responseType = Response::HTTP_BAD_REQUEST; + break; + } + + return [ + 'result' => $result ?? null, + 'responseType' => $responseType, + ]; + } + + /** + * Handles collection endpoints. + * + * @param Request $request + * @param array $info Array with some required info, must contain the following keys: object, body, fields, path, entity & extension. + * + * @throws Exception + * + * @return array + * @deprecated + */ + public function handleCollectionEndpoint(Request $request, array $info): array + { + // its a collection endpoint + switch ($request->getMethod()) { + case 'GET': + $result = $this->handleSearch($info['entity'], $request, $info['fields'], null, $info['extension']); + $responseType = Response::HTTP_OK; + break; + case 'POST': + // Transfer the variable to the service + $result = $this->handleMutation($info['object'], $info['body'], $info['fields'], $request); + $responseType = Response::HTTP_CREATED; + if (isset($result) && array_key_exists('type', $result) && $result['type'] == 'Forbidden') { + $responseType = Response::HTTP_FORBIDDEN; + } + break; + default: + $result = [ + 'message' => 'This method is not allowed on this endpoint, allowed methods are GET and POST', + 'type' => 'Bad Request', + 'path' => $info['path'], + 'data' => ['method' => $request->getMethod()], + ]; + $responseType = Response::HTTP_BAD_REQUEST; + break; + } + + return [ + 'result' => $result ?? null, + 'responseType' => $responseType, + ]; + } + + /** + * This function handles data mutations on EAV Objects. + * + * @param ObjectEntity $object + * @param array $body + * @param $fields + * + * @throws Exception + * + * @return array + * @deprecated + */ + public function handleMutation(ObjectEntity $object, array $body, $fields, Request $request): array + { + // Check if session contains an activeOrganization, so we can't do calls without it. So we do not create objects with no organization! + if ($this->parameterBag->get('app_auth') && empty($this->session->get('organization'))) { + return [ + 'message' => 'An active organization is required in the session, please login to create a new session.', + 'type' => 'Forbidden', + 'path' => $object->getEntity()->getName(), + 'data' => ['activeOrganization' => null], + ]; + } + + // Check if @owner is present in the body and if so unset it. + // note: $owner is allowed to be null! + $owner = 'owner'; + if (array_key_exists('@owner', $body)) { + $owner = $body['@owner']; + unset($body['@owner']); + } + + // Check optional conditional logic + $object->checkConditionlLogic(); // Old way of checking condition logic + + // Saving the data + $this->em->persist($object); + if ($request->getMethod() == 'POST' && $object->getEntity()->getFunction() === 'organization' && !array_key_exists('@organization', $body)) { + $object = $this->functionService->createOrganization($object, $object->getUri(), $body['type']); + } + $this->objectEntityService->handleOwner($object, $owner); // note: $owner is allowed to be null! + $this->em->persist($object); + $this->em->flush(); + + return $this->responseService->renderResult($object, $fields, null); + } + + /** + * Handles a get item api call. + * + * @param ObjectEntity $object + * @param array|null $fields + * @param array|null $extend + * @param string $acceptType + * + * @throws CacheException|InvalidArgumentException + * + * @return array + * @deprecated + */ + public function handleGet(ObjectEntity $object, ?array $fields, ?array $extend, string $acceptType = 'json'): array + { + return $this->responseService->renderResult($object, $fields, $extend, $acceptType); + } + + /** + * A function to replace Request->query->all() because Request->query->all() will replace some characters with an underscore. + * This function will not. + * + * @param string $method The method of the Request + * + * @return array An array with all query parameters. + * @deprecated (see CoreBundle RequestService->realRequestQueryAll()!) + */ + public function realRequestQueryAll(string $method = 'get'): array + { + $vars = []; + if (strtolower($method) === 'get' && empty($_SERVER['QUERY_STRING'])) { + return $vars; + } + $pairs = explode('&', $_SERVER['QUERY_STRING']); + foreach ($pairs as $pair) { + $nv = explode('=', $pair); + $name = urldecode($nv[0]); + $value = ''; + if (count($nv) == 2) { + $value = urldecode($nv[1]); + } + + $this->recursiveRequestQueryKey($vars, $name, explode('[', $name)[0], $value); + } + + return $vars; + } + + /** + * This function adds a single query param to the given $vars array. ?$name=$value + * Will check if request query $name has [...] inside the parameter, like this: ?queryParam[$nameKey]=$value. + * Works recursive, so in case we have ?queryParam[$nameKey][$anotherNameKey][etc][etc]=$value. + * Also checks for queryParams ending on [] like: ?queryParam[$nameKey][] (or just ?queryParam[]), if this is the case + * this function will add given value to an array of [queryParam][$nameKey][] = $value or [queryParam][] = $value. + * If none of the above this function will just add [queryParam] = $value to $vars. + * + * @param array $vars The vars array we are going to store the query parameter in + * @param string $name The full $name of the query param, like this: ?$name=$value + * @param string $nameKey The full $name of the query param, unless it contains [] like: ?queryParam[$nameKey]=$value + * @param string $value The full $value of the query param, like this: ?$name=$value + * + * @return void + * @deprecated + */ + private function recursiveRequestQueryKey(array &$vars, string $name, string $nameKey, string $value) + { + $matchesCount = preg_match('/(\[[^[\]]*])/', $name, $matches); + if ($matchesCount > 0) { + $key = $matches[0]; + $name = str_replace($key, '', $name); + $key = trim($key, '[]'); + if (!empty($key)) { + $vars[$nameKey] = $vars[$nameKey] ?? []; + $this->recursiveRequestQueryKey($vars[$nameKey], $name, $key, $value); + } else { + $vars[$nameKey][] = $value; + } + } else { + $vars[$nameKey] = $value; + } + } + + /** + * Handles a search (collection) api call. + * + * @param Entity $entity + * @param Request $request + * @param array|null $fields + * @param array|null $extend + * @param $extension + * @param null $filters + * @param string $acceptType + * @param array|null $query + * + * @throws CacheException + * @throws InvalidArgumentException + * + * @return array|array[] + * @deprecated + */ + public function handleSearch(Entity $entity, Request $request, ?array $fields, ?array $extend, $extension, $filters = null, string $acceptType = 'json', ?array $query = null): array + { + $query = $query ?? $this->realRequestQueryAll($request->getMethod()); + unset($query['limit']); + unset($query['page']); + unset($query['start']); + $limit = (int) ($request->query->get('limit') ?? 25); // These type casts are not redundant! + $page = (int) ($request->query->get('page') ?? 1); + $start = (int) ($request->query->get('start') ?? 1); + + if ($start > 1) { + $offset = $start - 1; + } else { + $offset = ($page - 1) * $limit; + } + + // Allowed order by + $this->stopwatch->start('orderParametersCheck', 'handleSearch'); + $orderCheck = $this->em->getRepository('App:ObjectEntity')->getOrderParameters($entity); + // todo: ^^^ add something to ObjectEntities just like bool searchable, use that to check for fields allowed to be used for ordering. + // todo: sortable? + + $order = []; + if (array_key_exists('order', $query)) { + $order = $query['order']; + unset($query['order']); + if (!is_array($order)) { + $orderCheckStr = implode(', ', $orderCheck); + $message = 'Please give an attribute to order on. Like this: ?order[attributeName]=desc/asc. Supported order query parameters: '.$orderCheckStr; + } + if (is_array($order) && count($order) > 1) { + $message = 'Only one order query param at the time is allowed.'; + } + if (is_array($order) && !in_array(strtoupper(array_values($order)[0]), ['DESC', 'ASC'])) { + $message = 'Please use desc or asc as value for your order query param, not: '.array_values($order)[0]; + } + if (is_array($order) && !in_array(array_keys($order)[0], $orderCheck)) { + $orderCheckStr = implode(', ', $orderCheck); + $message = 'Unsupported order query parameters ('.array_keys($order)[0].'). Supported order query parameters: '.$orderCheckStr; + } + if (isset($message)) { + return [ + 'message' => $message, + 'type' => 'error', + 'path' => is_array($order) ? $entity->getName().'?order['.array_keys($order)[0].']='.array_values($order)[0] : $entity->getName().'?order='.$order, + 'data' => ['order' => $order], + ]; + } + } + $this->stopwatch->stop('orderParametersCheck'); + + // Allowed filters + $this->stopwatch->start('filterParametersCheck', 'handleSearch'); + $filterCheck = $this->em->getRepository('App:ObjectEntity')->getFilterParameters($entity); + + // Lets add generic filters + $filterCheck = array_merge($filterCheck, ['fields', '_fields', 'extend', '_extend']); + if (!empty($entity->getSearchPartial())) { + $filterCheck = array_merge($filterCheck, ['search', '_search']); + } + + foreach ($query as $param => $value) { + if (!in_array($param, $filterCheck)) { + $filterCheckStr = implode(', ', $filterCheck); + + if (is_array($value)) { + $value = end($value); + } + + return [ + 'message' => 'Unsupported queryParameter ('.$param.'). Supported queryParameters: '.$filterCheckStr, + 'type' => 'error', + 'path' => $entity->getName().'?'.$param.'='.$value, + 'data' => ['queryParameter' => $param], + ]; + } + } + + if ($filters) { + $query = array_merge($query, $filters); + } + $this->stopwatch->stop('filterParametersCheck'); + + $this->stopwatch->start('valueScopesToFilters', 'handleSearch'); + $query = array_merge($query, $this->authorizationService->valueScopesToFilters($entity)); + $this->stopwatch->stop('valueScopesToFilters'); + + $this->stopwatch->start('findAndCountByEntity', 'handleSearch'); + $repositoryResult = $this->em->getRepository('App:ObjectEntity')->findAndCountByEntity($entity, $query, $order, $offset, $limit); + $this->stopwatch->stop('findAndCountByEntity'); + + // Lets see if we need to flatten te responce (for example csv use) + // todo: $flat and $acceptType = 'json' should have the same result, so remove $flat? + $flat = false; + if (in_array($request->headers->get('accept'), ['text/csv']) || in_array($extension, ['csv'])) { + $flat = true; + } + + $results = []; + $this->stopwatch->start('renderResults', 'handleSearch'); + foreach ($repositoryResult['objects'] as $object) { + // If orderBy is used on an attribute we needed to add the value of that attribute to the select of the query... + // In this^ case $object will be an array containing the object and this specific value we are ordering on. + if (is_array($object)) { + $object = $object[0]; + // $object['stringValue'] contains the value we are ordering on. + } + // todo: remove the following function + // This is a quick fix for a problem where filtering would return to many result if we are filtering on a value... + // ...that is also present in a subobject of the main $object we are filtering on. + if (!$this->checkIfFilteredCorrectly($query, $object)) { + continue; + } + $result = $this->responseService->renderResult($object, $fields, $extend, $acceptType, false, $flat); + $results[] = $result; + $this->stopwatch->lap('renderResults'); + } + $this->stopwatch->stop('renderResults'); + + // If we need a flattend responce we are al done + // todo: $flat and $acceptType = 'json' should have the same result, so remove $flat? + if ($flat) { + return $results; + } + + // If not lets make it pretty + return $this->handlePagination($acceptType, $entity, $results, $repositoryResult['total'], $limit, $offset); + } + + /** + * This is a quick fix for a problem where filtering would return to many result if we are filtering on a value + * that is also present in a subobject of the main $object we are filtering on. + * todo: remove this function. + * + * @param array $query The query/filters we need to check. + * @param ObjectEntity $object The object to check. + * + * @return bool true by default, false if filtering wasn't done correctly and this object should not be shown in the results. + * @deprecated + */ + private function checkIfFilteredCorrectly(array $query, ObjectEntity $object): bool + { + unset( + $query['search'], $query['_search'], + $query['fields'], $query['_fields'], + $query['extend'], $query['_extend'] + ); + if (!empty($query)) { + $resultDot = new Dot($object->toArray()); + foreach ($query as $filter => $value) { + $filter = str_replace('|valueScopeFilter', '', $filter); + $resultFilter = $resultDot->get($filter); + $resultFilter = $resultFilter === true ? 'true' : ($resultFilter === false ? 'false' : $resultDot->get($filter)); + if (!is_array($value) && $resultDot->get($filter) !== null && $resultFilter != $value && + (is_string($value) && !str_contains($value, 'NULL')) && !str_contains($value, '%')) { + return false; + } + } + } + + return true; + } + + /** + * Returns a response array including pagination for handleSearch function. This response is different depending on the acceptType. + * + * @param string $acceptType + * @param Entity $entity + * @param array $results + * @param int $total + * @param int $limit + * @param int $offset + * + * @return array[] + * @deprecated + */ + private function handlePagination(string $acceptType, Entity $entity, array $results, int $total, int $limit, int $offset): array + { + $pages = ceil($total / $limit); + $pages = $pages == 0 ? 1 : $pages; + $page = floor($offset / $limit) + 1; + + switch ($acceptType) { + case 'jsonhal': + $paginationResult = $this->handleJsonHal($entity, [ + 'results' => $results, 'limit' => $limit, 'total' => $total, + 'offset' => $offset, 'page' => $page, 'pages' => $pages, + ]); + break; + case 'jsonld': + // todo: try and match api-platform ? https://api-platform.com/docs/core/pagination/ + case 'json': + default: + $paginationResult = ['results' => $results]; + $paginationResult = $this->handleDefaultPagination($paginationResult, [ + 'results' => $results, 'limit' => $limit, 'total' => $total, + 'offset' => $offset, 'page' => $page, 'pages' => $pages, + ]); + break; + } + + return $paginationResult; + } + + /** + * @param Entity $entity + * @param array $data + * + * @return array + * @deprecated + */ + private function handleJsonHal(Entity $entity, array $data): array + { + $path = $entity->getName(); + if ($this->session->get('endpoint')) { + $endpoint = $this->em->getRepository('App:Endpoint')->findOneBy(['id' => $this->session->get('endpoint')]); + $path = implode('/', $endpoint->getPath()); + } + $paginationResult['_links'] = [ + 'self' => ['href' => '/api/'.$path.($data['page'] == 1 ? '' : '?page='.$data['page'])], + 'first' => ['href' => '/api/'.$path], + ]; + if ($data['page'] > 1) { + $paginationResult['_links']['prev']['href'] = '/api/'.$path.($data['page'] == 2 ? '' : '?page='.($data['page'] - 1)); + } + if ($data['page'] < $data['pages']) { + $paginationResult['_links']['next']['href'] = '/api/'.$path.'?page='.($data['page'] + 1); + } + $paginationResult['_links']['last']['href'] = '/api/'.$path.($data['pages'] == 1 ? '' : '?page='.$data['pages']); + $paginationResult = $this->handleDefaultPagination($paginationResult, $data); + $paginationResult['_embedded'] = [$path => $data['results']]; //todo replace $path with $entity->getName() ? + + return $paginationResult; + } + + /** + * @param array $paginationResult + * @param array $data + * + * @return array + * @deprecated + */ + private function handleDefaultPagination(array $paginationResult, array $data): array + { + $paginationResult['count'] = count($data['results']); + $paginationResult['limit'] = $data['limit']; + $paginationResult['total'] = $data['total']; + $paginationResult['start'] = $data['offset'] + 1; + $paginationResult['page'] = $data['page']; + $paginationResult['pages'] = $data['pages']; + + return $paginationResult; + } + /** * Handles a delete api call. * @@ -234,7 +1265,8 @@ public function handleDelete(ObjectEntity $object, ArrayCollection $maxDepth = n } // Remove this object from cache - $this->functionService->removeResultFromCache($object); + //todo: deprecated function +// $this->functionService->removeResultFromCache($object); $this->em->remove($object); $this->em->flush(); diff --git a/api/src/Service/EmailService.php b/api/src/Service/EmailService.php deleted file mode 100644 index ae5a38d7d..000000000 --- a/api/src/Service/EmailService.php +++ /dev/null @@ -1,130 +0,0 @@ -, Ruben van der Linde , Sarai Misidjan - * - * @license EUPL - * - * @category Service - */ -class EmailService -{ - private Environment $twig; - private array $data; - private array $configuration; - - public function __construct( - Environment $twig - ) { - $this->twig = $twig; - } - - /** - * Handles the sending of an email based on an event. - * - * @param array $data - * @param array $configuration - * - * @throws LoaderError|RuntimeError|SyntaxError|TransportExceptionInterface - * - * @return array - */ - public function EmailHandler(array $data, array $configuration): array - { - $this->data = $data; - $this->configuration = $configuration; - - $this->sendEmail(); - - return $data; - } - - /** - * Sends and email using an EmailTemplate with configuration for it. It is possible to use $object data in the email if configured right. - * - * @throws LoaderError - * @throws SyntaxError - * @throws TransportExceptionInterface - * - * @return bool - */ - private function sendEmail(): bool - { - // Create mailer with mailgun url - $transport = Transport::fromDsn($this->configuration['serviceDNS']); - $mailer = new Mailer($transport); - - // Ready the email template with configured variables - $variables = []; - - foreach ($this->configuration['variables'] as $key => $variable) { - if (array_key_exists($variable, $this->data['response'])) { - $variables[$key] = $this->data['response'][$variable]; - } - } - - // Render the template - $html = $this->twig->createTemplate(base64_decode($this->configuration['template']))->render($variables); - $text = strip_tags(preg_replace('##i', "\n", $html), '\n'); - - // Lets allow the use of values from the object Created/Updated with {attributeName.attributeName} in the these^ strings. - $subject = $this->twig->createTemplate($this->configuration['subject'])->render($variables); - $receiver = $this->twig->createTemplate($this->configuration['receiver'])->render($variables); - $sender = $this->twig->createTemplate($this->configuration['sender'])->render($variables); - - // If we have no sender, set sender to receiver - if (!$sender) { - $sender = $receiver; - } - - // Create the email - $email = (new Email()) - ->from($sender) - ->to($receiver) - //->cc('cc@example.com') - //->bcc('bcc@example.com') - //->replyTo('fabien@example.com') - //->priority(Email::PRIORITY_HIGH) - ->subject($subject) - ->html($html) - ->text($text); - - // Then we can handle some optional configuration - if (array_key_exists('cc', $this->configuration)) { - $email->cc($this->configuration['cc']); - } - - if (array_key_exists('bcc', $this->configuration)) { - $email->bcc($this->configuration['bcc']); - } - - if (array_key_exists('replyTo', $this->configuration)) { - $email->replyTo($this->configuration['replyTo']); - } - - if (array_key_exists('priority', $this->configuration)) { - $email->priority($this->configuration['priority']); - } - - // todo: attachments - - // Send the email - /** @var Symfony\Component\Mailer\SentMessage $sentEmail */ - $mailer->send($email); - - return true; - } -} diff --git a/api/src/Service/FunctionService.php b/api/src/Service/FunctionService.php index 5bd2a0d2f..3ce9ec623 100644 --- a/api/src/Service/FunctionService.php +++ b/api/src/Service/FunctionService.php @@ -38,6 +38,7 @@ public function __construct(CacheInterface $cache) * @throws InvalidArgumentException * * @return bool + * @deprecated */ public function removeResultFromCache(ObjectEntity $objectEntity, ?SymfonyStyle $io = null): bool { @@ -67,6 +68,7 @@ public function removeResultFromCache(ObjectEntity $objectEntity, ?SymfonyStyle * @throws InvalidArgumentException * * @return void + * @deprecated */ private function removeParentResultsFromCache(ObjectEntity $objectEntity, ?SymfonyStyle $io) { @@ -90,6 +92,7 @@ private function removeParentResultsFromCache(ObjectEntity $objectEntity, ?Symfo * @throws InvalidArgumentException * * @return void + * @deprecated */ private function removeChildResultsFromCache(ObjectEntity $objectEntity, ?SymfonyStyle $io) { diff --git a/api/src/Service/ObjectEntityService.php b/api/src/Service/ObjectEntityService.php index 416afec53..36be40dfd 100644 --- a/api/src/Service/ObjectEntityService.php +++ b/api/src/Service/ObjectEntityService.php @@ -198,4 +198,1888 @@ public function checkOwner(ObjectEntity $result): bool return false; } + + /** + * This function gets the object by its uri. + * + * @param string $uri The uri of the object + * @param array|null $fields The fields array that can be filtered on + * @param array|null $extend The extend array that can be extended + * + * @throws CacheException|InvalidArgumentException + * + * @return array + * @deprecated + */ + public function getObjectByUri(string $uri, ?array $fields = null, ?array $extend = null): array + { + $object = $this->entityManager->getRepository('App:ObjectEntity')->findOneBy(['uri' => $uri]); + if ($object instanceof ObjectEntity) { + return $this->responseService->renderResult($object, $fields, $extend, 'jsonld', true); + } + + return []; + } + + /** + * This function gets the object with its id and the related entity. + * + * @param Entity $entity The entity the object relates to + * @param string $id The id of the object entity + * @param array|null $fields The fields array that can be filtered on + * @param array|null $extend The extend array that can be extended + * + * @throws CacheException|InvalidArgumentException + * + * @return array + * @deprecated + */ + public function getObject(Entity $entity, string $id, ?array $fields = null, ?array $extend = null): array + { + $object = $this->entityManager->getRepository('App:ObjectEntity')->findOneBy(['entity' => $entity, 'id' => $id]); + if ($object instanceof ObjectEntity) { + return $this->responseService->renderResult($object, $fields, $extend, 'jsonld', true); + } + + return []; + } + + /** + * This function gets an object with the function set to person. + * + * @param string $id The id of the object entity + * @param array|null $fields The fields array that can be filtered on + * @param array|null $extend The extend array that can be extended + * + * @throws CacheException|InvalidArgumentException + * + * @return array + */ + public function getPersonObject(string $id, ?array $fields = null, ?array $extend = null): array + { + $entity = $this->entityManager->getRepository('App:Entity')->findOneBy(['function' => 'person']); + if ($entity instanceof Entity) { + return $this->getObject($entity, $id, $fields, $extend); + } + + return []; + } + + /** + * This function gets an object with the function set to organization. + * + * @param string $id The id of the object entity + * @param array|null $fields The fields array that can be filtered on + * @param array|null $extend The extend array that can be extended + * + * @throws CacheException|InvalidArgumentException + * + * @return array + */ + public function getOrganizationObject(string $id, ?array $fields = null, ?array $extend = null): array + { + $entity = $this->entityManager->getRepository('App:Entity')->findOneBy(['function' => 'organization']); //todo cache this!? + if ($entity instanceof Entity) { + return $this->getObject($entity, $id, $fields, $extend); + } + + return []; + } + + /** + * @TODO + * + * @param string $username The username of the person + * @param array|null $fields The fields array that can be filtered on + * @param array|null $extend The extend array that can be extended + * + * @throws CacheException|InvalidArgumentException + * + * @return array + */ + public function getUserObjectEntity(string $username, ?array $fields = null, ?array $extend = null): array + { + // Because inversedBy wil not set the UC->user->person when creating a person with a user in the gateway. + // We need to do this in order to find the person of this user: + $entity = $this->entityManager->getRepository('App:Entity')->findOneBy(['name' => 'users']); + + if ($entity == null) { + return []; + } + + $objects = $this->entityManager->getRepository('App:ObjectEntity')->findByEntity($entity, ['username' => $username]); + if (count($objects) == 1) { + $user = $this->responseService->renderResult($objects[0], $fields, $extend, 'jsonld', true); + // This: will be false if a user has no rights to do get on a person object + if (isset($user['person'])) { + return $user['person']; + } + } + + return []; + } + + /** + * This function get the filters array from the parameters. + * + * @return array + * @deprecated + */ + private function getFilterFromParameters(): array + { + if ($parameters = $this->session->get('parameters')) { + if (array_key_exists('path', $parameters)) { + foreach ($parameters['path'] as $key => $part) { + if ($key[0] === '{' && $key[strlen($key) - 1] === '}' && $part !== null) { + $key = substr($key, 1, -1); + $filters[$key] = $part; + + return $filters; + } else { + // @todo + } + } + } + } + + return []; + } + + /** + * This function handles the check for an object. + * + * @param string|null $id The id of the object + * @param string|null $method Method from request if there is a request + * @param Entity $entity The entity of the object + * + * @throws GatewayException + * + * @return ObjectEntity|array|mixed|null + * @deprecated + */ + public function checkGetObject(?string $id, string $method, Entity $entity) + { + // todo: re-used old code for getting an objectEntity + $object = $this->eavService->getObject($method === 'POST' ? null : $id, $method, $entity); + + if (is_array($object) && array_key_exists('type', $object) && $object['type'] == 'Bad Request') { + throw new GatewayException($object['message'], null, null, ['data' => $object['data'], 'path' => $object['path'], 'responseType' => Response::HTTP_BAD_REQUEST]); + } // Let's check if the user is allowed to view/edit this resource. + +// if (!$method == 'POST' && !$this->checkOwner($object)) { +// // TODO: do we want to throw a different error if there are no organizations in the session? (because of logging out for example) +// if ($object->getOrganization() && !in_array($object->getOrganization(), [])) { +// throw new GatewayException('You are forbidden to view or edit this resource.', null, null, ['data' => ['id' => $id ?? null], 'path' => $entity->getName(), 'responseType' => Response::HTTP_FORBIDDEN]); +// } +// } + + if ($object instanceof ObjectEntity && $object->getId() !== null) { + $this->session->set('object', $object->getId()->toString()); + } + + // Check for scopes, if forbidden to view/edit this, throw forbidden error + if (!isset($object) || is_array($object) || !$object->getUri() || !$this->checkOwner($object)) { + try { + //TODO what to do if we do a get collection and want to show objects this user is the owner of, but not any other objects? + $this->authorizationService->checkAuthorization([ + 'method' => $method, + 'entity' => $entity, + 'object' => $object ?? null, + ]); + } catch (AccessDeniedException $e) { + throw new GatewayException($e->getMessage(), null, null, ['data' => null, 'path' => $entity->getName(), 'responseType' => Response::HTTP_FORBIDDEN]); + } + } + + return $object; + } + + /** + * This function handles the check on operation types exceptions. + * + * @param Endpoint $endpoint The endpoint of the object + * @param Entity $entity The entity of the object + * @param array $data Data to be set into the eav + * + * @throws GatewayException + * + * @return ObjectEntity|string[]|void + * @deprecated + */ + public function checkGetOperationTypeExceptions(Endpoint $endpoint, Entity $entity, array &$data) + { + $operationType = $endpoint->getOperationType(); + if (((isset($operationType) && $operationType === 'item') || $endpoint->getOperationType() === 'item') && array_key_exists('results', $data) && count($data['results']) == 1) { // todo: $data['total'] == 1 + $data = $data['results'][0]; + isset($data['id']) && Uuid::isValid($data['id']) ?? $this->session->set('object', $data['id']); + } elseif ((isset($operationType) && $operationType === 'item') || $endpoint->getOperationType() === 'item') { + throw new GatewayException('No object found with these filters', null, null, ['data' => $filters ?? null, 'path' => $entity->getName(), 'responseType' => Response::HTTP_BAD_REQUEST]); + } + + return $data; + } + + /** + * This function handles the object entity exceptions. + * + * + * @param array|null $data Data to be set into the eav + * @param ObjectEntity|null $object The objects that is being checked on exceptions + * @param array|null $fields The fields array that can be filtered on + * @param array|null $extend The extend array that can be extended + * @param string $acceptType The acceptType of the call - defaulted to jsonld + * + * @throws CacheException + * @throws InvalidArgumentException + * + * @return string[] + * @deprecated + */ + public function checkGetObjectExceptions(?array &$data, ?ObjectEntity $object, ?array $fields, ?array $extend, string $acceptType): array + { + if ($object instanceof ObjectEntity) { + !$object->getSelf() ?? $object->setSelf($this->createSelf($object)); + if (isset($extend['x-commongateway-metadata']['dateRead']) + || isset($extend['x-commongateway-metadata']['all'])) { + $extend['x-commongateway-metadata']['dateRead'] = 'getItem'; + } + $data = $this->eavService->handleGet($object, $fields, $extend, $acceptType); + } else { + $data['error'] = $object; + } + + return $data; + } + + /** + * Gets fields and extend from the query params used in the request. + * + * @return array An array containing 2 keys: 'fields' & 'extend'. + * @deprecated + */ + private function getRequestQueryParams(): array + { + $fields = $this->eavService->getRequestFields($this->request); + + // Let's allow for extending + $extend = $this->eavService->getRequestExtend($this->request); + if (isset($extend['x-commongateway-metadata']) && $extend['x-commongateway-metadata'] === true) { + $extend['x-commongateway-metadata'] = []; + $extend['x-commongateway-metadata']['all'] = true; + } + + return [ + 'fields' => $fields, + 'extend' => $extend, + ]; + } + + /** + * This function handles the get case of an object entity. + * + * @param string|null $id The id of the object + * @param array|null $data Data to be set into the eav + * @param string $method The method of the call + * @param Entity $entity The entity of the object + * @param Endpoint|null $endpoint The endpoint of the object + * @param string $acceptType The acceptType of the call - defaulted to jsonld + * + * @throws CacheException + * @throws GatewayException + * @throws InvalidArgumentException + * + * @return array + * @deprecated + */ + public function getCase(?string $id, ?array &$data, string $method, Entity $entity, ?Endpoint $endpoint, string $acceptType): array + { + $queryParamData = $this->getRequestQueryParams(); + + if (isset($id)) { + $object = $this->checkGetObject($id, $method, $entity); + $data = $this->checkGetObjectExceptions($data, $object, $queryParamData['fields'], $queryParamData['extend'], $acceptType); + } else { + $data = $this->eavService->handleSearch( + $entity, + $this->request, + $queryParamData['fields'], + $queryParamData['extend'], + false, + $filters ?? [], + $acceptType + ); + + if (isset($endpoint)) { + $this->session->get('endpoint') ?? $data = $this->checkGetOperationTypeExceptions($endpoint, $entity, $data); + } + } + + return $data; + } + + /** + * This function checks and unsets the owner of the body of the call. + * + * @param array $data Data to be set into the eav + * + * @return string|null + * @deprecated + */ + public function checkAndUnsetOwner(array &$data): ?string + { + // todo: what about @organization? (See saveObject function, test it first, look at and compare with old code!) + // Check if @owner is present in the body and if so unset it. + // note: $owner is allowed to be null! + $owner = 'owner'; + if (array_key_exists('@owner', $data)) { + $owner = $data['@owner']; + unset($data['@owner']); + } + + return $owner; + } + + /** + * This function handles creating, updating and patching the object. + * + * @param array $data Data to be set into the eav + * @param ObjectEntity $object The objects that needs to be created/updated + * @param string $owner The owner of the object + * @param string $method The method of the call + * @param string $acceptType The acceptType of the call - defaulted to jsonld + * + * @throws CacheException + * @throws InvalidArgumentException + * + * @return string[] + * @deprecated + */ + public function createOrUpdateCase(array &$data, ObjectEntity $object, string $owner, string $method, string $acceptType): array + { + $queryParamData = $this->getRequestQueryParams(); + + // Save the object (this will remove this object result from the cache) + $this->functionService->removeResultFromCache = []; + $object = $this->saveObject($object, $data); + + // Handle Entity Function (note that this might be overwritten when handling the promise later!) + $object = $this->functionService->handleFunction($object, $object->getEntity()->getFunction(), [ + 'method' => $method, + 'uri' => $object->getUri(), + 'organizationType' => array_key_exists('type', $data) ? $data['type'] : null, + 'userGroupName' => array_key_exists('name', $data) ? $data['name'] : null, + ]); + + $this->handleOwner($object, $owner); // note: $owner is allowed to be null! + $object->setDateModified(new DateTime()); + + $this->entityManager->persist($object); + $this->entityManager->flush(); + + $data = $this->responseService->renderResult($object, $queryParamData['fields'], $queryParamData['extend'], $acceptType); + + return $data; + } + + /** + * This function handles deleting the object. + * + * @param string $id the id of the object + * @param array|null $data Data to be set into the eav + * @param string $method The method of the call + * @param Entity $entity The entity of the object + * + * @throws GatewayException + * @throws InvalidArgumentException + * + * @return string[] + * @deprecated + */ + public function deleteCase(string $id, ?array &$data, string $method, Entity $entity): array + { + $object = $this->checkGetObject($id, $method, $entity); + //todo: use PromiseMessage for delete promise and notification (re-use / replace code from eavService->handleDelete + + //todo: -start- old code... + //TODO: old code for deleting an ObjectEntity + + // delete object (this will remove this object result from the cache) + $this->functionService->removeResultFromCache = []; + $data = $this->eavService->handleDelete($object); + if (array_key_exists('type', $data) && $data['type'] == 'Forbidden') { + throw new GatewayException($data['message'], null, null, ['data' => $data['data'], 'path' => $data['path'], 'responseType' => Response::HTTP_FORBIDDEN]); + } + //todo: -end- old code... + + return $data; + } + + /** + * Saves an ObjectEntity in the DB using the $post array. NOTE: validation is and should only be done by the validatorService->validateData() function this saveObject() function only saves the object in the DB. + * + * @param array|null $data Data to be set into the eav + * @param Endpoint|null $endpoint The endpoint of the object + * @param Entity $entity The entity of the object + * @param string|null $id The id of the object + * @param string $method The method of the call + * @param string $acceptType The acceptType of the call - defaulted to jsonld + * + * @throws CacheException + * @throws ComponentException + * @throws GatewayException + * @throws InvalidArgumentException + * + * @return string[]|void + * @deprecated + */ + public function switchMethod(?array &$data, ?Endpoint $endpoint, Entity $entity, string $id = null, string $method = 'GET', string $acceptType = 'json') + { + // Get filters from query parameters + $filters = $this->getFilterFromParameters(); + + array_key_exists('id', $filters) && $id = $filters['id']; + !isset($id) && array_key_exists('uuid', $filters) && $id = $filters['uuid']; + + $validationErrors = null; + switch ($method) { + case 'GET': + $data = $this->getCase($id, $data, $method, $entity, $endpoint, $acceptType); + // todo: this dispatch should probably be moved to the getCase function!? + $this->dispatchEvent('commongateway.object.read', ['response' => $data, 'entity' => $entity->getId()->toString()]); + break; + case 'POST': + case 'PUT': + case 'PATCH': + $object = $this->checkGetObject($id, $method, $entity); + $owner = $this->checkAndUnsetOwner($data); + + // validate +// if ($validationErrors = $this->validatorService->validateData($data, $entity, $method)) { +// return $validationErrors; +// } + + $data = $this->createOrUpdateCase($data, $object, $owner, $method, $acceptType); + // todo: this dispatch should probably be moved to the createOrUpdateCase function!? + $this->dispatchEvent($method == 'POST' ? 'commongateway.object.create' : 'commongateway.object.update', ['response' => $data, 'entity' => $entity->getId()->toString()]); + break; + case 'DELETE': + $data = $this->deleteCase($id, $data, $method, $entity); + // todo: this dispatch should probably be moved to the deleteCase function!? + $this->dispatchEvent('commongateway.object.delete', ['response' => $data, 'entity' => $entity->getId()->toString()]); + break; + default: + throw new GatewayException('This method is not allowed', null, null, ['data' => ['method' => $method], 'path' => $entity->getName(), 'responseType' => Response::HTTP_FORBIDDEN]); + } + + return $validationErrors; + } + + /** + * A function to handle calls to eav. + * + * @param Handler $handler The handler the object relates to + * @param Endpoint $endpoint The endpoint of the object + * @param array|null $data Data to be set into the eav + * @param string|null $method Method from request if there is a request + * @param string $acceptType The acceptType of the call - defaulted to jsonld + * + * @throws GatewayException|CacheException|InvalidArgumentException|ComponentException|Exception + * + * @return array $data + * @deprecated + */ + public function handleObject(Handler $handler, Endpoint $endpoint, ?array $data = null, string $method = null, string $acceptType = 'json'): array + { + // Set application in the session or create new application for localhost if we need it. + $this->applicationService->getApplication(); + + // set session with sessionInfo + $sessionInfo = [ + 'entity' => $handler->getEntity()->getId()->toString(), + 'source' => $handler->getEntity()->getSource() ? $handler->getEntity()->getSource()->getId()->toString() : null, + ]; + $this->session->set('entitySource', $sessionInfo); + + $validationErrors = $this->switchMethod($data, $endpoint, $handler->getEntity(), null, $method, $acceptType); + if (isset($validationErrors)) { + throw new GatewayException('Validation errors', null, null, ['data' => $validationErrors, 'path' => $handler->getEntity()->getName(), 'responseType' => Response::HTTP_BAD_REQUEST]); + } + + // use events + return $data; + } + + /** + * Saves an ObjectEntity in the DB using the $post array. NOTE: validation is and should only be done by the validatorService->validateData() function this saveObject() function only saves the object in the DB. + * + * @param ObjectEntity $objectEntity + * @param array $post + * + * @throws Exception|InvalidArgumentException + * + * @return ObjectEntity + * @deprecated + */ + public function saveObject(ObjectEntity $objectEntity, array $post): ObjectEntity + { + $entity = $objectEntity->getEntity(); + + foreach ($entity->getAttributes() as $attribute) { + // Check attribute function + if ($attribute->getFunction() !== 'noFunction') { + $objectEntity = $this->handleAttributeFunction($objectEntity, $attribute); + continue; // Do not save this attribute(/value) in any other way! + } + + // Check if we have a value ( a value is given in the post body for this attribute, can be null) + // If no value is present in the post body for this attribute check for defaultValue and nullable. + if (key_exists($attribute->getName(), $post)) { + $objectEntity = $this->saveAttribute($objectEntity, $attribute, $post[$attribute->getName()]); + } elseif ($this->request->getMethod() == 'POST') { + if ($attribute->getDefaultValue()) { + // todo: defaultValue should maybe be a Value object, so that defaultValue can be something else than a string + // DefaultValue can be a uuid string to connect an object... + $objectEntity = $this->saveAttribute($objectEntity, $attribute, $this->twig->createTemplate($attribute->getDefaultValue())->render()); + } else { + // If no value is given when creating a new object, make sure we set a value to null for this attribute. + $objectEntity->setValue($attribute, null); + } + } + } + + if (!$objectEntity->getUri()) { + // Lets make sure we always set the uri + $objectEntity->setUri($this->createUri($objectEntity)); + } + if (!$objectEntity->getSelf()) { + // Lets make sure we always set the self (@id) + $objectEntity->setSelf($this->createSelf($objectEntity)); + } + + if (array_key_exists('@organization', $post) && $objectEntity->getOrganization() != $post['@organization']) { + $objectEntity->setOrganization($post['@organization']); + } + + // Only do this if we are changing an object, not when creating one. + if ($this->request->getMethod() != 'POST') { + // Handle setting an object as unread. + if (array_key_exists('@dateRead', $post) && $post['@dateRead'] == false) { + $this->setUnread($objectEntity); + } + + // If we change an ObjectEntity we should remove it from the result cache + $this->functionService->removeResultFromCache($objectEntity); + } + + return $objectEntity; + } + + /** + * Checks if there exists an unread object for the given ObjectEntity + current UserId. If not, creation one. + * + * @param ObjectEntity $objectEntity + * + * @return void + * @deprecated moved this function to CoreBundle->ReadUnreadService->setUnread() + */ + public function setUnread(ObjectEntity $objectEntity) + { + // First, check if there is an Unread object for this Object+User. If so, do nothing. + $user = $this->security->getUser(); + if ($user !== null) { + $unreads = $this->entityManager->getRepository('App:Unread')->findBy(['object' => $objectEntity, 'userId' => $user->getUserIdentifier()]); + if (empty($unreads)) { + $unread = new Unread(); + $unread->setObject($objectEntity); + $unread->setUserId($user->getUserIdentifier()); + $this->entityManager->persist($unread); + // Do not flush, will always be done after the api-call that triggers this function, if that api-call doesn't throw an exception. + } + } + } + + /** + * @return string + * @deprecated + */ + private function getUserName(): string + { + $user = $this->security->getUser(); + + if ($user instanceof AuthenticationUser) { + return $user->getName(); + } + + return ''; + } + + /** + * Handles saving the value for an Attribute when the Attribute has a function set. A function makes it 'function' (/behave) differently. + * + * @param ObjectEntity $objectEntity + * @param Attribute $attribute + * + * @throws Exception + * + * @return ObjectEntity + * @deprecated + */ + private function handleAttributeFunction(ObjectEntity $objectEntity, Attribute $attribute): ObjectEntity + { + switch ($attribute->getFunction()) { + case 'id': + $objectEntity->setValue($attribute, $objectEntity->getId()->toString()); + // Note: attributes with function = id should also be readOnly and type=string + break; + case 'self': + $self = $objectEntity->getSelf() ?? $objectEntity->setSelf($this->createSelf($objectEntity))->getSelf(); + $objectEntity->setValue($attribute, $self); + // Note: attributes with function = self should also be readOnly and type=string + break; + case 'uri': + $uri = $objectEntity->getUri() ?? $objectEntity->setUri($this->createUri($objectEntity))->getUri(); + $objectEntity->setValue($attribute, $uri); + // Note: attributes with function = uri should also be readOnly and type=string + break; + case 'externalId': + $objectEntity->setValue($attribute, $objectEntity->getExternalId()); + // Note: attributes with function = externalId should also be readOnly and type=string + break; + case 'dateCreated': + $objectEntity->setValue($attribute, $objectEntity->getDateCreated()->format("Y-m-d\TH:i:sP")); + // Note: attributes with function = dateCreated should also be readOnly and type=string||date||datetime + break; + case 'dateModified': + $objectEntity->setValue($attribute, $objectEntity->getDateModified()->format("Y-m-d\TH:i:sP")); + // Note: attributes with function = dateModified should also be readOnly and type=string||date||datetime + break; + case 'userName': + $objectEntity->getValueObject($attribute)->getValue() ?? $objectEntity->setValue($attribute, $this->getUserName()); + break; + } + + return $objectEntity; + } + + /** + * Saves a Value for an Attribute (of the Entity) of an ObjectEntity. + * + * @param ObjectEntity $objectEntity + * @param Attribute $attribute + * @param $value + * + * @throws Exception|InvalidArgumentException + * + * @return ObjectEntity + * @deprecated + */ + private function saveAttribute(ObjectEntity $objectEntity, Attribute $attribute, $value): ObjectEntity + { + //todo: check owner? +// try { +// if (!$this->checkOwner($objectEntity)) { +// $this->authorizationService->checkAuthorization([ +// 'method' => $this->request->getMethod(), +// 'attribute' => $attribute, +// 'value' => $value, +// ]); +// } +// } catch (AccessDeniedException $e) { +// throw new GatewayException('message', null, null, ['data' => ['info' => 'info'], 'path' => 'somePath', 'responseType' => Response::HTTP_FORBIDDEN]); +// } + + $valueObject = $objectEntity->getValueObject($attribute); + + // If the value given by the user is empty... + if (empty($value) && !(in_array($attribute->getType(), ['bool', 'boolean']) && $value === false)) { + if ($attribute->getMultiple() && $value === []) { + if ($attribute->getType() == 'object' && ($this->request->getMethod() == 'PUT' || $this->request->getMethod() == 'PATCH')) { + foreach ($valueObject->getObjects() as $object) { + // If we are not re-adding this object... + $object->removeSubresourceOf($valueObject); + if (count($object->getSubresourceOf()) == 0) { + $this->eavService->handleDelete($object); + } + } + $valueObject->getObjects()->clear(); + } else { + $valueObject->setValue([]); + } + } else { + $valueObject->setValue(null); + } + + return $objectEntity; + } + + // Save the actual value, unless type is object or file, we save those differently. + if (!in_array($attribute->getType(), ['object', 'file'])) { + $valueObject->setValue($value); + } elseif ($attribute->getMultiple()) { + // If multiple, this is an array, loop through $value and save as array of $attribute->getType() + $objectEntity = $this->saveAttributeMultiple($objectEntity, $attribute, $valueObject, $value); + } else { + $objectEntity = $this->saveAttributeType($objectEntity, $attribute, $valueObject, $value); + } + + return $objectEntity; + } + + /** + * Saves a subObject using saveObject. Will also set the owner, uri, organization and application. And check for a Entity function. + * + * @param ObjectEntity $subObject + * @param $object + * + * @throws InvalidArgumentException + * + * @return ObjectEntity + * @deprecated + */ + private function saveSubObject(ObjectEntity $subObject, $object): ObjectEntity + { + $subObject = $this->saveObject($subObject, $object); + $this->handleOwner($subObject); // Do this after all CheckAuthorization function calls + + // We need to set uri here in case we need it in $this->functionService->handleFunction later! + $subObject->setUri($this->createUri($subObject)); + + // todo remove if no longer needed, see value.php setValue() where we set owner, organization and application for subobjects + // Set organization for this object + if (count($subObject->getSubresourceOf()) > 0 && !empty($subObject->getSubresourceOf()->first()->getObjectEntity()->getOrganization())) { + $subObject->setOrganization($subObject->getSubresourceOf()->first()->getObjectEntity()->getOrganization()); + $subObject->setApplication($subObject->getSubresourceOf()->first()->getObjectEntity()->getApplication()); + } else { + $subObject->setOrganization($this->session->get('organization')); + $application = $this->entityManager->getRepository('App:Application')->findOneBy(['id' => $this->session->get('application')]); + $subObject->setApplication(!empty($application) ? $application : null); + } + + return $this->functionService->handleFunction($subObject, $subObject->getEntity()->getFunction(), [ + 'method' => $this->request->getMethod(), + 'uri' => $subObject->getUri(), + 'organizationType' => is_array($object) && array_key_exists('type', $object) ? $object['type'] : null, + 'userGroupName' => is_array($object) && array_key_exists('name', $object) ? $object['name'] : null, + ]); + } + + /** + * @TODO + * + * @param ObjectEntity $objectEntity + * @param Attribute $attribute + * @param Value $valueObject + * @param $value + * + * @throws InvalidArgumentException + * + * @return ObjectEntity + * @deprecated + */ + private function saveAttributeMultiple(ObjectEntity $objectEntity, Attribute $attribute, Value $valueObject, $value): ObjectEntity + { + switch ($attribute->getType()) { + case 'object': + $subObjectIds = []; + $saveSubObjects = new ArrayCollection(); // collection to store all new subobjects in before we actually connect them to the value + foreach ($value as $key => $object) { + // If we are not cascading and value is a string, then value should be an id. + if (is_string($object)) { + if (Uuid::isValid($object) == false) { + // We should also allow commonground Uri's like: https://taalhuizen-bisc.commonground.nu/api/v1/wrc/organizations/008750e5-0424-440e-aea0-443f7875fbfe + // TODO: support /$attribute->getObject()->getEndpoint()/uuid? + if ($object == $attribute->getObject()->getSource()->getLocation().'/'.$attribute->getObject()->getEndpoint().'/'.$this->commonGroundService->getUuidFromUrl($object)) { + $object = $this->commonGroundService->getUuidFromUrl($object); + } else { +// var_dump('The given value ('.$object.') is not a valid object, a valid uuid or a valid uri ('.$attribute->getObject()->getSource()->getLocation().'/'.$attribute->getObject()->getEndpoint().'/uuid).'); + continue; + } + } + + // Look for object in the gateway with this id (for ObjectEntity id and for ObjectEntity externalId) + // todo make one sql query for finding an ObjectEntity by id or externalId + if (!$subObject = $this->entityManager->getRepository('App:ObjectEntity')->findOneBy(['entity' => $attribute->getObject(), 'id' => $object])) { + if (!$subObject = $this->entityManager->getRepository('App:ObjectEntity')->findOneBy(['entity' => $attribute->getObject(), 'externalId' => $object])) { + // If gateway->location and endpoint are set on the attribute(->getObject) Entity look outside the gateway for an existing object. + if (!$subObject) { + // todo: throw error? +// var_dump('Could not find an object with id '.$object.' of type '.$attribute->getObject()->getName()); + continue; + } + } + } + // object toevoegen + $saveSubObjects->add($subObject); + continue; + } + + // If we are doing a PUT with a subObject that contains an id, find the object with this id and update it. + if (($this->request->getMethod() == 'PUT' || $this->request->getMethod() == 'PATCH') && array_key_exists('id', $object)) { + if (!is_string($object['id']) || Uuid::isValid($object['id']) == false) { +// var_dump('The given value ('.$object['id'].') is not a valid uuid.'); + continue; + } + $subObject = $valueObject->getObjects()->filter(function (ObjectEntity $item) use ($object) { + return $item->getId() == $object['id'] || $item->getExternalId() == $object['id']; + }); + if (count($subObject) == 0) { + // look outside the gateway + + if (!$subObject) { + // todo: throw error? +// var_dump('Could not find an object with id '.$object['id'].' of type '.$attribute->getObject()->getName()); + continue; + } + + // object toevoegen + $saveSubObjects->add($subObject); + continue; + } elseif (count($subObject) > 1) { +// var_dump('Found more than 1 object with id '.$object['id'].' of type '.$attribute->getObject()->getName()); + continue; + } else { + $subObject = $subObject->first(); + } + } elseif (($this->request->getMethod() == 'PUT' || $this->request->getMethod() == 'PATCH') + && count($value) == 1 + && count($valueObject->getObjects()) == 1) { + // If we are doing a PUT with a single subObject (and it contains no id) and the existing mainObject only has a single subObject, use the existing subObject and update that. + $subObject = $valueObject->getObjects()->first(); + $object['id'] = $subObject->getExternalId(); + } else { + //Lets do a cascade check here. + if (!$attribute->getCascade() && !is_string($object)) { + continue; + } + + // Create a new subObject (ObjectEntity) + $subObject = new ObjectEntity(); + $subObject->setEntity($attribute->getObject()); + $subObject->addSubresourceOf($valueObject); + } + + $subObject->setSubresourceIndex($key); + + $subObject = $this->saveSubObject($subObject, $object); + + // object toevoegen + $saveSubObjects->add($subObject); + $subObjectIds[] = $subObject->getId()->toString(); + } + $valueObject->setArrayValue($subObjectIds); + + // If we are doing a put, we want to actually clear (or remove) objects connected to this valueObject we no longer need + if ($this->request->getMethod() == 'PUT' || $this->request->getMethod() == 'PATCH') { + foreach ($valueObject->getObjects() as $object) { + // If we are not re-adding this object... allow delete on PUT + if (!$saveSubObjects->contains($object)) { + $object->removeSubresourceOf($valueObject); + if (count($object->getSubresourceOf()) == 0) { + $this->eavService->handleDelete($object); + } + } + } + $valueObject->getObjects()->clear(); + } + // Actually add the objects to the valueObject + foreach ($saveSubObjects as $saveSubObject) { + // Make sure we never connect the value of a multiple=false attribute to more than one object! Checks inversedBy + $this->disconnectNotMultipleObjects($objectEntity, $attribute, $saveSubObject); + + $valueObject->addObject($saveSubObject); + } + break; + case 'file': + foreach ($value as $file) { + $objectEntity = $this->saveFile($objectEntity, $valueObject, $this->base64ToFileArray($file)); + } + break; + default: + // do nothing + break; + } + + return $objectEntity; + } + + /** + * @TODO + * + * @param ObjectEntity $objectEntity + * @param Attribute $attribute + * @param Value $valueObject + * @param $value + * + * @throws Exception + * + * @return ObjectEntity + * @deprecated + */ + private function saveAttributeType(ObjectEntity $objectEntity, Attribute $attribute, Value $valueObject, $value): ObjectEntity + { + switch ($attribute->getType()) { + case 'object': + // Check for cascading (should already be done by validatorService... + if (!$attribute->getCascade() && !is_string($value)) { + break; + } + + // If we are not cascading and value is a string, then value should be an id. + if (is_string($value)) { + if (Uuid::isValid($value) == false) { + // We should also allow commonground Uri's like: https://taalhuizen-bisc.commonground.nu/api/v1/wrc/organizations/008750e5-0424-440e-aea0-443f7875fbfe + // TODO: support /$attribute->getObject()->getEndpoint()/uuid? +// if ($value == $attribute->getObject()->getSource()->getLocation().'/'.$attribute->getObject()->getEndpoint().'/'.$this->commonGroundService->getUuidFromUrl($value)) { +// $value = $this->commonGroundService->getUuidFromUrl($value); +// } else { + //// var_dump('The given value ('.$value.') is not a valid object, a valid uuid or a valid uri ('.$attribute->getObject()->getSource()->getLocation().'/'.$attribute->getObject()->getEndpoint().'/uuid).'); +// break; +// } + } + + // Look for object in the gateway with this id (for ObjectEntity id and for ObjectEntity externalId) + // todo make one sql query for finding an ObjectEntity by id or externalId + if (!$subObject = $this->entityManager->getRepository('App:ObjectEntity')->findOneBy(['entity' => $attribute->getObject(), 'id' => $value])) { + if (!$subObject = $this->entityManager->getRepository('App:ObjectEntity')->findOneBy(['entity' => $attribute->getObject(), 'externalId' => $value])) { + // If gateway->location and endpoint are set on the attribute(->getObject) Entity look outside the gateway for an existing object. + if (!$subObject) { + // todo: throw error? +// var_dump('Could not find an object with id '.$value.' of type '.$attribute->getObject()->getName()); + break; + } + } + } + + // Make sure we never connect the value of a multiple=false attribute to more than one object! Checks inversedBy. + $this->disconnectNotMultipleObjects($objectEntity, $attribute, $subObject); + + // Object toevoegen + $valueObject->getObjects()->clear(); // We start with a default object + $valueObject->addObject($subObject); + break; + } + + if (!$valueObject->getValue()) { + // Cascading... + $subObject = new ObjectEntity(); + $subObject->setEntity($attribute->getObject()); + $subObject->addSubresourceOf($valueObject); + } else { + // Put... + $subObject = $valueObject->getValue(); + } + + $subObject = $this->saveSubObject($subObject, $value); + + $this->entityManager->persist($subObject); + + $valueObject->setValue($subObject); + + break; + case 'file': + $objectEntity = $this->saveFile($objectEntity, $valueObject, $this->base64ToFileArray($value)); + break; + default: + // do nothing + break; + } + + return $objectEntity; + } + + /** + * This function will check if an attribute has inversedBy, if so, get the inversedBy value and check if this value does not already have given $objectEntity as a relation. + * If inversedBy value does not have the $objectEntity as relation and the attribute of this inversedBy value is multiple=false this inversedBy value should only contain one object. + * So, if the inversedBy value has already one or more other objects connected to it, disconnect all these objects, so we can add $objectEntity as the only relation after using this function. + * + * @param ObjectEntity $objectEntity The 'parent' objectEntity of $attribute we might want to add as a inversedBy relation to $subObject. + * @param Attribute $attribute The attribute we are going to check inversedBy on and get its value if it has a inversedBy attribute/value. + * @param ObjectEntity $subObject The 'child' objectEntity we want to add to the Value of the $attribute of $objectEntity. + * + * @return void + */ + private function disconnectNotMultipleObjects(ObjectEntity $objectEntity, Attribute $attribute, ObjectEntity $subObject) + { + // Make sure we never connect the value of a multiple=false attribute to more than one object! + if ($attribute->getInversedBy()) { + // If we have inversedBy on this attribute + $inversedByValue = $subObject->getValueObject($attribute->getInversedBy()); + if (!$inversedByValue->getObjects()->contains($objectEntity)) { // $valueObject->getObjectEntity() = $objectEntity + // If inversedBy attribute is not multiple it should only have one object connected to it + if (!$attribute->getInversedBy()->getMultiple() and count($inversedByValue->getObjects()) > 0) { + // Disconnect old objects + foreach ($inversedByValue->getObjects() as $object) { + // Clear any objects and there parent relations (subresourceOf) to make sure we only can have one object connected. + $object->removeSubresourceOf($inversedByValue); + } + } + } + } + } + + /** + * @TODO + * + * @param ObjectEntity $objectEntity + * @param Value $valueObject + * @param array $fileArray + * + * @return ObjectEntity + */ + private function saveFile(ObjectEntity $objectEntity, Value $valueObject, array $fileArray): ObjectEntity + { + if ($fileArray['name']) { + // Find file by filename (this can be the uuid of the file object) + $fileObject = $valueObject->getFiles()->filter(function (File $item) use ($fileArray) { + return $item->getName() == $fileArray['name']; + }); + if (count($fileObject) > 1) { +// var_dump($attribute->getName().'.name More than 1 file found with this name: '.$fileArray['name']); + // todo: throw error? + } + } + + if (isset($fileObject) && count($fileObject) == 1) { + // Update existing file if we found one using the given file name + $fileObject = $fileObject->first(); + } else { + // Create a new file + $fileObject = new File(); + } + $this->entityManager->persist($fileObject); // For getting the id if no name is given + $fileObject->setName($fileArray['name'] ?? $fileObject->getId()); + $fileObject->setExtension($fileArray['extension']); + $fileObject->setMimeType($fileArray['mimeType']); + $fileObject->setSize($fileArray['size']); + $fileObject->setBase64($fileArray['base64']); + + $valueObject->addFile($fileObject); + + return $objectEntity; + } + + /** + * Converts a mime type to an extension (or find all mime_types with an extension). + * + * @param $mime + * @param null $ext + * + * @return array|false|string + */ + private function mimeToExt($mime, $ext = null) + { + // todo: move this to a dedicated file and get it from there? + $mime_map = [ + 'video/3gpp2' => '3g2', + 'video/3gp' => '3gp', + 'video/3gpp' => '3gp', + 'application/x-compressed' => '7zip', + 'audio/x-acc' => 'aac', + 'audio/ac3' => 'ac3', + 'application/postscript' => 'ai', + 'audio/x-aiff' => 'aif', + 'audio/aiff' => 'aif', + 'audio/x-au' => 'au', + 'video/x-msvideo' => 'avi', + 'video/msvideo' => 'avi', + 'video/avi' => 'avi', + 'application/x-troff-msvideo' => 'avi', + 'application/macbinary' => 'bin', + 'application/mac-binary' => 'bin', + 'application/x-binary' => 'bin', + 'application/x-macbinary' => 'bin', + 'image/bmp' => 'bmp', + 'image/x-bmp' => 'bmp', + 'image/x-bitmap' => 'bmp', + 'image/x-xbitmap' => 'bmp', + 'image/x-win-bitmap' => 'bmp', + 'image/x-windows-bmp' => 'bmp', + 'image/ms-bmp' => 'bmp', + 'image/x-ms-bmp' => 'bmp', + 'application/bmp' => 'bmp', + 'application/x-bmp' => 'bmp', + 'application/x-win-bitmap' => 'bmp', + 'application/cdr' => 'cdr', + 'application/coreldraw' => 'cdr', + 'application/x-cdr' => 'cdr', + 'application/x-coreldraw' => 'cdr', + 'image/cdr' => 'cdr', + 'image/x-cdr' => 'cdr', + 'zz-application/zz-winassoc-cdr' => 'cdr', + 'application/mac-compactpro' => 'cpt', + 'application/pkix-crl' => 'crl', + 'application/pkcs-crl' => 'crl', + 'application/x-x509-ca-cert' => 'crt', + 'application/pkix-cert' => 'crt', + 'text/css' => 'css', + 'text/x-comma-separated-values' => 'csv', + 'text/comma-separated-values' => 'csv', + 'application/vnd.msexcel' => 'csv', + 'application/x-director' => 'dcr', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + 'application/x-dvi' => 'dvi', + 'message/rfc822' => 'eml', + 'application/x-msdownload' => 'exe', + 'video/x-f4v' => 'f4v', + 'audio/x-flac' => 'flac', + 'video/x-flv' => 'flv', + 'image/gif' => 'gif', + 'application/gpg-keys' => 'gpg', + 'application/x-gtar' => 'gtar', + 'application/x-gzip' => 'gzip', + 'application/mac-binhex40' => 'hqx', + 'application/mac-binhex' => 'hqx', + 'application/x-binhex40' => 'hqx', + 'application/x-mac-binhex40' => 'hqx', + 'text/html' => 'html', + 'image/x-icon' => 'ico', + 'image/x-ico' => 'ico', + 'image/vnd.microsoft.icon' => 'ico', + 'text/calendar' => 'ics', + 'application/java-archive' => 'jar', + 'application/x-java-application' => 'jar', + 'application/x-jar' => 'jar', + 'image/jp2' => 'jp2', + 'video/mj2' => 'jp2', + 'image/jpx' => 'jp2', + 'image/jpm' => 'jp2', + 'image/jpeg' => 'jpeg', + 'image/pjpeg' => 'jpeg', + 'application/x-javascript' => 'js', + 'application/json' => 'json', + 'text/json' => 'json', + 'application/vnd.google-earth.kml+xml' => 'kml', + 'application/vnd.google-earth.kmz' => 'kmz', + 'text/x-log' => 'log', + 'audio/x-m4a' => 'm4a', + 'audio/mp4' => 'm4a', + 'application/vnd.mpegurl' => 'm4u', + 'audio/midi' => 'mid', + 'application/vnd.mif' => 'mif', + 'video/quicktime' => 'mov', + 'video/x-sgi-movie' => 'movie', + 'audio/mpeg' => 'mp3', + 'audio/mpg' => 'mp3', + 'audio/mpeg3' => 'mp3', + 'audio/mp3' => 'mp3', + 'video/mp4' => 'mp4', + 'video/mpeg' => 'mpeg', + 'application/oda' => 'oda', + 'audio/ogg' => 'ogg', + 'video/ogg' => 'ogg', + 'application/ogg' => 'ogg', + 'font/otf' => 'otf', + 'application/x-pkcs10' => 'p10', + 'application/pkcs10' => 'p10', + 'application/x-pkcs12' => 'p12', + 'application/x-pkcs7-signature' => 'p7a', + 'application/pkcs7-mime' => 'p7c', + 'application/x-pkcs7-mime' => 'p7c', + 'application/x-pkcs7-certreqresp' => 'p7r', + 'application/pkcs7-signature' => 'p7s', + 'application/pdf' => 'pdf', + 'application/octet-stream' => 'pdf', + 'application/x-x509-user-cert' => 'pem', + 'application/x-pem-file' => 'pem', + 'application/pgp' => 'pgp', + 'application/x-httpd-php' => 'php', + 'application/php' => 'php', + 'application/x-php' => 'php', + 'text/php' => 'php', + 'text/x-php' => 'php', + 'application/x-httpd-php-source' => 'php', + 'image/png' => 'png', + 'image/x-png' => 'png', + 'application/powerpoint' => 'ppt', + 'application/vnd.ms-powerpoint' => 'ppt', + 'application/vnd.ms-office' => 'ppt', + 'application/msword' => 'doc', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', + 'application/x-photoshop' => 'psd', + 'image/vnd.adobe.photoshop' => 'psd', + 'audio/x-realaudio' => 'ra', + 'audio/x-pn-realaudio' => 'ram', + 'application/x-rar' => 'rar', + 'application/rar' => 'rar', + 'application/x-rar-compressed' => 'rar', + 'audio/x-pn-realaudio-plugin' => 'rpm', + 'application/x-pkcs7' => 'rsa', + 'text/rtf' => 'rtf', + 'text/richtext' => 'rtx', + 'video/vnd.rn-realvideo' => 'rv', + 'application/x-stuffit' => 'sit', + 'application/smil' => 'smil', + 'text/srt' => 'srt', + 'image/svg+xml' => 'svg', + 'application/x-shockwave-flash' => 'swf', + 'application/x-tar' => 'tar', + 'application/x-gzip-compressed' => 'tgz', + 'image/tiff' => 'tiff', + 'font/ttf' => 'ttf', + 'text/plain' => 'txt', + 'text/x-vcard' => 'vcf', + 'application/videolan' => 'vlc', + 'text/vtt' => 'vtt', + 'audio/x-wav' => 'wav', + 'audio/wave' => 'wav', + 'audio/wav' => 'wav', + 'application/wbxml' => 'wbxml', + 'video/webm' => 'webm', + 'image/webp' => 'webp', + 'audio/x-ms-wma' => 'wma', + 'application/wmlc' => 'wmlc', + 'video/x-ms-wmv' => 'wmv', + 'video/x-ms-asf' => 'wmv', + 'font/woff' => 'woff', + 'font/woff2' => 'woff2', + 'application/xhtml+xml' => 'xhtml', + 'application/excel' => 'xl', + 'application/msexcel' => 'xls', + 'application/x-msexcel' => 'xls', + 'application/x-ms-excel' => 'xls', + 'application/x-excel' => 'xls', + 'application/x-dos_ms_excel' => 'xls', + 'application/xls' => 'xls', + 'application/x-xls' => 'xls', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + 'application/vnd.ms-excel' => 'xlsx', + 'application/xml' => 'xml', + 'text/xml' => 'xml', + 'text/xsl' => 'xsl', + 'application/xspf+xml' => 'xspf', + 'application/x-compress' => 'z', + 'application/x-zip' => 'zip', + 'application/zip' => 'zip', + 'application/x-zip-compressed' => 'zip', + 'application/s-compressed' => 'zip', + 'multipart/x-zip' => 'zip', + 'text/x-scriptzsh' => 'zsh', + ]; + + if ($ext) { + $mime_types = []; + foreach ($mime_map as $mime_type => $extension) { + if ($extension == $ext) { + $mime_types[] = $mime_type; + } + } + + return $mime_types; + } + + return $mime_map[$mime] ?? false; + } + + /** + * Create a file array (matching the Entity File) from an array containing at least a base64 string and maybe a filename (not required). + * + * @param array $file + * + * @return array + */ + private function base64ToFileArray(array $file): array + { + // Get mime_type from base64 + $explode_base64 = explode(',', $file['base64']); + $imgdata = base64_decode(end($explode_base64)); + $f = finfo_open(); + $mime_type = finfo_buffer($f, $imgdata, FILEINFO_MIME_TYPE); + finfo_close($f); + + // Create file data + return [ + 'name' => array_key_exists('filename', $file) ? $file['filename'] : null, + // Get extension from filename, and else from the mime_type + 'extension' => array_key_exists('filename', $file) ? pathinfo($file['filename'], PATHINFO_EXTENSION) : $this->mimeToExt($mime_type), + 'mimeType' => $mime_type, + 'size' => $this->getBase64Size($file['base64']), + 'base64' => $file['base64'], + ]; + } + + /** + * Gets the memory size of a base64 file. + * + * @param $base64 + * + * @return Exception|float|int + */ + private function getBase64Size($base64) + { //return memory size in B, KB, MB + try { + $size_in_bytes = (int) (strlen(rtrim($base64, '=')) * 3 / 4); + $size_in_kb = $size_in_bytes / 1024; + $size_in_mb = $size_in_kb / 1024; + + return $size_in_bytes; + } catch (Exception $e) { + return $e; + } + } + + /** + * Create a file array (matching the Entity File) from an UploadedFile object. + * + * @param UploadedFile $file + * @param string|null $key + * + * @return array + */ + public function uploadedFileToFileArray(UploadedFile $file, string $key = null): array + { + return [ + 'name' => $file->getClientOriginalName() ?? null, + 'extension' => $file->getClientOriginalExtension() ?? $file->getClientMimeType() ? $this->mimeToExt($file->getClientMimeType()) : null, + 'mimeType' => $file->getClientMimeType() ?? null, + 'size' => $file->getSize() ?? null, + 'base64' => $this->uploadToBase64($file), + 'key' => $key, // Pass this through for showing correct error messages with multiple files + ]; + } + + /** + * Create a base64 string from an UploadedFile object. + * + * @param UploadedFile $file + * + * @return string + */ + private function uploadToBase64(UploadedFile $file): string + { + $content = base64_encode($file->openFile()->fread($file->getSize())); + $mimeType = $file->getClientMimeType(); + + return 'data:'.$mimeType.';base64,'.$content; + } + + /** + * @TODO + * + * @param ObjectEntity $objectEntity + * + * @return string + * @deprecated + */ + public function createUri(ObjectEntity $objectEntity): string + { + // We need to persist if this is a new ObjectEntity in order to set and getId to generate the uri... + $this->entityManager->persist($objectEntity); + if ($objectEntity->getEntity()->getSource() && $objectEntity->getEntity()->getSource()->getLocation() && $objectEntity->getExternalId()) { + return $objectEntity->getEntity()->getSource()->getLocation().'/'.$objectEntity->getEntity()->getEndpoint().'/'.$objectEntity->getExternalId(); + } + + $uri = isset($_SERVER['HTTP_HOST']) && $_SERVER['HTTP_HOST'] !== 'localhost' ? 'https://'.$_SERVER['HTTP_HOST'] : 'http://localhost'; + + if ($objectEntity->getEntity()->getRoute()) { + return $uri.'/api'.$objectEntity->getEntity()->getRoute().'/'.$objectEntity->getId(); + } + + return $uri.'/admin/object_entities/'.$objectEntity->getId(); + } + + /** + * Returns the string used for {at sign}id or self->href for the given objectEntity. This function will use the ObjectEntity->Entity + * to first look for the get item endpoint and else use the Entity route or name to generate the correct string. + * + * @param ObjectEntity $objectEntity + * + * @return string + * @deprecated + */ + public function createSelf(ObjectEntity $objectEntity): string + { + // We need to persist if this is a new ObjectEntity in order to set and getId to generate the self... + $this->entityManager->persist($objectEntity); + $endpoints = $this->entityManager->getRepository('App:Endpoint')->findGetItemByEntity($objectEntity->getEntity()); + if (count($endpoints) > 0 && $endpoints[0] instanceof Endpoint) { + $pathArray = $endpoints[0]->getPath(); + $foundId = in_array('{id}', $pathArray) ? $pathArray[array_search('{id}', $pathArray)] = $objectEntity->getId() : + (in_array('{uuid}', $pathArray) ? $pathArray[array_search('{uuid}', $pathArray)] = $objectEntity->getId() : false); + if ($foundId !== false) { + $path = implode('/', $pathArray); + + return '/api/'.$path; + } + } + + return '/api'.($objectEntity->getEntity()->getRoute() ?? $objectEntity->getEntity()->getName()).'/'.$objectEntity->getId(); + } + + /** + * Create a NRC notification for the given ObjectEntity. + * + * @param ObjectEntity $objectEntity + * @param string $method + * @deprecated + */ + public function notify(ObjectEntity $objectEntity, string $method) + { + if (!$this->commonGroundService->getComponent('nrc')) { + return; + } + // TODO: move this function to a notificationService? + $topic = $objectEntity->getEntity()->getName(); + switch ($method) { + case 'POST': + $action = 'Create'; + break; + case 'PUT': + case 'PATCH': + $action = 'Update'; + break; + case 'DELETE': + $action = 'Delete'; + break; + } + if (isset($action)) { + $notification = [ + 'topic' => $topic, + 'action' => $action, + 'resource' => $objectEntity->getUri(), + 'id' => $objectEntity->getExternalId(), + ]; + if (!$objectEntity->getUri()) { + // var_dump('Couldn\'t notifiy for object, because it has no uri!'); + // var_dump('Id: '.$objectEntity->getId()); + // var_dump('ExternalId: '.$objectEntity->getExternalId() ?? null); + // var_dump($notification); + return; + } + $this->commonGroundService->createResource($notification, ['component' => 'nrc', 'type' => 'notifications'], false, true, false); + } + } + + /** + * When rendering a single attribute value for the post body of the api-call/promise to update an object in a source outside the gateway, + * and when the type of this attribute is object and cascading on this attribute is not allowed, + * try and render/use the entire object for all subresources of this attribute. + * + * @param Collection $objects + * @param Attribute $attribute + * + * @return array|mixed|null + * @deprecated + */ + public function renderSubObjects(Collection $objects, Attribute $attribute) + { + $results = []; + foreach ($objects as $object) { + // We allow cascading on promises, but only if the gateway of the parent entity and subresource match. + $results[] = + $object->getEntity()->getSource() == $attribute->getEntity()->getSource() ? + $this->renderPostBody($object) : + $object->getUri(); + } + if (!$attribute->getMultiple()) { + if (count($results) == 1) { + return $results[0]; + } else { + return null; + } + } else { + return $results; + } + } + + /** + * When rendering a single attribute value for the post body of the api-call/promise to update an object in a source outside the gateway, + * and when the type of this attribute is object and cascading on this attribute is not allowed, + * only render/use the uri for all subresources of this attribute. + * + * @param Collection $objects + * @param Attribute $attribute + * + * @return array|mixed|string|null + * @deprecated + */ + public function getSubObjectIris(Collection $objects, Attribute $attribute) + { + $results = []; + foreach ($objects as $object) { + $results[] = + $object->getEntity()->getSource() == $attribute->getEntity()->getSource() ? + "/{$object->getEntity()->getEndpoint()}/{$object->getExternalId()}" : + $object->getUri(); + } + if (!$attribute->getMultiple()) { + if (count($results) == 1) { + return $results[0]; + } else { + return null; + } + } else { + return $results; + } + } + + /** + * Render a single attribute value for the post body of the api-call/promise to update an object in a source outside the gateway (before doing the api-call). + * + * @param Value $value + * @param Attribute $attribute + * + * @return File[]|Value[]|array|bool|Collection|float|int|mixed|string|void|null + * @deprecated + */ + public function renderValue(Value $value, Attribute $attribute) + { + $rendered = ''; + switch ($attribute->getType()) { + case 'object': + // We allow cascading on promises, but only if the gateway of the parent entity and subresource match. + if ($attribute->getCascade()) { + $rendered = $this->renderSubObjects($value->getObjects(), $attribute); + } else { + $rendered = $this->getSubObjectIris($value->getObjects(), $attribute); + } + break; + default: + $rendered = $value->getValue(); + } + + return $rendered; + } + + /** + * Render the post body, with all attributes to update/send with the api-call/promise to update an object in a source outside the gateway (before doing the api-call). + * + * @param ObjectEntity $objectEntity + * + * @return array + * @deprecated + */ + public function renderPostBody(ObjectEntity $objectEntity): array + { + $body = []; + foreach ($objectEntity->getEntity()->getAttributes() as $attribute) { + // todo: With this ===null check we can never set a value to null with a promise. + // todo: Maybe we should add a new bool to attribute that determines it shouldn't be added if value===null? + if (!$attribute->getPersistToGateway() || (!$attribute->getRequired() && $objectEntity->getValue($attribute) === null)) { + continue; + } + $body[$attribute->getName()] = $this->renderValue($objectEntity->getValueObject($attribute), $attribute); + } + + return $body; + } + + /** + * Encode body for the api-call/promise to update an object in a source outside the gateway, before doing the api-call. + * + * @param ObjectEntity $objectEntity + * @param array $body + * @param array $headers + * + * @throws Exception + * + * @return string + * @deprecated + */ + public function encodeBody(ObjectEntity $objectEntity, array $body, array &$headers): string + { + switch ($objectEntity->getEntity()->getSource()->getType()) { + case 'json': + $body = json_encode($body); + break; + case 'soap': + $xmlEncoder = new XmlEncoder(['xml_root_node_name' => 'S:Envelope']); + $body = $this->translationService->parse($xmlEncoder->encode($this->translationService->dotHydrator( + $objectEntity->getEntity()->getToSoap()->getRequest() ? $xmlEncoder->decode($objectEntity->getEntity()->getToSoap()->getRequest(), 'xml') : [], + $objectEntity->toArray(), + $objectEntity->getEntity()->getToSoap()->getRequestHydration() + ), 'xml', ['xml_encoding' => 'utf-8', 'remove_empty_tags' => true]), false); + $headers['Content-Type'] = 'application/xml;charset=UTF-8'; + break; + default: + throw new Exception('Encoding type not supported'); + } + + return $body; + } + + /** + * If there is special translation config for the api-calls/promises to update an object in a source outside the gateway, before doing the api-call. + * + * @param ObjectEntity $objectEntity + * @param string $method + * @param array $headers + * @param array $query + * @param string $url + * + * @return void + */ + public function getTranslationConfig(ObjectEntity $objectEntity, string &$method, array &$headers, array &$query, string &$url): void + { + $oldMethod = $method; + $config = $objectEntity->getEntity()->getTranslationConfig(); + if ($config && array_key_exists($method, $config)) { + !array_key_exists('method', $config[$oldMethod]) ?: $method = $config[$oldMethod]['method']; + !array_key_exists('headers', $config[$oldMethod]) ?: $headers = array_merge($headers, $config[$oldMethod]['headers']); + !array_key_exists('query', $config[$oldMethod]) ?: $headers = array_merge($query, $config[$oldMethod]['headers']); + !array_key_exists('endpoint', $config[$oldMethod]) ?: $url = $objectEntity->getEntity()->getSource()->getLocation().'/'.str_replace('{id}', $objectEntity->getExternalId(), $config[$oldMethod]['endpoint']); + } + } + + /** + * Decide what method and url to use for a promise to update an object in a source outside the gateway. + * + * @param ObjectEntity $objectEntity + * @param string $url + * @param string $method + * + * @return void + */ + public function decideMethodAndUrl(ObjectEntity $objectEntity, string &$url, string &$method): void + { + if ($method == 'POST' && $objectEntity->getUri() != $objectEntity->getEntity()->getSource()->getLocation().'/'.$objectEntity->getEntity()->getEndpoint().'/'.$objectEntity->getExternalId()) { + $url = $objectEntity->getEntity()->getSource()->getLocation().'/'.$objectEntity->getEntity()->getEndpoint(); + } elseif ($objectEntity->getUri()) { + $method = 'PUT'; + $url = $objectEntity->getUri(); + } elseif ($objectEntity->getExternalId()) { + $method = 'PUT'; + $url = $objectEntity->getEntity()->getSource()->getLocation().'/'.$objectEntity->getEntity()->getEndpoint().'/'.$objectEntity->getExternalId(); + } + } + + /** + * Makes sure if an ObjectEntity has any subresources these wil also result in promises to update those objects in a source outside the gateway. + * + * @param ObjectEntity $objectEntity + * + * @return void + */ + private function settleSubPromises(ObjectEntity $objectEntity): void + { + foreach ($objectEntity->getSubresources() as $sub) { + $promises = $sub->getPromises(); + } + + if (!empty($promises)) { + Utils::settle($promises)->wait(); + } + } + + /** + * Decodes the response of a successful promise to update an object in a source outside the gateway. + * + * @param $response + * @param ObjectEntity $objectEntity + * + * @throws Exception + * + * @return array + */ + private function decodeResponse($response, ObjectEntity $objectEntity): array + { + switch ($objectEntity->getEntity()->getSource()->getType()) { + case 'json': + $result = json_decode($response->getBody()->getContents(), true); + break; + case 'xml': + $xmlEncoder = new XmlEncoder(); + $result = $xmlEncoder->decode($response->getBody()->getContents(), 'xml'); + break; + case 'soap': + $xmlEncoder = new XmlEncoder(['xml_root_node_name' => 'soap:Envelope']); + $result = $response->getBody()->getContents(); + // $result = $this->translationService->parse($result); + $result = $xmlEncoder->decode($result, 'xml'); + $result = $this->translationService->dotHydrator([], $result, $objectEntity->getEntity()->getToSoap()->getResponseHydration()); + break; + default: + throw new Exception('Unsupported type'); + } + + return $result; + } + + /** + * Set externalId of an ObjectEntity after a successful promise to update an object in a source outside the gateway. + * + * @param ObjectEntity $objectEntity + * @param array $result + * @param string $url + * @param string $method + * + * @return ObjectEntity + */ + private function setExternalId(ObjectEntity $objectEntity, array $result, string $url, string $method): ObjectEntity + { + if (array_key_exists('id', $result) && !strpos($url, $result['id'])) { + $objectEntity->setUri($url.'/'.$result['id']); + $objectEntity->setExternalId($result['id']); + } else { + $objectEntity->setUri($url); + $objectEntity->setExternalId($this->commonGroundService->getUuidFromUrl($url)); + } + +// var_dump('GetUri: '.$objectEntity->getUri()); + + // Handle Function todo: what if @organization is used in the post body? than we shouldn't handle function organization here: + return $this->functionService->handleFunction($objectEntity, $objectEntity->getEntity()->getFunction(), [ + 'method' => $method, + 'uri' => $objectEntity->getUri(), + ]); + } + + /** + * Set externalResult of an ObjectEntity after a successful promise to update an object in a source outside the gateway. + * + * @param ObjectEntity $objectEntity + * @param array $result + * + * @return ObjectEntity + */ + private function setExternalResult(ObjectEntity $objectEntity, array $result): ObjectEntity + { + if (!is_null($objectEntity->getEntity()->getAvailableProperties())) { + $availableProperties = $objectEntity->getEntity()->getAvailableProperties(); + $result = array_filter($result, function ($key) use ($availableProperties) { + return in_array($key, $availableProperties); + }, ARRAY_FILTER_USE_KEY); + } + + return $objectEntity->setExternalResult($result); + } + + /** + * Handle successful/ok response of a promise to update an object in a source outside the gateway. + * Includes updating the Gateway ObjectEntity, Gateway Cache and sending an async notification. + * + * @param $response + * @param ObjectEntity $objectEntity + * @param string $url + * @param string $method + * + * @throws InvalidArgumentException + * + * @return ObjectEntity + */ + private function onFulfilled($response, ObjectEntity $objectEntity, string $url, string $method) + { + $result = $this->decodeResponse($response, $objectEntity); + $objectEntity = $this->setExternalId($objectEntity, $result, $url, $method); + + // Lets reset cache + $this->functionService->removeResultFromCache($objectEntity); +// $this->responseService->renderResult($objectEntity, null); // pre-load/re-load cache + + // Create Notification +// var_dump('NOTIFICATION: '.$objectEntity->getEntity()->getName().' - '.$objectEntity->getId()->toString().' - '.$objectEntity->getExternalId().' - '.$method); + $this->notifications[] = ['id' => $objectEntity->getId(), 'method' => $method]; + + // log +// $responseLog = new Response(json_encode($result), 201, []); +// $this->logService->saveLog($this->logService->makeRequest(), $responseLog, 13, json_encode($result), null, 'out'); + + return $this->setExternalResult($objectEntity, $result); + } + + /** + * Handle error response of a promise to update an object in a source outside the gateway. + * + * @param $error + * @param ObjectEntity $objectEntity + * + * @return void + */ + private function onError($error, ObjectEntity $objectEntity) + { + /* @todo lelijke code */ + if ($error->getResponse()) { + $errorBody = json_decode((string) $error->getResponse()->getBody(), true); + if ($errorBody && array_key_exists('message', $errorBody)) { + $error_message = $errorBody['message']; + } elseif ($errorBody && array_key_exists('hydra:description', $errorBody)) { + $error_message = $errorBody['hydra:description']; + } else { + $error_message = (string) $error->getResponse()->getBody(); + } + } else { + $error_message = $error->getMessage(); + } +// var_dump($error_message); + +// // log +// if ($error->getResponse() instanceof Response) { +// $responseLog = $error->getResponse(); +// } else { +// $responseLog = new Response($error_message, $error->getResponse()->getStatusCode(), []); +// } +// $log = $this->logService->saveLog($this->logService->makeRequest(), $responseLog, 14, $error_message, null, 'out'); + /* @todo eigenlijk willen we links naar error reports al losse property mee geven op de json error message */ + $objectEntity->addError('gateway endpoint on '.$objectEntity->getEntity()->getName().' said', $error_message.'. (see /admin/logs/'./*$log->getId().*/ ') for a full error report'); + } + + /** + * Creates a promise to update an object in a source outside the gateway. + * + * @param ObjectEntity $objectEntity + * @param string $method + * + * @throws Exception + * + * @return PromiseInterface + */ + public function createPromise(ObjectEntity $objectEntity, string &$method): PromiseInterface + { + $component = $this->gatewayService->sourceToArray($objectEntity->getEntity()->getSource()); + $query = []; + $headers = []; + $url = ''; + $this->decideMethodAndUrl($objectEntity, $url, $method); + + $this->settleSubPromises($objectEntity); + + $body = $this->renderPostBody($objectEntity); + $body = $this->encodeBody($objectEntity, $body, $headers); + $this->getTranslationConfig($objectEntity, $method, $headers, $query, $url); + +// // log +// $this->logService->saveLog($this->logService->makeRequest(), null, 12, $body, null, 'out'); + +// var_dump('CallServiceUrl: '.$url); +// var_dump($body); + + return $this->commonGroundService->callService($component, $url, $body, $query, $headers, true, $method)->then( + function ($response) use ($objectEntity, $url, $method) { +// var_dump('succes'); + $this->onFulfilled($response, $objectEntity, $url, $method); + }, + function ($error) use ($objectEntity) { +// var_dump('error'); + $this->onError($error, $objectEntity); + } + ); + } + + /** + * Implodes a multidimensional array to a string. + * + * @param array $array + * @param string $separator + * @param string $keyValueSeparator + * + * @return string + * @deprecated + */ + public function implodeMultiArray(array $array, string $separator = ', ', string $keyValueSeparator = '='): string + { + $str = ''; + + foreach ($array as $key => $value) { + $currentSeparator = $separator; + if ($key === array_key_first($array)) { + $currentSeparator = ''; + } + if (is_array($value)) { + $str .= "$currentSeparator\"$key\"{$keyValueSeparator}[{$this->implodeMultiArray($value, $separator, $keyValueSeparator)}]"; + } else { + $str .= "$currentSeparator\"$key\"$keyValueSeparator\"$value\""; + } + } + + return $str; + } } diff --git a/api/src/Service/SynchronizationService.php b/api/src/Service/SynchronizationService.php index 32d973444..136aea80f 100644 --- a/api/src/Service/SynchronizationService.php +++ b/api/src/Service/SynchronizationService.php @@ -14,6 +14,7 @@ use App\Exception\GatewayException; use CommonGateway\CoreBundle\Service\CallService; use CommonGateway\CoreBundle\Service\FileSystemHandleService; +use CommonGateway\CoreBundle\Service\GatewayResourceService; use CommonGateway\CoreBundle\Service\MappingService; use DateInterval; use DateTime; @@ -23,6 +24,7 @@ use Monolog\Logger; use Psr\Cache\CacheException; use Psr\Cache\InvalidArgumentException; +use Ramsey\Uuid\Uuid; use Safe\Exceptions\UrlException; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Style\SymfonyStyle; @@ -60,12 +62,14 @@ class SynchronizationService private SymfonyStyle $io; private Environment $twig; private MappingService $mappingService; + private GatewayResourceService $resourceService; private ActionEvent $event; private EventDispatcherInterface $eventDispatcher; private Logger $logger; private bool $asyncError = false; + private ?string $sha = null; /** * @param CallService $callService @@ -81,6 +85,7 @@ class SynchronizationService * @param EventDispatcherInterface $eventDispatcher * @param MappingService $mappingService * @param FileSystemHandleService $fileSystemService + * @param GatewayResourceService $resourceService */ public function __construct( CallService $callService, @@ -95,7 +100,8 @@ public function __construct( Environment $twig, EventDispatcherInterface $eventDispatcher, MappingService $mappingService, - FileSystemHandleService $fileSystemService + FileSystemHandleService $fileSystemService, + GatewayResourceService $resourceService ) { $this->callService = $callService; $this->entityManager = $entityManager; @@ -114,6 +120,7 @@ public function __construct( $this->logger = new Logger('installation'); $this->mappingService = $mappingService; $this->fileSystemService = $fileSystemService; + $this->resourceService = $resourceService; } /** @@ -326,7 +333,7 @@ private function loopThroughCollectionResults(array $results, array $config): ar array_key_exists('object', $this->configuration['apiSource']['location']) && $result = $dot->get($this->configuration['apiSource']['location']['object'], $result); // Lets grab the sync object, if we don't find an existing one, this will create a new one: - $synchronization = $this->findSyncBySource($config['source'], $config['entity'], $id); + $synchronization = $this->findSyncBySource($config['source'], $config['entity'], $id, $this->configuration['location'] ?? null); // todo: Another search function for sync object. If no sync object is found, look for matching properties... // todo: ...in $result and an ObjectEntity in db. And then create sync for an ObjectEntity if we find one this way. (nice to have) // Other option to find a sync object, currently not used: @@ -693,13 +700,22 @@ public function getSingleFromSource(Synchronization $synchronization): ?array $url = \Safe\parse_url($synchronization->getSource()->getLocation()); + $endpoint = $synchronization->getEndpoint().'/'.$synchronization->getSourceId(); + if (str_contains('http', $synchronization->getSourceId()) === true) { + $endpoint = $synchronization->getEndpoint(); + } + if (isset($this->configuration['location']) === true) { + $endpoint = $this->configuration['location']; + $synchronization->setEndpoint($endpoint); + } + if ($url['scheme'] === 'http' || $url['scheme'] === 'https') { // Get object form source with callservice try { $this->logger->info("getSingleFromSource with Synchronization->sourceId = {$synchronization->getSourceId()}"); $response = $this->callService->call( $callServiceConfig['source'], - $synchronization->getEndpoint() ?? $callServiceConfig['endpoint'], + $endpoint, $callServiceConfig['method'] ?? 'GET', [ 'body' => '', @@ -717,7 +733,7 @@ public function getSingleFromSource(Synchronization $synchronization): ?array $result = $this->callService->decodeResponse($callServiceConfig['source'], $response); } elseif ($url['scheme'] === 'ftp') { // This only works if a file data equals a single Object(Entity). Or if the mapping on the Source or Synchronization results in data for just a single Object. - $result = $this->fileSystemService->call($synchronization->getSource(), $synchronization->getEndpoint() ?? $callServiceConfig['endpoint']); + $result = $this->fileSystemService->call($synchronization->getSource(), isset($this->configuration['location']) === true ? $callServiceConfig['endpoint'] : $endpoint); } $dot = new Dot($result); // The place where we can find the id field when looping through the list of objects, from $result root, by object (dot notation) @@ -734,15 +750,20 @@ public function getSingleFromSource(Synchronization $synchronization): ?array /** * Finds a synchronization object if it exists for the current object in the source, or creates one if it doesn't exist. * - * @param Source $source The source that is requested - * @param Entity $entity The entity that is requested - * @param string $sourceId The id of the object in the source + * @param Source $source The source that is requested + * @param Entity $entity The entity that is requested + * @param string $sourceId The id of the object in the source + * @param string|null $endpoint The endpoint of the synchronization. * * @return Synchronization|null A synchronization object related to the object in the source */ - public function findSyncBySource(Source $source, Entity $entity, string $sourceId): ?Synchronization + public function findSyncBySource(Source $source, Entity $entity, string $sourceId, ?string $endpoint = null): ?Synchronization { - $synchronization = $this->entityManager->getRepository('App:Synchronization')->findOneBy(['gateway' => $source, 'entity' => $entity, 'sourceId' => $sourceId]); + $criteria = ['gateway' => $source, 'entity' => $entity, 'sourceId' => $sourceId]; + if (empty($endpoint) === false) { + $criteria['endpoint'] = $endpoint; + } + $synchronization = $this->entityManager->getRepository('App:Synchronization')->findOneBy($criteria); if ($synchronization instanceof Synchronization) { if (isset($this->io)) { @@ -754,6 +775,7 @@ public function findSyncBySource(Source $source, Entity $entity, string $sourceI } $synchronization = new Synchronization($source, $entity); + $synchronization->setEndpoint($endpoint); $synchronization->setSourceId($sourceId); $this->entityManager->persist($synchronization); // We flush later @@ -941,6 +963,29 @@ public function handleSync(Synchronization $synchronization, array $sourceObject return $synchronization; } + /** + * This function checks if the sha of $synchronization matches the given $sha. + * When $synchronization->getSha() doesn't match with the given $sha, the given $sha will be stored in the SynchronizationService. + * Always call the ->synchronize() function after this, because only then the stored $sha will be used to update $synchronization->setSha(). + * + * @param Synchronization $synchronization The Synchronization to check the sha of. + * @param string $sha The sha to check / compare. + * + * @return bool Returns True if sha matches, and false if it does not match. + */ + public function doesShaMatch(Synchronization $synchronization, string $sha): bool + { + $this->sha = null; + + if ($synchronization->getSha() === $sha) { + return true; + } + + $this->sha = $sha; + + return false; + } + /** * Executes the synchronization between source and gateway. * @@ -1012,6 +1057,12 @@ public function synchronize(Synchronization $synchronization, array $sourceObjec $sourceObject = $this->mappingService->mapping($synchronization->getMapping(), $sourceObject); } $synchronization->getObject()->hydrate($sourceObject, $unsafe); + + if ($this->sha !== null) { + $synchronization->setSha($this->sha); + $this->sha = null; + } + $this->entityManager->persist($synchronization->getObject()); $this->entityManager->persist($synchronization); @@ -1320,15 +1371,27 @@ private function syncToSource(Synchronization $synchronization, bool $existsInSo // $objectArray = $this->objectEntityService->checkGetObjectExceptions($data, $object, [], ['all' => true], 'application/ld+json'); // todo: maybe move this to foreach in getAllFromSource() (nice to have) - $callServiceConfig = $this->getCallServiceConfig($synchronization->getSource(), null, $objectArray); + $callServiceConfig = $this->getCallServiceConfig($synchronization->getSource(), $existsInSource ? $synchronization->getSourceId() : null, $objectArray); $objectArray = $this->mapOutput($objectArray); + $endpoint = $synchronization->getEndpoint(); + if ($existsInSource === true) { + $endpoint = $endpoint.'/'.$synchronization->getSourceId(); + } + if (str_contains('http', $synchronization->getSourceId()) === true) { + $endpoint = $synchronization->getEndpoint(); + } + if (isset($this->configuration['location']) === true) { + $endpoint = $this->configuration['location']; + $synchronization->setEndpoint($endpoint); + } + $objectString = $this->getObjectString($objectArray); try { $result = $this->callService->call( $callServiceConfig['source'], - $synchronization->getEndpoint() ?? $callServiceConfig['endpoint'], + $endpoint, $callServiceConfig['method'] ?? ($existsInSource ? 'PUT' : 'POST'), [ 'body' => $objectString, @@ -1463,41 +1526,35 @@ private function syncThroughComparing(Synchronization $synchronization): Synchro */ public function aquireObject(string $url, Entity $entity): ?ObjectEntity { - // 1. Get the domain from the url - $parse = \Safe\parse_url($url); - $location = $parse['scheme'].'://'.$parse['host']; - - // 2.c Try to establish a source for the domain - $source = $this->entityManager->getRepository('App:Gateway')->findOneBy(['location'=>$location]); - - // 2.b The source might be on a path e.g. /v1 so if whe cant find a source let try to cycle - if ($source instanceof Source === false && isset($parse['path']) === true) { - foreach (explode('/', $parse['path']) as $pathPart) { - if ($pathPart !== '') { - $location = $location.'/'.$pathPart; - } - $source = $this->entityManager->getRepository('App:Gateway')->findOneBy(['location'=>$location]); - if ($source !== null) { - break; - } - } - } - if ($source instanceof Source === false) { - return null; - } + $source = $this->resourceService->findSourceForUrl($url, 'conduction-nl/commonground-gateway', $endpoint); + $sourceId = $this->getSourceId($endpoint, $url); - // 3 If we have a source we can establish an endpoint. - $endpoint = str_replace($location, '', $url); - - // 4 Create sync - $synchronization = new Synchronization($source, $entity); - $synchronization->setSourceId($url); - $synchronization->setEndpoint($endpoint); - - $this->entityManager->persist($synchronization); + $synchronization = $this->findSyncBySource($source, $entity, $sourceId, $endpoint); $this->synchronize($synchronization); return $synchronization->getObject(); } + + /** + * A function best used after resourceService->findSourceForUrl and/or before $this->findSyncBySource. + * This function will get the uuid / int id from the end of an endpoint. This is the sourceId for a Synchronization. + * + * @param string $endpoint The endpoint to get the SourceId from. + * @param string|null $url The url used as back-up for SourceId if no proper SourceId can be found. + * + * @return string|null The sourceId, will be equal to $url if end part of the endpoint isn't an uuid or integer. + */ + public function getSourceId(string &$endpoint, ?string $url = null): ?string + { + $explodedEndpoint = explode('/', $endpoint); + $sourceId = end($explodedEndpoint); + if (Uuid::isValid($sourceId) === true || is_int((int) $sourceId) === true) { + $endpoint = str_replace("/$sourceId", '', $endpoint); + } else { + $sourceId = $url; + } + + return $sourceId; + } } diff --git a/api/src/Twig/MappingExtension.php b/api/src/Twig/MappingExtension.php index d2df0d88d..6308c6c99 100644 --- a/api/src/Twig/MappingExtension.php +++ b/api/src/Twig/MappingExtension.php @@ -12,6 +12,7 @@ public function getFunctions() return [ new TwigFunction('map', [MappingRuntime::class, 'map']), new TwigFunction('dotToObject', [MappingRuntime::class, 'dotToArray']), + new TwigFunction('arrayValues', [MappingRuntime::class, 'arrayValues']), ]; } } diff --git a/api/src/Twig/MappingRuntime.php b/api/src/Twig/MappingRuntime.php index 850832aa2..87b385b24 100644 --- a/api/src/Twig/MappingRuntime.php +++ b/api/src/Twig/MappingRuntime.php @@ -18,6 +18,20 @@ public function __construct(MappingService $mappingService, EntityManagerInterfa $this->mappingService = $mappingService; } + /** + * Uses CoreBundle MappingService to map data. + * If $list is set to true you could use the key 'listInput' in the $data array to pass along the list of items to map. + * This makes it possible to also pass along other key+value pairs to use in mapping besides just one array of items to map. + * + * @param string $mappingString The reference of a Mapping object. + * @param array $data The data to map. Or one list of items to map. + * @param bool $list False by default, if set to true mapping will be done for each item in the $data array. + * + * @return array The mapped result. + * + * @throws \Twig\Error\LoaderError + * @throws \Twig\Error\SyntaxError + */ public function map(string $mappingString, array $data, bool $list = false): array { $mapping = $this->entityManager->getRepository('App:Mapping')->findOneBy(['reference' => $mappingString]); @@ -27,10 +41,29 @@ public function map(string $mappingString, array $data, bool $list = false): arr return $value; } + /** + * Turns given array into an Adbar\Dot (dot notation). + * + * @param array $array The array to turn into a dot array. + * + * @return array The dot aray. + */ public function dotToArray(array $array): array { $dotArray = new Dot($array, true); return $dotArray->all(); } + + /** + * Makes it possible to use the php function array_values in twig. + * + * @param array $array The array to use array_values on. + * + * @return array The updated array. + */ + public function arrayValues(array $array): array + { + return array_values($array); + } } diff --git a/api/templates/emails/bisc/password-forgot-e-mail.html.twig b/api/templates/emails/bisc/password-forgot-e-mail.html.twig deleted file mode 100644 index a19fcbc74..000000000 --- a/api/templates/emails/bisc/password-forgot-e-mail.html.twig +++ /dev/null @@ -1,525 +0,0 @@ - - - - - - {{ subject }} - - - - - - - - - - - - - - - diff --git a/api/templates/emails/kiss/new-review-e-mail.html.twig b/api/templates/emails/kiss/new-review-e-mail.html.twig deleted file mode 100644 index 2b3720643..000000000 --- a/api/templates/emails/kiss/new-review-e-mail.html.twig +++ /dev/null @@ -1,491 +0,0 @@ -{# todo: move this to an email plugin (see EmailService.php) #} - - - - - - {{ subject }} - - - - - - - - - - - - - - - diff --git a/docker-compose-dex.yml b/docker-compose-dex.yml index 93090cb9c..82b8a58dd 100644 --- a/docker-compose-dex.yml +++ b/docker-compose-dex.yml @@ -8,14 +8,6 @@ x-cache: - ${CONTAINER_REGISTRY_BASE}/${CONTAINER_PROJECT_NAME}-cron services: - # gateway-frontend: - # &gateway-frontend - # image: ghcr.io/conductionnl/commonground-gateway-frontend:latest - # depends_on: - # - php - # ports: - # - "83:80" - cron: image: ${CONTAINER_REGISTRY_BASE}/${CONTAINER_PROJECT_NAME}_cron:${APP_ENV} build: ./dockerized-cron @@ -45,6 +37,7 @@ services: - ./api/var/certs:/var/certs:rw,cached - ./gateway:/srv/api/fixtures:rw,cached environment: + - APP_INIT='true' - CONTAINER_REGISTRY_BASE=${CONTAINER_REGISTRY_BASE} - CONTAINER_PROJECT_NAME=${CONTAINER_PROJECT_NAME} - DATABASE_URL=postgres://api-platform:!ChangeMe!@db/api?serverVersion=10.1 @@ -106,6 +99,7 @@ services: - CRON_RUNNER_ENABLED=${CRON_RUNNER_ENABLED} - CRON_RUNNER_CRONTAB=${CRON_RUNNER_CRONTAB} - CRON_RUNNER_CONCURRENCY_POLICY=${CRON_RUNNER_CONCURRENCY_POLICY} + - LOG_LEVEL=${LOG_LEVEL} ports: - "82:80" cap_drop: @@ -170,13 +164,14 @@ services: - "5432:5432" mongodb: - image: mongo + image: mongo:4.4.14 restart: always environment: MONGO_INITDB_ROOT_USERNAME: api-platform MONGO_INITDB_ROOT_PASSWORD: '!ChangeMe!' ports: - "27017:27017" + dex: image: dexidp/dex:latest ports: @@ -204,12 +199,12 @@ services: ports: - 389:389 - 636:636 - + rabbitmq: - image: rabbitmq:latest + image: rabbitmq:3.12.0 environment: - - RABBITMQ_DEFAULT_USER=${RABBITMQ_USERNAME} - - RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD} + - RABBITMQ_DEFAULT_USER=${RABBITMQ_USERNAME} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD} networks: diff --git a/publiccode.yaml b/publiccode.yaml new file mode 100644 index 000000000..0c6fcab70 --- /dev/null +++ b/publiccode.yaml @@ -0,0 +1,106 @@ +publiccodeYmlVersion: '0.1' +name: commonground-gateway +applicationSuite: null +url: https://github.com/ConductionNL/commonground-gateway +landingURL: https://conductor-gateway.app +isBasedOn: null +softwareVersion: null +logo: https://avatars.githubusercontent.com/u/46676163?v=4 +monochromeLogo: null +platforms: + - web +releaseDate: 2021-08-2 +categories: + - collaboration + - it-development +developmentStatus: beta +softwareType: !!! +description: + en: + localisedName: CommonGateway + genericName: null + shortDescription: CommonGateway + longDescription: "The Common Gateway repository provides a quick Kubernetes wrapper for the Common Gateway Symfony Bundle. In other words, it doesn't aim to be its own code base but simply contains the files needed to create Kubernetes images and Helm installers for the core bundle." + documentation: https://conductor-gateway.app + apiDocumentation: null + features: [] + screenshots: [] + videos: [] + awards: [] + nl: + localisedName: CommonGateway + genericName: null + shortDescription: CommonGateway + longDescription: "De Common Gateway-repository biedt een snelle Kubernetes-wrapper voor de Common Gateway Symfony-bundel. Met andere woorden, het is niet bedoeld als een eigen codebasis, maar bevat eenvoudigweg de bestanden die nodig zijn om Kubernetes-images en Helm-installatieprogramma's voor de core bundle te maken." + documentation: https://conductor-gateway.app + apiDocumentation: null + features: [] + screenshots: [] + videos: [] + awards: [] +intendedaudience: + countries: + - nl + unsupportedCountries: [] + scope: + - government + - local-authorities +legal: + license: EUPL-1.2-or-later + mainCopyrightOwner: legal/authorsFile + repoOwner: CommonGateway + authorsFile: null +maintenance: + type: internal + contractors: + - name: Conduction + until: 2035-01-01 + email: info\x64conduction.nl + website: https://www.conduction.nl + contacts: + - name: Ruben van der Linde + email: info\x64conduction.nl + phone: 085 303 6840 + affiliation: Conduction +localisation: + localisationReady: true + availableLanguages: + - en + - nl +dependsOn: + open: + - name: CoreBundle + versionMin: 1.2 + versionMax: 1.3 + version: 1.2.47 + optional: false + proprietary: + - name: Docker + versionMin: 24.0.6 + version: 24.0.6 + optional: false +roadmap: null +inputTypes: + - application/json + - application/xml + - application/x-yaml +outputTypes: + - application/json + - application/json+hal + - application/json+ld + - application/xml + - application/x-yaml +nl: + countryExtensionVersion: null + commonground: + intendedOrganisations: [] + installationType: null + layerType: service + gemma: + bedrijfsfuncties: [] + bedrijfsservices: [] + applicatiefunctie: null + model: null + upl: [] +downloadUrls: + - https://github.com/ConductionNL/commonground-gateway/archive/refs/heads/main.zip