diff --git a/.github/workflows/set-backlog-fields.yml b/.github/workflows/set-backlog-fields.yml index 1035ebf997..c5c803208a 100644 --- a/.github/workflows/set-backlog-fields.yml +++ b/.github/workflows/set-backlog-fields.yml @@ -2,7 +2,7 @@ name: Add Issue to Project on: issues: - types: [ opened ] + types: [ opened, reopened ] jobs: add_issue_to_project: @@ -30,7 +30,7 @@ jobs: repository(name:"web-languageforge", owner:$org) { issue(number: $issue_number) { id - projectNextItems(first:100) { + projectItems(first:100) { nodes { id } @@ -39,7 +39,7 @@ jobs: } }' -F issue_number=$ISSUE_NUMBER -f org=$ORGANIZATION > project_data.json - echo 'IN_PROJECT='$(jq '.data.repository.issue.projectNextItems[] | length' project_data.json) >> $GITHUB_ENV + echo 'IN_PROJECT='$(jq '.data.repository.issue.projectItems[] | length' project_data.json) >> $GITHUB_ENV - name: get required info for set operations env: @@ -47,24 +47,27 @@ jobs: ORGANIZATION: sillsdev run: | gh api graphql -f query=' - query($org: String!) { - organization(login: $org){ - projectNext(number: 1) { + query ($org: String!) { + organization(login: $org) { + projectV2(number: 1) { id - fields(first:100) { - nodes { + field(name: "ProductOwner") { + ... on ProjectV2SingleSelectField { id name - settings + options { + id + name + } } } } } }' -f org=$ORGANIZATION > project_data.json - echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV - echo 'FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "ProductOwner") | .id' project_data.json) >> $GITHUB_ENV - echo 'INCOMING_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "ProductOwner") | .settings | fromjson.options[] | select(.name=="Incoming") | .id' project_data.json) >> $GITHUB_ENV + echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV + echo 'FIELD_ID='$(jq '.data.organization.projectV2.field.id' project_data.json) >> $GITHUB_ENV + echo 'INCOMING_ID='$(jq '.data.organization.projectV2.field.options[] | select(.name=="Incoming") | .id' project_data.json) >> $GITHUB_ENV - name: Add issue to project if: env.IN_PROJECT == 0 @@ -74,12 +77,12 @@ jobs: run: | item_id="$( gh api graphql -f query=' mutation($project:ID!, $item:ID!) { - addProjectNextItem(input: {projectId: $project, contentId: $item}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $project, contentId: $item}) { + item { id } } - }' -f project=$PROJECT_ID -f item=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')" + }' -f project=$PROJECT_ID -f item=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')" echo 'ITEM_ID='$item_id >> $GITHUB_ENV @@ -95,13 +98,17 @@ jobs: $field: ID! $field_value: String!) { - updateProjectNextItemField(input: { + updateProjectV2ItemFieldValue(input: { projectId: $project itemId: $item fieldId: $field - value: $field_value}) + value: { + singleSelectOptionId: $field_value + } + } + ) { - projectNextItem { + projectV2Item { id } } diff --git a/Makefile b/Makefile index 4b1a8e02f5..8e67c6e00c 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,10 @@ unit-tests-ci: .PHONY: build build: npm install + + # ensure we start with a clean ui-dist volume for every build + docker volume rm web-languageforge_lf-ui-dist 2>/dev/null + docker compose build mail app lfmerge ld-api next-proxy next-app .PHONY: scan diff --git a/docker-compose.yml b/docker-compose.yml index 0f82c07b68..8c66c3dca1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -187,7 +187,7 @@ services: - SMTP_USERNAME=username - SMTP_PASSWORD=password - SERVER_HOSTNAME=nobody.localhost - command: sh -c "postconf -e 'default_transport = retry:no outbound email allowed' && /run.sh" + command: sh -c "postconf -e 'default_transport = discard' && /run.sh" db: image: mongo:6 diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index 01c37384c3..391c87531d 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -38,7 +38,7 @@ RUN npm run build:${NPM_BUILD_SUFFIX} # COMPOSER-BUILDER # download composer app dependencies # git - needed for composer install -FROM sillsdev/web-languageforge:base-php AS composer-builder +FROM sillsdev/web-languageforge:base-php-7.4 AS composer-builder ENV COMPOSER_ALLOW_SUPERUSER=1 RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* \ && install-php-extensions @composer @@ -47,7 +47,7 @@ COPY src/composer.json src/composer.lock /composer/ RUN composer install # PRODUCTION IMAGE -FROM sillsdev/web-languageforge:base-php AS production-app +FROM sillsdev/web-languageforge:base-php-7.4 AS production-app RUN rm /usr/local/bin/install-php-extensions RUN apt-get remove -y gnupg2 RUN mv $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini @@ -55,7 +55,7 @@ RUN mv $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini COPY --from=sillsdev/web-languageforge:wait-latest /wait /wait # DEVELOPMENT IMAGE -FROM sillsdev/web-languageforge:base-php AS development-app +FROM sillsdev/web-languageforge:base-php-7.4 AS development-app RUN install-php-extensions xdebug-^3.1 COPY docker/app/docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d RUN mv $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini diff --git a/docker/db/README.md b/docker/db/README.md index 0e0224b46a..db55d4e083 100644 --- a/docker/db/README.md +++ b/docker/db/README.md @@ -32,6 +32,19 @@ `db.users.find()` +``` +db.users.update( + { + email: 'admin@example.org' + }, + { + $set: { + role: 'admin' + } + } +) +``` + # Test data ## Activity diff --git a/docker/lfmerge/Dockerfile b/docker/lfmerge/Dockerfile index 07aed137c8..055ab1e82e 100644 --- a/docker/lfmerge/Dockerfile +++ b/docker/lfmerge/Dockerfile @@ -1,2 +1,2 @@ -FROM ghcr.io/sillsdev/lfmerge:2.0.131 +FROM ghcr.io/sillsdev/lfmerge:2.0.133 # Do not add anything to this Dockerfile, it should stay empty diff --git a/docker/test-php/Dockerfile b/docker/test-php/Dockerfile index 8ef5761810..b93f83d233 100644 --- a/docker/test-php/Dockerfile +++ b/docker/test-php/Dockerfile @@ -1,4 +1,4 @@ -FROM sillsdev/web-languageforge:base-php +FROM sillsdev/web-languageforge:base-php-7.4 # ----- LINES BELOW COPIED FROM APP DOCKERFILE ---------- diff --git a/next-app/package-lock.json b/next-app/package-lock.json index c07c3cad25..3a959822fb 100644 --- a/next-app/package-lock.json +++ b/next-app/package-lock.json @@ -13,9 +13,9 @@ "@tailwindcss/typography": "^0.5.2", "autoprefixer": "^10", "daisyui": "^2", - "rimraf": "^3", + "rimraf": "^4", "svelte": "^3", - "svelte-check": "^2", + "svelte-check": "^3", "tailwindcss": "^3", "tslib": "^2", "typescript": "^4", @@ -23,9 +23,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.14.tgz", - "integrity": "sha512-u0rITLxFIeYAvtJXBQNhNuV4YZe+MD1YvIWT7Nicj8hZAtRVZk2PgNH6KclcKDVHz1ChLKXRfX7d7tkbQBUfrg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", + "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", "cpu": [ "arm" ], @@ -39,9 +39,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.14.tgz", - "integrity": "sha512-hTqB6Iq13pW4xaydeqQrs8vPntUnMjbkq+PgGiBMi69eYk74naG2ftHWqKnxn874kNrt5Or3rQ0PJutx2doJuQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", + "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", "cpu": [ "arm64" ], @@ -55,9 +55,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.14.tgz", - "integrity": "sha512-jir51K4J0K5Rt0KOcippjSNdOl7akKDVz5I6yrqdk4/m9y+rldGptQUF7qU4YpX8U61LtR+w2Tu2Ph+K/UaJOw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", + "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", "cpu": [ "x64" ], @@ -71,9 +71,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.14.tgz", - "integrity": "sha512-vrlaP81IuwPaw1fyX8fHCmivP3Gr73ojVEZy+oWJLAiZVcG8o8Phwun/XDnYIFUHxIoUnMFEpg9o38MIvlw8zw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", + "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", "cpu": [ "arm64" ], @@ -87,9 +87,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.14.tgz", - "integrity": "sha512-KV1E01eC2hGYA2qzFDRCK4wdZCRUvMwCNcobgpiiOzp5QXpJBqFPdxI69j8vvzuU7oxFXDgANwEkXvpeQqyOyg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", + "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==", "cpu": [ "x64" ], @@ -103,9 +103,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.14.tgz", - "integrity": "sha512-xRM1RQsazSvL42BNa5XC7ytD4ZDp0ZyJcH7aB0SlYUcHexJUKiDNKR7dlRVlpt6W0DvoRPU2nWK/9/QWS4u2fw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", + "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", "cpu": [ "arm64" ], @@ -119,9 +119,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.14.tgz", - "integrity": "sha512-7ALTAn6YRRf1O6fw9jmn0rWmOx3XfwDo7njGtjy1LXhDGUjTY/vohEPM3ii5MQ411vJv1r498EEx2aBQTJcrEw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", + "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", "cpu": [ "x64" ], @@ -135,9 +135,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.14.tgz", - "integrity": "sha512-X6xULug66ulrr4IzrW7qq+eq9n4MtEyagdWvj4o4cmWr+JXOT47atjpDF9j5M2zHY0UQBmqnHhwl+tXpkpIb2w==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", + "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", "cpu": [ "arm" ], @@ -151,9 +151,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.14.tgz", - "integrity": "sha512-TLh2OcbBUQcMYRH4GbiDkDZfZ4t1A3GgmeXY27dHSI6xrU7IkO00MGBiJySmEV6sH3Wa6pAN6UtaVL0DwkGW4Q==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", + "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", "cpu": [ "arm64" ], @@ -167,9 +167,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.14.tgz", - "integrity": "sha512-oBZkcZ56UZDFCAfE3Fd/Jgy10EoS7Td77NzNGenM+HSY8BkdQAcI9VF9qgwdOLZ+tuftWD7UqZ26SAhtvA3XhA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", + "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", "cpu": [ "ia32" ], @@ -183,9 +183,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.14.tgz", - "integrity": "sha512-udz/aEHTcuHP+xdWOJmZ5C9RQXHfZd/EhCnTi1Hfay37zH3lBxn/fNs85LA9HlsniFw2zccgcbrrTMKk7Cn1Qg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", + "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", "cpu": [ "loong64" ], @@ -199,9 +199,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.14.tgz", - "integrity": "sha512-kJ2iEnikUOdC1SiTGbH0fJUgpZwa0ITDTvj9EHf9lm3I0hZ4Yugsb3M6XSl696jVxrEocLe519/8CbSpQWFSrg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", + "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", "cpu": [ "mips64el" ], @@ -215,9 +215,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.14.tgz", - "integrity": "sha512-kclKxvZvX5YhykwlJ/K9ljiY4THe5vXubXpWmr7q3Zu3WxKnUe1VOZmhkEZlqtnJx31GHPEV4SIG95IqTdfgfg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", + "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", "cpu": [ "ppc64" ], @@ -231,9 +231,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.14.tgz", - "integrity": "sha512-fdwP9Dc+Kx/cZwp9T9kNqjAE/PQjfrxbio4rZ3XnC3cVvZBjuxpkiyu/tuCwt6SbAK5th6AYNjFdEV9kGC020A==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", + "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", "cpu": [ "riscv64" ], @@ -247,9 +247,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.14.tgz", - "integrity": "sha512-++fw3P4fQk9nqvdzbANRqimKspL8pDCnSpXomyhV7V/ISha/BZIYvZwLBWVKp9CVWKwWPJ4ktsezuLIvlJRHqA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", + "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", "cpu": [ "s390x" ], @@ -263,9 +263,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.14.tgz", - "integrity": "sha512-TomtswAuzBf2NnddlrS4W01Tv85RM9YtATB3OugY6On0PLM4Ksz5qvQKVAjtzPKoLgL1FiZtfc8mkZc4IgoMEA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", + "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", "cpu": [ "x64" ], @@ -279,9 +279,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.14.tgz", - "integrity": "sha512-U06pfx8P5CqyoPNfqIJmnf+5/r4mJ1S62G4zE6eOjS59naQcxi6GnscUCPH3b+hRG0qdKoGX49RAyiqW+M9aSw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", + "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", "cpu": [ "x64" ], @@ -295,9 +295,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.14.tgz", - "integrity": "sha512-/Jl8XVaWEZNu9rZw+n792GIBupQwHo6GDoapHSb/2xp/Ku28eK6QpR2O9cPBkzHH4OOoMH0LB6zg/qczJ5TTGg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", + "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", "cpu": [ "x64" ], @@ -311,9 +311,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.14.tgz", - "integrity": "sha512-2iI7D34uTbDn/TaSiUbEHz+fUa8KbN90vX5yYqo12QGpu6T8Jl+kxODsWuMCwoTVlqUpwfPV22nBbFPME9OPtw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", + "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", "cpu": [ "x64" ], @@ -327,9 +327,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.14.tgz", - "integrity": "sha512-SjlM7AHmQVTiGBJE/nqauY1aDh80UBsXZ94g4g60CDkrDMseatiqALVcIuElg4ZSYzJs8hsg5W6zS2zLpZTVgg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", + "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", "cpu": [ "arm64" ], @@ -343,9 +343,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.14.tgz", - "integrity": "sha512-z06t5zqk8ak0Xom5HG81z2iOQ1hNWYsFQp3sczVLVx+dctWdgl80tNRyTbwjaFfui2vFO12dfE3trCTvA+HO4g==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", + "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", "cpu": [ "ia32" ], @@ -359,9 +359,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.14.tgz", - "integrity": "sha512-ED1UpWcM6lAbalbbQ9TrGqJh4Y9TaASUvu8bI/0mgJcxhSByJ6rbpgqRhxYMaQ682WfA71nxUreaTO7L275zrw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", + "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", "cpu": [ "x64" ], @@ -533,9 +533,9 @@ } }, "node_modules/@sveltejs/adapter-node": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.1.0.tgz", - "integrity": "sha512-br/KlTI24/TFOuQ+SmoTSfwzgWjslGXkSreOYALW8ElVsG8dAh8stDM07hDZjtQQ46r6snd2tev89Sagh8ZOtA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.1.3.tgz", + "integrity": "sha512-CXNYbYVAO8PlN95toqQRR6+QaFT8bejShDbmMwJfdokNpscjgxYbS8oAcWqrizxfPTBfyHlW+kDbu5KuWhxUNA==", "dev": true, "dependencies": { "@rollup/plugin-commonjs": "^24.0.0", @@ -548,16 +548,16 @@ } }, "node_modules/@sveltejs/kit": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.7.tgz", - "integrity": "sha512-u8JS4aXFWlrnu/tjl+EhJ/FvBEjLYDyMaLe7EAU4sW+PfDqnqyHBAPg/IQi5JuBg6l+Z816F4WrTe+zplUTQDg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.1.3.tgz", + "integrity": "sha512-LVsJjJmimUf+p26EpBEZ6lDdwk+WqNGPyy9MQ1HVYLgj5AEECATMeg8OlmUujh7bW2Tib0Cag1UWI7zMMe66WA==", "dev": true, "hasInstallScript": true, "dependencies": { "@sveltejs/vite-plugin-svelte": "^2.0.0", "@types/cookie": "^0.5.1", "cookie": "^0.5.0", - "devalue": "^4.2.0", + "devalue": "^4.2.2", "esm-env": "^1.0.0", "kleur": "^4.1.5", "magic-string": "^0.27.0", @@ -566,7 +566,7 @@ "set-cookie-parser": "^2.5.1", "sirv": "^2.0.2", "tiny-glob": "^0.2.9", - "undici": "5.14.0" + "undici": "5.15.0" }, "bin": { "svelte-kit": "svelte-kit.js" @@ -601,9 +601,9 @@ } }, "node_modules/@tailwindcss/typography": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.8.tgz", - "integrity": "sha512-xGQEp8KXN8Sd8m6R4xYmwxghmswrd0cPnNI2Lc6fmrC3OojysTBJJGSIVwPV56q4t6THFUK3HJ0EaWwpglSxWw==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz", + "integrity": "sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==", "dev": true, "dependencies": { "lodash.castarray": "^4.4.0", @@ -854,9 +854,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001442", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz", - "integrity": "sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==", + "version": "1.0.30001445", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001445.tgz", + "integrity": "sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==", "dev": true, "funding": [ { @@ -981,9 +981,9 @@ } }, "node_modules/daisyui": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.46.1.tgz", - "integrity": "sha512-i59+nLuzzPAVOhNhot3KLtt6stfYeCIPXs9uiLcpXjykpqxHfBA3W6hQWOUWPMwfqhyQd0WKub3sydtPGjzLtA==", + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.47.0.tgz", + "integrity": "sha512-svZpXKldtHjXTEdj/lu2n7b+EQJSatqvmVB59k4dhCDOYUhUZ3jtGuPrgOJlPysHhDjxjCRWWug/fgV5e8tc/w==", "dev": true, "dependencies": { "color": "^4.2", @@ -1062,9 +1062,9 @@ } }, "node_modules/devalue": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.2.0.tgz", - "integrity": "sha512-mbjoAaCL2qogBKgeFxFPOXAUsZchircF+B/79LD4sHH0+NHfYm8gZpQrskKDn5gENGt35+5OI1GUF7hLVnkPDw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.2.2.tgz", + "integrity": "sha512-Pkwd8qrI9O20VJ14fBNHu+on99toTNZFbgWRpZbC0zbDXpnE2WHYcrC1fHhMsF/3Ee+2yaW7vEujAT7fCYgqrA==", "dev": true }, "node_modules/didyoumean": { @@ -1092,9 +1092,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.14.tgz", - "integrity": "sha512-6xAn3O6ZZyoxZAEkwfI9hw4cEqSr/o1ViJtnkvImVkblmUN65Md04o0S/7H1WNu1XGf1Cjij/on7VO4psIYjkw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", + "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", "dev": true, "hasInstallScript": true, "bin": { @@ -1104,28 +1104,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.16.14", - "@esbuild/android-arm64": "0.16.14", - "@esbuild/android-x64": "0.16.14", - "@esbuild/darwin-arm64": "0.16.14", - "@esbuild/darwin-x64": "0.16.14", - "@esbuild/freebsd-arm64": "0.16.14", - "@esbuild/freebsd-x64": "0.16.14", - "@esbuild/linux-arm": "0.16.14", - "@esbuild/linux-arm64": "0.16.14", - "@esbuild/linux-ia32": "0.16.14", - "@esbuild/linux-loong64": "0.16.14", - "@esbuild/linux-mips64el": "0.16.14", - "@esbuild/linux-ppc64": "0.16.14", - "@esbuild/linux-riscv64": "0.16.14", - "@esbuild/linux-s390x": "0.16.14", - "@esbuild/linux-x64": "0.16.14", - "@esbuild/netbsd-x64": "0.16.14", - "@esbuild/openbsd-x64": "0.16.14", - "@esbuild/sunos-x64": "0.16.14", - "@esbuild/win32-arm64": "0.16.14", - "@esbuild/win32-ia32": "0.16.14", - "@esbuild/win32-x64": "0.16.14" + "@esbuild/android-arm": "0.16.17", + "@esbuild/android-arm64": "0.16.17", + "@esbuild/android-x64": "0.16.17", + "@esbuild/darwin-arm64": "0.16.17", + "@esbuild/darwin-x64": "0.16.17", + "@esbuild/freebsd-arm64": "0.16.17", + "@esbuild/freebsd-x64": "0.16.17", + "@esbuild/linux-arm": "0.16.17", + "@esbuild/linux-arm64": "0.16.17", + "@esbuild/linux-ia32": "0.16.17", + "@esbuild/linux-loong64": "0.16.17", + "@esbuild/linux-mips64el": "0.16.17", + "@esbuild/linux-ppc64": "0.16.17", + "@esbuild/linux-riscv64": "0.16.17", + "@esbuild/linux-s390x": "0.16.17", + "@esbuild/linux-x64": "0.16.17", + "@esbuild/netbsd-x64": "0.16.17", + "@esbuild/openbsd-x64": "0.16.17", + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" } }, "node_modules/escalade": { @@ -1232,9 +1232,9 @@ "dev": true }, "node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -1506,9 +1506,9 @@ } }, "node_modules/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -1671,9 +1671,9 @@ } }, "node_modules/postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "dev": true, "funding": [ { @@ -1887,66 +1887,24 @@ } }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz", + "integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==", "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "rimraf": "dist/cjs/src/bin.js" }, "engines": { - "node": "*" + "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rollup": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.9.1.tgz", - "integrity": "sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.10.0.tgz", + "integrity": "sha512-JmRYz44NjC1MjVF2VKxc0M1a97vn+cDxeqWmnwyAF4FvpjK8YFdHpaqvQB+3IxCvX05vJxKZkoMDU8TShhmJVA==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -2154,34 +2112,34 @@ } }, "node_modules/svelte": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.55.0.tgz", - "integrity": "sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==", + "version": "3.55.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.55.1.tgz", + "integrity": "sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==", "dev": true, "engines": { "node": ">= 8" } }, "node_modules/svelte-check": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-2.10.3.tgz", - "integrity": "sha512-Nt1aWHTOKFReBpmJ1vPug0aGysqPwJh2seM1OvICfM2oeyaA62mOiy5EvkXhltGfhCcIQcq2LoE0l1CwcWPjlw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.0.2.tgz", + "integrity": "sha512-DkhKhV0Jt0gh7q9DBB26+J2Vfb9y4/4JWxnbkXBZha7542LOhwvj3edJFjyJ+xjdaXyInZ+YRRYc3V6wytP2ew==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.9", + "@jridgewell/trace-mapping": "^0.3.17", "chokidar": "^3.4.1", "fast-glob": "^3.2.7", "import-fresh": "^3.2.1", "picocolors": "^1.0.0", "sade": "^1.7.4", - "svelte-preprocess": "^4.0.0", - "typescript": "*" + "svelte-preprocess": "^5.0.0", + "typescript": "^4.9.4" }, "bin": { "svelte-check": "bin/svelte-check" }, "peerDependencies": { - "svelte": "^3.24.0" + "svelte": "^3.55.0" } }, "node_modules/svelte-hmr": { @@ -2197,21 +2155,21 @@ } }, "node_modules/svelte-preprocess": { - "version": "4.10.7", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz", - "integrity": "sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz", + "integrity": "sha512-q7lpa7i2FBu8Pa+G0MmuQQWETBwCKgsGmuq1Sf6n8q4uaG9ZLcLP0Y+etC6bF4sE6EbLxfiI38zV6RfPe3RSfg==", "dev": true, "hasInstallScript": true, "dependencies": { - "@types/pug": "^2.0.4", - "@types/sass": "^1.16.0", - "detect-indent": "^6.0.0", - "magic-string": "^0.25.7", + "@types/pug": "^2.0.6", + "@types/sass": "^1.43.1", + "detect-indent": "^6.1.0", + "magic-string": "^0.27.0", "sorcery": "^0.10.0", "strip-indent": "^3.0.0" }, "engines": { - "node": ">= 9.11.2" + "node": ">= 14.10.0" }, "peerDependencies": { "@babel/core": "^7.10.2", @@ -2222,7 +2180,7 @@ "pug": "^3.0.0", "sass": "^1.26.8", "stylus": "^0.55.0", - "sugarss": "^2.0.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", "svelte": "^3.23.0", "typescript": "^3.9.5 || ^4.0.0" }, @@ -2236,9 +2194,6 @@ "less": { "optional": true }, - "node-sass": { - "optional": true - }, "postcss": { "optional": true }, @@ -2262,15 +2217,6 @@ } } }, - "node_modules/svelte-preprocess/node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, "node_modules/tailwindcss": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", @@ -2375,9 +2321,9 @@ } }, "node_modules/undici": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz", - "integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.15.0.tgz", + "integrity": "sha512-wCAZJDyjw9Myv+Ay62LAoB+hZLPW9SmKbQkbHIhMw/acKSlpn7WohdMUc/Vd4j1iSMBO0hWwU8mjB7a5p5bl8g==", "dev": true, "dependencies": { "busboy": "^1.6.0" @@ -2508,156 +2454,156 @@ }, "dependencies": { "@esbuild/android-arm": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.14.tgz", - "integrity": "sha512-u0rITLxFIeYAvtJXBQNhNuV4YZe+MD1YvIWT7Nicj8hZAtRVZk2PgNH6KclcKDVHz1ChLKXRfX7d7tkbQBUfrg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", + "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.14.tgz", - "integrity": "sha512-hTqB6Iq13pW4xaydeqQrs8vPntUnMjbkq+PgGiBMi69eYk74naG2ftHWqKnxn874kNrt5Or3rQ0PJutx2doJuQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", + "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.14.tgz", - "integrity": "sha512-jir51K4J0K5Rt0KOcippjSNdOl7akKDVz5I6yrqdk4/m9y+rldGptQUF7qU4YpX8U61LtR+w2Tu2Ph+K/UaJOw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", + "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.14.tgz", - "integrity": "sha512-vrlaP81IuwPaw1fyX8fHCmivP3Gr73ojVEZy+oWJLAiZVcG8o8Phwun/XDnYIFUHxIoUnMFEpg9o38MIvlw8zw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", + "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.14.tgz", - "integrity": "sha512-KV1E01eC2hGYA2qzFDRCK4wdZCRUvMwCNcobgpiiOzp5QXpJBqFPdxI69j8vvzuU7oxFXDgANwEkXvpeQqyOyg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", + "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.14.tgz", - "integrity": "sha512-xRM1RQsazSvL42BNa5XC7ytD4ZDp0ZyJcH7aB0SlYUcHexJUKiDNKR7dlRVlpt6W0DvoRPU2nWK/9/QWS4u2fw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", + "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.14.tgz", - "integrity": "sha512-7ALTAn6YRRf1O6fw9jmn0rWmOx3XfwDo7njGtjy1LXhDGUjTY/vohEPM3ii5MQ411vJv1r498EEx2aBQTJcrEw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", + "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.14.tgz", - "integrity": "sha512-X6xULug66ulrr4IzrW7qq+eq9n4MtEyagdWvj4o4cmWr+JXOT47atjpDF9j5M2zHY0UQBmqnHhwl+tXpkpIb2w==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", + "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.14.tgz", - "integrity": "sha512-TLh2OcbBUQcMYRH4GbiDkDZfZ4t1A3GgmeXY27dHSI6xrU7IkO00MGBiJySmEV6sH3Wa6pAN6UtaVL0DwkGW4Q==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", + "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.14.tgz", - "integrity": "sha512-oBZkcZ56UZDFCAfE3Fd/Jgy10EoS7Td77NzNGenM+HSY8BkdQAcI9VF9qgwdOLZ+tuftWD7UqZ26SAhtvA3XhA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", + "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.14.tgz", - "integrity": "sha512-udz/aEHTcuHP+xdWOJmZ5C9RQXHfZd/EhCnTi1Hfay37zH3lBxn/fNs85LA9HlsniFw2zccgcbrrTMKk7Cn1Qg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", + "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.14.tgz", - "integrity": "sha512-kJ2iEnikUOdC1SiTGbH0fJUgpZwa0ITDTvj9EHf9lm3I0hZ4Yugsb3M6XSl696jVxrEocLe519/8CbSpQWFSrg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", + "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.14.tgz", - "integrity": "sha512-kclKxvZvX5YhykwlJ/K9ljiY4THe5vXubXpWmr7q3Zu3WxKnUe1VOZmhkEZlqtnJx31GHPEV4SIG95IqTdfgfg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", + "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.14.tgz", - "integrity": "sha512-fdwP9Dc+Kx/cZwp9T9kNqjAE/PQjfrxbio4rZ3XnC3cVvZBjuxpkiyu/tuCwt6SbAK5th6AYNjFdEV9kGC020A==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", + "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.14.tgz", - "integrity": "sha512-++fw3P4fQk9nqvdzbANRqimKspL8pDCnSpXomyhV7V/ISha/BZIYvZwLBWVKp9CVWKwWPJ4ktsezuLIvlJRHqA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", + "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.14.tgz", - "integrity": "sha512-TomtswAuzBf2NnddlrS4W01Tv85RM9YtATB3OugY6On0PLM4Ksz5qvQKVAjtzPKoLgL1FiZtfc8mkZc4IgoMEA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", + "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.14.tgz", - "integrity": "sha512-U06pfx8P5CqyoPNfqIJmnf+5/r4mJ1S62G4zE6eOjS59naQcxi6GnscUCPH3b+hRG0qdKoGX49RAyiqW+M9aSw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", + "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.14.tgz", - "integrity": "sha512-/Jl8XVaWEZNu9rZw+n792GIBupQwHo6GDoapHSb/2xp/Ku28eK6QpR2O9cPBkzHH4OOoMH0LB6zg/qczJ5TTGg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", + "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.14.tgz", - "integrity": "sha512-2iI7D34uTbDn/TaSiUbEHz+fUa8KbN90vX5yYqo12QGpu6T8Jl+kxODsWuMCwoTVlqUpwfPV22nBbFPME9OPtw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", + "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.14.tgz", - "integrity": "sha512-SjlM7AHmQVTiGBJE/nqauY1aDh80UBsXZ94g4g60CDkrDMseatiqALVcIuElg4ZSYzJs8hsg5W6zS2zLpZTVgg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", + "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.14.tgz", - "integrity": "sha512-z06t5zqk8ak0Xom5HG81z2iOQ1hNWYsFQp3sczVLVx+dctWdgl80tNRyTbwjaFfui2vFO12dfE3trCTvA+HO4g==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", + "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.14.tgz", - "integrity": "sha512-ED1UpWcM6lAbalbbQ9TrGqJh4Y9TaASUvu8bI/0mgJcxhSByJ6rbpgqRhxYMaQ682WfA71nxUreaTO7L275zrw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", + "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", "dev": true, "optional": true }, @@ -2764,9 +2710,9 @@ } }, "@sveltejs/adapter-node": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.1.0.tgz", - "integrity": "sha512-br/KlTI24/TFOuQ+SmoTSfwzgWjslGXkSreOYALW8ElVsG8dAh8stDM07hDZjtQQ46r6snd2tev89Sagh8ZOtA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.1.3.tgz", + "integrity": "sha512-CXNYbYVAO8PlN95toqQRR6+QaFT8bejShDbmMwJfdokNpscjgxYbS8oAcWqrizxfPTBfyHlW+kDbu5KuWhxUNA==", "dev": true, "requires": { "@rollup/plugin-commonjs": "^24.0.0", @@ -2776,15 +2722,15 @@ } }, "@sveltejs/kit": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.7.tgz", - "integrity": "sha512-u8JS4aXFWlrnu/tjl+EhJ/FvBEjLYDyMaLe7EAU4sW+PfDqnqyHBAPg/IQi5JuBg6l+Z816F4WrTe+zplUTQDg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.1.3.tgz", + "integrity": "sha512-LVsJjJmimUf+p26EpBEZ6lDdwk+WqNGPyy9MQ1HVYLgj5AEECATMeg8OlmUujh7bW2Tib0Cag1UWI7zMMe66WA==", "dev": true, "requires": { "@sveltejs/vite-plugin-svelte": "^2.0.0", "@types/cookie": "^0.5.1", "cookie": "^0.5.0", - "devalue": "^4.2.0", + "devalue": "^4.2.2", "esm-env": "^1.0.0", "kleur": "^4.1.5", "magic-string": "^0.27.0", @@ -2793,7 +2739,7 @@ "set-cookie-parser": "^2.5.1", "sirv": "^2.0.2", "tiny-glob": "^0.2.9", - "undici": "5.14.0" + "undici": "5.15.0" } }, "@sveltejs/vite-plugin-svelte": { @@ -2811,9 +2757,9 @@ } }, "@tailwindcss/typography": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.8.tgz", - "integrity": "sha512-xGQEp8KXN8Sd8m6R4xYmwxghmswrd0cPnNI2Lc6fmrC3OojysTBJJGSIVwPV56q4t6THFUK3HJ0EaWwpglSxWw==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz", + "integrity": "sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==", "dev": true, "requires": { "lodash.castarray": "^4.4.0", @@ -2990,9 +2936,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001442", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz", - "integrity": "sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==", + "version": "1.0.30001445", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001445.tgz", + "integrity": "sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==", "dev": true }, "chokidar": { @@ -3081,9 +3027,9 @@ "dev": true }, "daisyui": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.46.1.tgz", - "integrity": "sha512-i59+nLuzzPAVOhNhot3KLtt6stfYeCIPXs9uiLcpXjykpqxHfBA3W6hQWOUWPMwfqhyQd0WKub3sydtPGjzLtA==", + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.47.0.tgz", + "integrity": "sha512-svZpXKldtHjXTEdj/lu2n7b+EQJSatqvmVB59k4dhCDOYUhUZ3jtGuPrgOJlPysHhDjxjCRWWug/fgV5e8tc/w==", "dev": true, "requires": { "color": "^4.2", @@ -3131,9 +3077,9 @@ } }, "devalue": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.2.0.tgz", - "integrity": "sha512-mbjoAaCL2qogBKgeFxFPOXAUsZchircF+B/79LD4sHH0+NHfYm8gZpQrskKDn5gENGt35+5OI1GUF7hLVnkPDw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.2.2.tgz", + "integrity": "sha512-Pkwd8qrI9O20VJ14fBNHu+on99toTNZFbgWRpZbC0zbDXpnE2WHYcrC1fHhMsF/3Ee+2yaW7vEujAT7fCYgqrA==", "dev": true }, "didyoumean": { @@ -3161,33 +3107,33 @@ "dev": true }, "esbuild": { - "version": "0.16.14", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.14.tgz", - "integrity": "sha512-6xAn3O6ZZyoxZAEkwfI9hw4cEqSr/o1ViJtnkvImVkblmUN65Md04o0S/7H1WNu1XGf1Cjij/on7VO4psIYjkw==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.16.14", - "@esbuild/android-arm64": "0.16.14", - "@esbuild/android-x64": "0.16.14", - "@esbuild/darwin-arm64": "0.16.14", - "@esbuild/darwin-x64": "0.16.14", - "@esbuild/freebsd-arm64": "0.16.14", - "@esbuild/freebsd-x64": "0.16.14", - "@esbuild/linux-arm": "0.16.14", - "@esbuild/linux-arm64": "0.16.14", - "@esbuild/linux-ia32": "0.16.14", - "@esbuild/linux-loong64": "0.16.14", - "@esbuild/linux-mips64el": "0.16.14", - "@esbuild/linux-ppc64": "0.16.14", - "@esbuild/linux-riscv64": "0.16.14", - "@esbuild/linux-s390x": "0.16.14", - "@esbuild/linux-x64": "0.16.14", - "@esbuild/netbsd-x64": "0.16.14", - "@esbuild/openbsd-x64": "0.16.14", - "@esbuild/sunos-x64": "0.16.14", - "@esbuild/win32-arm64": "0.16.14", - "@esbuild/win32-ia32": "0.16.14", - "@esbuild/win32-x64": "0.16.14" + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", + "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.16.17", + "@esbuild/android-arm64": "0.16.17", + "@esbuild/android-x64": "0.16.17", + "@esbuild/darwin-arm64": "0.16.17", + "@esbuild/darwin-x64": "0.16.17", + "@esbuild/freebsd-arm64": "0.16.17", + "@esbuild/freebsd-x64": "0.16.17", + "@esbuild/linux-arm": "0.16.17", + "@esbuild/linux-arm64": "0.16.17", + "@esbuild/linux-ia32": "0.16.17", + "@esbuild/linux-loong64": "0.16.17", + "@esbuild/linux-mips64el": "0.16.17", + "@esbuild/linux-ppc64": "0.16.17", + "@esbuild/linux-riscv64": "0.16.17", + "@esbuild/linux-s390x": "0.16.17", + "@esbuild/linux-x64": "0.16.17", + "@esbuild/netbsd-x64": "0.16.17", + "@esbuild/openbsd-x64": "0.16.17", + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" } }, "escalade": { @@ -3271,9 +3217,9 @@ "dev": true }, "glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -3482,9 +3428,9 @@ "dev": true }, "minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -3602,9 +3548,9 @@ "dev": true }, "postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "dev": true, "requires": { "nanoid": "^3.3.4", @@ -3721,53 +3667,15 @@ "dev": true }, "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz", + "integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==", + "dev": true }, "rollup": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.9.1.tgz", - "integrity": "sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.10.0.tgz", + "integrity": "sha512-JmRYz44NjC1MjVF2VKxc0M1a97vn+cDxeqWmnwyAF4FvpjK8YFdHpaqvQB+3IxCvX05vJxKZkoMDU8TShhmJVA==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -3919,25 +3827,25 @@ "dev": true }, "svelte": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.55.0.tgz", - "integrity": "sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==", + "version": "3.55.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.55.1.tgz", + "integrity": "sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==", "dev": true }, "svelte-check": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-2.10.3.tgz", - "integrity": "sha512-Nt1aWHTOKFReBpmJ1vPug0aGysqPwJh2seM1OvICfM2oeyaA62mOiy5EvkXhltGfhCcIQcq2LoE0l1CwcWPjlw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.0.2.tgz", + "integrity": "sha512-DkhKhV0Jt0gh7q9DBB26+J2Vfb9y4/4JWxnbkXBZha7542LOhwvj3edJFjyJ+xjdaXyInZ+YRRYc3V6wytP2ew==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.9", + "@jridgewell/trace-mapping": "^0.3.17", "chokidar": "^3.4.1", "fast-glob": "^3.2.7", "import-fresh": "^3.2.1", "picocolors": "^1.0.0", "sade": "^1.7.4", - "svelte-preprocess": "^4.0.0", - "typescript": "*" + "svelte-preprocess": "^5.0.0", + "typescript": "^4.9.4" } }, "svelte-hmr": { @@ -3948,28 +3856,17 @@ "requires": {} }, "svelte-preprocess": { - "version": "4.10.7", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz", - "integrity": "sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz", + "integrity": "sha512-q7lpa7i2FBu8Pa+G0MmuQQWETBwCKgsGmuq1Sf6n8q4uaG9ZLcLP0Y+etC6bF4sE6EbLxfiI38zV6RfPe3RSfg==", "dev": true, "requires": { - "@types/pug": "^2.0.4", - "@types/sass": "^1.16.0", - "detect-indent": "^6.0.0", - "magic-string": "^0.25.7", + "@types/pug": "^2.0.6", + "@types/sass": "^1.43.1", + "detect-indent": "^6.1.0", + "magic-string": "^0.27.0", "sorcery": "^0.10.0", "strip-indent": "^3.0.0" - }, - "dependencies": { - "magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "requires": { - "sourcemap-codec": "^1.4.8" - } - } } }, "tailwindcss": { @@ -4052,9 +3949,9 @@ "dev": true }, "undici": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz", - "integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.15.0.tgz", + "integrity": "sha512-wCAZJDyjw9Myv+Ay62LAoB+hZLPW9SmKbQkbHIhMw/acKSlpn7WohdMUc/Vd4j1iSMBO0hWwU8mjB7a5p5bl8g==", "dev": true, "requires": { "busboy": "^1.6.0" diff --git a/next-app/package.json b/next-app/package.json index af9431f409..2f8ae6d86b 100644 --- a/next-app/package.json +++ b/next-app/package.json @@ -17,9 +17,9 @@ "@tailwindcss/typography": "^0.5.2", "autoprefixer": "^10", "daisyui": "^2", - "rimraf": "^3", + "rimraf": "^4", "svelte": "^3", - "svelte-check": "^2", + "svelte-check": "^3", "tailwindcss": "^3", "tslib": "^2", "typescript": "^4", diff --git a/next-app/src/lib/Details.svelte b/next-app/src/lib/Details.svelte new file mode 100644 index 0000000000..d373c4f8a7 --- /dev/null +++ b/next-app/src/lib/Details.svelte @@ -0,0 +1,23 @@ + +
+ + +
+ +
+ +
+

+
+
+ + + diff --git a/next-app/src/routes/+page.svelte b/next-app/src/routes/+page.svelte index 356aa27a56..260c9113b1 100644 --- a/next-app/src/routes/+page.svelte +++ b/next-app/src/routes/+page.svelte @@ -1,5 +1,6 @@ -

LFNext app

- -
-

Migrated capabilities

-
    -
  1. Change password
  2. -
  3. Project landing page
  4. -
-
- -
-

Progress indicator

- - -
- -
-

Error handling

- -

Client

- - - - - -

Server

- The change password page has a few options: - -
- -
-

UI library //daisyui.com

- -

Button

- - - - - - -

Light/Dark mode

- - -

Stats

- -
- -
-

Custom components

- -

Button

- - - - - -

Input

- - - {value} - -

Form

-

Intended to remove some biolerplate and provide some consistent layout

-
- - - -
- - -

PageHeader

- Simple - - - elements spread across header - - link to this page - - -
+
+ + +
+

LFNext app

+ +
+

Migrated capabilities

+
    +
  1. Change password
  2. +
  3. Project landing page
  4. +
+
+ +
+

Progress indicator

+ + +
+ +
+

Error handling

+ +

Client

+ + + + + +

Server

+ The change password page has a few options: +
    +
  • Submit the form without supplying anything
  • +
  • Only supply the password and not the confirm
  • +
  • Try to submit the form without being authenticated
  • +
+
+ +
+

UI library //daisyui.com

+ +

Button

+ + + + + + +

Light/Dark mode

+ + +

Stats

+ + +

Drawer

+ +
+ +
+

Custom components

+ +

Button

+ + + + + +

Details

+
+
+ Favorite book of the Bible? + +
+ + Job +
+ +

Form

+

Intended to remove some biolerplate and provide some consistent layout

+
+ + + +
+ +

Input

+ + + {value} + +

PageHeader

+ Simple + + + elements spread across header + + link to this page + + +
+
+ +
+
+
diff --git a/next-app/src/routes/projects/[project_code]/Activity.svelte b/next-app/src/routes/projects/[project_code]/Activity.svelte index 39574abdbd..94376200dd 100644 --- a/next-app/src/routes/projects/[project_code]/Activity.svelte +++ b/next-app/src/routes/projects/[project_code]/Activity.svelte @@ -45,7 +45,7 @@ function byDateThenUser(a: AugmentedActivity, b: AugmentedActivity) { return a.date_iso === b.date_iso ? a.user === b.user ? des(a.time, b.time) - : asc(a.user, b.user) + : asc(a.user.username, b.user.username) : des(a.date_iso, b.date_iso) } @@ -72,7 +72,7 @@ {#each sorted_activities as activity} - { activity.user } + { activity.user.username } { activity.date_locale } { action_display[activity.action] || activity.action } { activity.entry || '—' } diff --git a/next-app/src/routes/projects/[project_code]/activities/+server.ts b/next-app/src/routes/projects/[project_code]/activities/+server.ts index 55bce46c43..717fd51818 100644 --- a/next-app/src/routes/projects/[project_code]/activities/+server.ts +++ b/next-app/src/routes/projects/[project_code]/activities/+server.ts @@ -20,6 +20,10 @@ type LegacyActivity = { id: string, action: string, date: string, + userRef: { + username: string, + avatar_ref: string + }, content: { user: string, entry?: string, @@ -31,7 +35,7 @@ export type Activity = { id: string, action: string, date: string, - user: string, + user: { username:string, avatar:string }, entry: string, fields: Field[], } @@ -66,12 +70,14 @@ export async function fetch_activities({ cookie, start_date, end_date }: Activit return activity.map(transform) } -function transform({ id, action, date, content }: LegacyActivity): Activity { +function transform({ id, action, date, content, userRef }: LegacyActivity): Activity { return { id, action, date, - user: content.user, + user: { username: userRef.username, + avatar: userRef.avatar_ref + }, entry: content.entry || '', fields: content.changes || [], } diff --git a/src/Api/Library/Shared/Communicate/Email.php b/src/Api/Library/Shared/Communicate/Email.php index 1199430195..4db70cbab4 100644 --- a/src/Api/Library/Shared/Communicate/Email.php +++ b/src/Api/Library/Shared/Communicate/Email.php @@ -16,13 +16,13 @@ class Email public static function send($from, $to, $subject, $content, $htmlContent = "") { // Create the Transport - $transport = \Swift_SmtpTransport::newInstance(Env::requireEnv("MAIL_HOST")); + $transport = new \Swift_SmtpTransport(Env::requireEnv("MAIL_HOST")); // Create the Mailer using your created Transport - $mailer = \Swift_Mailer::newInstance($transport); + $mailer = new \Swift_Mailer($transport); // Create a message - $message = \Swift_Message::newInstance($subject); + $message = new \Swift_Message($subject); $message->setFrom($from); $message->setTo($to); $message->setBody($content); diff --git a/src/Api/Library/Shared/LanguageData.php b/src/Api/Library/Shared/LanguageData.php index edabdbaef3..f0a796c12b 100644 --- a/src/Api/Library/Shared/LanguageData.php +++ b/src/Api/Library/Shared/LanguageData.php @@ -55,7 +55,7 @@ public function read() $unlisted = new Language("Unlisted Language", "qaa"); $unlisted->country[] = "?"; $unlistedCode = $unlisted->code->three; - if (!array_key_exists($unlistedCode, $this)) { + if (!$this->offsetExists($unlistedCode)) { $this[$unlistedCode] = $unlisted; } } diff --git a/src/Api/Model/Languageforge/Lexicon/Command/LexEntryCommands.php b/src/Api/Model/Languageforge/Lexicon/Command/LexEntryCommands.php index 73258773de..b5b4792815 100644 --- a/src/Api/Model/Languageforge/Lexicon/Command/LexEntryCommands.php +++ b/src/Api/Model/Languageforge/Lexicon/Command/LexEntryCommands.php @@ -49,7 +49,7 @@ private static function lookupFieldLabel(LexConfigFieldList $fieldList, array $p foreach ($parts as $part) { // Strip away anything after a @ character $fieldName = explode("@", $part, 2)[0]; - if (array_key_exists($fieldName, $currentList->fields)) { + if ($currentList->fields->offsetExists($fieldName)) { /** @var LexConfig $fieldConfig */ $fieldConfig = $currentList->fields[$fieldName]; $result = $fieldConfig->label; @@ -198,7 +198,7 @@ public static function getEntryLexeme($projectId, $entryId) $entry = new LexEntryModel($project, $entryId); $inputSystems = $project->config->entry->fields[LexConfig::LEXEME]->inputSystems; foreach ($inputSystems as $inputSystem) { - if (array_key_exists($inputSystem, $entry->lexeme) && !empty($entry->lexeme[$inputSystem])) { + if ($entry->lexeme->offsetExists($inputSystem) && !empty($entry->lexeme[$inputSystem])) { return $entry->lexeme[$inputSystem]->value; } } diff --git a/src/Api/Model/Languageforge/Lexicon/Command/LexEntryDecoder.php b/src/Api/Model/Languageforge/Lexicon/Command/LexEntryDecoder.php index 2006b19ef9..4e8a1662b3 100644 --- a/src/Api/Model/Languageforge/Lexicon/Command/LexEntryDecoder.php +++ b/src/Api/Model/Languageforge/Lexicon/Command/LexEntryDecoder.php @@ -13,13 +13,13 @@ class LexEntryDecoder extends JsonDecoder * @param array $values A mixed array of JSON (like) data. * @param string $id */ - public static function decode($model, $values, $id = "") + public static function decode($model, array $values, $id = "") { $decoder = new LexEntryDecoder(); $decoder->_decode($model, $values, $id); } - protected function _decode($model, $values, $id) + protected function _decode($model, array $values, string $id) { switch (get_class($model)) { case "Api\Model\Languageforge\Lexicon\LexMultiParagraph": diff --git a/src/Api/Model/Languageforge/Lexicon/Command/LexProjectCommands.php b/src/Api/Model/Languageforge/Lexicon/Command/LexProjectCommands.php index edb8f1d235..272837a00b 100644 --- a/src/Api/Model/Languageforge/Lexicon/Command/LexProjectCommands.php +++ b/src/Api/Model/Languageforge/Lexicon/Command/LexProjectCommands.php @@ -120,7 +120,7 @@ public static function updateCustomFieldViews($projectCode, $customFieldSpecs) public static function createNewCustomFieldViews($customFieldName, $customFieldType, &$config) { foreach ($config->roleViews as $role => $roleView) { - if (!array_key_exists($customFieldName, $roleView->fields)) { + if (!$roleView->fields->offsetExists($customFieldName)) { if ($customFieldType == "MultiUnicode" || $customFieldType == "MultiString") { $roleView->fields[$customFieldName] = new LexViewMultiTextFieldConfig(); } else { @@ -133,7 +133,7 @@ public static function createNewCustomFieldViews($customFieldName, $customFieldT } foreach ($config->userViews as $userId => $userView) { - if (!array_key_exists($customFieldName, $userView->fields)) { + if (!$userView->fields->offsetExists($customFieldName)) { if ($customFieldType == "MultiUnicode" || $customFieldType == "MultiString") { $userView->fields[$customFieldName] = new LexViewMultiTextFieldConfig(); } else { @@ -181,7 +181,7 @@ private static function removeDeletedCustomFieldView($customFieldSpecs, &$view) } foreach ($customFieldNamesToRemove as $customFieldName) { - if (array_key_exists($customFieldName, $view->fields)) { + if ($view->fields->offsetExists($customFieldName)) { unset($view->fields[$customFieldName]); } } diff --git a/src/Api/Model/Languageforge/Lexicon/Command/LexUploadCommands.php b/src/Api/Model/Languageforge/Lexicon/Command/LexUploadCommands.php index ecc1dc7026..a758e60900 100644 --- a/src/Api/Model/Languageforge/Lexicon/Command/LexUploadCommands.php +++ b/src/Api/Model/Languageforge/Lexicon/Command/LexUploadCommands.php @@ -101,8 +101,9 @@ public static function uploadAudioFile($projectId, $tmpFilePath) if ( strcmp($project->whenToConvertAudio, "always") == 0 || - (strcmp($project->whenToConvertAudio, "sr") == 0 && // restrictions on send/receive files 12-2022 - (filesize($filePath) > 1000000 || + (strcmp($project->whenToConvertAudio, "sr") == 0 && + // `MaximumFileSize` in https://github.com/sillsdev/chorus/blob/master/src/LibChorus/FileTypeHandlers/audio/AudioFileTypeHandler.cs + (filesize($filePath) > 10000000 || (strcmp(strtolower($fileExt), ".mp3") !== 0 && strcmp(strtolower($fileExt), ".wav") !== 0 && strcmp(strtolower($fileExt), ".webm") !== 0))) diff --git a/src/Api/Model/Languageforge/Lexicon/Config/LexConfig.php b/src/Api/Model/Languageforge/Lexicon/Config/LexConfig.php index 51f6e268be..875ec77543 100644 --- a/src/Api/Model/Languageforge/Lexicon/Config/LexConfig.php +++ b/src/Api/Model/Languageforge/Lexicon/Config/LexConfig.php @@ -93,7 +93,7 @@ public function __construct() * * @var array */ - private static $flexOptionlistCodes = [ + private static array $flexOptionlistCodes = [ self::POS => "grammatical-info", self::SEMDOM => "semantic-domain-ddp4", self::ENVIRONMENTS => self::ENVIRONMENTS, diff --git a/src/Api/Model/Languageforge/Lexicon/Config/LexRoleViewConfig.php b/src/Api/Model/Languageforge/Lexicon/Config/LexRoleViewConfig.php index 4c1a183a0c..6c40d7b1c2 100644 --- a/src/Api/Model/Languageforge/Lexicon/Config/LexRoleViewConfig.php +++ b/src/Api/Model/Languageforge/Lexicon/Config/LexRoleViewConfig.php @@ -10,7 +10,7 @@ class LexRoleViewConfig public function __construct() { $this->inputSystems = new ArrayOf(); - $this->fields = new MapOf(function ($data) { + $this->fields = new MapOf(function (array $data) { if (array_key_exists("overrideInputSystems", $data)) { return new LexViewMultiTextFieldConfig(); } else { diff --git a/src/Api/Model/Languageforge/Lexicon/Dto/LexDbeDto.php b/src/Api/Model/Languageforge/Lexicon/Dto/LexDbeDto.php index 6dc78c5808..e20bb969e2 100644 --- a/src/Api/Model/Languageforge/Lexicon/Dto/LexDbeDto.php +++ b/src/Api/Model/Languageforge/Lexicon/Dto/LexDbeDto.php @@ -72,9 +72,10 @@ public static function encode($projectId, $userId, $lastFetchTime = null, $offse }, $deletedCommentsModel->entries); } + // @type ArrayOf $lexemeInputSystems = $project->config->entry->fields[LexConfig::LEXEME]->inputSystems; - usort($entries, function ($a, $b) use ($lexemeInputSystems) { + usort($entries, function (array $a, array $b) use ($lexemeInputSystems) { $lexeme1Value = ""; if (array_key_exists(LexConfig::LEXEME, $a)) { $lexeme1 = $a[LexConfig::LEXEME]; diff --git a/src/Api/Model/Languageforge/Lexicon/Import/LiftDecoder.php b/src/Api/Model/Languageforge/Lexicon/Import/LiftDecoder.php index 77a3bb2381..5747c9ad1c 100644 --- a/src/Api/Model/Languageforge/Lexicon/Import/LiftDecoder.php +++ b/src/Api/Model/Languageforge/Lexicon/Import/LiftDecoder.php @@ -572,7 +572,7 @@ public function isSenseCustomField($nodeId) * @param string $nodeId * @return boolean */ - public function isExampleCustomField($nodeId) + public function isExampleCustomField(string $nodeId) { return $this->isCustomField($nodeId, "LexExampleSentence"); } @@ -584,7 +584,7 @@ public function isExampleCustomField($nodeId) * @param string $levelClass * @return boolean */ - private function isCustomField($nodeId, $levelClass) + private function isCustomField(string $nodeId, string $levelClass) { $fieldType = FileUtilities::replaceSpecialCharacters($nodeId); $customFieldSpecs = $this->getCustomFieldSpecs($fieldType); @@ -690,7 +690,7 @@ private function addCustomField($sxeNode, $nodeId, $customFieldNamePrefix, $leve $item->customFields[$customFieldName] = new LexValue(); $item->customFields[$customFieldName]->value((string) $sxeNode["value"]); } elseif ($customFieldSpecs["Type"] == "ReferenceCollection") { - if (!array_key_exists($customFieldName, $item->customFields)) { + if (!$item->customFields->offsetExists($customFieldName)) { $item->customFields[$customFieldName] = new LexMultiValue(); } $item->customFields[$customFieldName]->value((string) $sxeNode["value"]); @@ -717,7 +717,7 @@ private function createCustomField($fieldType, $customFieldNamePrefix, $customFi { $customFieldName = $customFieldNamePrefix . str_replace(" ", "_", $fieldType); $levelConfig->fieldOrder->ensureValueExists($customFieldName); - if (!array_key_exists($customFieldName, $levelConfig->fields)) { + if (!$levelConfig->fields->offsetExists($customFieldName)) { if ($customFieldSpecs["Type"] == "ReferenceAtom") { $levelConfig->fields[$customFieldName] = new LexConfigOptionList(); $levelConfig->fields[$customFieldName]->listCode = $customFieldSpecs["range"]; diff --git a/src/Api/Model/Languageforge/Lexicon/Import/LiftImport.php b/src/Api/Model/Languageforge/Lexicon/Import/LiftImport.php index d45b17fed0..c9ea369e6f 100644 --- a/src/Api/Model/Languageforge/Lexicon/Import/LiftImport.php +++ b/src/Api/Model/Languageforge/Lexicon/Import/LiftImport.php @@ -303,7 +303,7 @@ public static function rangeToOptionList( $optionList->items->exchangeArray([]); foreach ($liftRange->rangeElements as $id => $elem) { - if ($elem->label && array_key_exists($interfaceLang, $elem->label)) { + if ($elem->label && $elem->label->offsetExists($interfaceLang)) { $label = $elem->label[$interfaceLang]->value; if (isset($elem->abbrev) && isset($elem->abbrev[$interfaceLang])) { $abbrev = $elem->abbrev[$interfaceLang]->value; diff --git a/src/Api/Model/Languageforge/Lexicon/LexEntryModel.php b/src/Api/Model/Languageforge/Lexicon/LexEntryModel.php index 4c221b8a61..f4e8ccdba4 100644 --- a/src/Api/Model/Languageforge/Lexicon/LexEntryModel.php +++ b/src/Api/Model/Languageforge/Lexicon/LexEntryModel.php @@ -19,7 +19,7 @@ function generateSense() return new LexSense(); } -function generateCustomField($data) +function generateCustomField(array $data) { CodeGuard::checkTypeAndThrow($data, "array"); if (array_key_exists("type", $data)) { diff --git a/src/Api/Model/Languageforge/Lexicon/LexMultiText.php b/src/Api/Model/Languageforge/Lexicon/LexMultiText.php index 856a76d419..700fbd780c 100644 --- a/src/Api/Model/Languageforge/Lexicon/LexMultiText.php +++ b/src/Api/Model/Languageforge/Lexicon/LexMultiText.php @@ -18,7 +18,7 @@ public function __construct() public function form($inputSystem, $value) { - if (array_key_exists($inputSystem, $this)) { + if ($this->offsetExists($inputSystem)) { $this[$inputSystem]->value($value); } else { $this[$inputSystem] = new LexValue($value); @@ -27,7 +27,7 @@ public function form($inputSystem, $value) public function appendForm($inputSystem, $value, $separator = "; ") { - if (array_key_exists($inputSystem, $this)) { + if ($this->offsetExists($inputSystem)) { $oldValue = $this[$inputSystem]->value; $newValue = $oldValue . $separator . $value; $this[$inputSystem]->value($newValue); @@ -42,7 +42,7 @@ public function appendForm($inputSystem, $value, $separator = "; ") */ public function hasForm($inputSystem) { - return array_key_exists($inputSystem, $this); + return $this->offsetExists($inputSystem); } public function differences(LexMultiText $otherMultiText) diff --git a/src/Api/Model/Languageforge/Lexicon/LexProjectModel.php b/src/Api/Model/Languageforge/Lexicon/LexProjectModel.php index b09b03c015..76725c8158 100644 --- a/src/Api/Model/Languageforge/Lexicon/LexProjectModel.php +++ b/src/Api/Model/Languageforge/Lexicon/LexProjectModel.php @@ -71,7 +71,7 @@ public function __construct($id = "") public function addInputSystem($tag, $abbr = "", $name = "") { static $languages = null; - if (!array_key_exists($tag, $this->inputSystems)) { + if (!$this->inputSystems->offsetExists($tag)) { if (!$abbr) { $abbr = $tag; } @@ -81,7 +81,7 @@ public function addInputSystem($tag, $abbr = "", $name = "") $languages = new LanguageData(); } $languageCode = LanguageData::getCode($tag); - if (array_key_exists($languageCode, $languages)) { + if ($languages->offsetExists($languageCode)) { $name = $languages[$languageCode]->name; } } diff --git a/src/Api/Model/Languageforge/Lexicon/LexSense.php b/src/Api/Model/Languageforge/Lexicon/LexSense.php index e928cd0cf0..626dbd0343 100644 --- a/src/Api/Model/Languageforge/Lexicon/LexSense.php +++ b/src/Api/Model/Languageforge/Lexicon/LexSense.php @@ -230,7 +230,7 @@ public function searchExamplesMultiTextFor($propertyName, $tag, $text) foreach ($this->examples as $index => $example) { if ( isset($example->{$propertyName}) && - array_key_exists($tag, $example->{$propertyName}) && + $example->{$propertyName}->offsetExists($tag) && trim($example->{$propertyName}[$tag]) !== "" && $example->{$propertyName}[$tag] == $text ) { diff --git a/src/Api/Model/Shared/AccessTokenModel.php b/src/Api/Model/Shared/AccessTokenModel.php deleted file mode 100644 index a6137fd75f..0000000000 --- a/src/Api/Model/Shared/AccessTokenModel.php +++ /dev/null @@ -1,15 +0,0 @@ -users)) { + if ($project->users->offsetExists($userId)) { $sessionData["project"] = []; $sessionData["project"]["id"] = (string) $projectId; $sessionData["project"]["projectName"] = $project->projectName; diff --git a/src/Api/Model/Shared/Command/UserCommands.php b/src/Api/Model/Shared/Command/UserCommands.php index f14d13ca63..15c0d44fa4 100644 --- a/src/Api/Model/Shared/Command/UserCommands.php +++ b/src/Api/Model/Shared/Command/UserCommands.php @@ -24,6 +24,7 @@ use Palaso\Utilities\CodeGuard; use Symfony\Component\HttpFoundation\Session\Session; use Api\Library\Shared\UrlHelper; +use Api\Model\Shared\Command\ProjectCommands; class UserCommands { @@ -138,20 +139,70 @@ public static function updateUserProfile($params, $userId, DeliveryInterface $de * @param array $userIds * @return int Total number of users removed. */ - public static function deleteUsers($userIds) + public static function deleteAccounts($userIds, $currentId) { CodeGuard::checkTypeAndThrow($userIds, "array"); $count = 0; foreach ($userIds as $userId) { CodeGuard::checkTypeAndThrow($userId, "string"); - $userModel = new UserModel($userId); - $userModel->remove(); + self::deleteAccount($userId, $currentId); $count++; } return $count; } + /** + * @param $userId + * @return int 0 or 1 successful removal + * @throws \Exception + */ + public static function deleteAccount($userId, $currentUserId) + { + $user = new UserModelWithPassword($userId); + $currentUser = new UserModel($currentUserId); + + // Makes sure this user is not an owner on any projects + foreach ($user->projects->refs as $id) { + $project = new ProjectModel($id->asString()); + if ($project->ownerRef->asString() == $userId) { + throw new \Exception( + "The user '$user->username' owns one or more projects. Before account deletion, this user's projects must either be transfered to new owners or deleted." + ); + } + } + + // Makes sure the user doing the action has the right privileges + if ($currentUser->role != SystemRoles::SYSTEM_ADMIN && $userId != $currentUserId) { + throw new \Exception("The current user does not have sufficient privileges to delete the target account."); + } + + // Deactivates account and removes personal information from the user model. + // Will now use the user's id instead of name and username when displaying historical activity. + $user->active = false; + $user->isDeleted = true; + $user->password = null; + $user->username = "[Deleted User]"; + $user->name = "[Deleted User]"; + $user->languageDepotUsername = null; + $user->email = null; + $user->mobile_phone = null; + $user->age = null; + $user->gender = null; + $user->googleOAuthIds = null; + $user->facebookOAuthIds = null; + + $default_avatar = "anonymoose.png"; + $user->avatar_ref = $default_avatar; + $user->write(); + + // Removes the user from each project the user used to be in + foreach ($user->projects->refs as $projectIdObject) { + $projectId = $projectIdObject->asString(); + ProjectCommands::removeUsers($projectId, [$userId]); + } + } + /** * @return UserListModel */ diff --git a/src/Api/Model/Shared/Dto/ActivityListDto.php b/src/Api/Model/Shared/Dto/ActivityListDto.php index c6ff68e0f6..b722c0476f 100644 --- a/src/Api/Model/Shared/Dto/ActivityListDto.php +++ b/src/Api/Model/Shared/Dto/ActivityListDto.php @@ -367,7 +367,7 @@ private static function prepareActivityContentForEntryDifferences($item, $projec list($name, $position) = self::splitFieldIdPart($part); $position = $position + 1; // Mongo stores 0-based indices, but DTO wants 1-based // $guid not used in this DTO - if (array_key_exists($name, $currentConfig->fields)) { + if ($currentConfig->fields->offsetExists($name)) { $mostRecentName = $name; $mostRecentPosition = $position; // $fieldNameHierarchy[] = $name; diff --git a/src/Api/Model/Shared/Dto/RightsHelper.php b/src/Api/Model/Shared/Dto/RightsHelper.php index f699200cd5..1770636994 100644 --- a/src/Api/Model/Shared/Dto/RightsHelper.php +++ b/src/Api/Model/Shared/Dto/RightsHelper.php @@ -153,9 +153,6 @@ public function userCanAccessMethod($methodName) case "user_create": return $this->userHasSiteRight(Domain::USERS + Operation::EDIT); - case "user_delete": - return $this->userHasSiteRight(Domain::USERS + Operation::DELETE); - case "project_archive": return $this->userHasSiteRight(Domain::PROJECTS + Operation::ARCHIVE) || $this->userHasSiteRight(Domain::PROJECTS + Operation::CREATE); @@ -189,6 +186,7 @@ public function userCanAccessMethod($methodName) return $this->userHasSiteRight(Domain::USERS + Operation::VIEW_OWN); case "user_updateProfile": + case "user_delete": case "check_unique_identity": case "change_password": // change_password requires additional protection in the method itself return $this->userHasSiteRight(Domain::USERS + Operation::EDIT_OWN); diff --git a/src/Api/Model/Shared/Mapper/JsonDecoder.php b/src/Api/Model/Shared/Mapper/JsonDecoder.php index 8f8758e4e9..554c75c143 100644 --- a/src/Api/Model/Shared/Mapper/JsonDecoder.php +++ b/src/Api/Model/Shared/Mapper/JsonDecoder.php @@ -23,7 +23,7 @@ public static function is_assoc($array) * @param string $id * @throws \Exception */ - public static function decode($model, $values, $id = "") + public static function decode($model, array $values, $id = "") { $decoder = new JsonDecoder(); $decoder->_decode($model, $values, $id); @@ -36,7 +36,7 @@ public static function decode($model, $values, $id = "") * @param string $id * @throws \Exception */ - protected function _decode($model, $values, $id) + protected function _decode($model, array $values, string $id) { CodeGuard::checkTypeAndThrow($values, "array"); $propertiesToIgnore = $this->getPrivateAndReadOnlyProperties($model); diff --git a/src/Api/Model/Shared/Mapper/JsonEncoder.php b/src/Api/Model/Shared/Mapper/JsonEncoder.php index 8b4f412f9e..1fbc93f158 100644 --- a/src/Api/Model/Shared/Mapper/JsonEncoder.php +++ b/src/Api/Model/Shared/Mapper/JsonEncoder.php @@ -28,47 +28,54 @@ public static function encode($model) protected function _encode($model) { $data = []; - $properties = get_object_vars($model); - $privateProperties = []; - if (method_exists($model, "getPrivateProperties")) { - $privateProperties = (array) $model->getPrivateProperties(); - } - foreach ($properties as $key => $value) { - if (in_array($key, $privateProperties)) { - continue; + if (is_a($model, "Api\Model\Shared\Mapper\ArrayOf")) { + $data = $this->encodeArrayOf("", $model); + } elseif (is_a($model, "Api\Model\Shared\Mapper\MapOf")) { + $data = $this->encodeMapOf("", $model); + } else { + $properties = get_object_vars($model); + $privateProperties = []; + if (method_exists($model, "getPrivateProperties")) { + $privateProperties = (array) $model->getPrivateProperties(); } - if (is_a($value, "Api\Model\Shared\Mapper\IdReference")) { - $data[$key] = $this->encodeIdReference($key, $model->{$key}); - } elseif (is_a($value, "Api\Model\Shared\Mapper\Id")) { - $data[$key] = $this->encodeId($key, $model->{$key}); - } elseif (is_a($value, "Api\Model\Shared\Mapper\ArrayOf")) { - $data[$key] = $this->encodeArrayOf($key, $model->{$key}); - } elseif (is_a($value, "Api\Model\Shared\Mapper\MapOf")) { - $data[$key] = $this->encodeMapOf($key, $model->{$key}); - } elseif (is_a($value, "DateTime")) { - $data[$key] = $this->encodeDateTime($model->{$key}); - } elseif (is_a($value, "Litipk\Jiffy\UniversalTimestamp")) { - $data[$key] = $this->encodeUniversalTimestamp($model->{$key}); - } elseif (is_a($value, "Api\Model\Shared\Mapper\ReferenceList")) { - $data[$key] = $this->encodeReferenceList($key, $model->{$key}); - } else { - // Data type protection - if (is_array($value)) { - throw new \Exception("Must not encode array in '" . get_class($model) . "->" . $key . "'"); - } - // Special hack to help debugging our app - if ($key == "projects" || $key == "users") { - throw new \Exception("Possible bad write of '$key'\n" . var_export($model, true)); + + foreach ($properties as $key => $value) { + if (in_array($key, $privateProperties)) { + continue; } - if (is_object($value)) { - $data[$key] = $this->_encode($value); + if (is_a($value, "Api\Model\Shared\Mapper\IdReference")) { + $data[$key] = $this->encodeIdReference($key, $model->{$key}); + } elseif (is_a($value, "Api\Model\Shared\Mapper\Id")) { + $data[$key] = $this->encodeId($key, $model->{$key}); + } elseif (is_a($value, "Api\Model\Shared\Mapper\ArrayOf")) { + $data[$key] = $this->encodeArrayOf($key, $model->{$key}); + } elseif (is_a($value, "Api\Model\Shared\Mapper\MapOf")) { + $data[$key] = $this->encodeMapOf($key, $model->{$key}); + } elseif (is_a($value, "DateTime")) { + $data[$key] = $this->encodeDateTime($model->{$key}); + } elseif (is_a($value, "Litipk\Jiffy\UniversalTimestamp")) { + $data[$key] = $this->encodeUniversalTimestamp($model->{$key}); + } elseif (is_a($value, "Api\Model\Shared\Mapper\ReferenceList")) { + $data[$key] = $this->encodeReferenceList($key, $model->{$key}); } else { - // Default encode - if (is_null($value)) { - $value = ""; + // Data type protection + if (is_array($value)) { + throw new \Exception("Must not encode array in '" . get_class($model) . "->" . $key . "'"); + } + // Special hack to help debugging our app + if ($key == "projects" || $key == "users") { + throw new \Exception("Possible bad write of '$key'\n" . var_export($model, true)); + } + if (is_object($value)) { + $data[$key] = $this->_encode($value); + } else { + // Default encode + if (is_null($value)) { + $value = ""; + } + $data[$key] = $value; } - $data[$key] = $value; } } } diff --git a/src/Api/Model/Shared/Mapper/MapOf.php b/src/Api/Model/Shared/Mapper/MapOf.php index a89b103786..a756ff0799 100644 --- a/src/Api/Model/Shared/Mapper/MapOf.php +++ b/src/Api/Model/Shared/Mapper/MapOf.php @@ -35,7 +35,6 @@ public function hasGenerator() public function offsetGet($index) { CodeGuard::checkTypeAndThrow($index, "string"); - return parent::offsetGet($index); } @@ -44,4 +43,10 @@ public function offsetSet($index, $newval) CodeGuard::checkTypeAndThrow($index, "string"); parent::offsetSet($index, $newval); } + + public function offsetExists($index) + { + CodeGuard::checkTypeAndThrow($index, "string"); + return parent::offsetExists($index); + } } diff --git a/src/Api/Model/Shared/Mapper/MongoEncoder.php b/src/Api/Model/Shared/Mapper/MongoEncoder.php index 0b58b5eae3..f9979df959 100644 --- a/src/Api/Model/Shared/Mapper/MongoEncoder.php +++ b/src/Api/Model/Shared/Mapper/MongoEncoder.php @@ -26,37 +26,43 @@ public static function encode($model) * @return array * @throws \Exception */ - protected function _encode($model, $encodeId = false) + protected function _encode(object $model, $encodeId = false) { $data = []; - $properties = get_object_vars($model); - foreach ($properties as $key => $value) { - if (is_a($value, "Api\Model\Shared\Mapper\ArrayOf")) { - $data[$key] = $this->encodeArrayOf($model->{$key}); - } elseif (is_a($value, "Api\Model\Shared\Mapper\MapOf")) { - $data[$key] = $this->encodeMapOf($model->{$key}); - } elseif (is_a($value, "Api\Model\Shared\Mapper\IdReference")) { - $data[$key] = $this->encodeIdReference($model->{$key}); - } elseif (is_a($value, "Api\Model\Shared\Mapper\Id")) { - if ($encodeId) { - $data[$key] = $this->encodeId($model->{$key}); - } - } elseif (is_a($value, "DateTime")) { - $data[$key] = $this->encodeDateTime($model->{$key}); - } elseif (is_a($value, "Litipk\Jiffy\UniversalTimestamp")) { - $data[$key] = $this->encodeUniversalTimestamp($model->{$key}); - } elseif (is_a($value, "Api\Model\Shared\Mapper\ReferenceList")) { - $data[$key] = $this->encodeReferenceList($model->{$key}); - } else { - // Data type protection - if (is_array($value)) { - throw new \Exception("Must not encode array in '" . get_class($model) . "->" . $key . "'"); - } - if (is_object($value)) { - $data[$key] = $this->_encode($value, true); + if (is_a($model, "Api\Model\Shared\Mapper\ArrayOf")) { + $data = $this->encodeArrayOf($model); + } elseif (is_a($model, "Api\Model\Shared\Mapper\MapOf")) { + $data = $this->encodeMapOf($model); + } else { + $properties = get_object_vars($model); + foreach ($properties as $key => $value) { + if (is_a($value, "Api\Model\Shared\Mapper\ArrayOf")) { + $data[$key] = $this->encodeArrayOf($model->{$key}); + } elseif (is_a($value, "Api\Model\Shared\Mapper\MapOf")) { + $data[$key] = $this->encodeMapOf($model->{$key}); + } elseif (is_a($value, "Api\Model\Shared\Mapper\IdReference")) { + $data[$key] = $this->encodeIdReference($model->{$key}); + } elseif (is_a($value, "Api\Model\Shared\Mapper\Id")) { + if ($encodeId) { + $data[$key] = $this->encodeId($model->{$key}); + } + } elseif (is_a($value, "DateTime")) { + $data[$key] = $this->encodeDateTime($model->{$key}); + } elseif (is_a($value, "Litipk\Jiffy\UniversalTimestamp")) { + $data[$key] = $this->encodeUniversalTimestamp($model->{$key}); + } elseif (is_a($value, "Api\Model\Shared\Mapper\ReferenceList")) { + $data[$key] = $this->encodeReferenceList($model->{$key}); } else { - // Default encode - $data[$key] = $value; + // Data type protection + if (is_array($value)) { + throw new \Exception("Must not encode array in '" . get_class($model) . "->" . $key . "'"); + } + if (is_object($value)) { + $data[$key] = $this->_encode($value, true); + } else { + // Default encode + $data[$key] = $value; + } } } } diff --git a/src/Api/Model/Shared/ProjectModel.php b/src/Api/Model/Shared/ProjectModel.php index 60d341c4f7..66271bdae9 100644 --- a/src/Api/Model/Shared/ProjectModel.php +++ b/src/Api/Model/Shared/ProjectModel.php @@ -178,7 +178,7 @@ public function addUserByInviteToken($userId) */ public function removeUser($userId) { - if (array_key_exists($userId, $this->users)) { + if ($this->users->offsetExists($userId)) { unset($this->users[$userId]); } } @@ -198,7 +198,7 @@ public function listUsers() $userList->read(); for ($i = 0, $l = count($userList->entries); $i < $l; $i++) { $userId = $userList->entries[$i]["id"]; - if (!array_key_exists($userId, $this->users)) { + if (!$this->users->offsetExists($userId)) { continue; } $userList->entries[$i]["role"] = $this->users[$userId]->role; @@ -211,7 +211,7 @@ public function listInvitees() $invitees = new InviteeListProjectModel($this->id->asString()); $invitees->read(); foreach ($invitees->entries as $i => $invitee) { - if (array_key_exists($invitee["id"], $this->users)) { + if ($this->users->offsetExists($invitee["id"])) { $invitees->entries[$i]["role"] = $this->users[$invitee["id"]]->role; } } diff --git a/src/Api/Model/Shared/UserListModel.php b/src/Api/Model/Shared/UserListModel.php index d727a5e0eb..e4a4e945f8 100644 --- a/src/Api/Model/Shared/UserListModel.php +++ b/src/Api/Model/Shared/UserListModel.php @@ -10,7 +10,7 @@ public function __construct() { parent::__construct( UserModelMongoMapper::instance(), - ["username" => ['$regex' => ""]], + ["username" => ['$regex' => ""], "isDeleted" => false], ["username", "email", "name", "avatar_ref", "role", "projects", "active"] ); } diff --git a/src/Api/Model/Shared/UserModel.php b/src/Api/Model/Shared/UserModel.php index 72aa6c1c88..eb0749208d 100644 --- a/src/Api/Model/Shared/UserModel.php +++ b/src/Api/Model/Shared/UserModel.php @@ -57,13 +57,12 @@ public function __construct($id = "") $this->siteRole = new MapOf(); $this->googleOAuthIds = new ArrayOf(); $this->facebookOAuthIds = new ArrayOf(); - $this->paratextOAuthIds = new ArrayOf(); - $this->paratextAccessToken = new AccessTokenModel(); $this->validationExpirationDate = new \DateTime(); $this->resetPasswordExpirationDate = new \DateTime(); $this->projectsProperties = new MapOf(function () { return new ProjectProperties(); }); + $this->isDeleted = false; /* * We don't need to set 'role' to ReadOnly because we control where it's modified @@ -115,9 +114,6 @@ public function __construct($id = "") /** @var ArrayOf */ public $googleOAuthIds; - /** @var ArrayOf */ - public $paratextOAuthIds; - /** @var ArrayOf */ public $facebookOAuthIds; @@ -130,14 +126,9 @@ public function __construct($id = "") /** @var boolean */ public $isInvited; - //public $groups; - /** @var ReferenceList */ public $projects; - /** @var AccessTokenModel */ - public $paratextAccessToken; - /* * User Profile accessible */ @@ -162,6 +153,9 @@ public function __construct($id = "") /** @var string */ public $gender; + /** @var boolean */ + public $isDeleted; + /** * @var int timestamp, see time() */ @@ -200,21 +194,6 @@ public function setProperties($properties, $params) } } - /** - * Removes a user from the collection - * Project references to this user are also removed - */ - public function remove() - { - foreach ($this->projects->refs as $id) { - /* @var Id $id */ - $project = new ProjectModel($id->asString()); - $project->removeUser($this->id->asString()); - $project->write(); - } - UserModelMongoMapper::instance()->remove($this->id->asString()); - } - /** * @param string $projectId * @return bool diff --git a/src/Api/Service/Sf.php b/src/Api/Service/Sf.php index 3e04628180..d3ae5bc22c 100644 --- a/src/Api/Service/Sf.php +++ b/src/Api/Service/Sf.php @@ -18,7 +18,6 @@ use Api\Model\Shared\Command\SessionCommands; use Api\Model\Shared\Command\UserCommands; use Api\Model\Shared\Command\LdapiCommands; -use Api\Model\Shared\Communicate\EmailSettings; use Api\Model\Shared\Dto\ActivityListDto; use Api\Model\Shared\Dto\ProjectInsightsDto; use Api\Model\Shared\Dto\ProjectListDto; @@ -144,7 +143,11 @@ public function user_updateProfile($params) */ public function user_delete($userIds) { - return UserCommands::deleteUsers($userIds); + $result = UserCommands::deleteAccounts($userIds, $this->userId); + if (in_array($this->userId, $userIds)) { + $this->app["session"]->getFlashBag()->add("infoMessage", "Your account was permanently deleted"); + } + return $result; } /** diff --git a/src/Site/OAuth/SelectAccountAuthorizationParametersTrait.php b/src/Site/OAuth/SelectAccountAuthorizationParametersTrait.php index 38b5490e23..f55abd3606 100644 --- a/src/Site/OAuth/SelectAccountAuthorizationParametersTrait.php +++ b/src/Site/OAuth/SelectAccountAuthorizationParametersTrait.php @@ -7,7 +7,7 @@ */ trait SelectAccountAuthorizationParametersTrait { - protected function getAuthorizationParameters(array $options) + protected function getAuthorizationParameters(array $options): array { // Default provider adds "approval_prompt=auto", but using both "prompt" and "approval_prompt" together is not allowed $params = parent::getAuthorizationParameters($options); diff --git a/src/Site/views/languageforge/container/languageforge.html.twig b/src/Site/views/languageforge/container/languageforge.html.twig index 527e2116c6..422e5915ac 100644 --- a/src/Site/views/languageforge/container/languageforge.html.twig +++ b/src/Site/views/languageforge/container/languageforge.html.twig @@ -73,7 +73,7 @@ {% verbatim %}
- {{ setting.label }} + {{ setting.label }} {{ setting.label }}
{% endverbatim %} @@ -81,7 +81,7 @@ diff --git a/src/angular-app/bellows/apps/siteadmin/site-admin-users.component.ts b/src/angular-app/bellows/apps/siteadmin/site-admin-users.component.ts index 2737f8654c..9bd10ef0dd 100644 --- a/src/angular-app/bellows/apps/siteadmin/site-admin-users.component.ts +++ b/src/angular-app/bellows/apps/siteadmin/site-admin-users.component.ts @@ -207,7 +207,7 @@ export class SiteAdminUsersController implements angular.IController { return; } - this.userService.remove(userIds, result => { + this.userService.deleteAccounts(userIds, result => { if (result.ok) { if (result.data === 1) { this.notice.push(this.notice.SUCCESS, '1 user was deleted'); diff --git a/src/angular-app/bellows/apps/userprofile/user-profile-app.component.html b/src/angular-app/bellows/apps/userprofile/user-profile-app.component.html index cc91b7cf6c..e9d446803d 100644 --- a/src/angular-app/bellows/apps/userprofile/user-profile-app.component.html +++ b/src/angular-app/bellows/apps/userprofile/user-profile-app.component.html @@ -2,7 +2,7 @@
-
+
@@ -69,6 +69,11 @@
+
+
+ +
+
@@ -105,6 +110,62 @@
Tell us about yourself...
+
+
+ +
+
+ + +
+
+
+ Investigating the feasibility of account deletion... +
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+
Deleting your account is a permanent action and cannot be undone. Your account will be deleted, and you will be removed from all projects you are a member of. You will no longer be able to log in.
+
+
+
You are a project owner.
+
+ First, transfer your projects to a new owner or delete them: +
+ + + +
+ + +
+
+
@@ -143,13 +204,13 @@
Tell us about yourself...
+
+
+ +
+
-
-
- -
-
diff --git a/src/angular-app/bellows/apps/userprofile/user-profile-app.component.ts b/src/angular-app/bellows/apps/userprofile/user-profile-app.component.ts index c451eda3ee..3b7bc796e6 100644 --- a/src/angular-app/bellows/apps/userprofile/user-profile-app.component.ts +++ b/src/angular-app/bellows/apps/userprofile/user-profile-app.component.ts @@ -1,6 +1,7 @@ import * as angular from 'angular'; import {UserService} from '../../core/api/user.service'; +import { ProjectService } from '../../core/api/project.service'; import {ApplicationHeaderService} from '../../core/application-header.service'; import {BreadcrumbService} from '../../core/breadcrumbs/breadcrumb.service'; import {SiteWideNoticeService} from '../../core/site-wide-notice-service'; @@ -8,6 +9,7 @@ import {ModalService} from '../../core/modal/modal.service'; import {NoticeService} from '../../core/notice/notice.service'; import {UtilityService} from '../../core/utility.service'; import {UserProfile} from '../../shared/model/user-profile.model'; +import { Project } from '../../shared/model/project.model'; interface UserProfileAppControllerScope extends angular.IScope { userprofileForm: angular.IFormController; @@ -29,21 +31,28 @@ export class UserProfileAppController implements angular.IController { emailExists: boolean; usernameExists: boolean; + ownsAProject: boolean = false; + + projects: Project[] = []; + finishedLoadingOwnedProjects: boolean = false; + ownedProjects: Project[] = []; + private initColor = ''; private initShape = ''; static $inject = ['$scope', '$window', - 'userService', 'modalService', 'silNoticeService', + 'userService', 'projectService', 'modalService', 'silNoticeService', 'breadcrumbService', 'siteWideNoticeService', 'applicationHeaderService']; constructor(private $scope: UserProfileAppControllerScope, private $window: angular.IWindowService, - private userService: UserService, private modalService: ModalService, private notice: NoticeService, + private userService: UserService, private projectService: ProjectService, private modalService: ModalService, private notice: NoticeService, private breadcrumbService: BreadcrumbService, private siteWideNoticeService: SiteWideNoticeService, private applicationHeaderService: ApplicationHeaderService) {} $onInit(): void { + this.finishedLoadingOwnedProjects = false; this.user.avatar_ref = UserProfileAppController.getAvatarRef('', ''); this.$scope.$watch(() => this.user.avatar_color, () => { @@ -62,7 +71,8 @@ export class UserProfileAppController implements angular.IController { this.siteWideNoticeService.displayNotices(); - this.loadUser(); // load the user data right away + this.loadUser() // load the user data right away + .then(() => this.queryProjectsForUser()); this.dropdown.avatarColors = [ { value: 'purple4', label: 'Purple' }, @@ -101,6 +111,7 @@ export class UserProfileAppController implements angular.IController { { value: 'sheep', label: 'Sheep' }, { value: 'tortoise', label: 'Tortoise' } ]; + } validateForm(): void { @@ -160,13 +171,13 @@ export class UserProfileAppController implements angular.IController { // catch is necessary to properly implement promise API, which angular 1.6 complains if we // don't have a catch }).catch(() => {}); - } else { + } else if (this.user.active) { this.updateUser(); } } - private loadUser(): void { - this.userService.readProfile(result => { + private loadUser(): angular.IPromise { + return this.userService.readProfile(result => { if (result.ok) { this.user = result.data.userProfile; this.originalUsername = this.user.username; @@ -203,6 +214,36 @@ export class UserProfileAppController implements angular.IController { }); } + queryProjectsForUser() { + this.finishedLoadingOwnedProjects = false; + this.projects = []; + this.ownedProjects = []; + this.projectService.list().then((projects: Project[]) => { + this.projects = projects || []; + this.ownedProjects = projects.filter(proj => proj.ownerId === this.user.id); + this.ownsAProject = this.ownedProjects.length > 0; + this.finishedLoadingOwnedProjects = true; + }).catch(console.error); + } + + deleteOwnAccount() { + const modalOptions = { + closeButtonText: 'Cancel', + actionButtonText: 'Delete', + headerText: 'Permanently delete your account?', + bodyText: 'Are you sure you want to delete your account?\n' + + 'This is a permanent action and cannot be restored.' + }; + this.modalService.showModal({}, modalOptions).then(() => { + this.userService.deleteAccounts([this.user.id]).then(() => { + this.notice.push(this.notice.SUCCESS, 'Your account was permanently deleted'); + this.$window.location.href = '/auth/logout'; // goes to the log in screen + }); + }, () => {}); + + } + + static getAvatarUrl(avatarRef: string): string { if (avatarRef) { return (avatarRef.startsWith('http')) ? avatarRef : '/Site/views/shared/image/avatar/' + avatarRef; diff --git a/src/angular-app/bellows/core/api/json-rpc.service.ts b/src/angular-app/bellows/core/api/json-rpc.service.ts index 717a62b713..c762c08ae0 100644 --- a/src/angular-app/bellows/core/api/json-rpc.service.ts +++ b/src/angular-app/bellows/core/api/json-rpc.service.ts @@ -100,11 +100,7 @@ export class JsonRpcService { type = 'You don\'t have sufficient privileges.'; break; default: - // silently swallow unknown exceptions and don't bug (heh) the user about things they can't fix - // this.error.notify('Exception', - // 'An exception occurred in the application, but\nthe developers have already been notified.', - // response.data.error.message); - return; + type = 'Exception'; } this.error.error(type, response.data.error.message); diff --git a/src/angular-app/bellows/core/api/user.service.ts b/src/angular-app/bellows/core/api/user.service.ts index 9e04d95e53..f67dfe1b95 100644 --- a/src/angular-app/bellows/core/api/user.service.ts +++ b/src/angular-app/bellows/core/api/user.service.ts @@ -25,7 +25,7 @@ export class UserService { return this.api.call('user_updateProfile', [params], callback); } - remove(userIds: string[], callback?: JsonRpcCallback) { + deleteAccounts(userIds: string[], callback?: JsonRpcCallback) { return this.api.call('user_delete', [userIds], callback); } diff --git a/src/angular-app/bellows/core/navbar.controller.ts b/src/angular-app/bellows/core/navbar.controller.ts index 71c365f4a4..4ef0f61760 100644 --- a/src/angular-app/bellows/core/navbar.controller.ts +++ b/src/angular-app/bellows/core/navbar.controller.ts @@ -90,13 +90,14 @@ export class NavbarController implements angular.IController { } } - openShareWithOthersModal(): void { + openShareWithOthersModal(event: UIEvent): void { const modalInstance = this.$modal.open({ component: 'shareWithOthersModal' }); modalInstance.result.then(data => { // TODO: save the data if not already }, () => {}); + event.preventDefault(); } private isNotInProject(): boolean { diff --git a/src/angular-app/languageforge/lexicon/core/lexicon-project.service.ts b/src/angular-app/languageforge/lexicon/core/lexicon-project.service.ts index 216bb06c36..42245b11ae 100644 --- a/src/angular-app/languageforge/lexicon/core/lexicon-project.service.ts +++ b/src/angular-app/languageforge/lexicon/core/lexicon-project.service.ts @@ -54,7 +54,7 @@ export class LexiconProjectService { settings.push(new HeaderSetting( 'userManagementLink', 'Share With Others', - '', + '#', false )); settings.push(new HeaderSetting( diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts b/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts index 1d8228d772..2990f2f1d0 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts @@ -115,8 +115,9 @@ export class FieldAudioController implements angular.IController { this.dcFilename = response.data.data.fileName; this.showAudioUpload = false; this.notice.push(this.notice.SUCCESS, 'File uploaded successfully.'); - if(response.data.data.fileSize > 1000000){ //1 MB file size limit 2022-10 - this.notice.push(this.notice.WARN, 'WARNING: Because the audio file - ' + response.data.data.fileName + ' - is larger than 1 MB, it will not be synced with FLEx.'); + if (response.data.data.fileSize > 10000000) { + // `MaximumFileSize` in https://github.com/sillsdev/chorus/blob/master/src/LibChorus/FileTypeHandlers/audio/AudioFileTypeHandler.cs + this.notice.push(this.notice.WARN, 'WARNING: Because the audio file - ' + response.data.data.fileName + ' - is larger than 10 MB, it will not be synced with FLEx.'); } } else { this.notice.push(this.notice.ERROR, response.data.data.errorMessage); diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-picture.component.ts b/src/angular-app/languageforge/lexicon/editor/field/dc-picture.component.ts index 6a61936c75..df9bf2b7d1 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-picture.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-picture.component.ts @@ -113,8 +113,9 @@ export class FieldPictureController implements angular.IController { if (isUploadSuccess) { this.upload.progress = 100.0; this.addPicture(response.data.data.fileName); - if(response.data.data.fileSize > 1000000){ //1 MB file size limit 2022-10 - this.notice.push(this.notice.WARN, 'WARNING: Because the image file - ' + response.data.data.fileName + ' - is larger than 1 MB, it will not be synced with FLEx.'); + if (response.data.data.fileSize > 10000000) { + // `MaximumFileSize` in https://github.com/sillsdev/chorus/blob/master/src/LibChorus/FileTypeHandlers/image/ImageFileTypeHandler.cs + this.notice.push(this.notice.WARN, 'WARNING: Because the image file - ' + response.data.data.fileName + ' - is larger than 10 MB, it will not be synced with FLEx.'); } this.upload.showAddPicture = false; } else { diff --git a/src/composer.json b/src/composer.json index 572985a70c..709c3a98f5 100644 --- a/src/composer.json +++ b/src/composer.json @@ -13,14 +13,14 @@ "guzzlehttp/guzzle": "^7", "litipk/php-jiffy": "^1", "league/oauth2-client": "^2", - "league/oauth2-google": "^2", + "league/oauth2-google": "^4", "league/oauth2-facebook": "^2", "mongodb/mongodb": "^1", "ocramius/lazy-property": "dev-feature/factory", "ramsey/uuid": "^4.2", "silex/silex": "~1.3.2", "sillsdev/web-php-utilities": "dev-master", - "swiftmailer/swiftmailer": "^5", + "swiftmailer/swiftmailer": "^6", "symfony/security": "^2.7", "symfony/twig-bridge": "^2.7", "twig/twig": "^2", diff --git a/src/composer.lock b/src/composer.lock index 5f91ee96d1..2007e26993 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d2449b529538a4390358d652937682cb", + "content-hash": "dd7f053295cdfdcc5b121a0078cb0f0f", "packages": [ { "name": "brick/math", @@ -64,6 +64,176 @@ ], "time": "2021-08-15T20:50:18+00:00" }, + { + "name": "doctrine/deprecations", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5|^8.5|^9.5", + "psr/log": "^1|^2|^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" + }, + "time": "2022-05-02T15:47:09+00:00" + }, + { + "name": "doctrine/lexer", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^10", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^4.11 || ^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": ["annotations", "docblock", "lexer", "parser", "php"], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-12-14T08:49:07+00:00" + }, + { + "name": "egulias/email-validator", + "version": "3.2.5", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "b531a2311709443320c786feb4519cfaf94af796" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b531a2311709443320c786feb4519cfaf94af796", + "reference": "b531a2311709443320c786feb4519cfaf94af796", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.2|^2", + "php": ">=7.2", + "symfony/polyfill-intl-idn": "^1.15" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.8|^9.3.3", + "vimeo/psalm": "^4" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": ["email", "emailvalidation", "emailvalidator", "validation", "validator"], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/3.2.5" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2023-01-02T17:26:14+00:00" + }, { "name": "firebase/php-jwt", "version": "v6.3.2", @@ -581,26 +751,26 @@ }, { "name": "league/oauth2-google", - "version": "2.2.0", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-google.git", - "reference": "c0faed29ec6d665ce3234e01f62029516cee4c02" + "reference": "db6d8ad67cdd7d014a1e5dd5c204a319a966de86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-google/zipball/c0faed29ec6d665ce3234e01f62029516cee4c02", - "reference": "c0faed29ec6d665ce3234e01f62029516cee4c02", + "url": "https://api.github.com/repos/thephpleague/oauth2-google/zipball/db6d8ad67cdd7d014a1e5dd5c204a319a966de86", + "reference": "db6d8ad67cdd7d014a1e5dd5c204a319a966de86", "shasum": "" }, "require": { - "league/oauth2-client": "^2.0" + "league/oauth2-client": "^2.0", + "php": ">=7.3" }, "require-dev": { - "eloquent/phony": "^0.14.6", - "phpunit/phpunit": "^5.7", - "satooshi/php-coveralls": "^2.0", - "squizlabs/php_codesniffer": "^2.0" + "eloquent/phony-phpunit": "^6.0 || ^7.1", + "phpunit/phpunit": "^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.0" }, "type": "library", "autoload": { @@ -621,9 +791,9 @@ "keywords": ["Authentication", "authorization", "client", "google", "oauth", "oauth2"], "support": { "issues": "https://github.com/thephpleague/oauth2-google/issues", - "source": "https://github.com/thephpleague/oauth2-google/tree/master" + "source": "https://github.com/thephpleague/oauth2-google/tree/4.0.0" }, - "time": "2018-03-19T17:28:55+00:00" + "time": "2021-03-04T21:12:06+00:00" }, { "name": "litipk/php-jiffy", @@ -1404,29 +1574,36 @@ }, { "name": "swiftmailer/swiftmailer", - "version": "v5.4.12", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "181b89f18a90f8925ef805f950d47a7190e9b950" + "reference": "8a5d5072dca8f48460fce2f4131fcc495eec654c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/181b89f18a90f8925ef805f950d47a7190e9b950", - "reference": "181b89f18a90f8925ef805f950d47a7190e9b950", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/8a5d5072dca8f48460fce2f4131fcc495eec654c", + "reference": "8a5d5072dca8f48460fce2f4131fcc495eec654c", "shasum": "" }, "require": { - "php": ">=5.3.3" + "egulias/email-validator": "^2.0|^3.1", + "php": ">=7.0.0", + "symfony/polyfill-iconv": "^1.0", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" }, "require-dev": { - "mockery/mockery": "~0.9.1", - "symfony/phpunit-bridge": "~3.2" + "mockery/mockery": "^1.0", + "symfony/phpunit-bridge": "^4.4|^5.4" + }, + "suggest": { + "ext-intl": "Needed to support internationalized email addresses" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.4-dev" + "dev-master": "6.2-dev" } }, "autoload": { @@ -1448,10 +1625,20 @@ "keywords": ["email", "mail", "mailer"], "support": { "issues": "https://github.com/swiftmailer/swiftmailer/issues", - "source": "https://github.com/swiftmailer/swiftmailer/tree/v5.4.12" + "source": "https://github.com/swiftmailer/swiftmailer/tree/v6.3.0" }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/swiftmailer/swiftmailer", + "type": "tidelift" + } + ], "abandoned": "symfony/mailer", - "time": "2018-07-31T09:26:32+00:00" + "time": "2021-10-18T15:26:12+00:00" }, { "name": "symfony/debug", @@ -1848,6 +2035,226 @@ ], "time": "2022-11-03T14:55:06+00:00" }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "927013f3aac555983a5059aada98e1907d842695" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/927013f3aac555983a5059aada98e1907d842695", + "reference": "927013f3aac555983a5059aada98e1907d842695", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-iconv": "*" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": ["bootstrap.php"], + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": ["compatibility", "iconv", "polyfill", "portable", "shim"], + "support": { + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "639084e360537a19f9ee352433b84ce831f3d2da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", + "reference": "639084e360537a19f9ee352433b84ce831f3d2da", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": ["bootstrap.php"], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": ["compatibility", "idn", "intl", "polyfill", "portable", "shim"], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": ["bootstrap.php"], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": ["Resources/stubs"] + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": ["compatibility", "intl", "normalizer", "polyfill", "portable", "shim"], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.27.0", @@ -2988,16 +3395,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.2", + "version": "v4.15.3", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", "shasum": "" }, "require": { @@ -3031,9 +3438,9 @@ "keywords": ["parser", "php"], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" }, - "time": "2022-11-12T15:38:23+00:00" + "time": "2023-01-16T22:05:37+00:00" }, { "name": "phar-io/manifest", @@ -3425,20 +3832,20 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.27", + "version": "9.5.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38" + "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a2bc7ffdca99f92d959b3f2270529334030bba38", - "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/954ca3113a03bf780d22f07bf055d883ee04b65e", + "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -3495,7 +3902,7 @@ "keywords": ["phpunit", "testing", "xunit"], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.27" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.28" }, "funding": [ { @@ -3511,7 +3918,7 @@ "type": "tidelift" } ], - "time": "2022-12-09T07:31:23+00:00" + "time": "2023-01-14T12:32:24+00:00" }, { "name": "sebastian/cli-parser", diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 1551a7001c..92cfd7497a 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -125,3 +125,7 @@ export const files = [ ] as const; export const appUrl = 'http://localhost:3238/'; + +export const SEC = 1000; +export const MIN = SEC * 60; +export const YEAR = 365 * 24 * 60 * MIN; diff --git a/test/e2e/global-setup.ts b/test/e2e/global-setup.ts index 7b985ab0b9..0729cbb793 100644 --- a/test/e2e/global-setup.ts +++ b/test/e2e/global-setup.ts @@ -1,16 +1,14 @@ import { BrowserType, chromium, firefox, FullConfig, Page, webkit } from '@playwright/test'; import * as fs from 'fs'; -import { appUrl, users } from './constants'; +import { appUrl, users, YEAR } from './constants'; import { getStorageStatePath, login, UserDetails, UserTestService } from './utils'; -const SESSION_LIFETIME = 365 * 24 * 60 * 60 * 1000; // 1 year, in milliseconds - async function initE2EUser(page: Page, user: UserDetails) { const context = page.context(); await new UserTestService(context.request).createUser(user); // Now log in and ensure there's a storage state saved - const sessionCutoff = Date.now() - SESSION_LIFETIME; + const sessionCutoff = Date.now() - YEAR; const path = getStorageStatePath(user); if (fs.existsSync(path) && fs.statSync(path)?.ctimeMs >= sessionCutoff) { // Storage state file is recent, no need to re-create it diff --git a/test/e2e/pages/base-page.ts b/test/e2e/pages/base-page.ts index 41c1ac64bd..2fda1ef11d 100644 --- a/test/e2e/pages/base-page.ts +++ b/test/e2e/pages/base-page.ts @@ -1,8 +1,8 @@ import { Locator, Page, APIRequestContext } from '@playwright/test'; +import { appUrl } from '../constants'; import { NoticeElement, PageHeader } from "./components"; export interface GotoOptions { - waitFor?: Locator; expectRedirect?: boolean; } @@ -21,10 +21,13 @@ export abstract class BasePage } readonly request: APIRequestContext; - readonly header = new PageHeader(this.page); readonly noticeList = new NoticeElement(this.page); + protected get isCurrentPage(): boolean { + return this.page.url().startsWith(appUrl) && !!this.page.url().match(this.urlPattern); + } + private readonly locators: Locator[]; private readonly urlPattern = this.url.includes('#') ? new RegExp(`${this.url}`) : new RegExp(`${this.url}/?(#|$)`); @@ -39,14 +42,16 @@ export abstract class BasePage * @returns the page for convenience/chaining */ async goto(options?: GotoOptions): Promise { - await Promise.all([ - this.page.goto(this.url), - options?.expectRedirect || this.waitFor(), // this page won't load if a redirect happens - options?.waitFor?.waitFor(), - ]).catch(error => { - console.error(error); - throw error; - }); + if (this.isCurrentPage) { + return this; + } + + await this.page.goto(this.url) + .then(_ => options?.expectRedirect ? undefined : this.waitFor()) // this page won't load if a redirect happens) + .catch(error => { + console.error(error); + throw error; + }); return this; } @@ -56,7 +61,7 @@ export abstract class BasePage */ async waitFor(): Promise { await Promise.all([ - this.page.url().match(this.urlPattern) ?? this.page.waitForURL(this.urlPattern), + this.isCurrentPage || this.page.waitForURL(this.urlPattern), ...this.locators.map(wait => wait.waitFor()), ]); return this; diff --git a/test/e2e/pages/configuration-input-systems.tab.ts b/test/e2e/pages/configuration-input-systems.tab.ts index 848909c7f7..6d478be9c4 100644 --- a/test/e2e/pages/configuration-input-systems.tab.ts +++ b/test/e2e/pages/configuration-input-systems.tab.ts @@ -20,18 +20,6 @@ export class ConfigurationPageInputSystemsTab extends ConfigurationPage { super(page, project, page.locator(inputSystemListSelector)); } - async goto(options?: GotoOptions): Promise { - await Promise.all([ - this.page.goto(this.url), - options?.waitFor?.waitFor(), - ]); - await Promise.all([ - this.tabLinks.inputSystems.click(), - this.waitFor(), - ]); - return this; - } - inputSystemOption(inputSystem: string): Locator { return this.inputSystemList.locator(`div:has-text("${inputSystem}")`); } diff --git a/test/e2e/pages/editor.page.ts b/test/e2e/pages/editor.page.ts index ec780eaf6b..7f3d80b692 100644 --- a/test/e2e/pages/editor.page.ts +++ b/test/e2e/pages/editor.page.ts @@ -84,6 +84,10 @@ export class EditorPage extends BasePage { readonly comments = this.commentContainer.locator(`> div:visible`); readonly comment = (n: number) => this.locator(`.commentListContainer > div:nth-child(${n}) .commentContainer`); + protected override get isCurrentPage(): boolean { + return this.page.url().endsWith(this.url) || this.page.url().includes('#!/editor'); + } + private readonly lexAppToolbar = { backToListButton: this.locator('#toListLink'), toggleCommentsButton: this.locator('#toCommentsLink'), @@ -106,18 +110,13 @@ export class EditorPage extends BasePage { } async goto(options?: EditorGotoOptions): Promise { - const alreadyOnPage = this.page.url().endsWith(this.url) || this.page.url().includes('#!/editor'); await super.goto(options); - const entryId = options?.entryId; - if (entryId || alreadyOnPage) { - // If we're navigating from one entry to another, then goto doesn't cause angular to load the new entry + if (options?.entryId) { + // Navigating from one entry to another via URL/goto doesn't cause angular to load the new entry // clicking is a more realistic test anyway - await Promise.all([ - this.locator(`[id^=entryId_${entryId ?? ''}]`).first().click(), - this.waitFor(), - ]); + await this.locator(`[id^=entryId_${options.entryId}]`).click(); + await this.waitFor(); } - await expect(this.locator('.page-name >> text=' + this.project.name)).toBeVisible(); return this; } diff --git a/test/e2e/pages/signup.page.ts b/test/e2e/pages/signup.page.ts index 286cb76ce8..89d61cf1ce 100644 --- a/test/e2e/pages/signup.page.ts +++ b/test/e2e/pages/signup.page.ts @@ -62,7 +62,7 @@ export class SignupPage extends BasePage { this.waitFor(), ]); } else { - await super.goto(); + await super.goto(options); } return this; } diff --git a/test/e2e/pages/tabbed-page.ts b/test/e2e/pages/tabbed-page.ts index 65fe4b50aa..1b7d3cf350 100644 --- a/test/e2e/pages/tabbed-page.ts +++ b/test/e2e/pages/tabbed-page.ts @@ -12,13 +12,10 @@ export abstract class TabbedBasePage extends BasePage { protected abstract get tabLink(): Locator; async goto(options?: GotoOptions): Promise { - await Promise.all([ - this.page.goto(this.url), - options?.waitFor?.waitFor(), - ]); + await this.page.goto(this.url); await Promise.all([ this.tabLink.click(), - this.waitFor(), + options?.expectRedirect || this.waitFor(), ]); return this; } diff --git a/test/e2e/pages/user-profile.page.ts b/test/e2e/pages/user-profile.page.ts index 44b12f22fb..8d731c18d8 100644 --- a/test/e2e/pages/user-profile.page.ts +++ b/test/e2e/pages/user-profile.page.ts @@ -5,7 +5,8 @@ export class UserProfilePage extends BasePage { readonly activitiesList = this.locator('[data-ng-repeat="item in filteredActivities"]'); readonly tabs = { aboutMe: this.locator('#AboutMeTab'), - myAccount: this.locator('#myAccountTab') + myAccount: this.locator('#myAccountTab'), + deleteAccount: this.locator('#DeleteTab') }; readonly accountTab = { @@ -21,13 +22,26 @@ export class UserProfilePage extends BasePage { genderField: this.page.getByLabel('Gender'), }; - private readonly saveBtn = this.locator('#saveBtn'); + readonly deleteTab = { + confirmField: this.page.getByLabel('Confirm deletion by typing DELETE into the box below'), + } + + private readonly saveBtn = this.locator('#saveMyAccountBtn:visible, #saveAboutMeBtn:visible'); + private readonly deleteAccountButton = this.locator('#deleteAccountBtn') - readonly modal = { + + readonly changesModal = { saveChangesBtn: this.locator('.modal-dialog button:has-text("Save changes")'), }; - private readonly successNotice = this.noticeList.success('Profile updated successfully'); + readonly deleteModal = { + confirmDeletionBtn: this.locator('.modal-dialog button:has-text("Delete")'), + }; + + + + private readonly successfulUpdateNotice = this.noticeList.success('Profile updated successfully'); + private readonly successfulDeleteNotice = this.noticeList.success('Your account was permanently deleted'); constructor(page: Page) { super(page, '/app/userprofile', page.locator('.page-name >> text=\'s User Profile')); @@ -38,9 +52,21 @@ export class UserProfilePage extends BasePage { await Promise.race([ // Some changes require confirmation and then log the user out (e.g. username) - this.modal.saveChangesBtn.click(), + this.changesModal.saveChangesBtn.click(), // others just show a success message - this.successNotice.waitFor(), + this.successfulUpdateNotice.waitFor(), ]); } + + async deleteMyAccount(): Promise { + await this.deleteAccountButton.click(); + + await Promise.race([ + // deletion confirmation + this.deleteModal.confirmDeletionBtn.click(), + // success message + this.successfulDeleteNotice.waitFor(), + ]); + } + } diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index e374459f6f..9ece04e7b0 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -1,6 +1,6 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices, expect } from '@playwright/test'; -import { appUrl } from './constants'; +import { appUrl, MIN, SEC } from './constants'; import { matchers } from './utils/custom-matchers'; /** @@ -20,13 +20,13 @@ const config: PlaywrightTestConfig = { snapshotPathTemplate: '{testDir}/__expected-screenshots__/{testFilePath}/{arg}-{projectName}{ext}', /* Maximum time one test can run for. For individual slower tests use test.slow(). */ - timeout: 30 * 1000, + timeout: 40 * SEC, expect: { /** * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 5000, + timeout: 5 * SEC, }, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, @@ -48,7 +48,7 @@ const config: PlaywrightTestConfig = { reportSlowTests: { max: 10, - threshold: 30 * 1000, + threshold: 1 * MIN, }, /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -106,14 +106,14 @@ const config: PlaywrightTestConfig = { if (!process.env._loggedAppStartup) { // Provide some feedback process.env._loggedAppStartup = '1'; const timeout = (this as any).timeout; - console.log(`Building and starting e2e-app if not already running (timeout: ${timeout / 60000}m)\n\n`); + console.log(`Building and starting e2e-app if not already running (timeout: ${timeout / MIN}m)\n\n`); } return 'make e2e-app && docker attach e2e-app'; }, cwd: '../..', // navigate to makefile url: appUrl, reuseExistingServer: true, - timeout: 3 * 60000, // 3m + timeout: 3 * MIN, }, }; diff --git a/test/e2e/tests/signup.spec.ts b/test/e2e/tests/signup.spec.ts index 70912a60e2..c4b30b6829 100644 --- a/test/e2e/tests/signup.spec.ts +++ b/test/e2e/tests/signup.spec.ts @@ -4,131 +4,133 @@ import { test } from '../fixtures'; import { ProjectsPage, SignupPage } from '../pages'; test.describe('Signup', () => { - let signupPage: SignupPage; - const validPassword = 'languageforge'; - const unusedName = 'Super amazing unused name'; - const unusedEmail = 'unused-email@valuable-but-unnoticed.com'; - - test.beforeEach(async ({ tab }) => { - signupPage = new SignupPage(tab); - }) - - test('Cannot submit if email is invalid', async () => { - await signupPage.goto(); - - await signupPage.nameInput.fill(unusedName); - await signupPage.emailInput.fill('email-without-at'); - await signupPage.passwordInput.fill(validPassword); - await signupPage.captcha.setInvalidCaptcha(); - await expect(signupPage.emailInvalid).toBeVisible(); - await expect(signupPage.signupButton).toBeDisabled(); - }); - - test('Cannot submit if password is weak', async () => { - await signupPage.goto(); - - await signupPage.nameInput.fill(unusedName); - await signupPage.emailInput.fill(unusedEmail); - await signupPage.passwordInput.fill(validPassword); - await signupPage.captcha.setInvalidCaptcha(); - await expect(signupPage.signupButton).toBeEnabled(); - const passwordTooShort = '123456'; - await signupPage.passwordInput.fill(passwordTooShort); - await signupPage.captcha.setInvalidCaptcha(); - await expect(signupPage.passwordIsWeak).toBeVisible(); - await expect(signupPage.signupButton).not.toBeEnabled(); - }); - - test('Can submit if the password is strong', async () => { - await signupPage.goto(); - - await signupPage.nameInput.fill(unusedName); - await signupPage.emailInput.fill(unusedEmail); - await signupPage.passwordInput.fill(validPassword); - await signupPage.captcha.setInvalidCaptcha(); - await expect(signupPage.passwordIsWeak).not.toBeVisible(); - await expect(signupPage.signupButton).toBeEnabled(); - }); - - test('Can submit if password is showing and not weak', async () => { - await signupPage.goto(); - - await signupPage.nameInput.fill(unusedName); - await signupPage.emailInput.fill(unusedEmail); - await signupPage.passwordInput.fill(validPassword); - await signupPage.showPassword.click(); - await signupPage.captcha.setInvalidCaptcha(); - await expect(signupPage.passwordIsWeak).not.toBeVisible(); - await expect(signupPage.signupButton).toBeEnabled(); - }); - - test('Cannot submit if captcha not selected', async () => { - await signupPage.goto(); - - await signupPage.nameInput.fill(unusedName); - await signupPage.emailInput.fill(unusedEmail); - await signupPage.passwordInput.fill(validPassword); - await expect(signupPage.signupButton).toBeDisabled(); - }); - - test('Captcha is invalid but user can still submit form', async () => { - await signupPage.goto(); - - await signupPage.nameInput.fill(unusedName); - await signupPage.emailInput.fill(unusedEmail); - await signupPage.passwordInput.fill(validPassword); - await signupPage.captcha.setInvalidCaptcha(); - await signupPage.signupButton.click(); - await expect(signupPage.captchaInvalid).toBeVisible(); - }); - - test('Finds the admin user (case insensitive) already exists', async () => { - await signupPage.goto(); - - await signupPage.nameInput.fill(unusedName); - await signupPage.emailInput.fill(users.admin.email.toUpperCase()); - await signupPage.passwordInput.fill(validPassword); - await signupPage.captcha.setValidCaptcha(); - await expect(signupPage.signupButton).toBeEnabled(); - await signupPage.signupButton.click(); - await expect(signupPage.emailTaken).toBeVisible(); - }); - - test('Can prefill email address that can\'t be changed', async () => { - await signupPage.goto({ email: unusedEmail }); - - await expect(signupPage.emailInput).toBeDisabled(); - }); - - test('Can prefill email address that already exists', async () => { - await signupPage.goto({ email: users.admin.email }); - - await signupPage.nameInput.fill(unusedName); - await signupPage.passwordInput.fill(validPassword); - await signupPage.captcha.setValidCaptcha(); - await signupPage.signupButton.click(); - await expect(signupPage.emailTaken).toBeVisible(); - }); - - test('Can signup a new user', async () => { - await signupPage.goto(); - - await signupPage.nameInput.fill(unusedName); - await signupPage.emailInput.fill(unusedEmail); - await signupPage.passwordInput.fill(validPassword); - await signupPage.captcha.setValidCaptcha(); - await expect(signupPage.signupButton).toBeEnabled(); - await signupPage.signupButton.click(); - // Verify new user logged in and redirected to projects page - await new ProjectsPage(signupPage.page).waitFor(); + test.describe('New user', () => { + let signupPage: SignupPage; + const validPassword = 'languageforge'; + const unusedName = 'Super amazing unused name'; + const unusedEmail = 'unused-email@valuable-but-unnoticed.com'; + + test.beforeEach(async ({ tab }) => { + signupPage = new SignupPage(tab); + }) + + test('Cannot submit if email is invalid', async () => { + await signupPage.goto(); + + await signupPage.nameInput.fill(unusedName); + await signupPage.emailInput.fill('email-without-at'); + await signupPage.passwordInput.fill(validPassword); + await signupPage.captcha.setInvalidCaptcha(); + await expect(signupPage.emailInvalid).toBeVisible(); + await expect(signupPage.signupButton).toBeDisabled(); + }); + + test('Cannot submit if password is weak', async () => { + await signupPage.goto(); + + await signupPage.nameInput.fill(unusedName); + await signupPage.emailInput.fill(unusedEmail); + await signupPage.passwordInput.fill(validPassword); + await signupPage.captcha.setInvalidCaptcha(); + await expect(signupPage.signupButton).toBeEnabled(); + const passwordTooShort = '123456'; + await signupPage.passwordInput.fill(passwordTooShort); + await signupPage.captcha.setInvalidCaptcha(); + await expect(signupPage.passwordIsWeak).toBeVisible(); + await expect(signupPage.signupButton).not.toBeEnabled(); + }); + + test('Can submit if the password is strong', async () => { + await signupPage.goto(); + + await signupPage.nameInput.fill(unusedName); + await signupPage.emailInput.fill(unusedEmail); + await signupPage.passwordInput.fill(validPassword); + await signupPage.captcha.setInvalidCaptcha(); + await expect(signupPage.passwordIsWeak).not.toBeVisible(); + await expect(signupPage.signupButton).toBeEnabled(); + }); + + test('Can submit if password is showing and not weak', async () => { + await signupPage.goto(); + + await signupPage.nameInput.fill(unusedName); + await signupPage.emailInput.fill(unusedEmail); + await signupPage.passwordInput.fill(validPassword); + await signupPage.showPassword.click(); + await signupPage.captcha.setInvalidCaptcha(); + await expect(signupPage.passwordIsWeak).not.toBeVisible(); + await expect(signupPage.signupButton).toBeEnabled(); + }); + + test('Cannot submit if captcha not selected', async () => { + await signupPage.goto(); + + await signupPage.nameInput.fill(unusedName); + await signupPage.emailInput.fill(unusedEmail); + await signupPage.passwordInput.fill(validPassword); + await expect(signupPage.signupButton).toBeDisabled(); + }); + + test('Captcha is invalid but user can still submit form', async () => { + await signupPage.goto(); + + await signupPage.nameInput.fill(unusedName); + await signupPage.emailInput.fill(unusedEmail); + await signupPage.passwordInput.fill(validPassword); + await signupPage.captcha.setInvalidCaptcha(); + await signupPage.signupButton.click(); + await expect(signupPage.captchaInvalid).toBeVisible(); + }); + + test('Finds the admin user (case insensitive) already exists', async () => { + await signupPage.goto(); + + await signupPage.nameInput.fill(unusedName); + await signupPage.emailInput.fill(users.admin.email.toUpperCase()); + await signupPage.passwordInput.fill(validPassword); + await signupPage.captcha.setValidCaptcha(); + await expect(signupPage.signupButton).toBeEnabled(); + await signupPage.signupButton.click(); + await expect(signupPage.emailTaken).toBeVisible(); + }); + + test('Can prefill email address that can\'t be changed', async () => { + await signupPage.goto({ email: unusedEmail }); + + await expect(signupPage.emailInput).toBeDisabled(); + }); + + test('Can prefill email address that already exists', async () => { + await signupPage.goto({ email: users.admin.email }); + + await signupPage.nameInput.fill(unusedName); + await signupPage.passwordInput.fill(validPassword); + await signupPage.captcha.setValidCaptcha(); + await signupPage.signupButton.click(); + await expect(signupPage.emailTaken).toBeVisible(); + }); + + test('Can signup a new user', async () => { + await signupPage.goto(); + + await signupPage.nameInput.fill(unusedName); + await signupPage.emailInput.fill(unusedEmail); + await signupPage.passwordInput.fill(validPassword); + await signupPage.captcha.setValidCaptcha(); + await expect(signupPage.signupButton).toBeEnabled(); + await signupPage.signupButton.click(); + + // Verify new user logged in and redirected to projects page + await new ProjectsPage(signupPage.page).waitFor(); + }); }); - test('Redirects to projects page if already logged in', async ({ memberTab }) => { - const signupPageMember = new SignupPage(memberTab); - await Promise.all([ - new ProjectsPage(memberTab).waitFor(), - signupPageMember.goto(), - ]); + test.describe('Existing user', () => { + test('Redirects to projects page if already logged in', async ({ memberTab }) => { + await new SignupPage(memberTab).goto({ expectRedirect: true }); + await new ProjectsPage(memberTab).waitFor(); + }); }); }); diff --git a/test/e2e/tests/user-profile.spec.ts b/test/e2e/tests/user-profile.spec.ts index 391dc8decb..5b5d614dab 100644 --- a/test/e2e/tests/user-profile.spec.ts +++ b/test/e2e/tests/user-profile.spec.ts @@ -99,4 +99,24 @@ test.describe('User Profile', () => { await expect(userProfilePage.aboutMeTab.genderField).toHaveSelectedOption({ label: 'Female' }); }); + test('Delete user account', async ({ tab, userService }) => { + const user = await userService.createRandomUser(); + await login(tab, user); + + const userProfilePage = new UserProfilePage(tab); + await userProfilePage.goto(); + await userProfilePage.tabs.deleteAccount.click(); + + const confirmation = 'DELETE'; + await userProfilePage.deleteTab.confirmField.fill(confirmation); + + // User clicks 'Delete My Account' and is redirected to the login page + const [loginPage] = await Promise.all([ + LoginPage.waitFor(tab), + userProfilePage.deleteMyAccount(), + ]); + + }); + + }); diff --git a/test/php/model/shared/UserModelTest.php b/test/php/model/shared/UserModelTest.php index 1c42077cb0..fb9c570cc1 100644 --- a/test/php/model/shared/UserModelTest.php +++ b/test/php/model/shared/UserModelTest.php @@ -337,28 +337,6 @@ public function testUserSetProperties_AdminAccessible_AllPropertiesSet() $this->assertEquals($params["interfaceLanguageCode"], $user->interfaceLanguageCode); } - public function testUserRemove_UserMemberOfProject_ProjectLinkRemovedAsWell() - { - $environ = new MongoTestEnvironment(); - $environ->clean(); - $userId = $environ->createUser("user1", "user1", "user1@example.com"); - $user = new UserModel($userId); - $project = $environ->createProject("testProject", "testProjectCode"); - $project->addUser($userId, ProjectRoles::CONTRIBUTOR); - $project->write(); - $user->addProject($project->id->asString()); - $user->write(); - - // delete the user - $user->remove(); - - // re-read the project - $projectId = $project->id->asString(); - $project = new \Api\Model\Shared\ProjectModel($projectId); - - $this->assertFalse($project->userIsMember($userId)); - } - public function testHasForgottenPassword_KeyNotSetNoKeyConsume_HasNotForgotten() { $environ = new MongoTestEnvironment(); diff --git a/test/php/model/shared/commands/UserCommandsTest.php b/test/php/model/shared/commands/UserCommandsTest.php index 46ec4aad23..c1daa591ae 100644 --- a/test/php/model/shared/commands/UserCommandsTest.php +++ b/test/php/model/shared/commands/UserCommandsTest.php @@ -49,19 +49,20 @@ protected function setUp(): void self::$environ->clean(); } - public function testDeleteUsers_1User_1Deleted() + public function testDeleteOwnAccount() { + //User deletes own account $userId = self::$environ->createUser("somename", "Some Name", "somename@example.com"); - $count = UserCommands::deleteUsers([$userId]); + $count = UserCommands::deleteAccounts([$userId], $userId); $this->assertEquals(1, $count); } - public function testDeleteUsers_NoId_Exception() + public function testDeleteUsers_NoIds_Exception() { $this->expectException(Exception::class); ini_set("display_errors", "0"); // do not show xdebug stack traces in PHPUnit output - UserCommands::deleteUsers(null); + UserCommands::deleteAccounts(null, null); ini_set("display_errors", "1"); // do not show xdebug stack traces in PHPUnit output } diff --git a/test/php/model/shared/dto/ActivityListDtoTest.php b/test/php/model/shared/dto/ActivityListDtoTest.php index 141f55027f..ff55296d0b 100644 --- a/test/php/model/shared/dto/ActivityListDtoTest.php +++ b/test/php/model/shared/dto/ActivityListDtoTest.php @@ -14,6 +14,7 @@ use Api\Model\Shared\Dto\ActivityListDto; use Api\Model\Shared\Rights\ProjectRoles; use Api\Model\Shared\UserModel; +use Api\Model\Shared\Command\UserCommands; use PHPUnit\Framework\TestCase; class ActivityListDtoTest extends TestCase @@ -797,4 +798,67 @@ public function testGetActivityForUser_DeleteReplyToEntryComment_DtoAsExpected() $this->assertEquals($expected["regarding"]["fieldValue"], $actual[ActivityModel::LEX_COMMENT_FIELD_VALUE]); $this->assertEquals(["label" => "Sentence", "sense" => 2, "example" => 1], $actual["fieldLabel"]); } + + /** @throws Exception */ + public function testGetActivityForProject_DeleteAccount_DtoAsExpected() + { + $environ = new LexiconMongoTestEnvironment(); + $environ->clean(); + + $project = $environ->createProject(SF_TESTPROJECT, SF_TESTPROJECTCODE); + $projectId = $project->id->asString(); + + $this->assertEquals($project->appName, LfProjectModel::LEXICON_APP); + + $userOneId = $environ->createUser("user1", "User One", "user1@email.com"); + $project->addUser($userOneId, ProjectRoles::CONTRIBUTOR); + $userTwoId = $environ->createUser("user2", "User Two", "user2@email.com"); + $project->addUser($userOneId, ProjectRoles::CONTRIBUTOR); + $project->write(); + + $entry = new LexEntryModel($project); + $entry->lexeme->form("en", "bank"); + $sense1 = new LexSense(); + $sense1->definition->form("en", "the sides of a river"); + $entry->senses[] = $sense1; + $sense2 = new LexSense(); + $sense2->definition->form("en", "a place to store money"); + $entry->senses[] = $sense2; + $example1 = new LexExample(); + $example1->sentence->form("en", "money in the bank"); + $sense2->examples[] = $example1; + $example2 = new LexExample(); + $example2->sentence->form("en", "a run on the bank"); + $sense2->examples[] = $example2; + $entryId = $entry->write(); + + $regarding = [ + "field" => "sentence", + "fieldNameForDisplay" => "Sentence", + "fieldValue" => "a run on the bank", + "inputSystem" => "en", + "word" => "bank", + "meaning" => "a place to store money", + ]; + $data = [ + "id" => "", + "entryRef" => $entryId, + "content" => "Comment on the sentence", + "regarding" => $regarding, + "contextGuid" => " sense#" . $sense2->guid . " example#" . $example1->guid . " sentence.en", + "isRegardingPicture" => false, + ]; + $commentId = LexCommentCommands::updateComment($projectId, $userOneId, $data); + + UserCommands::deleteAccounts([$userOneId], $userOneId); + + $dto = ActivityListDto::getActivityForOneProject($project, $userTwoId); + $activity = $dto["activity"]; + $activityRecord = array_shift($activity); + $this->assertArrayHasKey("userRef", $activityRecord); + $actual = $activityRecord["userRef"]; + + // After account is deleted, the username on the activity record should say "[Deleted User]" + $this->assertEquals("[Deleted User]", $actual["username"]); + } }