From 4c88a26e145286bf7d18f98099a74e5d8b14129b Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Thu, 5 Dec 2019 20:59:26 +0100 Subject: [PATCH 001/272] Update README --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9aaa120..3334e83 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ phone, and you're good to go! ## System Requirements -- Web server running LEMP or LAMP -- PHP Memcached or Memcache extension installed on websever. +- Web server running PHP and Memcached or Redis +- PHP `memcached`, `memcache` or `redis` extension installed on the web server. - Android 6 or above to run the [companion Android app](https://f-droid.org/packages/info.varden.hauk/). ## Installation instructions @@ -35,13 +35,10 @@ phone, and you're good to go! to install Hauk in, for example `/var/www/html`. Follow the instructions given by the install script. Make sure to set a secure hashed password and edit your site's domain in the configuration file after installation. -3. Start the webserver and make sure Memcached is running. +3. Start the web server and make sure Memcached or Redis is running. 4. Install the [companion Android app](https://f-droid.org/packages/info.varden.hauk/) on your phone and enter your server's settings. -When you visit the webroot you may see an experation notice. Hauk uses randomly -generated URL which will be provided by the app. - ## Manual installation If you prefer not to use the install script, you can instead choose to copy the @@ -52,7 +49,7 @@ files manually. in your web root, for example `/var/www/html`. 3. Modify `include/config.php` to your liking. Make sure to set a secure hashed password and edit your site's domain in this file. -4. Start the webserver and make sure Memcached is running. +4. Start the web server and make sure Memcached or Redis is running. 5. Install the [companion Android app](https://f-droid.org/packages/info.varden.hauk/) on your phone and enter your server's settings. From 86c47180559a095882dd5da6be29deffd533c461 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 12:37:58 +0100 Subject: [PATCH 002/272] Fix notice-level error, ref. #64 --- backend-php/api/create.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-php/api/create.php b/backend-php/api/create.php index d624ff8..6a612b7 100644 --- a/backend-php/api/create.php +++ b/backend-php/api/create.php @@ -84,7 +84,7 @@ switch ($mod) { // Tell the session that it is posting to this share. $host ->addTarget($share) - ->setEncrypted($encrypted, $_POST["salt"]) + ->setEncrypted($encrypted, filter_input(INPUT_POST, "salt")) ->save(); $output = array( From 9340f78c6a7a725ddb707ddbf27eea2a3c10eda0 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 16:15:15 +0100 Subject: [PATCH 003/272] Add multiarch build hooks for Docker; see #65 --- .gitignore | 2 ++ hooks/build | 8 ++++++++ hooks/post_checkout | 6 ++++++ hooks/post_push | 10 ++++++++++ hooks/pre_build | 4 ++++ hooks/pre_push | 6 ++++++ 6 files changed, 36 insertions(+) create mode 100644 .gitignore create mode 100755 hooks/build create mode 100755 hooks/post_checkout create mode 100755 hooks/post_push create mode 100755 hooks/pre_build create mode 100755 hooks/pre_push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c20c67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Autogenerated Dockerfiles +/docker/Dockerfile.* diff --git a/hooks/build b/hooks/build new file mode 100755 index 0000000..ce6b730 --- /dev/null +++ b/hooks/build @@ -0,0 +1,8 @@ +#!/bin/sh + +# Build main amd64 image +docker build -f docker/Dockerfile.amd64 -t $IMAGE_NAME-amd64 -t $IMAGE_NAME . + +# Build multiarch images +docker build -f docker/Dockerfile.arm32v7 -t $IMAGE_NAME-arm32v7 . +docker build -f docker/Dockerfile.arm64v8 -t $IMAGE_NAME-arm64v8 . diff --git a/hooks/post_checkout b/hooks/post_checkout new file mode 100755 index 0000000..39d81ec --- /dev/null +++ b/hooks/post_checkout @@ -0,0 +1,6 @@ +#!/bin/sh + +# Generate architecture-specific Dockerfiles +for arch in amd64 arm32v7 arm64v8; do + cat Dockerfile | sed -e "s/php:apache/${arch}\/php:apache/" > docker/Dockerfile.${arch} +done diff --git a/hooks/post_push b/hooks/post_push new file mode 100755 index 0000000..f70d2fd --- /dev/null +++ b/hooks/post_push @@ -0,0 +1,10 @@ +#!/bin/sh + +# Create manifest and annotate multiarch image +docker manifest create $IMAGE_NAME $IMAGE_NAME-amd64 $IMAGE_NAME-arm32v7 $IMAGE_NAME-arm64v8 +docker manifest annotate $IMAGE_NAME $IMAGE_NAME-amd64 --os linux --arch amd64 +docker manifest annotate $IMAGE_NAME $IMAGE_NAME-arm32v7 --os linux --arch arm --variant v7 +docker manifest annotate $IMAGE_NAME $IMAGE_NAME-arm64v8 --os linux --arch arm64 + +# Replace image in registry +docker manifest push --purge $IMAGE_NAME diff --git a/hooks/pre_build b/hooks/pre_build new file mode 100755 index 0000000..43065c2 --- /dev/null +++ b/hooks/pre_build @@ -0,0 +1,4 @@ +#!/bin/sh + +# Set up QEMU for cross compilation +docker run --rm --privileged multiarch/qemu-user-static --reset -p yes diff --git a/hooks/pre_push b/hooks/pre_push new file mode 100755 index 0000000..3453822 --- /dev/null +++ b/hooks/pre_push @@ -0,0 +1,6 @@ +#!/bin/sh + +# Push multiarch images +for arch in amd64 arm32v7 arm64v8; do + docker push $IMAGE_NAME-${arch} +done From 3ae9ae4dee9f226450901f1ee2b486ac91f53b3d Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 16:30:16 +0100 Subject: [PATCH 004/272] Use register tag for QEMU (#65) --- hooks/pre_build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/pre_build b/hooks/pre_build index 43065c2..9f205ea 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -1,4 +1,4 @@ #!/bin/sh # Set up QEMU for cross compilation -docker run --rm --privileged multiarch/qemu-user-static --reset -p yes +docker run --rm --privileged multiarch/qemu-user-static:register --reset From 59c8ba574867443eeb7cabbf969ae3dbe9844acd Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 17:00:43 +0100 Subject: [PATCH 005/272] Fetch QEMU from apt instead (#65) --- hooks/pre_build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hooks/pre_build b/hooks/pre_build index 9f205ea..c39f709 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -1,4 +1,5 @@ #!/bin/sh # Set up QEMU for cross compilation -docker run --rm --privileged multiarch/qemu-user-static:register --reset +apt update +apt install -y qemu-user-static From 4f0e62c557f0d7435d833d9219a4ab2a1b99a6dd Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 17:14:24 +0100 Subject: [PATCH 006/272] Register before installing QEMU (#65) --- hooks/pre_build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hooks/pre_build b/hooks/pre_build index c39f709..80310e2 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -1,5 +1,6 @@ #!/bin/sh # Set up QEMU for cross compilation -apt update +docker run --rm --privileged multiarch/qemu-user-static:register --reset +apt update -y apt install -y qemu-user-static From eaaec730d832ff7a4c2b3f08d754d3447469308c Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 17:42:41 +0100 Subject: [PATCH 007/272] Put QEMU inside the container for build (#65) --- .gitignore | 2 -- docker/Dockerfile.amd64 | 16 ++++++++++++++++ docker/Dockerfile.arm32v7 | 18 ++++++++++++++++++ docker/Dockerfile.arm64v8 | 18 ++++++++++++++++++ hooks/post_checkout | 6 ------ hooks/pre_build | 3 ++- 6 files changed, 54 insertions(+), 9 deletions(-) delete mode 100644 .gitignore create mode 100644 docker/Dockerfile.amd64 create mode 100644 docker/Dockerfile.arm32v7 create mode 100644 docker/Dockerfile.arm64v8 delete mode 100755 hooks/post_checkout diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0c20c67..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Autogenerated Dockerfiles -/docker/Dockerfile.* diff --git a/docker/Dockerfile.amd64 b/docker/Dockerfile.amd64 new file mode 100644 index 0000000..6daea0f --- /dev/null +++ b/docker/Dockerfile.amd64 @@ -0,0 +1,16 @@ +FROM amd64/php:apache +COPY backend-php/ /var/www/html/ +COPY frontend/ /var/www/html/ +COPY docker/start.sh . + +RUN apt-get update && \ + apt-get install -y memcached libmemcached-dev zlib1g-dev && \ + pecl install memcached && \ + docker-php-ext-enable memcached + +EXPOSE 80/tcp +VOLUME /etc/hauk + +STOPSIGNAL SIGINT +RUN chmod +x ./start.sh +CMD ["./start.sh"] diff --git a/docker/Dockerfile.arm32v7 b/docker/Dockerfile.arm32v7 new file mode 100644 index 0000000..012cf4c --- /dev/null +++ b/docker/Dockerfile.arm32v7 @@ -0,0 +1,18 @@ +FROM arm32v7/php:apache +COPY qemu-arm-static /usr/bin +COPY backend-php/ /var/www/html/ +COPY frontend/ /var/www/html/ +COPY docker/start.sh . + +RUN apt-get update && \ + apt-get install -y memcached libmemcached-dev zlib1g-dev && \ + pecl install memcached && \ + docker-php-ext-enable memcached + +EXPOSE 80/tcp +VOLUME /etc/hauk + +STOPSIGNAL SIGINT +RUN chmod +x ./start.sh +RUN rm -f /usr/bin/qemu-arm-static +CMD ["./start.sh"] diff --git a/docker/Dockerfile.arm64v8 b/docker/Dockerfile.arm64v8 new file mode 100644 index 0000000..2cf089e --- /dev/null +++ b/docker/Dockerfile.arm64v8 @@ -0,0 +1,18 @@ +FROM arm64v8/php:apache +COPY qemu-aarch64-static /usr/bin +COPY backend-php/ /var/www/html/ +COPY frontend/ /var/www/html/ +COPY docker/start.sh . + +RUN apt-get update && \ + apt-get install -y memcached libmemcached-dev zlib1g-dev && \ + pecl install memcached && \ + docker-php-ext-enable memcached + +EXPOSE 80/tcp +VOLUME /etc/hauk + +STOPSIGNAL SIGINT +RUN chmod +x ./start.sh +RUN rm -f /usr/bin/qemu-aarch64-static +CMD ["./start.sh"] diff --git a/hooks/post_checkout b/hooks/post_checkout deleted file mode 100755 index 39d81ec..0000000 --- a/hooks/post_checkout +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -# Generate architecture-specific Dockerfiles -for arch in amd64 arm32v7 arm64v8; do - cat Dockerfile | sed -e "s/php:apache/${arch}\/php:apache/" > docker/Dockerfile.${arch} -done diff --git a/hooks/pre_build b/hooks/pre_build index 80310e2..41aa546 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -1,6 +1,7 @@ #!/bin/sh # Set up QEMU for cross compilation -docker run --rm --privileged multiarch/qemu-user-static:register --reset apt update -y apt install -y qemu-user-static +cp /usr/bin/qemu-*-static . +docker run --rm --privileged multiarch/qemu-user-static:register --reset From 0d4081f6a9ddaaa8dec67b22d544411298a9b079 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 18:22:31 +0100 Subject: [PATCH 008/272] Use latest Docker for builds (#65) --- hooks/pre_build | 1 + 1 file changed, 1 insertion(+) diff --git a/hooks/pre_build b/hooks/pre_build index 41aa546..6aa9e67 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -2,6 +2,7 @@ # Set up QEMU for cross compilation apt update -y +apt -y --only-upgrade install docker-ee apt install -y qemu-user-static cp /usr/bin/qemu-*-static . docker run --rm --privileged multiarch/qemu-user-static:register --reset From c43119a396945f27ab3b8b2296d41cfe8e7fbffd Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 19:05:38 +0100 Subject: [PATCH 009/272] Enable experimental CLI for cross-compilation (#65) --- hooks/pre_build | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hooks/pre_build b/hooks/pre_build index 6aa9e67..d91b677 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -1,8 +1,16 @@ #!/bin/sh # Set up QEMU for cross compilation -apt update -y +apt -y update apt -y --only-upgrade install docker-ee -apt install -y qemu-user-static +apt -y install qemu-user-static jq cp /usr/bin/qemu-*-static . + +# Enable Docker CLI experimental mode +TMPFILE=`mktemp` +jq '.experimental="enabled"' ~/.docker/config.json > "$TMPFILE" +rm ~/.docker/config.json +mv "$TMPFILE" ~/.docker/config.json + +# Register QEMU docker run --rm --privileged multiarch/qemu-user-static:register --reset From a1564ec0f4ae20dc8170e12e92b68dc0853ace40 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 19:39:49 +0100 Subject: [PATCH 010/272] Check if config exists first (#65) --- hooks/pre_build | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/hooks/pre_build b/hooks/pre_build index d91b677..d331bf9 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -7,10 +7,18 @@ apt -y install qemu-user-static jq cp /usr/bin/qemu-*-static . # Enable Docker CLI experimental mode -TMPFILE=`mktemp` -jq '.experimental="enabled"' ~/.docker/config.json > "$TMPFILE" -rm ~/.docker/config.json -mv "$TMPFILE" ~/.docker/config.json +CONFPATH=~/.docker/config.json +if ! [ -f "$CONFPATH" ]; then + if ! [ -d "~/.docker" ]; then + mkdir -p "~/.docker" + fi + jq -n '.experimental="enabled"' > "$CONFPATH" +else + TMPFILE=`mktemp` + jq '.experimental="enabled"' "$CONFPATH" > "$TMPFILE" + rm "$CONFPATH" + mv "$TMPFILE" "$CONFPATH" +fi # Register QEMU docker run --rm --privileged multiarch/qemu-user-static:register --reset From 925b7b83c084a0b23ce0b64fd7c24c1411849090 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 20:26:33 +0100 Subject: [PATCH 011/272] Fix directory creation (#65) --- hooks/pre_build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/pre_build b/hooks/pre_build index d331bf9..7618f71 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -9,8 +9,8 @@ cp /usr/bin/qemu-*-static . # Enable Docker CLI experimental mode CONFPATH=~/.docker/config.json if ! [ -f "$CONFPATH" ]; then - if ! [ -d "~/.docker" ]; then - mkdir -p "~/.docker" + if ! [ -d ~/.docker ]; then + mkdir -pv ~/.docker fi jq -n '.experimental="enabled"' > "$CONFPATH" else From 37d7e4727eababad0a54e1b3aad437fd82341037 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 21:02:25 +0100 Subject: [PATCH 012/272] Reset CLI config after annotating (#65) Permission to push was denied --- hooks/post_push | 21 +++++++++++++++++++++ hooks/pre_build | 14 -------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/hooks/post_push b/hooks/post_push index f70d2fd..8e32443 100755 --- a/hooks/post_push +++ b/hooks/post_push @@ -1,10 +1,31 @@ #!/bin/sh +# Enable Docker CLI experimental mode +CREATED=false +CONFPATH=~/.docker/config.json +if ! [ -f "$CONFPATH" ]; then + CREATED=true + if ! [ -d ~/.docker ]; then + mkdir -pv ~/.docker + fi + jq -n '.experimental="enabled"' > "$CONFPATH" +else + TMPFILE=`mktemp` + jq '.experimental="enabled"' "$CONFPATH" > "$TMPFILE" + rm "$CONFPATH" + mv "$TMPFILE" "$CONFPATH" +fi + # Create manifest and annotate multiarch image docker manifest create $IMAGE_NAME $IMAGE_NAME-amd64 $IMAGE_NAME-arm32v7 $IMAGE_NAME-arm64v8 docker manifest annotate $IMAGE_NAME $IMAGE_NAME-amd64 --os linux --arch amd64 docker manifest annotate $IMAGE_NAME $IMAGE_NAME-arm32v7 --os linux --arch arm --variant v7 docker manifest annotate $IMAGE_NAME $IMAGE_NAME-arm64v8 --os linux --arch arm64 +# If the config was created, delete it after we're done +if [ "$CREATED" = "true" ]; then + rm "$CONFPATH" +fi + # Replace image in registry docker manifest push --purge $IMAGE_NAME diff --git a/hooks/pre_build b/hooks/pre_build index 7618f71..53ebca6 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -6,19 +6,5 @@ apt -y --only-upgrade install docker-ee apt -y install qemu-user-static jq cp /usr/bin/qemu-*-static . -# Enable Docker CLI experimental mode -CONFPATH=~/.docker/config.json -if ! [ -f "$CONFPATH" ]; then - if ! [ -d ~/.docker ]; then - mkdir -pv ~/.docker - fi - jq -n '.experimental="enabled"' > "$CONFPATH" -else - TMPFILE=`mktemp` - jq '.experimental="enabled"' "$CONFPATH" > "$TMPFILE" - rm "$CONFPATH" - mv "$TMPFILE" "$CONFPATH" -fi - # Register QEMU docker run --rm --privileged multiarch/qemu-user-static:register --reset From 82ee59c366a288a2473472eaac46e074adb866f0 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 21:29:56 +0100 Subject: [PATCH 013/272] Don't reset the config, actually (#65) --- hooks/post_push | 7 ------- 1 file changed, 7 deletions(-) diff --git a/hooks/post_push b/hooks/post_push index 8e32443..317950d 100755 --- a/hooks/post_push +++ b/hooks/post_push @@ -1,10 +1,8 @@ #!/bin/sh # Enable Docker CLI experimental mode -CREATED=false CONFPATH=~/.docker/config.json if ! [ -f "$CONFPATH" ]; then - CREATED=true if ! [ -d ~/.docker ]; then mkdir -pv ~/.docker fi @@ -22,10 +20,5 @@ docker manifest annotate $IMAGE_NAME $IMAGE_NAME-amd64 --os linux --arch amd64 docker manifest annotate $IMAGE_NAME $IMAGE_NAME-arm32v7 --os linux --arch arm --variant v7 docker manifest annotate $IMAGE_NAME $IMAGE_NAME-arm64v8 --os linux --arch arm64 -# If the config was created, delete it after we're done -if [ "$CREATED" = "true" ]; then - rm "$CONFPATH" -fi - # Replace image in registry docker manifest push --purge $IMAGE_NAME From fc6eed091b56acf0b7f48da6411a3fc69c7d4fe8 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 21:59:49 +0100 Subject: [PATCH 014/272] Just use the environment variable instead (#65) --- hooks/post_push | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/hooks/post_push b/hooks/post_push index 317950d..6135a49 100755 --- a/hooks/post_push +++ b/hooks/post_push @@ -1,18 +1,7 @@ #!/bin/sh # Enable Docker CLI experimental mode -CONFPATH=~/.docker/config.json -if ! [ -f "$CONFPATH" ]; then - if ! [ -d ~/.docker ]; then - mkdir -pv ~/.docker - fi - jq -n '.experimental="enabled"' > "$CONFPATH" -else - TMPFILE=`mktemp` - jq '.experimental="enabled"' "$CONFPATH" > "$TMPFILE" - rm "$CONFPATH" - mv "$TMPFILE" "$CONFPATH" -fi +export DOCKER_CLI_EXPERIMENTAL=enabled # Create manifest and annotate multiarch image docker manifest create $IMAGE_NAME $IMAGE_NAME-amd64 $IMAGE_NAME-arm32v7 $IMAGE_NAME-arm64v8 From 8179836e8dc2832a702f61ce8e86a5109d58cef8 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sat, 7 Dec 2019 23:18:10 +0100 Subject: [PATCH 015/272] Clean up multiarch Docker setup Remove jq as it is no longer needed, and add warnings to the Dockerfiles meant for Docker Hub. --- docker/Dockerfile.amd64 | 17 +++++++++++++++++ docker/Dockerfile.arm32v7 | 17 +++++++++++++++++ docker/Dockerfile.arm64v8 | 17 +++++++++++++++++ hooks/pre_build | 4 +--- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile.amd64 b/docker/Dockerfile.amd64 index 6daea0f..c5d7a48 100644 --- a/docker/Dockerfile.amd64 +++ b/docker/Dockerfile.amd64 @@ -1,3 +1,20 @@ +#------------------------------------------------------------------------------# +# DO NOT BUILD THIS FILE DIRECTLY! # +#------------------------------------------------------------------------------# +# This Dockerfile is for automated builds on Docker Hub only. If you want to # +# build the Hauk backend for x86_64, you should instead build the main # +# Dockerfile in the root of the repository on an x86_64-capable CPU. # +# # +# If you want to cross-compile Hauk for x86_64 on a non-x86_64 CPU, and you # +# are absolutely certain that you know what you are doing and that this is # +# what you want to do: you must have qemu-user-static binaries installed and # +# registered on your build system. # +# # +# Then change the FROM instruction in the root Dockerfile (NOT *this* file) so # +# that it pulls amd64/php:apache instead of php:apache to ensure you fetch # +# Apache and PHP from upstream for the right architecture. # +#------------------------------------------------------------------------------# + FROM amd64/php:apache COPY backend-php/ /var/www/html/ COPY frontend/ /var/www/html/ diff --git a/docker/Dockerfile.arm32v7 b/docker/Dockerfile.arm32v7 index 012cf4c..1267628 100644 --- a/docker/Dockerfile.arm32v7 +++ b/docker/Dockerfile.arm32v7 @@ -1,3 +1,20 @@ +#------------------------------------------------------------------------------# +# DO NOT BUILD THIS FILE DIRECTLY! # +#------------------------------------------------------------------------------# +# This Dockerfile is for automated builds on Docker Hub only. If you want to # +# build the Hauk backend for armv7l, you should instead build the main # +# Dockerfile in the root of the repository on an armv7l-capable CPU. # +# # +# If you want to cross-compile Hauk for armv7l on a non-armv7l CPU, and you # +# are absolutely certain that you know what you are doing and that this is # +# what you want to do: you must have qemu-user-static binaries installed and # +# registered on your build system. # +# # +# Then change the FROM instruction in the root Dockerfile (NOT *this* file) so # +# that it pulls arm32v7/php:apache instead of php:apache to ensure you fetch # +# Apache and PHP from upstream for the right architecture. # +#------------------------------------------------------------------------------# + FROM arm32v7/php:apache COPY qemu-arm-static /usr/bin COPY backend-php/ /var/www/html/ diff --git a/docker/Dockerfile.arm64v8 b/docker/Dockerfile.arm64v8 index 2cf089e..aa41577 100644 --- a/docker/Dockerfile.arm64v8 +++ b/docker/Dockerfile.arm64v8 @@ -1,3 +1,20 @@ +#------------------------------------------------------------------------------# +# DO NOT BUILD THIS FILE DIRECTLY! # +#------------------------------------------------------------------------------# +# This Dockerfile is for automated builds on Docker Hub only. If you want to # +# build the Hauk backend for aarch64, you should instead build the main # +# Dockerfile in the root of the repository on an aarch64-capable CPU. # +# # +# If you want to cross-compile Hauk for aarch64 on a non-aarch64 CPU, and you # +# are absolutely certain that you know what you are doing and that this is # +# what you want to do: you must have qemu-user-static binaries installed and # +# registered on your build system. # +# # +# Then change the FROM instruction in the root Dockerfile (NOT *this* file) so # +# that it pulls arm64v8/php:apache instead of php:apache to ensure you fetch # +# Apache and PHP from upstream for the right architecture. # +#------------------------------------------------------------------------------# + FROM arm64v8/php:apache COPY qemu-aarch64-static /usr/bin COPY backend-php/ /var/www/html/ diff --git a/hooks/pre_build b/hooks/pre_build index 53ebca6..f8a5e88 100755 --- a/hooks/pre_build +++ b/hooks/pre_build @@ -3,8 +3,6 @@ # Set up QEMU for cross compilation apt -y update apt -y --only-upgrade install docker-ee -apt -y install qemu-user-static jq +apt -y install qemu-user-static cp /usr/bin/qemu-*-static . - -# Register QEMU docker run --rm --privileged multiarch/qemu-user-static:register --reset From f350a7defe61785a5d8d24d71671cc026615d25b Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sun, 8 Dec 2019 10:46:24 +0100 Subject: [PATCH 016/272] Don't use CDN; fixes #67 --- frontend/index.html | 9 +- frontend/lib/leaflet/1.6.0/leaflet.css | 640 +++++++++++++++++++++++++ frontend/lib/leaflet/1.6.0/leaflet.js | 5 + 3 files changed, 648 insertions(+), 6 deletions(-) create mode 100644 frontend/lib/leaflet/1.6.0/leaflet.css create mode 100644 frontend/lib/leaflet/1.6.0/leaflet.js diff --git a/frontend/index.html b/frontend/index.html index 3a5000b..a3283b6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,13 +2,10 @@ + - - + + diff --git a/frontend/lib/leaflet/1.6.0/leaflet.css b/frontend/lib/leaflet/1.6.0/leaflet.css new file mode 100644 index 0000000..609a662 --- /dev/null +++ b/frontend/lib/leaflet/1.6.0/leaflet.css @@ -0,0 +1,640 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg, +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + } + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-tile { + will-change: opacity; + } +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +.leaflet-zoom-anim .leaflet-zoom-animated { + will-change: transform; + } +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline: 0; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-container a.leaflet-active { + outline: 2px solid orange; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a, +.leaflet-bar a:hover { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.7); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover { + text-decoration: underline; + } +.leaflet-container .leaflet-control-attribution, +.leaflet-container .leaflet-control-scale { + font-size: 11px; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + font-size: 11px; + white-space: nowrap; + overflow: hidden; + -moz-box-sizing: border-box; + box-sizing: border-box; + + background: #fff; + background: rgba(255, 255, 255, 0.5); + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 19px; + line-height: 1.4; + } +.leaflet-popup-content p { + margin: 18px 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + padding: 4px 4px 0 0; + border: none; + text-align: center; + width: 18px; + height: 14px; + font: 16px/14px Tahoma, Verdana, sans-serif; + color: #c3c3c3; + text-decoration: none; + font-weight: bold; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover { + color: #999; + } +.leaflet-popup-scrolled { + overflow: auto; + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } +.leaflet-oldie .leaflet-popup-tip-container { + margin-top: -1px; + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-clickable { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } diff --git a/frontend/lib/leaflet/1.6.0/leaflet.js b/frontend/lib/leaflet/1.6.0/leaflet.js new file mode 100644 index 0000000..bc9ef0f --- /dev/null +++ b/frontend/lib/leaflet/1.6.0/leaflet.js @@ -0,0 +1,5 @@ +/* @preserve + * Leaflet 1.6.0+Detached: 0c81bdf904d864fd12a286e3d1979f47aba17991.0c81bdf, a JS library for interactive maps. http://leafletjs.com + * (c) 2010-2019 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(t.L={})}(this,function(t){"use strict";var i=Object.freeze;function h(t){var i,e,n,o;for(e=1,n=arguments.length;e=this.min.x&&e.x<=this.max.x&&i.y>=this.min.y&&e.y<=this.max.y},intersects:function(t){t=R(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>=i.x&&n.x<=e.x,r=o.y>=i.y&&n.y<=e.y;return s&&r},overlaps:function(t){t=R(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>i.x&&n.xi.y&&n.y=n.lat&&e.lat<=o.lat&&i.lng>=n.lng&&e.lng<=o.lng},intersects:function(t){t=D(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>=i.lat&&n.lat<=e.lat,r=o.lng>=i.lng&&n.lng<=e.lng;return s&&r},overlaps:function(t){t=D(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>i.lat&&n.lati.lng&&n.lng';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}();function Bt(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var At=(Object.freeze||Object)({ie:it,ielt9:et,edge:nt,webkit:ot,android:st,android23:rt,androidStock:ht,opera:ut,chrome:lt,gecko:ct,safari:_t,phantom:dt,opera12:pt,win:mt,ie3d:ft,webkit3d:gt,gecko3d:vt,any3d:yt,mobile:xt,mobileWebkit:wt,mobileWebkit3d:Pt,msPointer:Lt,pointer:bt,touch:Tt,mobileOpera:zt,mobileGecko:Mt,retina:Ct,passiveEvents:Et,canvas:St,svg:Zt,vml:kt}),It=Lt?"MSPointerDown":"pointerdown",Ot=Lt?"MSPointerMove":"pointermove",Rt=Lt?"MSPointerUp":"pointerup",Nt=Lt?"MSPointerCancel":"pointercancel",Dt=["INPUT","SELECT","OPTION"],jt={},Wt=!1,Ht=0;function Ft(t,i,e,n){return"touchstart"===i?function(t,i,e){var n=a(function(t){if("mouse"!==t.pointerType&&t.MSPOINTER_TYPE_MOUSE&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(Dt.indexOf(t.target.tagName)<0))return;ji(t)}Gt(t,i)});t["_leaflet_touchstart"+e]=n,t.addEventListener(It,n,!1),Wt||(document.documentElement.addEventListener(It,Ut,!0),document.documentElement.addEventListener(Ot,Vt,!0),document.documentElement.addEventListener(Rt,qt,!0),document.documentElement.addEventListener(Nt,qt,!0),Wt=!0)}(t,e,n):"touchmove"===i?function(t,i,e){function n(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&Gt(t,i)}t["_leaflet_touchmove"+e]=n,t.addEventListener(Ot,n,!1)}(t,e,n):"touchend"===i&&function(t,i,e){function n(t){Gt(t,i)}t["_leaflet_touchend"+e]=n,t.addEventListener(Rt,n,!1),t.addEventListener(Nt,n,!1)}(t,e,n),this}function Ut(t){jt[t.pointerId]=t,Ht++}function Vt(t){jt[t.pointerId]&&(jt[t.pointerId]=t)}function qt(t){delete jt[t.pointerId],Ht--}function Gt(t,i){for(var e in t.touches=[],jt)t.touches.push(jt[e]);t.changedTouches=[t],i(t)}var Kt=Lt?"MSPointerDown":bt?"pointerdown":"touchstart",Yt=Lt?"MSPointerUp":bt?"pointerup":"touchend",Xt="_leaflet_";function Jt(t,o,i){var s,r,a=!1;function e(t){var i;if(bt){if(!nt||"mouse"===t.pointerType)return;i=Ht}else i=t.touches.length;if(!(1this.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,D(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},panInside:function(t,i){var e=I((i=i||{}).paddingTopLeft||i.padding||[0,0]),n=I(i.paddingBottomRight||i.padding||[0,0]),o=this.getCenter(),s=this.project(o),r=this.project(t),a=this.getPixelBounds(),h=a.getSize().divideBy(2),u=R([a.min.add(e),a.max.subtract(n)]);if(!u.contains(r)){this._enforcingBounds=!0;var l=s.subtract(r),c=I(r.x+l.x,r.y+l.y);(r.xu.max.x)&&(c.x=s.x-l.x,0u.max.y)&&(c.y=s.y-l.y,0=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,i){for(var e,n=[],o="mouseout"===i||"mouseover"===i,s=t.target||t.srcElement,r=!1;s;){if((e=this._targets[u(s)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){r=!0;break}if(e&&e.listens(i,!0)){if(o&&!Yi(s,t))break;if(n.push(e),o)break}if(s===this._container)break;s=s.parentNode}return n.length||r||o||!Yi(s,t)||(n=[this]),n},_handleDOMEvent:function(t){if(this._loaded&&!Ki(t)){var i=t.type;"mousedown"!==i&&"keypress"!==i&&"keyup"!==i&&"keydown"!==i||Mi(t.target||t.srcElement),this._fireDOMEvent(t,i)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,i,e){if("click"===t.type){var n=h({},t);n.type="preclick",this._fireDOMEvent(n,n.type,e)}if(!t._stopped&&(e=(e||[]).concat(this._findEventTargets(t,i))).length){var o=e[0];"contextmenu"===i&&o.listens(i,!0)&&ji(t);var s={originalEvent:t};if("keypress"!==t.type&&"keydown"!==t.type&&"keyup"!==t.type){var r=o.getLatLng&&(!o._radius||o._radius<=10);s.containerPoint=r?this.latLngToContainerPoint(o.getLatLng()):this.mouseEventToContainerPoint(t),s.layerPoint=this.containerPointToLayerPoint(s.containerPoint),s.latlng=r?o.getLatLng():this.layerPointToLatLng(s.layerPoint)}for(var a=0;athis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o))&&(M(function(){this._moveStart(!0,!1)._animateZoom(t,i,!0)},this),!0)},_animateZoom:function(t,i,e,n){this._mapPane&&(e&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,mi(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:n}),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&fi(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),M(function(){this._moveEnd(!0)},this))}});function Qi(t){return new te(t)}var te=S.extend({options:{position:"topright"},initialize:function(t){p(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return mi(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(li(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),n=this._map.hasLayer(t.layer);t.overlay?((i=document.createElement("input")).type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=n):i=this._createRadioElement("leaflet-base-layers_"+u(this),n),this._layerControlInputs.push(i),i.layerId=u(t.layer),ki(i,"click",this._onInputClick,this);var o=document.createElement("span");o.innerHTML=" "+t.name;var s=document.createElement("div");return e.appendChild(s),s.appendChild(i),s.appendChild(o),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=e.length-1;0<=s;s--)t=e[s],i=this._getLayer(t.layerId).layer,t.checked?n.push(i):t.checked||o.push(i);for(s=0;si.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),ee=te.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=ui("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,i,e,n,o){var s=ui("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),Di(s),ki(s,"click",Wi),ki(s,"click",o,this),ki(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";fi(this._zoomInButton,i),fi(this._zoomOutButton,i),!this._disabled&&t._zoom!==t.getMinZoom()||mi(this._zoomOutButton,i),!this._disabled&&t._zoom!==t.getMaxZoom()||mi(this._zoomInButton,i)}});$i.mergeOptions({zoomControl:!0}),$i.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new ee,this.addControl(this.zoomControl))});var ne=te.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i="leaflet-control-scale",e=ui("div",i),n=this.options;return this._addScales(n,i+"-line",e),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),e},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=ui("div",i,e)),t.imperial&&(this._iScale=ui("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;5280Leaflet'},initialize:function(t){p(this,t),this._attributions={}},onAdd:function(t){for(var i in(t.attributionControl=this)._container=ui("div","leaflet-control-attribution"),Di(this._container),t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t=[];for(var i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});$i.mergeOptions({attributionControl:!0}),$i.addInitHook(function(){this.options.attributionControl&&(new oe).addTo(this)});te.Layers=ie,te.Zoom=ee,te.Scale=ne,te.Attribution=oe,Qi.layers=function(t,i,e){return new ie(t,i,e)},Qi.zoom=function(t){return new ee(t)},Qi.scale=function(t){return new ne(t)},Qi.attribution=function(t){return new oe(t)};var se=S.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}});se.addTo=function(t,i){return t.addHandler(i,this),this};var re,ae={Events:Z},he=Tt?"touchstart mousedown":"mousedown",ue={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},le={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},ce=k.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){p(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(ki(this._dragStartTarget,he,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(ce._dragging===this&&this.finishDrag(),Ai(this._dragStartTarget,he,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!pi(this._element,"leaflet-zoom-anim")&&!(ce._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((ce._dragging=this)._preventOutline&&Mi(this._element),Ti(),Qt(),this._moving)))){this.fire("down");var i=t.touches?t.touches[0]:t,e=Ei(this._element);this._startPoint=new B(i.clientX,i.clientY),this._parentScale=Si(e),ki(document,le[t.type],this._onMove,this),ki(document,ue[t.type],this._onUp,this)}},_onMove:function(t){if(!t._simulated&&this._enabled)if(t.touches&&1i.max.x&&(e|=2),t.yi.max.y&&(e|=8),e}function ge(t,i,e,n){var o,s=i.x,r=i.y,a=e.x-s,h=e.y-r,u=a*a+h*h;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||je.prototype._containsPoint.call(this,t,!0)}});var He=ke.extend({initialize:function(t,i){p(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=v(t)?t:t.features;if(o){for(i=0,e=o.length;iu.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c])}},_onCloseButtonClick:function(t){this._close(),Wi(t)},_getAnchor:function(){return I(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}});$i.mergeOptions({closePopupOnClick:!0}),$i.include({openPopup:function(t,i,e){return t instanceof sn||(t=new sn(e).setContent(t)),i&&t.setLatLng(i),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),Se.include({bindPopup:function(t,i){return t instanceof sn?(p(t,i),(this._popup=t)._source=this):(this._popup&&!i||(this._popup=new sn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){return this._popup&&this._map&&(i=this._popup._prepareOpen(this,t,i),this._map.openPopup(this._popup,i)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;this._popup&&this._map&&(Wi(t),i instanceof Re?this.openPopup(t.layer||t.target,t.latlng):this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var rn=on.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){on.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){on.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=on.prototype.getEvents.call(this);return Tt&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=ui("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i=this._map,e=this._container,n=i.latLngToContainerPoint(i.getCenter()),o=i.layerPointToContainerPoint(t),s=this.options.direction,r=e.offsetWidth,a=e.offsetHeight,h=I(this.options.offset),u=this._getAnchor();t="top"===s?t.add(I(-r/2+h.x,-a+h.y+u.y,!0)):"bottom"===s?t.subtract(I(r/2-h.x,-h.y,!0)):"center"===s?t.subtract(I(r/2+h.x,a/2-u.y+h.y,!0)):"right"===s||"auto"===s&&o.xthis.options.maxZoom||ethis.options.maxZoom||void 0!==this.options.minZoom&&oe.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return D(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e);return[i.unproject(n,t.z),i.unproject(o,t.z)]},_tileCoordsToBounds:function(t){var i=this._tileCoordsToNwSe(t),e=new N(i[0],i[1]);return this.options.noWrap||(e=this._map.wrapLatLngBounds(e)),e},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new B(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(li(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){mi(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=l,t.onmousemove=l,et&&this.options.opacity<1&&yi(t,this.options.opacity),st&&!rt&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var e=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&M(a(this._tileReady,this,t,null,o)),Pi(o,e),this._tiles[n]={el:o,coords:t,current:!0},i.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,i,e){i&&this.fire("tileerror",{error:i,tile:e,coords:t});var n=this._tileCoordsToKey(t);(e=this._tiles[n])&&(e.loaded=+new Date,this._map._fadeAnimated?(yi(e.el,0),C(this._fadeFrame),this._fadeFrame=M(this._updateOpacity,this)):(e.active=!0,this._pruneTiles()),i||(mi(e.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:e.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),et||!this._map._fadeAnimated?M(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new B(this._wrapX?r(t.x,this._wrapX):t.x,this._wrapY?r(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new O(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var un=hn.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,(i=p(this,i)).detectRetina&&Ct&&0')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),fn={_initContainer:function(){this._container=ui("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(_n.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=mn("shape");mi(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=mn("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[u(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;li(i),t.removeInteractiveTarget(i),delete this._layers[u(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i||(i=t._stroke=mn("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=v(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e||(e=t._fill=mn("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){_i(t._container)},_bringToBack:function(t){di(t._container)}},gn=kt?mn:$,vn=_n.extend({getEvents:function(){var t=_n.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=gn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=gn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){li(this._container),Ai(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_onZoomStart:function(){this._update()},_update:function(){if(!this._map._animatingZoom||!this._bounds){_n.prototype._update.call(this);var t=this._bounds,i=t.getSize(),e=this._container;this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),Pi(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update")}},_initPath:function(t){var i=t._path=gn("path");t.options.className&&mi(i,t.options.className),t.options.interactive&&mi(i,"leaflet-interactive"),this._updateStyle(t),this._layers[u(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){li(t._path),t.removeInteractiveTarget(t._path),delete this._layers[u(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,Q(t._parts,i))},_updateCircle:function(t){var i=t._point,e=Math.max(Math.round(t._radius),1),n="a"+e+","+(Math.max(Math.round(t._radiusY),1)||e)+" 0 1,0 ",o=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+n+2*e+",0 "+n+2*-e+",0 ";this._setPath(t,o)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){_i(t._path)},_bringToBack:function(t){di(t._path)}});function yn(t){return Zt||kt?new vn(t):null}kt&&vn.include(fn),$i.include({getRenderer:function(t){var i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer;return i||(i=this._renderer=this._createRenderer()),this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=this._createRenderer({pane:t}),this._paneRenderers[t]=i),i},_createRenderer:function(t){return this.options.preferCanvas&&pn(t)||yn(t)}});var xn=We.extend({initialize:function(t,i){We.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=D(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});vn.create=gn,vn.pointsToPath=Q,He.geometryToLayer=Fe,He.coordsToLatLng=Ve,He.coordsToLatLngs=qe,He.latLngToCoords=Ge,He.latLngsToCoords=Ke,He.getFeature=Ye,He.asFeature=Xe,$i.mergeOptions({boxZoom:!0});var wn=se.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){ki(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){Ai(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){li(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),Qt(),Ti(),this._startPoint=this._map.mouseEventToContainerPoint(t),ki(document,{contextmenu:Wi,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=ui("div","leaflet-zoom-box",this._container),mi(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new O(this._point,this._startPoint),e=i.getSize();Pi(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(li(this._box),fi(this._container,"leaflet-crosshair")),ti(),zi(),Ai(document,{contextmenu:Wi,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0);var i=new N(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});$i.addInitHook("addHandler","boxZoom",wn),$i.mergeOptions({doubleClickZoom:!0});var Pn=se.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});$i.addInitHook("addHandler","doubleClickZoom",Pn),$i.mergeOptions({dragging:!0,inertia:!rt,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var Ln=se.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new ce(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))}mi(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){fi(this._map._container,"leaflet-grab"),fi(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t=this._map;if(t._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var i=D(this._map.options.maxBounds);this._offsetLimit=R(this._map.latLngToContainerPoint(i.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(i.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){if(this._map.options.inertia){var i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(e),this._times.push(i),this._prunePositions(i)}this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1i.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)i.getMaxZoom()&&1 Date: Sun, 8 Dec 2019 12:20:21 +0100 Subject: [PATCH 017/272] Add x-icon favicon; fixes #68 --- frontend/favicon.ico | Bin 0 -> 308898 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 frontend/favicon.ico diff --git a/frontend/favicon.ico b/frontend/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3bb9bfb87f149584cd807e6578e2c702e6bb7d4c GIT binary patch literal 308898 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 Date: Sun, 8 Dec 2019 13:23:15 +0100 Subject: [PATCH 018/272] Allow disabling E2E without clearing key; fixes #71 --- .../main/java/info/varden/hauk/Constants.java | 1 + .../info/varden/hauk/global/Receiver.java | 8 +++- .../info/varden/hauk/ui/MainActivity.java | 28 ++++++++++-- .../EncryptionEnabledChangeListener.java | 44 +++++++++++++++++++ .../hauk/utils/DeprecationMigrator.java | 5 +++ .../app/src/main/res/layout/activity_main.xml | 23 +++++++++- android/app/src/main/res/values/strings.xml | 3 +- 7 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 android/app/src/main/java/info/varden/hauk/ui/listener/EncryptionEnabledChangeListener.java diff --git a/android/app/src/main/java/info/varden/hauk/Constants.java b/android/app/src/main/java/info/varden/hauk/Constants.java index 991166e..cb16464 100644 --- a/android/app/src/main/java/info/varden/hauk/Constants.java +++ b/android/app/src/main/java/info/varden/hauk/Constants.java @@ -30,6 +30,7 @@ public enum Constants { public static final Preference PREF_DURATION = new Preference.Integer("duration", 30); public static final Preference PREF_INTERVAL = new Preference.Integer("interval", 1); public static final Preference PREF_CUSTOM_ID = new Preference.String("requestLink", ""); + public static final Preference PREF_ENABLE_E2E = new Preference.Boolean("enableE2E", false); public static final Preference PREF_E2E_PASSWORD = new Preference.EncryptedString("e2ePassword", ""); public static final Preference PREF_NICKNAME = new Preference.String("nickname", ""); public static final Preference PREF_DURATION_UNIT = new Preference.Integer("durUnit", Constants.DURATION_UNIT_MINUTES); diff --git a/android/app/src/main/java/info/varden/hauk/global/Receiver.java b/android/app/src/main/java/info/varden/hauk/global/Receiver.java index cda0ea0..eef4e75 100644 --- a/android/app/src/main/java/info/varden/hauk/global/Receiver.java +++ b/android/app/src/main/java/info/varden/hauk/global/Receiver.java @@ -170,7 +170,13 @@ public final class Receiver extends BroadcastReceiver { int duration = intent.hasExtra(Constants.EXTRA_SESSION_DURATION) ? intent.getIntExtra(Constants.EXTRA_SESSION_DURATION, 0) : TimeUtils.timeUnitsToSeconds(fallback.get(Constants.PREF_DURATION), fallback.get(Constants.PREF_DURATION_UNIT)); int interval = intent.hasExtra(Constants.EXTRA_SESSION_INTERVAL) ? intent.getIntExtra(Constants.EXTRA_SESSION_INTERVAL, 0) : fallback.get(Constants.PREF_INTERVAL); String customID = intent.hasExtra(Constants.EXTRA_SESSION_CUSTOM_ID) ? intent.getStringExtra(Constants.EXTRA_SESSION_CUSTOM_ID) : fallback.get(Constants.PREF_CUSTOM_ID); - String e2ePass = intent.hasExtra(Constants.EXTRA_SESSION_E2E_PASSWORD) ? intent.getStringExtra(Constants.EXTRA_SESSION_E2E_PASSWORD) : fallback.get(Constants.PREF_E2E_PASSWORD); + + String e2ePass = ""; + if (intent.hasExtra(Constants.EXTRA_SESSION_E2E_PASSWORD)) { + e2ePass = intent.getStringExtra(Constants.EXTRA_SESSION_E2E_PASSWORD); + } else if (fallback.get(Constants.PREF_ENABLE_E2E)) { + e2ePass = fallback.get(Constants.PREF_E2E_PASSWORD); + } assert server != null; server = server.endsWith("/") ? server : server + "/"; diff --git a/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java b/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java index c760280..bb9b477 100644 --- a/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java +++ b/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java @@ -40,6 +40,7 @@ import info.varden.hauk.system.LocationPermissionsNotGrantedException; import info.varden.hauk.system.LocationServicesDisabledException; import info.varden.hauk.system.powersaving.DeviceChecker; import info.varden.hauk.ui.listener.AddLinkClickListener; +import info.varden.hauk.ui.listener.EncryptionEnabledChangeListener; import info.varden.hauk.ui.listener.InitiateAdoptionClickListener; import info.varden.hauk.ui.listener.RememberPasswordPreferenceChangedListener; import info.varden.hauk.ui.listener.SelectionModeChangedListener; @@ -122,6 +123,17 @@ public final class MainActivity extends AppCompatActivity { )); loadPreferences(); + + // Add an on checked handler to the enable E2E checkbox to toggle the E2E state. This must + // be done after loading preferences to ensure that the checkbox doesn't trigger the event + // when hidden. + ((CompoundButton) findViewById(R.id.chkUseE2E)).setOnCheckedChangeListener( + new EncryptionEnabledChangeListener(this, new View[] { + findViewById(R.id.rowE2EPassword), + findViewById(R.id.rowRemember) + }) + ); + this.manager.resumeShares(new ResumePrompt() { @Override public void promptForResumption(Context ctx, Session session, Share[] shares, PromptCallback response) { @@ -167,6 +179,7 @@ public final class MainActivity extends AppCompatActivity { int duration = Integer.parseInt(((TextView) findViewById(R.id.txtDuration)).getText().toString()); int interval = Integer.parseInt(((TextView) findViewById(R.id.txtInterval)).getText().toString()); String customID = ((TextView) findViewById(R.id.txtCustomID)).getText().toString().trim(); + boolean useE2E = ((Checkable) findViewById(R.id.chkUseE2E)).isChecked(); String e2ePass = ((TextView) findViewById(R.id.txtE2EPassword)).getText().toString(); String nickname = ((TextView) findViewById(R.id.txtNickname)).getText().toString().trim(); @SuppressWarnings("OverlyStrongTypeCast") ShareMode mode = ShareMode.fromMode(((Spinner) findViewById(R.id.selMode)).getSelectedItemPosition()); @@ -187,6 +200,7 @@ public final class MainActivity extends AppCompatActivity { prefs.set(Constants.PREF_DURATION_UNIT, durUnit); prefs.set(Constants.PREF_NICKNAME, nickname); prefs.set(Constants.PREF_ALLOW_ADOPTION, allowAdoption); + prefs.set(Constants.PREF_ENABLE_E2E, useE2E); // If password saving is enabled, save the password as well. if (((Checkable) findViewById(R.id.chkRemember)).isChecked()) { @@ -195,6 +209,9 @@ public final class MainActivity extends AppCompatActivity { prefs.set(Constants.PREF_E2E_PASSWORD, e2ePass); } + // Ignore E2E password if E2E is disabled. + if (!useE2E) e2ePass = ""; + assert mode != null; server = server.endsWith("/") ? server : server + "/"; @@ -256,8 +273,11 @@ public final class MainActivity extends AppCompatActivity { view.setVisibility(View.GONE); findViewById(R.id.rowUpdateInterval).setVisibility(View.VISIBLE); findViewById(R.id.rowCustomID).setVisibility(View.VISIBLE); - findViewById(R.id.rowE2EPassword).setVisibility(View.VISIBLE); - findViewById(R.id.rowRemember).setVisibility(View.VISIBLE); + findViewById(R.id.rowUseE2E).setVisibility(View.VISIBLE); + if (((Checkable) findViewById(R.id.chkUseE2E)).isChecked()) { + findViewById(R.id.rowE2EPassword).setVisibility(View.VISIBLE); + findViewById(R.id.rowRemember).setVisibility(View.VISIBLE); + } } /** @@ -279,7 +299,8 @@ public final class MainActivity extends AppCompatActivity { findViewById(R.id.selMode), findViewById(R.id.txtNickname), findViewById(R.id.txtGroupCode), - findViewById(R.id.chkAllowAdopt) + findViewById(R.id.chkAllowAdopt), + findViewById(R.id.chkUseE2E) }; this.uiResetTask = new ResetTask(); @@ -324,6 +345,7 @@ public final class MainActivity extends AppCompatActivity { ((Spinner) findViewById(R.id.selUnit)).setSelection(prefs.get(Constants.PREF_DURATION_UNIT)); ((Checkable) findViewById(R.id.chkRemember)).setChecked(prefs.get(Constants.PREF_REMEMBER_PASSWORD)); ((Checkable) findViewById(R.id.chkAllowAdopt)).setChecked(prefs.get(Constants.PREF_ALLOW_ADOPTION)); + ((Checkable) findViewById(R.id.chkUseE2E)).setChecked(prefs.get(Constants.PREF_ENABLE_E2E)); } /** diff --git a/android/app/src/main/java/info/varden/hauk/ui/listener/EncryptionEnabledChangeListener.java b/android/app/src/main/java/info/varden/hauk/ui/listener/EncryptionEnabledChangeListener.java new file mode 100644 index 0000000..edb3e17 --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/ui/listener/EncryptionEnabledChangeListener.java @@ -0,0 +1,44 @@ +package info.varden.hauk.ui.listener; + +import android.content.Context; +import android.view.View; +import android.widget.CompoundButton; + +import info.varden.hauk.Constants; +import info.varden.hauk.utils.Log; +import info.varden.hauk.utils.PreferenceManager; + +/** + * On-checked-change listener for the checkbox that lets users change their preference of whether or + * not they want to enable end-to-end encryption. + * + * @see info.varden.hauk.ui.MainActivity + * @author Marius Lindvall + */ +public final class EncryptionEnabledChangeListener implements CompoundButton.OnCheckedChangeListener { + /** + * Android application context. + */ + private final Context ctx; + + /** + * The list of views which should be enabled/disabled when the checkbox is changed. + */ + private final View[] e2eViews; + + public EncryptionEnabledChangeListener(Context ctx, View[] views) { + this.ctx = ctx; + this.e2eViews = views.clone(); + } + + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + Log.i("End-to-end encryption preference changed, enabled=%s", isChecked); //NON-NLS + // Show/hide the end-to-end encryption views. + PreferenceManager prefs = new PreferenceManager(this.ctx); + for (View view : this.e2eViews) { + view.setVisibility(isChecked ? View.VISIBLE : View.GONE); + } + prefs.set(Constants.PREF_ENABLE_E2E, isChecked); + } +} diff --git a/android/app/src/main/java/info/varden/hauk/utils/DeprecationMigrator.java b/android/app/src/main/java/info/varden/hauk/utils/DeprecationMigrator.java index 3705f88..bda90ad 100644 --- a/android/app/src/main/java/info/varden/hauk/utils/DeprecationMigrator.java +++ b/android/app/src/main/java/info/varden/hauk/utils/DeprecationMigrator.java @@ -42,5 +42,10 @@ public final class DeprecationMigrator { prefs.set(Constants.PREF_PASSWORD_ENCRYPTED, pass); prefs.clear(Constants.PREF_PASSWORD); } + if (!prefs.has(Constants.PREF_ENABLE_E2E)) { + boolean enableE2E = !prefs.get(Constants.PREF_E2E_PASSWORD).isEmpty(); + Log.i("Setting E2E enabled preference to %s based on stored preferences", String.valueOf(enableE2E)); //NON-NLS + prefs.set(Constants.PREF_ENABLE_E2E, enableE2E); + } } } diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 0daa7ac..1381a8b 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -207,7 +207,27 @@ android:autofillHints="username" /> - + + + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 3dbbe29..4a0e7c4 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -12,8 +12,9 @@ Update interval (s): Preferred link ID: <randomly generated> + Encrypt share: + Enable end-to-end encryption Encryption key: - (for end-to-end encryption) Sharing mode: Allow adoption: Allow others to add my share into a group share From ae19fbf5d5ff19b40a4da55d30ea58726feb37d2 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Sun, 8 Dec 2019 13:41:40 +0100 Subject: [PATCH 019/272] Drop high ico resolutions, see #68 Dropping 256x256 and 192x192 sizes. --- frontend/favicon.ico | Bin 308898 -> 99678 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/favicon.ico b/frontend/favicon.ico index 3bb9bfb87f149584cd807e6578e2c702e6bb7d4c..cb994ab3b588f3d715060871aac3b8cf09087713 100644 GIT binary patch delta 121 zcmZ4VR_I{=I5eph^WO zUx0zZuYi$307!%MXRt6Zd~0B2XlJTq+RjwTti!SWnkdVK^zD2zSXKyZx7);$!U6z{ CVis2b literal 308898 zcmeF42|N|u8^`Bf`;v%I2%)khscgv-A?>^N(*CyTZGS6EQW8b`zA5Q#*S_C2(Oz26 z;-!o*|kFS||=FFLMo_WrhbIzQZ%P<^9ks;wQ22A5R3=@NO z9hwX@+r#lV)bOZ|hk_ zu_h(7N=q;Ijy7*sr{~O`8Z5u&MW{o1XmD6P6_rpWD=Yp^d0AD}=FJ_H z6e71R824ApLrI^ueSGk8-|Md9Uwqg$rCt5kuO~lmd1zFZyZfKtoP62-^<=H~8ZA1A z|CtOlZmqPoUiE?6R(d-FRgzBD3+rg#mNP->JX}YQjWc=+qE+#zZ{hnXNS&nKsG&ptNQe##>|3=%`O`v$||J zu}R%Cd`?h{>Vef{>Lh14-+A;=J?L#hTA!IKjvegSUS_R(P?XIl&jXHvzt+Y--tDq~ zor}_rdX_bOcRSB~_bSAPV?5Gn%8hpy7fm-~OcYO#_|T2xsbx1&`NR2;S%E{^r1dh5 z<+fiRcz23Z&86M>cRKA{^F-g>lLXBCc4$nMt_wkWQL2sXr*;hrH zyNX-SwZE~S)4|sn?kjJ-Iot2Jviygn;AM9E=jrh{NmVy??#w|5?Q3rbH`4S;_q(UP zGVQJ1$LgoYB<~PRG>neoMJu`}D6X>kH0%BdIqis}T78{A-u!r(=l}V*!X(~1sVpus z(EcgUzn*j2{UaUh%$YfP%|0urJkez)Eb}+$vz88T_HYe1aBSzk)i1rC9FE<_v&PoC zxchZAHLZ1Oj=(4hY&#)F^ny6@9w`pe&EOJ_s`*XOx)|K!BvX*bk8R`o#Q zyWl_P;39Y>n+9sn&u*aKQgI#=7cH}--)JU-$zS;)yFV{GaLlxTd5n{X%NuE2my<5s zr8Ws;A3U7PSe|m~=F;rS@(k8xzSv~E#`b!aUVPn~pY~mxY-_i2fT6d{z?xb6PK~*|q4(RK3D;|X zT6)y$*mVD6SNAsbVkB_WL-^$p$)7vk-EbU3t7`^Vd z<(39Fem?c=ww9mnJ+f=ptY%R53jf$T+DdnVHM;jwn!2Vn5~{Ib{fj>Qu1U7*Ivh|Cq|fdLop#p$T<^c8)kqD$$*E~gcp(q}$dcyF-q+DFy1h}eg+tO5GmhrEjPSON zN?WyGduIFOy|zJ}YrN^Qol!Rm96vC(&ycaT*FUR2@W8?b7L9`5Mz&GVU!XK~b%UJ8 zwO4vp8L)Xo-2?J9@&&b8>c(m9YH(!k*cJ73>S(wO2wKs`Kz5OBq`p<(c56_Rb%<`? z;O^olInTLIIy)E)aZmfulCO9=Gr7tU>0a&ocW1_q3T+$Jv58mikevrI0%OjR@wtjSm5&kMcnr~34?Usr<#iv7$#7%`FdYwnu4D0cVg?!A3} z=mqPwCLHSxUfZnew^}x43;&NX+n+wLO;30lAIsGFs6Xp+{D9Z7QO*C}B|T{M6Z`Qi z9UWwca8BoHxP~t4k!vzFQJQ~Tb(5`8l4g=3zn1ZEZzo@sH}4Iy?tip6I$?9PTS$v+ zYuUxkwMO{%Um9(6%JJe9E#I@&*43=GF{@8ZuGw#h-pdnCTkZNPKCRhY(@D4P;;S!u z8h2g3cysuSX@fQ`y09|1`l&tKrzcbDC%>Ad-y-ILr^B;+8BYU`#kPT3femUL>)US9 zY#AeqtbiOH&COQrq)v{wwJUPs?p@cFs>miOG8;lquB&}{%yYHe>O4i`Ru_ha_K(%f zv3OO*``93j#ElPq&J38de`jXzm_ePwVmLV~H#f)*=H|3JvMu^%$iPWQ=d^xp71`m` z_-TS29&ra2E>~Wk?(;Fif7d8q^Va9P);Z*Td`5#|QvN{?bsJCD*KjdUukxt6TTtJq zfG|}zvopPBwr*C-&AQ5HDW_4nt_S)z(o&8{d^2SAia8U3n;EK^@JpM+4i( z9vXdRsPwVf?nh(1k92LB(}Hu|&@ifx#r-p5Y-&ar3*tI9@okXVWXzK$0`~@+t=dYR z+_UHQT;)%rvrbL>*n-3KiqLlV-WYi%+0uKf+|~Gy^h*(0)s+6W*gB}~Ss$PND`llk z;x1Qd6RLE$LFNK(^31LBdvtR1jn~h(C%aGTvSAbc<7NIN6p8 z6>8pyk;x2gJ1}?HbG^$a+9@)|Zd`6a6y_O0>W-Zxn9$*92{e5{Qw z)^Qxfua+P`wpv_|CcF2XUo=<0I>YnKiZ^&#y?3%){Vp>mbvXLhh@t1=yBP#tFzDsM z^H-_8XjepfH<`>j9cx|i`Fn%Kp@w$#cNq-sUw!Y9At6I*s@k<{qvey(nExV1CTk0q zIj}nK{k_&#JLQ_ldg{&5GU^|FX6L&mk3-f;zgLKnsV5~DsWM7;=7Qwh?i^j0j&(i- ztxGr;X!w_0(u#T7C*{+o*k~HeuKGA}P(O>f^u4u`l}DUxe@*?kS>R@`>NlFd=bN3W zr|h8?AG%HV&g7sqvc0_T_X>0!r0lD_GPK^o(aTj&?oi|#8mn#S>)q5O@=;<~1J9BD zu5x8pT{zuiUqqw4K6*oH&aY|N*Xrhy*&eB=*W+q^?80dsA29rQl4{+#mph%f>N4i0 zK-Xj9*r!{)6CLDZjA|_z!t_nZ&1gP-5r21Qo25A&k(!&=8r-)Zpc;JLR#k4UXKY|^ z-t_s_`7wDqweu|b?WPIh6T(k;8MGScF|C1Ff}!$8x$G_6NhkL(DyJi@(%(k);?Cpf zyM=Y^Rkhcv8(Jg#W$jw%l8u|oS&e8ULu*~Ce;|4)T z=0!){iRhn|Kh`ZQJt=+i^uC@<1F0PY{9YdQs^--qRr}iERj<#qI2xC}WTquVj&_0Vkh^6>sYGyQ{;m>ruM_m7s^HNMNj zwPu0-uRIsGdv$t*_0@3`jDqTqHnKc*T>6!4zD|c;bI0>Lz1zNi>hbg8^(|J9<}d5q z-|p4jCwmMdj;9}ET)Qy_9{Vp_S=AqEV{uuRe>-9RE%(#WBkg=2+AB`dn05W$Bg@w} znjW~r-T0@){Ykfu#5Z@?9^iQ=(Y)%R6%H=nv3!P3{k5f-r< zGg4*^*iyxDL8IAi`_0qo=ygg^->-JZy*-+7nx#AOwT+WcH>eSEZFBRQ?kcA9wL1AX zI1qhnx8JB|OH9MgoBQ>3MTOdj@7r%nOV^XJj7wL!#Rl(hbvw#83HGj=86H*7G%2#x-$6#^llyXfb>kfVtUJ@1>o!h<@!IieM?&6t z;}^RpULW&jXy~d=Ee*{YSDEDOkk`Pys%*0!(Hj^hzedXkg9avUoVU`quI61W!;~3j ziCN=U`b}DP{DIkNx#qi`m?#gDn*P+3QOaq0;jb!|#^?3+-?`*G;dH-2H5&A7m31S0 zeY`(^cpaJ0ELmXdIWB+@Z~BrOuq!FZv7YmsVZSY@|}%$tgYCdHU+*bxplE`dxe9=XMQ34{;|k zW8-;A>G}0;FM4XS_s*#opEX{7zPnQ)QL5jJq{fV+3$1d#nvW0RS%lA9Bmq(F#JmV>&0F9 zHdC+UxOy_5TYH90uG#ZYP0#rcYFwOXpSrMX{^t>Wd^$X|S1?|6!@Kc=9X%&7o(BEz z$6r+o%v70wI!U|DyPY$o9Q#}JyXr8y(VVdEQQ8sL_wf!!un~PS`4MS3WpS zv8Iyatm}@k)^$c?4&{tG9k?w~p$j9G5uYi~tX-v{7r%bdHl@A;R(G1`q?yy`T27P1 zA!9~J`SI&<8&>xJ0l`wnCBYpwCgPwF6J z;EfoU+}C23>ZfZuF*1p>%mO*#w*G-_J8o_CTwS&2 zv4}?3cB|KyTP>~PIVIn>rD5B_x0Y+3Rzb>?PtzTcpfWP0Kfp zS<})bMP}timt8(->uOG8(l%_r*Z)9r*63k5jpw_JS6p#1d&g0owrEt6L5AG*nN#c_o`jp_&L zbmlzVRKxUkhR$ETZPG0}-nn42{Yje-SGOrm8$49je?$*?+gR?-C&T+$xb{`)VmWnR zzPGv7uEl9r*2+5X-Xqm+ndZqV&lN{p-tDD0n`slS`0Sb8-qdKcK|_-umrcy25vMcd`za?L=@8U+u=%`e$@)h(%jl#N zPm)h|O|256(rv_0sbhnBg!*2c%6y#7(U;DbIpwN%!+6;Vs~%4uG0hy(vKvf^I=$IL z-N9^Qhv3eK?#{Fu>vQPY<3FeDe6wRqdi5bvg5ZJUp7HX|Z|J#m-)G*i>H))>89eV4 zs2;S&aL|df%QQU{EZ%#W9prT%6rCZLX=40tqGoE&qHB{@F*g38kIh`TrdN;XZX3R1 z;Uz=Hm$jccKGz2?Kq=6hu=28w@d3lWMpH1 z`Tg8U4R&(9V`JCVlgjWs7k|{ZhiU_bw;KE|?h5t1H)qIgckvFG6}EFOb1y>WbM?_f z87EhH9cSsORg!|T`N66F-fN#Vf1GD7pR&_=6+)%YnDSTj!g&)mxMaNK={CQUV|c_U zaAZPN`&1-u?NGT|p5fES%h>LFQ=-4^CM6 z;?KN|?y*^Jlh6%0<#lD|dX4qfbE?lzHtLA6?)Dz@LuDo&80M)-2~_x3sZXfY?78?8Hc z7$?8Vfpu5T*!Z2y(cbl!LCTynBad8k%h+U++{DnazhjrYxdS$D3{&aY>w?3*^>-6@ z?5lAg`n6z*(dxe*)R?4oIB*ruKH*AVQ=|S({PbMBZBxdmPEys1&^M4Ze>SD7g74I2 zH7*R%*q>$7My(rHJ$C(KGiIjRlTnX%E+2pW_N@MUIJUKq=0)(bcrv_(p+T=(@a4Q_ ztnN7Y&p2k!sN0=2K*?yMInx_wh#PBl}s^ugA^rF*D>tLx)C- zrH4FRwq8Hc@6}+Hy`9HL*;gCx9@9Ilo&1Xx!wrv4b+YL+Bil#IZ3)LM!%3j~+PC#k zxsI2NLmS?Xxt1HH{6y_>mCbcN%BSx*u9Ks_Gk(vVb4>p(4AZwo`qAB{W+@mxx0~h^ zZl^y)(ar61y|-hv-CEiuN1cs)IC<_A6}xKP&-EPEW3b5rtK)kwOErknF?`&Ev-L@M z2d_AJ&&3_i$F{HA?cC+&TKRU1liT$On%MgMmWR~_%P{lrO;(qR$Y(N#@2^|;sZZFP zg}yh8YpCUVE=yERKQb#yX5O3=3NN}_Ej^&Tkvrq_wvn!jmmQlq(Ep%X&c;tkot7ME z7U$qQA<)b>e4Op`*(P4wt?TcYzhY%Xcb?ozr_E3NsCXP>s# zTeV^3&D`B%ws|c&S4~F7s@@^HaZZ-OZ4d3T(`YDjCHPFU>xVCHnEXb^;)?G!C->@s zRl3%CqTjU7eS^EN?kF~I(X98>DBHVtCOfwE4H-6hQ7_!B`ZpUpi_tpREMH}U$2Fau z5q(y#ojp9Iqn~m6@aA32(3){Ht}#v1GWv>t+h#gL8`Y0%nELe5u;m{uR_HBqQNEOX zVY52yF>t-2LDi6z0l6P*HO=8PwO8HsF~DZzyEhi~d!6xc$hhGWw`BXEm_c2vJIF+a zoOv$2VR2Kvq=+*^k}ot!R!>|P;G}Z(e9ay{=X&(nudnv3F>lC>Ci=X!DG4WcTco#3 zo_1`7<@OD8UygHlKOt(cZcCLRcDEX>`nb%r#-YQp%Nx%dvZD3*Sa}C-!pTIR_sSQy z4%X{A^j7?97xiXGPP%UQH99|eRo$T(0X@37J?;KS52ry)7iO4`>a65*Q`g+RG-3OL zHZ^M>TBW7PA;Czu!lPN0ShMX$(rJh21a=9L;Jjs!}`Szo2=1= z2{<)f`?U4+uKTWC3eHH1e{Hz!UEGpaAH7~VkFRPIZWicdvul!}PmTUtChlzXrls?F z*Q#B0TXJ*F{LHN`?P=YiMRgZ_&93p8&N7adIZ3`D%j~Swava;sIWiX-)$e@0z04I) zcUo;|MI!bAF&q0#h!>GGO` zGDE!(ZCg9trE87~hV>Yy$Pe&fW^vM(eFl$CFRDFz50~ls0@N70j^|ajA zXT#O((IZ3S{Kk&j+_O=~xfc4-QA>7L`_p~Qix%D|hOh1573*=w?XJz#q@GHdZnHjh zPn{S(%>LAo@lux;eZJWF+$HUPQXL=Nde`dF;HL6D3o-wR@blgbGY^A>!&O4ZJZP~LWu~%+#GPrjZo;)VI%X8^s zd$XQK%flP*Tp6LAbS>b@G_R^XOb4vKySVwnBo%(0J6+V)E;=mPfj@-jsk;CE z-_>3ZYPIh5&21xk%K43+eSD2?;N*mz#|5+U`UE{V&V3j8o_o5VKyF#`TfM%Th(&WgIfUncJeZ`bgI&-b(YwthM9 zxzE7&qqE0`cX8`-sg?EooSD{lPxZA*W7Z$r7^rjZ-6z?(24}caYYu(-DZp?*&u%x< zR`I6oAM^0^yH#hc=J%gm%T;Id$U%_-wK89wb2K{>q%m~sL}%BI%AG$o*^fI~jJ^Fa$b;{n)I2A|z?6yf*du%3FmH^YisKBQ z7gGY8ZttJE%S+DcZqwcE1(Qvc%tFv>wT~?>4dMEvBT5Nr^d-8q3yU0Hh#*X49IzOpCYtge>_4zIN zm!BTFIk%7O$z6N1hN$7bmVRJ&PIT+pjrddFb`H`6!Qg3_lZI;EK(QhVjJ2lfB zP6#o&Av_NGpCqw(vNx#un&DFdpN3(^HJBB$oBWL z7q*<)DzgtKLrU}E$L(^ep7oNdT&~*FJTrSxZkt8Sm}=K}Z<~axU_^K2qM_&4G0ziU z`!v>6XlHXbCvWG=byLsp@aAdH>b(5!oUN<$UHU{GHce|~Et4aiX?E|4WpePe=Pyj2 zc3vxYV0Y`|!-vZM(Ic~CtjC+PJri;#4`|u#{6P7g@&`0p=Gw=-{xoy3Uz?^A?vL1d zr22v7K?i2nvG#QNZ0^$MlVc7?so~>Vvsy3G*k}>%8l%;hlOLvh-h9#CoJHKYGir6$ z$?~1}%En{P*=;zXe|7&f-CgsRzS?y(*&!~D$6A8UiL&O zss85{rPd7G-7BHiCO_tyMqY;=U-c{RGV1k{pQVyoMGPQ-RhSK^@b^*S=4MwIOog+ z<@AT0gTs$4Hd1Wg!R(~Z>Bxyj2W!ken*3quu@jDY8`^Epe}3luu-knG9_an_*rC8p zsso;We7JvBV*^FsfV8gjlH0x6`0Sdz=f?D@p}jl}ckI`T8GUVLV(6nh?SUcdoI{7z z>gc=WAaB}Hx50HD&r0l_ehyIH4QXU}_`@_y4lL+|gJiS@&* zxsw#K65bwu(@A60Mr)IV)mGD@O{_Ez$Su>{u)}}rUVEn?iv>50&PR-o8ST>Hjhfrc zox#JdoH2{&*(PnP+y$5CCL>xr&h*r@^Vje*^xHG+M0FGWIi{DU?UP$(Il^P>yHS^i zd(>91e#XxAyn;{Lu2I7x|7f8c)Nr7-&JY8mEdSGoU-cOnk!pFP^XRlDGp%-Zy*qto zteF#ybhS!+H)p8Zk(cSAO&n)yA9|j!wf;ozL4~@1McKcZ7d3EUbY$lT4{|?@Uso?| zbm)TNpGTkWEt}pnQSb3+*|0N)&iB;0^^W)q5prt`T&5hX%*Z9xp1J zhy%yLU_c+4V}-RqWv~E^>z@VW({vvYBJ^?E&HzWjUhozO_Z7E1h5fpqq6yv8ssYh^ zns^%7aw~3;-+vnuyO|Y${Jc1S7S{>eKjM1f_Cv(u2`Y+^-#dY2APLaDO?-Td`a6vP z-Md@>EB`GEcp$safY673Wtok5hEx#q)kQkyfOH@%w={-D?Kz>)w}Gh-lniKGYa?Lg zzhwdPf!W{@cn^N`UQk>)p=13(c?s3s`oJ2Pfm(q2D%6iZ1IVXE(@E*0XBoma8l{!W zgfPrbteXORKyiJmy@cJY{3aHlGSC=|08fF?AAWX8{uKqNJz*;b`KUQC0-At)_g97b z@6@iLIzA21^J(HWC@Jl^*tZtYec&<>hNHB<0QUg3<0zgmpbAI~Kyy$Bu=0Cafcl8! zPePyg*(J3zs6M4KB!t>lw9W_DK|I(B=$_LP&^b#Z`Z(w?_}Mg4cxtCnI`h7B{9Wv) za%%_ZTr~bg>GuW}peyJFTmU^I;R04FuLY=19s_QI+u)YSI{EM?P#S+L=@_b~13?u~ zoRD4z$Q84*6?AVvNpY8jp32B}AbOvmcw@g)r+8Y@fUsGH(wnF#)KyV2BNt*4ul8S@cSd#$RiTcfX+ZQ^r(k;!D6VY* z^$!d6Y4-xrxPNAy{F>@@QE|Zr^*?_mo{AXuD)K=60#?e|0vuF3Tt=Dd(b3={I1F}! zGeB5h)7*z~Kmn9isGSlGO6z}APf|G)K7Xx__5HsPUun}JiQ}mMK=(;j%Gm;3Mv2ZS zSCwftr4}O4d}j1eNVdPbd9LbTiX5_^$#|H()theA(jDQ8Km+mET5%~ zvmzb07WomDZLG3V9u{cC9KeA7c?`lQTiP-yhwPZtDUQqoH7%wOon9TB2SPu2jPpXfR*yMfRr4g<7vU{IBv_N=Atabq8v0p zGU$2NdLZgEA=v)bxDa+1E7X4o2fs2FMCGp?hyr;)G+mW_o%%*pN3c@97NC1BRrY)8 zGD{c0@884!1>mAJbJ~~7$gRabp>L7zMgVzG8kvTJgmIN-N#i>-&iUdO<0;L#C4Edh z^5hBVnKvtCX8{^l7z+$bh|(2$8soWDlNmhBj=6KA$o~b6nTs!~Fiq02Pk8Og|0jde ziXRUA)q0zr9skNbM3|R~w)}v6r2*>GdjM8`-2z-h5eOcF&OltKu89TY3wc~dA)*O$ zbr1RhsQ**o{}%*R7;8~Kp)r^tptRzS1NlHSPImqO?Lp588o%sP zOjM1a6RqFx)nX>Z*)q4X$^RQLhXnFWEfN3Zyyq~o1E~3rc2P0WeOE=}N`C{9IN9}L z>_PL1ngUizZvk@9S%BIKPXNuu5|>&~oCHFD%$H`WJ*>q{$~9zy1*%MQ0sLIJPrzZM z1S*Wp9zAA)IgX*e8ubHycFd0ML&E2^|HKU)yZ*%nJlI1Iuu^&pG{m8)fc)wnZ~^oT z$^zH|2hbeEfUnxqTt*&#uUM=YUgv_I&?AJw0a`27b0)!6B@OSfD4?sZT|IMdKx_z(#Nr&>YRzAPZ2K()uDDM|}c% z4)q8e2VuYmXn>NWH+H-Stj~XKgO9LDB%pC;R({O_eQ>zY|H)53gYxpXmpE4(Z~@Xl zTnwP#f$~aEWjPPcJ7L>BFhUtfJ;Xs{%KzrIr}oznAPdB00u;YG7W!}It*p;Y_v60e zMq;;r$pX!AG!OinbATV<{8R?S?*mTI-3DdtN0l)?x?F7{r4}k(>2tO-zd7x|D4JZQ= z>K`8fmECuLCH*vZa2U|Nkd+E!0S-Ra>4HJhlxdg?LG$78JouKNHZxNoRl>L^lKwqE zpnLpdunbV!pT?r7pZvY!e#iZvkZ(Tl2ekn!70d$gd3?M-*#^s(`%*+#D{!eB1 zO-ZJo#&k}Cp@1FFuLQncQWBeB`kXQIgoAOQ>E!>^2e4%J36z-nlJuX-8aF*-JP*bI z1Heipw*dJ+e4pAut>FJpsSMaKN$3N&sH{FJiFDI^%|ze_XwC^MzmWy(;r~DjzJI## z9*`o?Xa2(cXn9HaQe}#h#_&&oUO*F+=9>UGjFpOQfkKyr??-^oR0bL{C-5GOE#8MI z^|Rp0P*z4h&F=b$1Hj0XF0r)$rsM$B2t^D8?h#g6I(mx50Vl%1W~m@1|P>@an@34W3V*WMe~UHE;h zf_4y%iPCpcYBA#ka!ifirVQY_z=EobgP<<641S+b@a%)iJMw?hj=7~q{?E#P!vf~` z)FxeTjDyz3G9DE9zHpg}anb8|HZ~V?q>U?mpMdUVnoL*p^G_n5sYM9$nmh&dKg|pI zZ(Mvfmw0hSQ1Bc8z956^KJWkiU+5dR&`t;|XfwcUmGWMI_Z|e=OfS^=@#20@W&S1V z0M{n?gg~=`SJznQ_^(*t6x_LJK2Ue~!RzA7Kx#o-L7iz;DSaPfn(+O;1?_x%2bKI? zoWnNEEnVnY|NpNzeo;Qn(buDKP_%&!as4kA;~NFOLhlJsU!XFMi_$X{C^t!U%Ha{zZCr6?_2&KZ^PWyYQP-*KfADsN^JppH+~+V`7GqKpTSH}tWf=D zU)Tn|{r~o6nc2!e8pwF|4Pv=(TJ zl+$3I&}Yg2=^K2-3i&_$K>^GEYXcR&BfLwX$kY+WRjHPWOx=S1d(k+5Q8}RRz+d{H z#q?YRgZKlQ??!b7E5C;YXx?rf5cSn<*e>=R)g~DCb3_vfePL09sm%%md!Pbqi?pviO*pWj67#9$zl zDp)ZA7J%kKvr>61K;Q6R0gCfcKJ@*-SM%EI;Ip=c^FdRTP*0)_M1GQhzT;SdETdgq zJe5kP&UhE}VW=NK-~PhyeH54ejO-@~!dE_u<<` zOM0d-2^auYBxeCFM12^r{$KHI@B!ClH=uD;RwQEqx(8Do#QJ~5v;mEy?f|-gm9n?M zAVf*^4D0_D*9LTbCIEKq2u3Jl2|#?Jy80j^U!z$xSF|6h)d`o7Bn>+==g z2K0>b4u}Bt0V}_50s1Y{rGWMS%3}i>H=;RNT>#DBV&&H?K$^ak- zSSh^)%y8&g!1{mXvjLTXoDNec`_cyc`b9 z<+W!IE3^RJM{j~3c_)VLGgqu~l8fsW1=x9`Fv2fLYaC0zWy_XY5N-EB2WH zj8I0Rk9eqGKy!uCKsk(){yyJSfxW)xuVuZT#xP6(D`jf|c|=F=Vr&2sZ5JspM)(b` z&iKvIX85gn#j^H~-#tG1J{Ntrj1|dPfSwhk0*U!iHO3R);k;S!ZAD%DE(QJW^zZbY zElJuAaZ@{WE1>6^tdzY4=>2S(gD5for{5xK$eh6MY23#5IFkehOd!g@@ALbrlC)h> z-1Kg-EnubWEkI*KWM7H-Kh^j3m=y*7Pv6+3-_@wi%n-;kh7vocB=hqx#7*~r1%RCw z1S6EO(6>A2S&PJck$ykSfblQ*J*C2LtS6%k+(4OFEl|Vvc7O5voDxg7ByCcPxG&-u z3&2X*SwII-9S0Kkp<2v%`2QXB1HSyu82v7(HFFyGf?4>jZIep-TN~wJKb+$u7ztP@ zI}7wdRGC2H{!iZ^r}n?7{}-+o{yu4aCQeYD>BQa(zI`oXaIL7n#>%f-;9I|!QRd~L zxcdL=vOvGRk%-@HnJmC}*vtIag7{2YT#4FD^@W&wR1PQP&_Zr?KBrr*#rVWNJ}_l0F4 zrJ!F-zs1BpCo8x{)c1`8^iBsWzixq{h=S%8mbt%+#>vI+&FL`1QJ*Ij)9)4fdP&O^ zfjJY)J|if+M)Y1V`+FNOLKz6nC5#0U^?iz8kueomGJ8rG_bJKmzt-1d)(F&@Rsto) z0>8_rCjR@S63;_<*y#t(@c?0(09Jn80yK|gH;}mR)L;gp?!W$%ZJ)3Gzpzi7Bxu0w zM>}eM!EgG~IH?R%{l8QesGmmfbF*_GV1!>3MI65iNYwv147Xq`(~h}Yinfn<8KCXd zg7G7IhSr$5ATVXtyi#T?>Akj>lFmy7i9ZkNp!eci0ai+H0c!uyyI>Oae<`L4#yscz zz?f%ozWzO33bm;&)Mb{_d;ni?Mbi1HK=IRDT_?aw=`FAohf37{KPlA^ejku%JxOEtg?-=ewPi}RKgEW* zrDIp{Q{G?YUJqMT2Fl@`PRZKu2d>W}gmD9`{G0{M5&Qy>p#O6OT1=19)ayTV4E%nn zJ(D6|jQ^7*3;?z7e)lp!_qokL8L;x>7U+qf5}orY&8QYV^DR}~{=G7gi}qBP#&~>< z`O_ukS|2+$g7-jz<@{fd`#JK*jt9XAKPXBFNAD3y%qQfTT7nwPpn@?<3AFpEY+MDV z0D~ZxzLjm7Cp;TGm1KS^K-}AqhAM!S?^}SLA>IRJ?*CGZ66WuB#JK$0GI-|o&HKi3 z8zw~&{a-u`y8XQzQ`=xPkm&oJlCbAb#Q7Lu+XGg$0A(7RK=>o@QC$XjE;O34LNDck-?yfhX%6R^+2U<5ItZ^ldl z^vv)_Z27Zc-XPwA0R7QqKMDGOI>!5r5kH-(w9*#`(zkzqHjkAdJP-MzGS14^7HEV` znl~t^Yq%eAYhm06{(s8>{`piwWgr&gK1#pn{{~3w8421#Qa1jPcxha(E@0)01yr$0 z{l0wgBR2fmFm&Gy1$Y*9m%*F_ZTRPI3HX0L;vA^OWZ)e1^KhjWnlDFfAW7NdXYE&( z;Xfc>lS^%x-|s+b3sSpBlJ|QWPl+PaIWaGoM(|De=@dZzT9#u!cM)Gh0RN{UtY4DW zIQj)BOW&&~anU=%a=&Dq-{W8zWTQ8@4SJ*=Na$^|8@VTdY^os4~nx@S?lQC6H9;r=oCT?zJI(7{h#Lh z_&LDu8~wWPQ!>dvB7h{G1D3UIehqSd|I=KV#XvkS zm3jL$^0Ngv0~+W0-G!cYbOSt)10;Fv$p2|SG?%Ig~bjdM}GLeCWH z0z*K3?XLvyWfhFk!1rk!DDxM~z#)wJs34B7&QV4i{Si-jw-w8yJkUMbr;Kb=(V`Ky zb;u#g%kPEPjOzXGwlB&5slEpK@Y@r==>LD(GB>yqs{bvJpG!cPzu#p^ez6wNcbwHlkpm81=H~g*?*8lW=R&odMqLgJ|H`+eh z^vuhyY{z~?)87gk^bW0PKB<18cG16SYti)l?D{jLy*c#Cq{HqrLiIn*)j?mN z4Sf0SPu>G~7(-R3asMs_?)O8dEN!5gIFRftZ10f&)A$|9J>I(+_=KK|wS8r%qYgz%-tF z5KtX2^nZ%$FYpmmhPq#v#&t-q8mR2j3>lznAOFXg4Oi!L%i_hs_S zvb0Z0anXDjQ&477I1W@WAE-V2pWfg7k#_a+p|O-D<#Y{7dre;=eKeoxm-}ppqoRl@5<-2M(%R%_ zk6VLt{qz{m_l+f>F(9HI{QBCr{2$|`VfM^D%$rF4ezfHmLK>%<0zQGCwP|^VPel48 zaQzT#`3pV2mw2i1ypDS!u-$DW~~1lBz$bTv4CtIiPxj%9_N+3~Vrt3BL!os0~!o zH$u;$?XOGYoI@p#(<15jE;UV zwSj1kgXlfr2K>JUbmf|+pEw!$za-nEzfvaX8q>p)p`fzg2gom9fuFSp+29D+1gIENm8q+9O5gf2HrR{;6@zb-BDTSZ0BgXw+V^&~!h9Z-E= z?sYxRS*$cd(~G{73*R3Oa*E2pMJ($A+#f1r+n@4MS^S^cxWkJze#KYP^XuZ;nAElr zEfe${hT8hH6xDCRwy0FNXMyDZh2#6ljp6UYHqa7`cVd1-g?i>CYDZeP2NiMrNA&s& z*Owu!%9Q9zCNq#;ai6BTUR+1{q`8{+fH2Rr+zKkZP(K*R!T)yw`aQe8G)KrvGVi#T zl|5)abQ%z~cST#LHo*Zv^B^i)KbW4o7B}ug<2BUo6u$?MuTy(Rw69Pp{J#l&z33h= z7QTNT6#SNN1#bJp%mvR#&S6=S&r`dDC~1Fr=&3$50+mr{e1__DaXUSMjt|7!`rDxE z1Ret6F_psqkzVuxs0^f7!1u#3*B`%USE1WJlw=JM3xu{7UFJX^1%kl8Sq7->Ol|1O zDE*LV;h2r6ZTCTc8i?v>ou1#CfL%a%ZxC<)V^_r#nhyv6m&MYEe4oCVRl!9D=?euy zTZ>)~8W*MU(YoL&5Zzav>(ox34JxDfBhkg#HW+$p=ZM>JKXf%fM?igH+P?u*3ZeSH zF~&m++d;P~MVgVw381)ZlmxvQkOp&s__Zp}?L#;}-K#2-Xd$uGo)Ndx2j~WZwZ-h) z3mqLtW1Z^&-TMXuRwQSEF^F2c9ZJ_M9thjr1F%kgHt}m&UfU0FZZA-ogvJ*5K-@-D zuQUP+fq1<`<)u0x(F3kPnKoFFm<4EDJr{^yr=!r(H6Wq;zy%;4ro6VPFESKVCUHYz z-vDtNQ5#;;x}Wk|6|ho%7U+OeJqM!Kfz}g& zF%ZgRtWZ5H9*+8H^bD;m%fLCrNA;&JAb+TAqig;Vi266hPwksX;0(fnc-f(Q%6K3TDx-+k?bOac4Lks~jf6t&O1iIR z1G38)z)JaAz!0aSYa{e~;gZ@!b|Oike%ZBR!ciM{B$!pqKC0WdfP+BvnLeF^+Biml z#y8di;dzD24Cwj;QGdZ^r4f4e8wFN_!GPLw#pOj6dVjD6Gy$xXuLU%4x&uJycfuvT ztLG?^6l#wgEhZfKHT6&1fhWc6pAB6dKxIJmK9Ga$iGZXNp!Ur|xJ7W8(-K7WA z!E#XCcnQU=1Xy7$ASnw_8%5}^!sRpQMB6_y*rxk5)%U_M!sT}8q`@d4KBi6g5f>l@ z<^a)qz=vHtHj=!dgI57TAo4 zMSYUic|acsh3fCofZFZiVabQJfE-u=J^|4%7qCq}PWO;?V*6fVyE_mS3P4sa-Ak$^WN-;@ba5psxW)>VZo@)W2zc6(CUrv@Y5Y2*b84V1>27 zzhQym`oDjPC4~H+bmDCr^6dklIv^qco)5(R{}6P-er!XmpDzkS?VsL&71jd(mIb!r z#G?L6bv(88MTPu-Jjey2VQHPlJ!$+?DC4jqTEA16G|;>#0QGa-0kxY}0D88@3TuIX z!vfUq-VQ{4`ZczDh$ZG*{oh>(OZC1``eWrC5RH@W8=_^3#)DLV5@3b3z`tbyYUA$& zqQ07nZF{i&4JzFkbX<Ef8r+EE;2)g3hBwe8w zuh+@{ssA7bKz5zAKn1YCA4mf|izR=hcFGJ;oOna|7KrRf#TXd!gMyninvdIhHkzvfXLDS>r|e^?-ev3j>;P=tOY8Y1*l(}1w?(C z<^zo=CL=Vivk8cggHpd(_^hm$!&nt-feLGZrbro$jf(pJb8LH-km8=ui;ovQf{x;0 zg|$FsvjF++DRY2&=^ezn5@ASOuTQU$kh2y5zuwL(5 zf!Tem1uC!wCL(RrhM_V*bC;dK_k_w#2sjFQ18KktYk|sW0eVJ8e*6%u0h-`PgxW|3 zK;}mRvtd{ZR8R}hJWv|%p)t>I^?NI5^4Vmu7GN#FT7b0xYXQ~*tOZyLuohq~z*>N{ z0BeE&rUeA$BKi8opC(_b1U*x{o?!xt+ksiOKdaX-As$W%`eGA=^A_j>Plz2=sLyA_ zWBazAJn-v?0-@2Tvxw!7^i>P=U!Q^W^hzT{UnC|z4Oh){{lbwXU7-4Kp=XAs6UA614OQu zs9r=Z8qb$xB7jJE(bEbsQp#NI^ zkGFlfScL(E@%*TsS9D&n@EoCDEL$9YQTVU*`6cRwF7$Q$dH;MqwG%UxNOh41AsbFExRf{!2oOysBtXlK#uJ5B%zY zFM8fr`Y##g|Cm0YgpB>87t6pu`hUt};d#F3h5lctcNPfc>jagg*B1sC@CqX+`u}}> zUC7 z&O-kc>H|yA7phT%6|EG77oILaOfO9KKYHh{PK$C_6i?BG=h0>$nHT>^iY{=$dLT7E z0}ED*^o6JWe|>?~I0bs1X!ruXGmql=O3$Z2#pnwHIrHhBQxHU$p8wYurvKaeOiFc8 zJR;!(3iL%6`iovlkV&_wA`5=e>lf;aEdE9BjG(kt7*U}x+s31^O!d59>PNzqB{L9G8diCFl)`)8|VuR>kWB zi`By&%Svxo+<6gyNqWZ;!k46XDn2~VU&JlH7*4SKP|F8vgkav2YtPd~*-+LDaL0GB87NB|bwLyJAbLnX) zX$YYC__vDI**W$w#BV6{jWYU%Fdxu2TGasweS0_<(CFA>-~d?p{VY%ySxN_Vowoyd zAQakn5752AOmFogV=53A!)D0oq680189jYYhOB?k7;%=)EW|5En_GTl{>#^LAfk?jsOh zXVK+n=xJPBR7dN_Kvf_Kp|*z;SO@4goxFfh8e=5{xC#~Q@|P_@-`1ylKYfRjuDAHL zE^hlK!co~Q3!!_7888I(0JS?G0pWQMVx7)K_a5qxkbV|01=Lq1e_`dD3C>_lK-8ZOT!~QM`jPMge;{Yqc9gqxC0p0IKuZ3`(?g^q$y+POMf9;(Id>qBu zhewiax%b{AV;fhDjd3qBy@wJ?2pv93=rsfaNDv_O8hS6G1p)~HObA5u9x%nmT@=%c zG1!)I+3)%9?VMMqD|=NZ`QD!%P1%{*nRjNVZ8^;ibnWW*J2nEp==;6uYy8*i$+(q{ zO*a+uA>u#frJ*))FT4o)PM-RSeL(G_2nrIqK1QGjp=-r2x@I)@SD)M$W`OEQ*WXH@TBAojALGVt%I6=})HH_Nr?y$j0GjCgWg!$Y5jWe@O-NLL=Kd=2g_36^* z9iZ_!k&Gw67Z9Wl-XNUDkxL*^9~~sUOoTa`ylkk_t*hDX57hr>6V1GPhMT{RLlJh~ zRD)gZA(0FvKn(<`i*E>bF=##*Bz!KyC_n!BC(b4(T`19XXznXnE9}>ln0AkCZvOZ0 z31*%%*1Y~fOEX|0{?fZl%?%PsZvtp-5Tvej&DRDD8zj9_9fIMs`;{~2&7oCKFcc>P6vJFEl3^;g8>*^+|UIbA08`#S0RuNln)--b+{BNu5sL7qE6QPg%(Pv`` zvxK5*-Z};f*SuHn))-IyPY$(#$_Zw!GsZlxeSyYOxAF3+HP1~j5hB;Ew)h*o>0STo z11hNh?>9GZIUUT@czVtEW183A0~HYdM9KzV(Qy<&y;J{lyU+$+QXkOA9G&lV94&vk z-tU9$L3`5$38;aI)cI%}giOt7HCFY+83NcAsnUm#Sb7t^vnr9ys32 z?M6FzU1I?EhMoTA^t@aP=w21~sGPy(>g8k2E4yxFE(~7OLdd8Z7|$cVW6W1Y)c?ic zs0~aovz?92b=(^sk{A7e?o+AWhnO2_pKm#gRqszS3;SSlgX_bn8lHXH{NRy?gERqw6P`xg`_ny9$z?2KHiD_3FO| z=G*J*|07r5>IdvI0-PzQZ9#S4%N)iv|F+v6y}Yf!+;cpy0bS^3C%y7-fU&hF(#~IQ z`h42Jf@1V~g}?sQ2aGWat>uhFTrv3*vNd0#+dpP6G`?k;b+Q7Vi#dG`k(9YoVe4&{H8(bo{TxA zrjN6!naMmv{r_xdkh!w9x#_%sW6#3CaOMk^AQ~7+pijWo=N#U9nG5W} zb-!G7UuMo|u(_Y|srla^a~bu&0P_m}o`m|Zqe0IhPJ{iy|6B^rf<$Ow0`4!s*1ybG zLH9>p>0)*+ebL2-t2{bjCLV?uA8*AOfV_4jhmuqJ}?yYeJJgdXP`gGkNd`yB~xwk z2h!5Jg-u;LQNUwBqHl`GU1W+Idppp5 zRH}&({AASFLQpqKhadB4+Ya)H?4Zo zxUX-?tbb=G^zP1Zb06OqnMT@;wl7$UZkqBu@IcD9GTr)bV@7)KZ0S8>y+;~C&+ugPf;d;0AFH>zmeM5UQF|G5Yo6i*E zYoA2(qoycZP^fPqt$OKbD%|%4L*Ecsf9*%Mfnnx$r-kX6*16KnYm)K5B>XneROJs8 zEUo$|F>U!~#P#mH-(TO+?X#Vs<~H8z-{1YdKnv3&-`5DbAGmE(t$fN=nQr}u=KcP8 zU-awWwg-&`0z?J$e+)&K7eS|>F%N&EV5Z^pap{&;=A zt^2y>Tz6i;oZ%|!U+?nG1ihb{kL^J1b0=tOas~xTOW!@rZ{6#;Zr#T31AJkBqTTG& z-~Gu6_ctf?oU-;@(#lho;=e|^<5hh` zAQcos^V<}3T*Q68GWUCd`fgzIW53%MEa%$a9avny6RA(N%}V$owfv=0roeIZ{7hrN zC#d(M|B|9^*R?Zamos-?8yd&!?)QcKqW1NXdqW=(wy)}cLiF!BwIF||!Otn?Glg~? zs0}P2Kf|Fhi{1%X45{d7MNf0UL;Q~54@uN@WZlbs!z44GZ~4{f`y+L#XV>pfF`ucF zEl3>AO^%PuXd`lK-lzM0sjjuY>}0msm2btonMVDuB<|i|_H=)qD6aOl=BS>{4^lU| z3UfR8QUBB!Mc;i{2&wAoDSj`c3;Q?`-wX`Y=i7c@4s~Cq`k!pnoyHZ-1^x}G<~JMh zG-2r;H`n=jQ1^T8fmC&(wfWI~%?bK$+^@j*&A_DU|JHG4ZgYKmETCu}kZOOGYFXpO z)4X)YfO*fCV4Aa@0?Q#@+2S43*mn%~dsP4WX5f2Zuj!y>Ephe(9d^aUU{Y;hKHqWONcFF8&<7OtTTei` zdCx?Ay+d^|Yy^!#dXh=)TV-kvRZUtQhnc(72mBiJUFM+IzQWx_y|=(5nWQ9N#(+iS zdQ?(5ZS8E^KyD-zCGllj386jLAuX&XuQ83G}he&XFy+|?&s2f&uK<|-ke1J zFCyMx!ZY@NsAQ56ea}l1hD`L~nMh*=c{wbZ0^}fUH_mY)WKvI6UcNPl2+OWigX$6X z1mgfJ5qYDHXo#dOgsgvAXDX2I`=`nGV(hdw*Jeh z&$$8We>Gta*SCf@ORfINhWq>NwV2A6#w7o^l*uv&P3so&kPrJnyOWO^2%7I?!aYm6 z+JmVlJi9>~nBAOuy(2*Ve@x#$Oyfop>%PcymgAv;L-|+TzXh3^*CwMAh1EJBAHEZy zb;!GrZ2l94U+$IhMoi5m>&XP}+f(oQe)xfys}i(mYC4abhwy`Hh_pY! z{eMQ7O%!&#`zg*qNyiJMwScbsJ-k5L-^le`K=Z8MdgpIQL)U}q&0m+P*tJ%io@_yE zA3)t63R>SsTL(PG7|}*^zYI!C(z0_eJ(Ke(q*Cr=L6(Re{-&U*!$~3KAy}# zxiywO4+9`+na=cUTxc$M6!omRo_`E@f<2$MN!;s6`utu5tp$SCU#j7ja*pfa2xtQh zEys|7T1d4#+9x~<_JeXLf}~|KeY>u2#)1);pZe;59rjasC!AxmlTJ6u2qzIfRXqfa zr@sG?B=eQ6;JQ{5t@|}6_RouD*S>}I)4ZVs(iY9_pf&aVcVGQ~#k_wUZncfH&ygSj zT?=+!AZUH25>E5J@d@&hi^D4KYKT_`>EmV4*sgHeC-^g*38%wQ$cRj%j>oz6{LyjL zxA$!DNB_p&UiF^?Ii7Rab{$VAnK-8Ve|jdRp`;xr{ue|mi|X932i>z%|E|8`NpSDq zVP;e&hw5I}`KHwO=b$;j8I$PudDbhV=ZcZo$@oXpN4ESl-weD@5+l#4dXFID$6(9m zuW!uOwt?@l>%NF}tDnz_Ok|8#eZy(+9CVU9M{*eFNi-h%>n|O*LM{TU z7wO}7urn<4`P<{^nCfSA-=!gB5+}9Sw#uOO$faQWd#&$uEvVe;eD{K!iRxd^^{d~{ zVc)Ov^#qmI*6RxFzXtWuvwZ&cc)I4pq}dSjzhgMBz1CF*jRk7wc0TYO_6pFtUh|VS zupx;0MSnf(xm8^QlD|N@^=;!z&)>q6&>3&o+|unA~Rcsuz0aBfhd zzW5Q44usZ32ZBU%HO<-VF*|3nb7t%BH+79a0h(J|f4%poebKdnX+CtErv%LnW`nIqy}x%3==!mK(&x)y*N_@VcY{4a;T2A4-Va@% zAw>PZ`gWD80(9TgijHjq`+;o}_SlAE{)vLx`jmO9S8$CDS_|4^s^gR3L7$(l^IhO! zpP!ERi11Tft^GB>DoB25Kzkjwj%A+cWv>tEOI3@_)J8{7=GPGw#Ss@IEr{<`kR!q%Yp3O5(Z!HU{P zd+R8ed8GmM>9&q*u-p6FEwJC=JEky~fMgf2`-7U_oeTF?83ujhNm`eUk&10^@d|$wh5t%;=x4-`F-hhG~1zOkJ1F>#X<2pTeb;!a0(DC4IH+d%<-pfx7DY@dgms+v8{cz>oz?E)aPm%dmFp2cUKQz%2p>9Z zY;hdVZL7z+!x`|7^W)zkvF@@6w+&#A8!tfE?Ugv}_Hqz*dm0M6y&Q$zUXj{>vRYeP!6~p3a2aQh1F5no8G@yIM{Y@|L@*zPq%l?(x#9BFD|O z7e4GR3&k(3_Kth*it3V3spZzmaJ$(Ixr>|{YNDD0xY0K5(vZ8NzMFU??j0vQ?hejU zGEx(YAL*`@R%_QE$4wOw(FX+iR;{_Z)96pC+^_fa| zovX3P#$>cZD6{VHbx21u;{kq;U7v}pVruE!W=82C+n`fPtrd&Gssl>FM?uChFl;Qay zq^hrM#G6fi`bW!b9{C+hUNnE00-A$M$I7?U#*A3e!Ay3FOmiq++ro5zw8)ek!m)`^ z4w2)*pYY3u9#c*8Y4Xzv@>O(g-UXVszXdh$HPk|sG8f5DW9$#0`QJmZBWNBZ83{EI zC2r3BJ|-WNA>U;;GWZFks;sddnGpn3U!K<)Zw zxUQuc@M3#2(P?D{pqt`Y`qA}pB$R^cB38Iu`0HA}A9_M#61Ckn&>GaQmt2Gzng2{3 zXJ&mr!aU$KH(lfDPxDjNo%*GC;j(#5#`nZ7Qd$`BgeP%0}Ez$?nB4%$MyZW8FNYd+C4r zL^J=#GV{l5=rENuYf!TG*~WnS%_b{q|Ii8jzp{z>r&DVBrlPNm#9{5RrpPqA9c61z zMDwjCFZx}iDJ*Nb1}@~cGn_%@3hJPDM)ZmD0;i)jcn`Dx%e~AYU0a%dObI$2&{=iLUoq9u&=F+|B3U5gN z^hN07X{WuJkkvY9ZH7?Sccb^YoO2$(RW*02e!C)vzgLnMl{}sHHaYaWO_xglp*mPV zUv#CrH$)~gUJvTybe*ZLy>q_AZ-jLm?|zmcFVvFFOmrae(_wF5eOzR(f$h-6&msNK zM?d#C#imupb}!rYlWg==UYfW_-~R;ZRqIOkUiJjG=z8$^E!yH$>Ju~< zRsZaT%edLhbaGgKyVn479?d&faxUFN*4{{W(0z)gDC&E(4`+4y4EBvw&_4Wx&=~Xl z?i-sMR<$*o+T&R=i`;onA9F0%STlQ$C1gYU<=QKs1-f^k{5Bq9JdXvK&p~5)7D)HQ zbT2Gl^qGl#=$?@FN*jZ;A(NW(WTM>pl*WH3llmr&Z}}{`|M&#tlkPH=moF)o=B)WX zPVY}1S2WQ&&^Y}&$mi=oYXFS_n%iZn?d4NC8l%<5@>%r!#-or=ddp;9bnkdlK34#F z)0*-gsD(_HJzvt&J39F;?aAQ2@GyJ}N$HMzwn>FgHvKfizefD7&=^Gfj(S${YY3>* zBB!U>-|1xzb=Wfs7$<1j840KNG8VIaQ-MbynvqNR(OsARY7&u-s=SVZG z`q#+kxbezMIfz_o&oQtbJDcsehc%lz_|O?*ZgTG#XX~C#GWqg_{TzE^SQ~3j`j{`@ zf$E@>+0xa0=pN#HXEXDP)7u=CZ~f6}lq=t58W~J_{OyCTrF-483^C8GkN)P=tlhgO zpMlzjFWgyqu6(T(wYQs8xp;rZ8Eu|H@AG{6S3S&iHaGv{dD2~yN-x>-2Mn)yo$hDm zvuF?fRY*c##ZEb&pYuIGQm+oim=_3h4)w4r?-vZ>dT5Zj>LZjZ-}T>-Jid{P{&nwg zxVbNm{zKP+o^R0j%yqJaI=Gp0e3i_3G7xqxY5W6p&n90*YcQ=<0=NH`d|z_3`475{ z?0f7uP}k%-`THpIFYUc*jj<@1b7d&(wWQM=@=fxP_`w2XYe!0B)PfbQo&Or86WeNMQM zjLV!NEHkkE4+7VD2EuB;QTLznNp!E`HVC8#-q))gVs2SG-n{SEZzB8S=)WaImU|Q) zN%ea{^BE(Y?hR}Q`6Oe>jMj)j^uM-~+2WrQ&4RB2wtHJI$4mFq>b8HY16_OS8-koa z6JhjTO}>cc{Z~WKaxH6RHrgBgKa+(1za;D!=|9G^&02SBeiAef>4tleb7{Vv53&uJ zNMgU!{T5<+n6i+u#XYam_S28sU-=h3r%4J|{+K$2@N_qoy5 z6U_X{=#%#)0*$?=O7~-<_dQkzL$TCA5dEbaM)xK)2X07FpZ^>rTb9p=Gkgo)xdDt?(bf0PeZy zeSvcW=>I>2QC$%&p-3<5AZWf)38%I2OwjtnU5H@jLNv$R8uXlVQv3Uae+Lc)?cq-4 zU74bBc%jn@_z~>~B)zvx z9OYN*;$9GziS*^@y)(QTNB`e*Y!U>0pROk-cpI#avT5p?)-&zdmhqh6bx5ZynlFs@ zXLO?buHHP=0q+dQZ2!-*-fbUR^Xl}0P{3sp?+u0?dKO zK>I^snZmnDYS*we{1l@ODmCAq>P~CIQ4?9zCunT?4o-le>%wHq_zY>cfozKAbYDQS zdDB>{abkby3!OmEOot`Zr-$Deelv>xm*O`K%)##I!%>4+b|H|~NF(4%NH+gL!p|aI z^>^8nwj_QF1j&y-%)7+tmndV>tv3!_7DfLrhrTKC5g*jn@R5|LuFFAl#LeL=@aHdG zyT(DauO5(1Q8_MwWb^wU;;8Q8iuCWXJ?8R=I=F!Selsq4-AA5N*Iu$~#6;!2i!`&{ z_EnBaumlq2C-V4A;%MwhDBibI^v>`*KK(Cat+l1rTRaz(FpUHOv@Ux9BJ-H4TmA7p zpmk6-MeX+?2%1l=do(6hf%Zmp?IaZT|7qjz6`}j9efrloB)UMt_yGfGzcOe!5{1)z zqlV^?ncfikZBWwzqD`}0Kl#JyyX3e~%!c0ZivEnQxy?s*2NWeH+S1bho`PblH>G{2a*$cGZ*ZeXqQ6JAZt3K#vn8kXl zyVhKB_L`GDU#9ghz5D}OCYzCf;rAw-`WT(37Sy+E&*u$LJ-h}9=Qo52B->N1YVWWY zT*O{a(fG{uJ<3BB*tVpytN@wTFIn?j;_e4YU+=^TB6iKPn!s~Fru444sN8Qtp!p5q z5{lRVNdL6^kA?zqq>_xuZ%IjIP@-BFp8=nF?qqdu)0j;BY>1xYsRo@#{oEa(wlBBF zoj-!+a~Zw%^c_Wg%To7Pro*Jnw*RfjukAZEs1JrmKxImmbBUV`8BU^~gYxwr91a?1 zb-f-2+VkiG85M6G;O=u~;oD(T$*krrD$@th1CI0D@=K;K(tPi2I2{7bu?d%q>_A{U z@6#sq5YzKG^)045s?513}lr8_-zW zB3aM5wB?NIH>7JlFWMcX|3^T6;%L|c>^OZs_H97@zwRl|07)6>nMd0<$gd#rHQ;am zPZ3meXQ4H$)&DWr{r!}#m)>wIs7`b}6eKkbXia1LdiC9!H-A+CL?YcF!`HTCSq^{;mQUHcmE zUBXHZtnt=6vTL_jlB}Jmq!SX^Ls>JS{DcmK3J|h)P#7;z#d>=vc*s9gET=|kdS!R* zp{P!^&)>C&iWu_ubQj!nJ=y~ZTTeG2BOS2;(MyPRQR)(zCn8tql1`*hjllFyi8my~;fo_KDVBV~T2v zRGEaS(GScigV?LFm&i^&!*aU(>o60(2YWo4 zsfnH6e+6!}lMaw`Y0b7VzeCuvhuQC>7N)!EYxe`u_E{lR9YgZ9!ZJ%4|GG!c$0!%z6ljp82}E|Hsgqqz`t z4dqG>Ub{Tq5d-V5nJK$D5B`Qm*$5{vDY_D9`?4wW( z_rpT^!0_*U@>_Vz7^>d%jva;B8Pr~MeWzN!MDboAZSCddO7`PK3*Z-^yyi>Fy|mc0 zB%ek8ysE!a|I`Hzg~1_ZebtUfRxV7FiCJ`3cat zr*SOuc&cutt3GrO@Ly{4m-)Nm^7Ct)FeA8lzdrLO&s=pmM`W18@&UAqC zZv{W|jX|fMIVt7xum64N`RnLcj^x|Fth8F?UE#@)<^mdnw_+Zs@^|5z(Ql47?>Sw} zjwzR)@07Z2h;Jmv!Yz?eh+1 zs?yf;AXmY4a5&`CJ=;q1qdaPS()#}F64QEiCcf1k`c0gAeW>;X9s$jJ@<9$J6PcXX zNtZ$RlyLh)IrIVTIW&amd8O45B+r=+Q_Fd4fHj!=Zp2=gxt4P{VW!KKYI=*vi`Lr3 zIaI<)c~pMsO=FSrl&dw7*4^6gOtsFk5l?$9M?=ZLjyz~+r(>QPdXM4FSu zEDQUjd}GT0duJcHzjrpw+~e?DjO+Ma9F5V^x*Eq5UV(Kx8V@nCUC*(p7}Y^YV992 z%_RI)Cr3jzWipBD9(~ljvY+)|uKQ0TuYIrHRd#&?)BW~YbJNY{QyX5|SBaWtlKxK- zUe6e0LzGWFn-D9Xt7!ZCGv+TVv;~gfMj}N7M`Yo-SbX{YPdcz+En zhiK>3b*E=E#6DBacN3MW22P+`pua(s{Aoz6#iOMErWE%1NEh)kdA0vj-_Aguepmlv72EWP&W5Fmn!2Z z_|m6?RoHg}0)C*N(WDukmx0>w17OdStf@58l?zB9(RtO*`a_*{hsp@8;7PE3;2i9I zfCr~OiI+FcizdJ{&>T5Y9wLvckGc@*6Q} z%{kGcMDvp#-h#+;CUXCmIDMien1tUZgw-BvtaE9-p=)ews3*Q_U^B|E-}YGp`tBt< z(d80IG|zk-V&x&4zt(|ALOOe*YByI$oOd2DXT@>*&1odP zz}j7p9RJ&a^Qmst52{aBxi!DJ$}=Cq{0C@IJp?qdejZehO+hrjQ2pKv?LcKy`&U_C zh1sAnX(_1B9|XUJ*3cA0V~)zNeo$q45!4=bhKoVE*ca3{9tKTKbS-NvJ`h^Ke?Vp4 z3Jw7E0hfdN=2B>C(uV?_3+jW^H)%esv1vMN0^fng=?>7;Me{-Rb(-G{hgP6||7#cq zOHvwIffUe02-2D2?S|bUaSEkNe->lS^84zn$IH7p! z%^LT3g;VW5j9I4gkER4l&YCBB?mxI@k&(HEtvHgxEY*N5vnphEI9Fz^Df7(A=UsP^ z^CD&i-^0+iEh=iI1D}b@b=Nf&7^zZVpSjd?Ta(0whVp9;#teTaLNiCr_Qi%P8e`PA zYHrc55vt*L;WLERJ>tO*r2fITeDIM!kUM6x+d3p!xP{ z56!zD0L{Jr3^%}MV9V(@f5iU>SOTwu%AOS&%vJ#J4{h1i>~b0P@GvLo?K{zA>_36c z+Z^oDx4(Yuv3Ta)#8KanN$Emjs^j)NGjZoIbLU4bOfTtk9Pbs{^9;uFDcHQJE>`;7 z)<2Q?6>)-GM+6HjI}-9qxQ6^LEj4}hyz3g?2YD913qftDCEN;$@|WoNGSb)yGAaE@ zOnszoM!w9uEbo+>ertX2v*?^B!2zJQpD2&9jw|lP&NWSi_^*s)b*l1Hm5ScRIyJ#ZxHurJ|1kYcKFUmP<<1w$bEm6Xa0Vq z4kvG9&d+JyIoVL?yO(je*E07(%Heb~dj!tEu9?y18E2BId1;zi*1o&>O)m4VYfJs; znSJ@Wu*v4rgEuoTt&Q@1ftck{-^+WWJXd?1`m(9e4N4#vqH*j!&{)4m2Q%r&_GZEg zhRc}wC0w0j#ioTr9Qz*SLXN8(8bdVxWm{Tt0?pZ!C-wR2Gwb9p4Y&HCZJ;mcnzr*k ztQixHu}?v!@*J6t=5iWCK7{=s)1vwMTM&8PY`DMRJV$}X%}j~v^pB9r7@JBNxwhPI zVr26A*1G90kjmULl`?$Cxu!rSMc47p@DkL^litT*Y^~=XEo?wPt%0&3*O6$we5!vO zVIE?f-I95BNxkFI;w11>AFF;TlTuD%bD>`Tl>c#NmiwKDzPvN9_rs!vOTtfm^$f_Q z97tmIu4Cmj^gf5)<(Wr0-gVz?Yi$OvOQSB{=Uio(%sgqa{a9c3n#*l!o({i@6nZv) zjx)x*Fqbut##vpD$<~L~qE|u-$dp`4LiO^&x98k;6ZuX7&)MFHpZec}Kx^E3aTECc z%yF$_Ga-FQM0Hvx56sQJ-qg(eIWq688GeCZk8y9|xu@Tc|H@CD_yODx5GE7j*=eM+ zF7kY|oojfHshYgn=Qyo77r%a74@7ZA^VokwviaBCq8w5c)&Cx#u|3+kPv?6@dIrzt z+nTrYOkOem{_|?RA1#e|es2?}pFfc#?EMLQ5k$NGwF9F98A!GAueUAe;hXl@%JuUi29&_`?GKydwSIG?F`Wm~9(czL$R zY6!OlBqQ4r_VA-{0nLzH@=HXWo5X}*^~36i>dnh1a#-!0JYNA1Lo?++f$YZd)nLyP%Y2zI0~4g>KRgo0m!R~P z!MiX9+CWrMUOl$Pbo-r2iSw_yxUQvD5U(DVb3Ba4mF(xZ>qs?_{)< zUDtF18OL#*=OLI4vGgw;YR@I8sJ&^Pa63E*%6oXjAA3~s@^9-QYM2Pmp}4<>bKn+G z9cb-n%b|R$er|=0Ac$zJ{XfvPHNrD7vLby*brD*!@rdS3!V;0X8x20~+z9mwe0&=od@=RxC8W09U@bT?=Q z>Z_&uQfSiRR9EmI5d%iG9tM-yo`I+ovAfwC6;N!$y7Reh>AZR zx(b;Uo@xD+j*b_3rW|r>c3Q`*@Z2)1I4-jqvn;}-S8)AhDjeoI44KNeYPEK~S?;+H zmPrTVnKMpQ_@$=W{Z*|RYnIf6+#NplOr9tsTuoVB6J@&ouBm*=UFn)mmCw`wF4XYC z&uR6-VZqwOq%Y;sxUV92%(JqoKi1mh7|{Ce&oBt$Niyq-c&lcUc*O<^SLwI1bJLg;PJ*1~Ml6P5}99slHNU?*P~mj)w=}2hhHq z7IIs|?x6YJTyQ)zM_mhx;X`-1hUR$#>}X;r{s7`ra;Xp?3E# zSmn8`e>_uVc?mRc&zP_WZ%#H&^G(fDPeeSw6E83RW6GD-6Nf{WjAhK1koKwhrqPL^ z?;UIO4pyhMS%>e$-$i(>hq5BvgQ1~0Jb$xb^mz0BH~q|s++Rxk{6RZ2)){FYSx&sI z^c-P;Wq7Lj--(EO9YP(mmpgN)xlO;Hl2C{J&FOqU@YR}L=KqFg`I(e!$)DDBBg#$X zRqf5hgn1(@->jZ2)b^f+D`5(hK&GXN1eK>6_|CHj%40;>Z*ZRp<)E>y7&0MelSnH0 zi55rW-6x>EgM%OwQb{7w&Xum8>f&T*mS!e3FHpalZhoT0UqQN>FQhAjN#JUz*Z%a} zUYhr_UnAX4>1L4l?cf!tqX&J9u`hk1Uh0WF7SJsn?gHsb1qm#LI{D~tPUF7NJcn;0 z^4wvMI>#cz#&Ms;0bzEwCbMJhnckg^I@6O!L?;Y&?6Jt)ja&YokdDk_q{jzlB zG7_kB4)wu$u7dqxm(VvC=iJR)Y!AYI92q{2dkM!TK&sM_h%Z7of82XoImFz;eeOEn z0p(jbbNA&rS)FHVP+u30lfZr($5S0&4j^uwxgK+qN6CxsDTVciUEiu1z_SqqcjbKk z2hAVz^OW{Pw;`V5MxN(V>ht1={QVVoiGG8{lc|_LMu{7lmfY_{83dKS1k^hBYw)M1 zEt6YeEcx4({H*ZusC$bC0S!q@I9D_ueFyyUW7)NCJ=Ber_+KA_R6}H1nvX1i?OO9% zGRMv}RL37nxq9JLq*hoV2tSij4mxe&x`-Ju_z-=RwaH$DT^^Cf=eWaTCG$XgziDUkrEeD=4*t3&cpbSB zS7e_2?!R%oO`RCg9U8~9N3Xc6;SIPMrb98f63UBvZ?ZS`r(uNr;(TXFb5wtRBJHy{ zJ|vWS-2bWz%_Y@d7Q&uT1ogzrqv}-OKx!>_9JzpFD?w>T=1uNpFclKXF$7S5qBea7 zltWZee!Op06;I-tNN`_N>wTrE^@8%Fe!!p4*|^6*BGEN;8ps?QFYx9R!+ZY1ZHxM@ zSR7Fw|1KN?THi1BO!Wu04cYx2ykd&NXq;*0nK1(Ngo}c zp=1wEto~^lNO$TBv@Sdn#zRBPNHVYwjDmTv1ysXPkWVt4%&4BrK-cuukWbQ;%$x+R zVIWL_e3NiLpD1IFUVSH!V@}lVBXvkcGU=RLvo_=xUU)2x?Y8lB+esUj?t%%^-K@cM zJ3E<`q2s#EN9m*Zskv%jq(-Etf3o{zf6U%;lIr4;tnCR zy=PAM%nGNp z8=6Dbg|$C*(}n_TJKp^+=1Z8L!iAt}_vfP08yPvG-K? zE7ZVSa5;>I<3V}T9Qz0O7M_Pk;rF1vAQfrZ&W!2R%N%+gy10P%ER=@I{0ykce+jR6 zcDbYR6aLamvZ6dQk0`pnt=VJ+_enG+91TO@Y>0NQSbpaaK3P$l(|B|>-?G1zeeUI6 zm-8Wh()$XCbw2CAnlO(;8Kfk<`_9c4ryYCPFZu3otR*pZG4?!>rp~K+ISAAaQx@)< zaTV-DTxn4Y6m@dcXArP&%TWI7+Kb^ySeG=3^(`u z!an@2J={;_W#H_g@3Gy(?;Q;qYHoeIr6~`ekJRhfwZBzn{;-<+JXdi{eO22G^At!$CJ-sScBC!v+qy?O`i)uqlRUWCyonEbF z=J>UUtYugw}8=B}M=J~@cvn}OO%IG-`>SKyya=eQl= z=>tOPvbXsL`@-`%uXld;x%t7JL&NNDgj;FD#xQ5&uNhzvnM$Cy!Ip`!rRX}!&HGv5 zy;VK;-n>odvA=GFo#hK}tM_5H#Ht;9v4eyaNrAA2%7WO5#KlXJ8^F90lWFB6Dafy z&C~eldW@_`jiZ}Kg!elS#&J7b1X1;5&uLAU(1p>iDus-JAjrJP`2P(4(_!%zXzP1JJXpGi?0D~HEn zXLu6&K`x~=Cp#BfQ;j{u49tT|sz=w9RG$b=4QF)W0p!{aLrrLsIvO*)PoZUZFAt13qB2R( z@vf;j(W4*YcMQo(a~;&bW8ixDHTZSd0=IIh>(%d%Gm(UX^Sfd_3Aq>C1#f|_(XHWE za6fziufR?4d(ioU2s$Ymzd-wU$*u?;2%WF2y_ka5zk`n zwFLW}a3-t(n^vT`ieo225K(@+@O;D29%kPUu`8|bfjLOM^yT+F>;o+!kno$GZnzQV z!E1HD9Y;O%4W4dh_rLL6S4TaMmZY>J{LXw=V9zph2A4O$D0S) z^Qw>W)*nUMV_@C0K)=1amS-)l;MlVeO?SE`PJ=`;mH?V>IqY?P!t)W#Y2CHO7t_pA zno?Fc-{1aHD< zU;2u_BSc<<>buKyFhcs^R-Y!lt8ESd3i>yt-x3Z1_kC9Rd!l;!7W`#cjC-(m+;41! z<25)3)P7tU%Il)aGn9I!JtR~0{3Of)okQ(sHVlMF>5IoG=;l9Q)AXCZKHQ4J`=Isz`yz9-gMEkRk zwtz6|ENa2V{RO-F)-55bwB-`$ZH{WkH-!G`|E{8bCz(0J%r<632X77;H9ljI+U7}o z6Y$^7Oh}q*FL)J5orD} z6{3k+$Ya1h+$lUqdBFglFLm>TY14QG zdnfRV`XKdL>xxb5>&Kf9X43aOdzNA74LvKe3e(0{yVwM*s7>4hI({E?WbF7I@lPG@ zzFzFEn{M`wD~$BGF<4Q(ybXVZE_|z4{c1lbpX3ggZm`4oueHD8*MQs4V@h Date: Sun, 8 Dec 2019 14:50:03 +0100 Subject: [PATCH 020/272] Remove x-icon favicon --- frontend/favicon.ico | Bin 99678 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 frontend/favicon.ico diff --git a/frontend/favicon.ico b/frontend/favicon.ico deleted file mode 100644 index cb994ab3b588f3d715060871aac3b8cf09087713..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99678 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 From dbfb58a502c9ac8bf7e0fab71a38b2b00aacb31d Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 00:00:54 +0100 Subject: [PATCH 021/272] Add different link styles; fixes #73 --- backend-php/include/config-sample.php | 45 ++++++++++++++ backend-php/include/inc.php | 85 ++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/backend-php/include/config-sample.php b/backend-php/include/config-sample.php index ca09537..104a151 100644 --- a/backend-php/include/config-sample.php +++ b/backend-php/include/config-sample.php @@ -132,6 +132,51 @@ // not be honored. "reserve_whitelist" => false, +// The type of links to generate when making new links for shares. Can be any +// of the following: +// +// | Link style | Example | No. of combinations | Avg. bruteforce time | +// +----------------------------+---------------------------------------+-----------------------+-------------------------------+ +// | LINK_4_PLUS_4_UPPER_CASE | EIRG-0CYE | 2.82 * 10^12 (36^8) | 44.7 years | +// | LINK_4_PLUS_4_LOWER_CASE | qae3-ulna | 2.82 * 10^12 (36^8) | 44.7 years | +// | LINK_4_PLUS_4_MIXED_CASE | WRho-uHLG | 1.68 * 10^14 (60^8) | 2663 years | +// | LINK_UUID_V4 | 09c8a3b1-e78f-48b1-a604-0da49e99cb5d | 5.32 * 10^36 (2^122) | 84.2 septillion years | +// | LINK_16_HEX | 6cde14c4c6551b41 | 1.84 * 10^19 (2^64) | 292 million years | +// | LINK_16_UPPER_CASE | 49OFGRK6SGPU93KV | 7.95 * 10^24 (36^16) | 126 trillion years | +// | LINK_16_LOWER_CASE | bdyslxszs14cj359 | 7.95 * 10^24 (36^16) | 126 trillion years | +// | LINK_16_MIXED_CASE | NTHX2HDsTn0kS3aj | 2.82 * 10^28 (60^16) | 447 quadrillion years | +// | LINK_32_HEX | 22adf21f11491ae8f3ae128e23a6782f | 3.40 * 10^38 (2^128) | 5.39 octillion years | +// | LINK_32_UPPER_CASE | MG42MW2DKIMHM87B4AO0WAB2PIY26TR1 | 6.33 * 10^49 (36^32) | 1 duodecillion years | +// | LINK_32_LOWER_CASE | itgbolrbq1c02eot5o46c5wixhdrdb5m | 6.33 * 10^49 (36^32) | 1 duodecillion years | +// | LINK_32_MIXED_CASE | cTK82MJ7rUOP138WNVznQR0Ck3BwZp6b | 7.96 * 10^57 (60^32) | 12.6 quattuordecillion years | +// +// For any MIXED_CASE variants, upper-case I and lower-case L will not appear +// because they are visually very similar and are easily confused. +// +// The default value is LINK_4_PLUS_4_UPPER_CASE, which is still considered very +// secure. The bruteforce times in the table below are the average time it would +// take to find a valid sharing link, when there is one link active, at 1000 +// guesses per second. For the default setting, this means it would take almost +// 45 years to find the link. +// +// This is assuming that the link is active 24/7 for that entire time. If you +// only have a link active 2% of the time, it would take over 2200 years. +// +// At 1000 guesses per second, you will likely notice that your server is +// noticeably slower and rapidly filling up with access logs. +// +// Very long links are also time-consuming to type, should you find yourself +// in need of typing in a link manually on another computer. This is the reason +// that short links are default. +// +// ---- PLEASE NOTE ---- +// This option is provided to you only because several people have requested it +// as a convenience. You are free to change it, but you should know that +// changing the default here gives you, for all intents and purposes, no +// security advantage in practice. +// +"link_style" => LINK_4_PLUS_4_UPPER_CASE, + // Leaflet tile URI template for the map frontend. Here are some examples: // // - OpenStreetMap directly: diff --git a/backend-php/include/inc.php b/backend-php/include/inc.php index 308ef7c..8ef49e5 100644 --- a/backend-php/include/inc.php +++ b/backend-php/include/inc.php @@ -29,6 +29,20 @@ const REDIS = 1; const PASSWORD = 0; const HTPASSWD = 1; +// Share link types. +const LINK_4_PLUS_4_UPPER_CASE = 0; +const LINK_4_PLUS_4_LOWER_CASE = 1; +const LINK_4_PLUS_4_MIXED_CASE = 2; +const LINK_UUID_V4 = 3; +const LINK_16_HEX = 4; +const LINK_16_UPPER_CASE = 5; +const LINK_16_LOWER_CASE = 6; +const LINK_16_MIXED_CASE = 7; +const LINK_32_HEX = 8; +const LINK_32_UPPER_CASE = 9; +const LINK_32_LOWER_CASE = 10; +const LINK_32_MIXED_CASE = 11; + const KILOMETERS_PER_HOUR = array( // Relative distance per second multiplied by number of seconds per hour. "mpsMultiplier" => 3.6, @@ -114,6 +128,7 @@ const DEFAULTS = array( "allow_link_req" => true, "reserved_links" => [], "reserve_whitelist" => false, + "link_style" => LINK_4_PLUS_4_UPPER_CASE, "map_tile_uri" => 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', "map_attribution" => 'Map data © OpenStreetMap contributors, CC-BY-SA', "default_zoom" => 14, @@ -300,8 +315,74 @@ class Share { protected function generateLinkID() { $s = ""; do { - $s = strtoupper(base_convert(hash("sha256", openssl_random_pseudo_bytes(LINK_ID_RAND_BYTES)), 16, 36)); - $s = substr($s, 0, 4)."-".substr($s, -4); + switch (getConfig("link_style")) { + case LINK_UUID_V4: + // UUID version 4. + $uuid = openssl_random_pseudo_bytes(16); + $uuid[6] = chr(ord($uuid[6]) & 0x0f | 0x40); + $uuid[8] = chr(ord($uuid[8]) & 0x3f | 0x80); + $s = vsprintf("%s%s-%s-%s-%s-%s%s%s", str_split(bin2hex($uuid), 4)); + break; + case LINK_16_HEX: + // 16-char (8-byte) hexadecimal string. + $s = bin2hex(openssl_random_pseudo_bytes(8)); + break; + case LINK_16_LOWER_CASE: + // 16-char lower-case alphanumeric string. + $alpha = "0123456789abcdefghijklmnopqrstuvwxyz"; + for ($i = 0; $i < 16; $i++) $s .= $alpha[random_int(0, strlen($alpha)-1)]; + break; + case LINK_16_MIXED_CASE: + // 16-char mixed-case alphanumeric string. + $alpha = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + for ($i = 0; $i < 16; $i++) $s .= $alpha[random_int(0, strlen($alpha)-1)]; + break; + case LINK_16_UPPER_CASE: + // 16-char upper-case alphanumeric string. + $alpha = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + for ($i = 0; $i < 16; $i++) $s .= $alpha[random_int(0, strlen($alpha)-1)]; + break; + case LINK_32_HEX: + // 32-char (16-byte) hexadecimal string. + $s = bin2hex(openssl_random_pseudo_bytes(16)); + break; + case LINK_32_LOWER_CASE: + // 32-char lower-case alphanumeric string. + $alpha = "0123456789abcdefghijklmnopqrstuvwxyz"; + for ($i = 0; $i < 32; $i++) $s .= $alpha[random_int(0, strlen($alpha)-1)]; + break; + case LINK_32_MIXED_CASE: + // 32-char mixed-case alphanumeric string. + // 'l' and 'I' not included because of visual similarity. + $alpha = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + for ($i = 0; $i < 32; $i++) $s .= $alpha[random_int(0, strlen($alpha)-1)]; + break; + case LINK_32_UPPER_CASE: + // 32-char upper-case alphanumeric string. + $alpha = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + for ($i = 0; $i < 32; $i++) $s .= $alpha[random_int(0, strlen($alpha)-1)]; + break; + case LINK_4_PLUS_4_LOWER_CASE: + // 4+4-char lower-case alphanumeric string. + $alpha = "0123456789abcdefghijklmnopqrstuvwxyz"; + for ($i = 0; $i < 8; $i++) $s .= $alpha[random_int(0, strlen($alpha)-1)]; + $s = substr($s, 0, 4)."-".substr($s, -4); + break; + case LINK_4_PLUS_4_MIXED_CASE: + // 4+4-char mixed-case alphanumeric string. + // 'l' and 'I' not included because of visual similarity. + $alpha = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + for ($i = 0; $i < 8; $i++) $s .= $alpha[random_int(0, strlen($alpha)-1)]; + $s = substr($s, 0, 4)."-".substr($s, -4); + break; + case LINK_4_PLUS_4_UPPER_CASE: + default: + // 4+4-char upper-case alphanumeric string. + $alpha = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + for ($i = 0; $i < 8; $i++) $s .= $alpha[random_int(0, strlen($alpha)-1)]; + $s = substr($s, 0, 4)."-".substr($s, -4); + break; + } } while ($this->memcache->get(PREFIX_LOCDATA.$s) !== false); return $s; } From 04c0f63dc8316a89a6ce5bd8ba56edbd2deb8a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Borr=C3=A0s=20i=20Viv=C3=B3?= Date: Mon, 9 Dec 2019 11:00:09 +0000 Subject: [PATCH 022/272] Added translation using Weblate (Catalan) --- android/app/src/main/res/values-ca/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 android/app/src/main/res/values-ca/strings.xml diff --git a/android/app/src/main/res/values-ca/strings.xml b/android/app/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/android/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 49aabf478cbd419f3131b0078db9e64ea807c3fe Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 13:11:52 +0100 Subject: [PATCH 023/272] Add last update timer; fixes #76 --- frontend/assets/lang/en.json | 4 ++++ frontend/main.js | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/frontend/assets/lang/en.json b/frontend/assets/lang/en.json index 1197cd6..17c5068 100644 --- a/frontend/assets/lang/en.json +++ b/frontend/assets/lang/en.json @@ -17,6 +17,10 @@ "dialog_connection_body": "Connection to the Hauk server was lost. Hauk will try to reconnect in the background. Please check that you have network connectivity.", "status_expired": "Expired", "status_offline": "Offline", + "last_update_seconds": "{{time}}s ago", + "last_update_minutes": "{{time}}m ago", + "last_update_hours": "{{time}}h ago", + "last_update_days": "{{time}}d ago", "btn_dismiss": "Dismiss", "btn_cancel": "Cancel", "btn_decrypt": "Decrypt", diff --git a/frontend/main.js b/frontend/main.js index c1100f2..35593f3 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -537,8 +537,7 @@ function processUpdate(data, init) { '' + '0.0 ' + VELOCITY_UNIT.unit + - '' + - 'Offline' + + '' + '' + '

' + '', @@ -636,10 +635,30 @@ function processUpdate(data, init) { var last = shares[user].points.length - 1; var point = shares[user].points[last]; var eLabel = document.getElementById("label-" + shares[user].id); + var eLastSeen = document.getElementById("last-seen-" + shares[user].id); if (point.time < (Date.now() / 1000) - OFFLINE_TIMEOUT) { eArrow.className = eArrow.className.split("live").join("dead"); if (eLabel !== null) eLabel.className = 'dead'; + if (eLastSeen !== null) { + // Calculate time since last update and choose an + // appropriate unit. + var time = Math.round((Date.now() / 1000) - point.time); + var unit = LANG["last_update_seconds"]; + if (time >= 60) { + time = Math.floor(time / 60); + unit = LANG["last_update_minutes"]; + if (time >= 60) { + time = Math.floor(time / 60); + unit = LANG["last_update_hours"]; + if (time >= 24) { + time = Math.floor(time / 24); + unit = LANG["last_update_days"]; + } + } + } + eLastSeen.textContent = unit.split("{{time}}").join(time); + } shares[user].circle.setStyle({ fillColor: '#555555', color: '#555555' From 3449cba1a29b13a47dac2d1c475f869e148829b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Borr=C3=A0s=20i=20Viv=C3=B3?= Date: Mon, 9 Dec 2019 11:00:38 +0000 Subject: [PATCH 024/272] Translated using Weblate (Catalan) Currently translated at 100.0% (104 of 104 strings) Translation: Hauk/Android client Translate-URL: https://traduki.varden.info/projects/hauk/android/ca/ --- .../app/src/main/res/values-ca/strings.xml | 124 +++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/res/values-ca/strings.xml b/android/app/src/main/res/values-ca/strings.xml index a6b3dae..23b707a 100644 --- a/android/app/src/main/res/values-ca/strings.xml +++ b/android/app/src/main/res/values-ca/strings.xml @@ -1,2 +1,124 @@ - \ No newline at end of file + + Hauk + Nom d\'usuari: + <opcional> + Password: + Durada de la compartició: + Interval d\'actualització (s): + Activa el xifrat d\'extrem a extrem + Clau de xifrat: + Sistema de compartició: + Permetre l\'adopció: + (Què és això\?) + Nom d\'usuari: + Xifra la compartició: + PIN del grup: + Estat: + Comparteix ID/URL: + Recorda la contrasenya de xifrat + Mostra les opcions avançades + Comença a compartir + Crear un no enllaç de seguiment + Afegeix una compartició existent + Atura + Només la teva localització + Compartició de grup (host) + Compartició de grup (membre) + Mostra només la meva localització + Unir-se a una compartició de grup + minut(s) + hora(es) + dia(es) + Compartició de localització inactiva + Esperant per la fixació inicial del GNSS… + Compartició de localització activa! + Esperant per la fixació d\'alta precisió… + PIN DEL TEU GRUP: + Comparteix aquest PIN amb els altres membres del teu grup per deixar-los unir-se al teu grup de compartició de localització + Adopció compartida + Crea un nou enllaç de compartició + D\'acord + Cancel·la + + Podeu adoptar una quota d’ubicació individual existent per incorporar-la a aquesta quota de grup. Enganxeu l\'URL o l\'identificador de la participació que vulgueu adoptar (per exemple,% s\?XXXX-XXXX) i assigneu un sobrenom a aquest recurs. + Podeu crear diversos enllaços per accedir a la ubicació compartida. Cada enllaç es pot aturar individualment, permetent un control minuciós sobre qui pot veure la vostra ubicació en qualsevol moment. +\n +\nEls enllaços addicionals que creeu d\'aquesta manera seran compartits d\'un sol usuari que només mostrin la vostra ubicació. + Crea + Tanca + Copia l\'enllaç + l\'enllaç de Hauk s\'ha copiat + Atura la compartició + Compartició de localització activa + Connectant + Connectant a Hauk… + Adoptant + Creant l\'enllaç + Creant un nou enllaç de compartició… + Configuració incorrecta + Error de connexió + Error de servidor + L\'adreça URL del servidor introduïda és invàlida. + El servidor ha retornat una resposta buida. + Rebut el %s HTTP del servidor! + Connexió establerta + La compartició de localització és activa! Prem el botó de compartir per copiar l\'enllaç públic de la teva compartició. + Compartició acabada + La teva compartició de localització ha expirat. + Compartició adoptada + %s s\'ha afegit a la teva compartició! + Resumeix la sessió + Enllaç creat + S\'ha creat un nou enllaç per compartir la teva localització de manera correcta! Prem el botó de compartir per copiar l\'enllaç públic de compartició. + Requereix permís + Estalvi de bateria agressiu + Huawei + OnePlus + Xiaomi + Comparteix l\'enllaç Hauk via + Segueix la meva localització a Hauk! + Sol·licitud d’autorització de transmissió + "Hauk ha rebut i blocat un intent de crear una nova sessió de localització des d\'una font de transmissió que s\'ha identificat a si mateixa com a" + Voleu permetre que en el futur es crein sessions a partir d’emissions amb aquesta identificació\? + Hauk + Ús compartit de la ubicació de codi obert + URL del servidor: + Identificador d\'enllaç preferit: + <generat aleatòriament> + Permetre als altres d\'afegir la meva compartició en un grup compartit + Desa el password: + Atura la compartició [%s] + Comparteix + Enllaços actius actualment + Crear una compartició de grup + Adopta una compartició existent + No + Obre les opcions + Hauk està compartint la teva localització a %s + Adoptant %s… + El permís de localització és requerit per usar aquesta aplicació. + Els serveis de localització estan desactivats. Si us plau, activa la localització d\'alta precisió per compartir la teva localització. + El servidor està desactualitzat + El servidor no suporta el xifrat d\'extrem a extrem. El xifrat d\'extrem a extrem està suportat en la versió %s i posteriors; el servidor actualment està usant la versió %s. La teva compartició es crearà sense xifratge d\'extrem a extrem per mantenir la compatibilitat. + Tens una sessió inacabada: +\n +\nLink(s) de compartició actiu(s): %d +\nExpira: %s +\n +\nVols resumir la sessió\? + Aquesta aplicació requereix accés a la teva localització per funcionar, però aquest permís encara no s\'ha concedit encara. Si us plau, aprova la següent petició de permisos, i després prem el botó d\'inici de nou per tornar-ho a intentar. + Sembla que uses un mòbil %s. Aquests terminals tenen un sistema d\'estalvi de bateria estricte que eviten que Hauk enviï actualitzacions de localització en segon pla. +\n +\nEt recomanem que t\'asseguris que Hauk està exempt de l\'estalvi de bateria per assegurar-te que l\'aplicació no deixa de funcionar inesperadament. +\n +\nAquest missatge no es tornarà a mostrar més cops. + Hauk us permet crear sessions de compartició d’ubicacions tant per a vosaltres com per a un grup de persones. Si algú crea un grup compartit, obté un codi de grup que altres poden utilitzar per unir-se a la sessió de compartició. +\n +\nTanmateix, la persona que crea la participació de grup també té l’opció d’afegir una quota d’ubicació ja existent al grup. Aquest procés s’anomena adopció. +\n +\nPodeu decidir si voleu o no permetre que altres persones incorporin el vostre percentatge d’ubicació al seu grup compartit marcant aquesta casella de selecció. +\n +\nSi activeu aquesta opció i compartiu la vostra ubicació amb un grup d’amics i algú altre també vol compartir la seva ubicació, pot crear un grup compartit i afegir-lo al seu mapa, de manera que tots dos aparegueu al mateix mapa d’ubicació compartit. Això és útil si és conduir i no es pot utilitzar el telèfon, ja que l\'adopció de la seva participació per part d\'altres persones no requereix cap interacció per part seva. + El servidor no admet comparticions de grup. Les accions del grup són compatibles amb la versió %s i superiors; actualment el servidor està executant la versió %s. La vostra participació s\'ha convertit en una participació individual. + \ No newline at end of file From 394cb90491b74822dbb7295e1adeba6b632be85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Borr=C3=A0s=20i=20Viv=C3=B3?= Date: Mon, 9 Dec 2019 12:12:40 +0000 Subject: [PATCH 025/272] Added translation using Weblate (Catalan) --- backend-php/include/lang/ca/texts.php | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend-php/include/lang/ca/texts.php diff --git a/backend-php/include/lang/ca/texts.php b/backend-php/include/lang/ca/texts.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/backend-php/include/lang/ca/texts.php @@ -0,0 +1 @@ + Date: Mon, 9 Dec 2019 12:38:08 +0000 Subject: [PATCH 026/272] Added translation using Weblate (Catalan) --- frontend/assets/lang/ca.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 frontend/assets/lang/ca.json diff --git a/frontend/assets/lang/ca.json b/frontend/assets/lang/ca.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/frontend/assets/lang/ca.json @@ -0,0 +1 @@ +{} From fb7bc0f4c78a8224b2d72345b7c93ffbb4c4d48e Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 12:14:57 +0000 Subject: [PATCH 027/272] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (27 of 27 strings) Translation: Hauk/Web frontend Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nb_NO/ --- frontend/assets/lang/nb_NO.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/assets/lang/nb_NO.json b/frontend/assets/lang/nb_NO.json index 262bac4..4c0d8a6 100644 --- a/frontend/assets/lang/nb_NO.json +++ b/frontend/assets/lang/nb_NO.json @@ -21,5 +21,9 @@ "e2e_incorrect": "Krypteringsnøkkelen du skrev inn er feil. Vennligst prøv igjen.", "e2e_password_prompt": "Denne delte posisjonen er beskyttet med ende-til-ende-kryptering. Vennligst skriv inn krypteringsnøkkelen for å få tilgang til delingen.", "e2e_placeholder": "Krypteringsnøkkel", - "e2e_title": "Ende-til-ende-kryptering" + "e2e_title": "Ende-til-ende-kryptering", + "last_update_days": "{{time}}d siden", + "last_update_hours": "{{time}}t siden", + "last_update_minutes": "{{time}}m siden", + "last_update_seconds": "{{time}}s siden" } From 0affc83c0f23403eb0149f5ebca0cd7ccc7edec8 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 12:16:43 +0000 Subject: [PATCH 028/272] Translated using Weblate (Norwegian Nynorsk) Currently translated at 100.0% (27 of 27 strings) Translation: Hauk/Web frontend Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nn/ --- frontend/assets/lang/nn.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/assets/lang/nn.json b/frontend/assets/lang/nn.json index 27a5d57..d8d5788 100644 --- a/frontend/assets/lang/nn.json +++ b/frontend/assets/lang/nn.json @@ -21,5 +21,9 @@ "e2e_incorrect": "Krypteringsnøkkelen du skreiv inn er feil. Ver vennleg og prøv igjen.", "e2e_password_prompt": "Denne delte posisjonen er kryptert med ende-til-ende-kryptering. Ver vennleg og skriv inn krypteringsnøkkelen for å få tilgang til delinga.", "e2e_placeholder": "Krypteringsnøkkel", - "e2e_title": "Ende-til-ende-kryptering" + "e2e_title": "Ende-til-ende-kryptering", + "last_update_days": "{{time}}d sidan", + "last_update_hours": "{{time}}t sidan", + "last_update_minutes": "{{time}}m sidan", + "last_update_seconds": "{{time}}s sidan" } From eddd168b1664fff83e11a662c15d31cea3cb79e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Borr=C3=A0s=20i=20Viv=C3=B3?= Date: Mon, 9 Dec 2019 12:16:03 +0000 Subject: [PATCH 029/272] Translated using Weblate (Catalan) Currently translated at 100.0% (19 of 19 strings) Translation: Hauk/PHP backend Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/ca/ --- backend-php/include/lang/ca/texts.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/backend-php/include/lang/ca/texts.php b/backend-php/include/lang/ca/texts.php index b3d9bbc..af669bb 100644 --- a/backend-php/include/lang/ca/texts.php +++ b/backend-php/include/lang/ca/texts.php @@ -1 +1,20 @@ Date: Mon, 9 Dec 2019 12:38:21 +0000 Subject: [PATCH 030/272] Translated using Weblate (Catalan) Currently translated at 100.0% (27 of 27 strings) Translation: Hauk/Web frontend Translate-URL: https://traduki.varden.info/projects/hauk/frontend/ca/ --- frontend/assets/lang/ca.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/assets/lang/ca.json b/frontend/assets/lang/ca.json index 0967ef4..1867c2e 100644 --- a/frontend/assets/lang/ca.json +++ b/frontend/assets/lang/ca.json @@ -1 +1,29 @@ -{} +{ + "google_play_badge_url": "https://play.google.com/intl/ca_ES/badges/static/images/badges/ca_badge_web_generic.png", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ca.png", + "btn_decrypt": "Desxifrar", + "btn_cancel": "Cancel·la", + "btn_dismiss": "Rebutjar", + "last_update_days": "Fa {{time}} dies", + "last_update_hours": "Fa {{time}} hores", + "last_update_minutes": "Fa {{time}} minuts", + "last_update_seconds": "Fa {{time}} segons", + "status_offline": "Fora de línia", + "status_expired": "Caducat", + "dialog_connection_body": "S'ha perdut la connexió amb el servidor Hauk. Hauk intentarà tornar a connectar en segon pla. Comproveu que teniu connexió a la xarxa.", + "dialog_connection_head": "Error de connexió", + "dialog_expired_body": "Aquesta compartició d’ubicació ha caducat.", + "dialog_expired_head": "La compartició ha caducat", + "point_app_to": "Apunteu l'aplicació Hauk a aquest servidor per compartir la vostra ubicació:", + "gnss_signal_body": "El remitent està esperant el senyal GPS", + "gnss_signal_head": "Si us plau, esperi", + "e2e_unsupported": "Aquest compartiment està protegit per xifrat d'extrem a extrem. Sembla que el vostre navegador no admet les funcions criptogràfiques necessàries per desxifrar aquestes accions. Torneu-ho a provar amb un altre navegador web.", + "e2e_unavailable_secure": "Aquest compartiment està protegit per xifrat d'extrem a extrem. Actualment, el desxiframent no està disponible perquè no utilitzeu HTTPS. Assegureu-vos que utilitzeu HTTPS i, després, torneu-ho a provar.", + "e2e_incorrect": "La contrasenya de xifrat que heu introduït no és correcta. Si us plau torna-ho a provar.", + "e2e_password_prompt": "Aquest compartiment està protegit per xifrat d'extrem a extrem. Introduïu la contrasenya de xifratge per accedir al recurs compartit.", + "e2e_placeholder": "Contrasenya de xifrat", + "e2e_title": "Xifrat d'extrem a extrem", + "expired_body": "La ubicació compartida a la qual heu intentat accedir no s'ha trobat al servidor. Si aquest enllaç funcionava abans, la participació podria haver caducat.", + "expired_head": "L'ubicació ha expirat", + "page_title": "Hauk" +} From c6c919f272e76143cd4622f0315d8e63bef37b93 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 12:18:39 +0000 Subject: [PATCH 031/272] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (104 of 104 strings) Translation: Hauk/Android client Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/ --- android/app/src/main/res/values-nb-rNO/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/app/src/main/res/values-nb-rNO/strings.xml b/android/app/src/main/res/values-nb-rNO/strings.xml index f5a01cd..76a8439 100644 --- a/android/app/src/main/res/values-nb-rNO/strings.xml +++ b/android/app/src/main/res/values-nb-rNO/strings.xml @@ -121,4 +121,6 @@ Serveren støtter ikke ende-til-ende-kryptering. Ende-til-ende-kryptering støttes i versjon %s og høyere; serveren kjører nå versjon %s. Delingen din vil bli opprettet uten ende-til-ende-kryptering for å beholde kompatibilitet. (for ende-til-ende-kryptering) Krypteringsnøkkel: + Aktiver ende-til-ende-kryptering + Krypter deling: \ No newline at end of file From ed08ece16a6e2709ce95cddc2d4372dbbff10d99 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 12:19:05 +0000 Subject: [PATCH 032/272] Translated using Weblate (Norwegian Nynorsk) Currently translated at 100.0% (104 of 104 strings) Translation: Hauk/Android client Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/ --- android/app/src/main/res/values-nn/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/app/src/main/res/values-nn/strings.xml b/android/app/src/main/res/values-nn/strings.xml index 7d5ffd2..07b409a 100644 --- a/android/app/src/main/res/values-nn/strings.xml +++ b/android/app/src/main/res/values-nn/strings.xml @@ -121,4 +121,6 @@ Serveren støttar ikkje ende-til-ende-kryptering. Ende-til-ende-kryptering er støtta i versjon %s og høgare; serveren køyrer no versjon %s. Delingen din vil bli oppretta utan ende-til-ende-kryptering for å behalda kompatibilitet. (for ende-til-ende-kryptering) Krypteringsnøkkel: + Aktiver ende-til-ende-kryptering + Krypter deling: \ No newline at end of file From 9f430a445efebc7f00e0f0f3d7d88e428e5492c2 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 13:58:09 +0100 Subject: [PATCH 033/272] Use bulleted list of translators --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3334e83..4d8abe0 100644 --- a/README.md +++ b/README.md @@ -121,15 +121,15 @@ Hauk depends on volunteers to translate the project. Want to help out? Head over [![Translation status](https://traduki.varden.info/widgets/hauk/-/287x66-white.png)](https://traduki.varden.info/engage/hauk/) -**Basque** - osoitz -**Dutch** - Jdekoning141 -**French** - thifranc -**German** - natrius, hurradiegams and lemmerk -**Norwegian Bokmål** - bilde2910 -**Norwegian Nynorsk** - bilde2910 -**Polish** - krystiancha and RuralYak -**Russian** - RuralYak -**Ukrainian** - RuralYak +- **Basque** - osoitz +- **Dutch** - Jdekoning141 +- **French** - thifranc +- **German** - natrius, hurradiegams and lemmerk +- **Norwegian Bokmål** - bilde2910 +- **Norwegian Nynorsk** - bilde2910 +- **Polish** - krystiancha and RuralYak +- **Russian** - RuralYak +- **Ukrainian** - RuralYak ### Translation status From 21446811ab11abfb76831c654f076f53f2bfeabb Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 13:59:52 +0100 Subject: [PATCH 034/272] Credits for Catalan translation --- README.md | 1 + backend-php/include/inc.php | 2 +- frontend/main.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d8abe0..56e8406 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Hauk depends on volunteers to translate the project. Want to help out? Head over [![Translation status](https://traduki.varden.info/widgets/hauk/-/287x66-white.png)](https://traduki.varden.info/engage/hauk/) - **Basque** - osoitz +- **Catalan** - xordiet - **Dutch** - Jdekoning141 - **French** - thifranc - **German** - natrius, hurradiegams and lemmerk diff --git a/backend-php/include/inc.php b/backend-php/include/inc.php index 8ef49e5..bd18f86 100644 --- a/backend-php/include/inc.php +++ b/backend-php/include/inc.php @@ -4,7 +4,7 @@ // backend. It loads the configuration file and declares it as a constant. const BACKEND_VERSION = "1.5"; -const LANGUAGES = ["de", "en", "eu", "fr", "nb_NO", "nl", "nn", "ru", "uk"]; +const LANGUAGES = ["ca", "de", "en", "eu", "fr", "nb_NO", "nl", "nn", "ru", "uk"]; // Create mode for create.php. Corresponds with the constants from the Android // app in android/app/src/main/java/info/varden/hauk/HaukConst.java. diff --git a/frontend/main.js b/frontend/main.js index 35593f3..85cf903 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -7,7 +7,7 @@ const EARTH_DIAMETER_KM = 6371 * 2; const HAV_MOD = EARTH_DIAMETER_KM * 1000; // Find preferred language. -var locales = ['de', 'en', 'eu', 'fr', 'nb_NO', 'nl', 'nn', 'ru', 'uk']; +var locales = ['ca', 'de', 'en', 'eu', 'fr', 'nb_NO', 'nl', 'nn', 'ru', 'uk']; var prefLang = 'en'; if (navigator.languages) { for (var i = navigator.languages.length - 1; i >= 0; i--) { From 319200ea544839cde16a44d578e0e93f9a681191 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 16:01:41 +0100 Subject: [PATCH 035/272] Fixed location service issues when recreating MainActivity Fixes #77 Fixes #80 --- .../hauk/manager/AutoResumptionPrompter.java | 2 +- .../hauk/manager/ServiceRelauncher.java | 48 ++++++++++++++++++ .../hauk/manager/SessionInitiationReason.java | 34 +++++++++++++ .../varden/hauk/manager/SessionListener.java | 3 +- .../varden/hauk/manager/SessionManager.java | 41 +++++++++++---- .../info/varden/hauk/ui/MainActivity.java | 50 ++++++++++--------- 6 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 android/app/src/main/java/info/varden/hauk/manager/ServiceRelauncher.java create mode 100644 android/app/src/main/java/info/varden/hauk/manager/SessionInitiationReason.java diff --git a/android/app/src/main/java/info/varden/hauk/manager/AutoResumptionPrompter.java b/android/app/src/main/java/info/varden/hauk/manager/AutoResumptionPrompter.java index 901bd85..802ebe7 100644 --- a/android/app/src/main/java/info/varden/hauk/manager/AutoResumptionPrompter.java +++ b/android/app/src/main/java/info/varden/hauk/manager/AutoResumptionPrompter.java @@ -71,7 +71,7 @@ public final class AutoResumptionPrompter implements ResumeHandler { Log.i("Resuming shares..."); //NON-NLS AutoResumptionPrompter.this.resumptionHandler.clearResumableSession(); for (Share share : this.shares) { - AutoResumptionPrompter.this.manager.shareLocation(share); + AutoResumptionPrompter.this.manager.shareLocation(share, SessionInitiationReason.USER_RESUMED); } } diff --git a/android/app/src/main/java/info/varden/hauk/manager/ServiceRelauncher.java b/android/app/src/main/java/info/varden/hauk/manager/ServiceRelauncher.java new file mode 100644 index 0000000..66011a3 --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/manager/ServiceRelauncher.java @@ -0,0 +1,48 @@ +package info.varden.hauk.manager; + +import android.content.Context; + +import info.varden.hauk.caching.ResumableSessions; +import info.varden.hauk.caching.ResumeHandler; +import info.varden.hauk.struct.Session; +import info.varden.hauk.struct.Share; +import info.varden.hauk.utils.Log; + +/** + * {@link ResumeHandler} implementation used by {@link SessionManager} to automatically resume + * sessions following a service relaunch. This can happen if the main activity is terminated, but + * the share itself keeps running in the background. + * + * @author Marius Lindvall + */ +public class ServiceRelauncher implements ResumeHandler { + /** + * The session manager to call to resume the shares. + */ + private final SessionManager manager; + + /** + * The manager's resumption handler. This is used to clear the resumption data before the shares + * are resumed by the session manager, as the session manager will re-flag the shares as + * resumable when it adds them to its internal share list. + */ + private final ResumableSessions resumptionHandler; + + public ServiceRelauncher(SessionManager manager, ResumableSessions resumptionHandler) { + this.manager = manager; + this.resumptionHandler = resumptionHandler; + } + + @Override + public void onSharesFetched(Context ctx, Session session, Share[] shares) { + Log.i("Resuming %s share(s) automatically found for session %s", shares.length, session); //NON-NLS + // The shares provided by ResumableSessions do not have a session attached to them. Attach + // it to the shares so that they can be shown properly by the prompt and so that the updates + // have a backend to be broadcast to when the shares are resumed. + this.resumptionHandler.clearResumableSession(); + for (Share share : shares) { + share.setSession(session); + this.manager.shareLocation(share, SessionInitiationReason.SERVICE_RELAUNCH); + } + } +} diff --git a/android/app/src/main/java/info/varden/hauk/manager/SessionInitiationReason.java b/android/app/src/main/java/info/varden/hauk/manager/SessionInitiationReason.java new file mode 100644 index 0000000..eb771bd --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/manager/SessionInitiationReason.java @@ -0,0 +1,34 @@ +package info.varden.hauk.manager; + +import info.varden.hauk.struct.Session; +import info.varden.hauk.struct.Share; + +/** + * Describes the reason a session was initiated. + * + * @author Marius Lindvall + */ +public enum SessionInitiationReason { + /** + * The user requested to start a new sharing session. + */ + USER_STARTED, + + /** + * The user requested to resume a previous sharing session. + */ + USER_RESUMED, + + /** + * The sharing session is automatically resumed as a result of a relaunch of the location + * sharing service. + */ + SERVICE_RELAUNCH, + + /** + * The session was created because a share was added to it. This should never be received by + * {@link SessionListener#onSessionCreated(Session, Share, SessionInitiationReason)} under any + * normal circumstances. + */ + SHARE_ADDED +} diff --git a/android/app/src/main/java/info/varden/hauk/manager/SessionListener.java b/android/app/src/main/java/info/varden/hauk/manager/SessionListener.java index 6f08c12..21cff76 100644 --- a/android/app/src/main/java/info/varden/hauk/manager/SessionListener.java +++ b/android/app/src/main/java/info/varden/hauk/manager/SessionListener.java @@ -15,8 +15,9 @@ public interface SessionListener { * * @param session The session that was created. * @param share The share that the session was created for. + * @param reason The reason the session was created. */ - void onSessionCreated(Session session, Share share); + void onSessionCreated(Session session, Share share, SessionInitiationReason reason); /** * Called if the session could not be initiated due to missing location permissions. diff --git a/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java b/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java index ade2c23..c4435d1 100644 --- a/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java +++ b/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java @@ -63,6 +63,12 @@ public abstract class SessionManager { */ private final StopSharingCallback stopCallback; + /** + * Intent for the location pusher, so that it can be stopped if already running when launching + * the app. + */ + private static Intent pusher = null; + /** * Android application context. */ @@ -182,7 +188,19 @@ public abstract class SessionManager { * any are found in storage. */ public final void resumeShares(ResumePrompt prompt) { - this.resumable.tryResumeShare(new AutoResumptionPrompter(this, this.resumable, prompt)); + // Check if the location push service is already running. This will happen if the main UI + // activity is killed/stopped, but the app itself and the pushing service keeps running in + // the background. If this happens, the push service should be silently restarted to ensure + // it behaves properly with new instances of GNSSActiveHandler and StopSharingTask that will + // be created and attached when creating a new SessionManager in MainActivity. There is + // probably a cleaner way to do this. + if (pusher != null) { + this.ctx.stopService(pusher); + pusher = null; + this.resumable.tryResumeShare(new ServiceRelauncher(this, this.resumable)); + } else { + this.resumable.tryResumeShare(new AutoResumptionPrompter(this, this.resumable, prompt)); + } } /** @@ -194,7 +212,7 @@ public abstract class SessionManager { * @throws LocationServicesDisabledException if location services are disabled. * @throws LocationPermissionsNotGrantedException if location permissions have not been granted. */ - private SessionInitiationPacket.ResponseHandler preSessionInitiation(final SessionInitiationResponseHandler upstreamCallback) throws LocationServicesDisabledException, LocationPermissionsNotGrantedException { + private SessionInitiationPacket.ResponseHandler preSessionInitiation(final SessionInitiationResponseHandler upstreamCallback, final SessionInitiationReason reason) throws LocationServicesDisabledException, LocationPermissionsNotGrantedException { // Check for location permission and prompt the user if missing. This returns because the // checking function creates async dialogs here - the user is prompted to press the button // again instead. @@ -215,7 +233,7 @@ public abstract class SessionManager { Log.i("Session was initiated for share %s; setting session resumable", share); //NON-NLS // Proceed with the location share. - shareLocation(share); + shareLocation(share, reason); upstreamCallback.onSuccess(); } @@ -250,7 +268,7 @@ public abstract class SessionManager { * @throws LocationPermissionsNotGrantedException if location permissions have not been granted. */ public final void shareLocation(SessionInitiationPacket.InitParameters initParams, SessionInitiationResponseHandler upstreamCallback, AdoptabilityPreference allowAdoption) throws LocationPermissionsNotGrantedException, LocationServicesDisabledException { - SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback); + SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback, SessionInitiationReason.USER_STARTED); // Create a handshake request and handle the response. The handshake transmits the duration // and interval to the server and waits for the server to return a session ID to confirm @@ -269,7 +287,7 @@ public abstract class SessionManager { * @throws LocationPermissionsNotGrantedException if location permissions have not been granted. */ public final void shareLocation(SessionInitiationPacket.InitParameters initParams, SessionInitiationResponseHandler upstreamCallback, String nickname) throws LocationPermissionsNotGrantedException, LocationServicesDisabledException { - SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback); + SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback, SessionInitiationReason.USER_STARTED); // Create a handshake request and handle the response. The handshake transmits the duration // and interval to the server and waits for the server to return a session ID to confirm @@ -289,7 +307,7 @@ public abstract class SessionManager { * @throws LocationPermissionsNotGrantedException if location permissions have not been granted. */ public final void shareLocation(SessionInitiationPacket.InitParameters initParams, SessionInitiationResponseHandler upstreamCallback, String nickname, String groupPin) throws LocationPermissionsNotGrantedException, LocationServicesDisabledException { - SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback); + SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback, SessionInitiationReason.USER_STARTED); // Create a handshake request and handle the response. The handshake transmits the duration // and interval to the server and waits for the server to return a session ID to confirm @@ -304,10 +322,10 @@ public abstract class SessionManager { * * @param share The share to run against the server. */ - public final void shareLocation(Share share) { + public final void shareLocation(Share share, SessionInitiationReason reason) { // If we are not already sharing our location, initiate a new session. if (this.activeSession == null) { - initiateSessionForExistingShare(share); + initiateSessionForExistingShare(share, reason); } Log.i("Attaching to share, share=%s", share); //NON-NLS @@ -349,7 +367,7 @@ public abstract class SessionManager { * * @param share The share whose session should be pushed to. */ - private void initiateSessionForExistingShare(Share share) { + private void initiateSessionForExistingShare(Share share, SessionInitiationReason reason) { this.activeSession = share.getSession(); this.resumable.setSessionResumable(this.activeSession); @@ -381,6 +399,7 @@ public abstract class SessionManager { // When both the notification and pusher are created, we can update the stop task with // these so that they can be canceled when the location share ends. this.stopTask.updateTask(pusher); + SessionManager.pusher = pusher; // stopTask is scheduled for expiration, but it could also be called if the user // manually stops the share, or if the app is destroyed. @@ -393,7 +412,7 @@ public abstract class SessionManager { listener.onStarted(); } for (SessionListener listener : this.upstreamSessionListeners) { - listener.onSessionCreated(share.getSession(), share); + listener.onSessionCreated(share.getSession(), share, reason); } } else { Log.w("Location permission has not been granted; sharing will not commence"); //NON-NLS @@ -460,7 +479,7 @@ public abstract class SessionManager { // that can be initiated by a remote user (through adoption). Share newShare = new Share(this.session, String.format(linkFormat, shareID), shareID, ShareMode.JOIN_GROUP); Log.i("Received unknown share %s from server", newShare); //NON-NLS - shareLocation(newShare); + shareLocation(newShare, SessionInitiationReason.SHARE_ADDED); } } for (Iterator> it = SessionManager.this.knownShares.entrySet().iterator(); it.hasNext();) { diff --git a/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java b/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java index bb9b477..13bb4dd 100644 --- a/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java +++ b/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java @@ -27,6 +27,7 @@ import info.varden.hauk.dialog.CustomDialogBuilder; import info.varden.hauk.dialog.DialogService; import info.varden.hauk.http.SessionInitiationPacket; import info.varden.hauk.manager.PromptCallback; +import info.varden.hauk.manager.SessionInitiationReason; import info.varden.hauk.manager.SessionInitiationResponseHandler; import info.varden.hauk.manager.SessionListener; import info.varden.hauk.manager.SessionManager; @@ -476,7 +477,7 @@ public final class MainActivity extends AppCompatActivity { */ private final class SessionListenerImpl implements SessionListener { @Override - public void onSessionCreated(Session session, final Share share) { + public void onSessionCreated(Session session, final Share share, SessionInitiationReason reason) { // We now have a link to share, so we enable the additional link creation button if the backend supports it. Add an event handler to handle the user clicking on it. if (session.getBackendVersion().isAtLeast(Constants.VERSION_COMPAT_VIEW_ID)) { boolean allowNewLinkAdoption = ((Checkable) findViewById(R.id.chkAllowAdopt)).isChecked(); @@ -485,7 +486,7 @@ public final class MainActivity extends AppCompatActivity { btnLink.setOnClickListener(new AddLinkClickListener(MainActivity.this, session, allowNewLinkAdoption) { @Override public void onShareCreated(Share share) { - MainActivity.this.manager.shareLocation(share); + MainActivity.this.manager.shareLocation(share, SessionInitiationReason.SHARE_ADDED); } }); btnLink.setEnabled(true); @@ -501,29 +502,32 @@ public final class MainActivity extends AppCompatActivity { // Re-enable the start (stop) button and inform the user. findViewById(R.id.btnShare).setEnabled(true); - MainActivity.this.dialogSvc.showDialog(R.string.ok_title, R.string.ok_message, Buttons.OK_SHARE, new CustomDialogBuilder() { - @Override - public void onPositive() { - // OK button - } + // Service relaunches should be handled silently. + if (reason != SessionInitiationReason.SERVICE_RELAUNCH) { + MainActivity.this.dialogSvc.showDialog(R.string.ok_title, R.string.ok_message, Buttons.OK_SHARE, new CustomDialogBuilder() { + @Override + public void onPositive() { + // OK button + } - @Override - public void onNegative() { - // Share button - Log.i("User requested to share %s", share); //NON-NLS - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType(Constants.INTENT_TYPE_COPY_LINK); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, MainActivity.this.getString(R.string.share_subject)); - shareIntent.putExtra(Intent.EXTRA_TEXT, share.getViewURL()); - MainActivity.this.startActivity(Intent.createChooser(shareIntent, MainActivity.this.getString(R.string.share_via))); - } + @Override + public void onNegative() { + // Share button + Log.i("User requested to share %s", share); //NON-NLS + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType(Constants.INTENT_TYPE_COPY_LINK); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, MainActivity.this.getString(R.string.share_subject)); + shareIntent.putExtra(Intent.EXTRA_TEXT, share.getViewURL()); + MainActivity.this.startActivity(Intent.createChooser(shareIntent, MainActivity.this.getString(R.string.share_via))); + } - @Nullable - @Override - public View createView(Context ctx) { - return null; - } - }); + @Nullable + @Override + public View createView(Context ctx) { + return null; + } + }); + } } @Override From 9ecb7c08a9dcc94d7e7e53f7eccf534314629d8c Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 16:11:30 +0100 Subject: [PATCH 036/272] Don't log sensitive information; fixes #83 --- .../info/varden/hauk/struct/KeyDerivable.java | 3 ++- .../info/varden/hauk/utils/Preference.java | 26 +++++++++++++++++++ .../varden/hauk/utils/PreferenceManager.java | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/info/varden/hauk/struct/KeyDerivable.java b/android/app/src/main/java/info/varden/hauk/struct/KeyDerivable.java index e9e396f..8c07115 100644 --- a/android/app/src/main/java/info/varden/hauk/struct/KeyDerivable.java +++ b/android/app/src/main/java/info/varden/hauk/struct/KeyDerivable.java @@ -28,6 +28,7 @@ public final class KeyDerivable implements Serializable { /** * End-to-end password to encrypt outgoing data with. */ + @SuppressWarnings("FieldNotUsedInToString") private final String password; /** @@ -62,7 +63,7 @@ public final class KeyDerivable implements Serializable { @Override public String toString() { - return "KeyDerivable{password=" + this.password + return "KeyDerivable{password=" + ",salt=0x" + StringUtils.bytesToHex(this.salt) + "}"; } diff --git a/android/app/src/main/java/info/varden/hauk/utils/Preference.java b/android/app/src/main/java/info/varden/hauk/utils/Preference.java index 9f21655..7b20e8c 100644 --- a/android/app/src/main/java/info/varden/hauk/utils/Preference.java +++ b/android/app/src/main/java/info/varden/hauk/utils/Preference.java @@ -45,6 +45,12 @@ public abstract class Preference { */ abstract void clear(SharedPreferences.Editor prefs); + /** + * Returns whether or not this preference is expected to contain sensitive information that + * should not be logged. + */ + abstract boolean isSensitive(); + /** * Represents a String-value preference. */ @@ -77,6 +83,11 @@ public abstract class Preference { prefs.remove(this.key); } + @Override + boolean isSensitive() { + return false; + } + @SuppressWarnings("DuplicateStringLiteralInspection") @Override public java.lang.String toString() { @@ -128,6 +139,11 @@ public abstract class Preference { prefs.remove(this.key); } + @Override + boolean isSensitive() { + return true; + } + @SuppressWarnings("DuplicateStringLiteralInspection") @Override public java.lang.String toString() { @@ -167,6 +183,11 @@ public abstract class Preference { prefs.remove(this.key); } + @Override + boolean isSensitive() { + return false; + } + @SuppressWarnings("DuplicateStringLiteralInspection") @Override public java.lang.String toString() { @@ -207,6 +228,11 @@ public abstract class Preference { prefs.remove(this.key); } + @Override + boolean isSensitive() { + return false; + } + @SuppressWarnings("DuplicateStringLiteralInspection") @Override public java.lang.String toString() { diff --git a/android/app/src/main/java/info/varden/hauk/utils/PreferenceManager.java b/android/app/src/main/java/info/varden/hauk/utils/PreferenceManager.java index 38b154c..a3a33a9 100644 --- a/android/app/src/main/java/info/varden/hauk/utils/PreferenceManager.java +++ b/android/app/src/main/java/info/varden/hauk/utils/PreferenceManager.java @@ -39,7 +39,7 @@ public final class PreferenceManager { * @see Constants */ public void set(Preference pair, T value) { - Log.v("Setting preference %s, value=%s", pair, value); //NON-NLS + Log.v("Setting preference %s, value=%s", pair, pair.isSensitive() ? "" : value); //NON-NLS SharedPreferences.Editor editor = this.prefs.edit(); pair.set(editor, value); editor.apply(); From 7703ec10de4dbd9bcee70875ae3f8f9c90e4bd82 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 16:29:19 +0100 Subject: [PATCH 037/272] Stop session if stopping last remaining share Fixes #82 --- .../varden/hauk/ui/ShareLinkLayoutManager.java | 11 +++++++++-- .../ui/listener/StopLinkClickListener.java | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/info/varden/hauk/ui/ShareLinkLayoutManager.java b/android/app/src/main/java/info/varden/hauk/ui/ShareLinkLayoutManager.java index a833774..60d5788 100644 --- a/android/app/src/main/java/info/varden/hauk/ui/ShareLinkLayoutManager.java +++ b/android/app/src/main/java/info/varden/hauk/ui/ShareLinkLayoutManager.java @@ -24,7 +24,7 @@ import info.varden.hauk.utils.Log; * * @author Marius Lindvall */ -final class ShareLinkLayoutManager { +public final class ShareLinkLayoutManager { /** * The activity on which the links should be placed. */ @@ -64,6 +64,13 @@ final class ShareLinkLayoutManager { this.act.runOnUiThread(new AddTask(share)); } + /** + * Returns the number of shares that are visible on the UI. + */ + public int getShareViewCount() { + return this.shareViewMap.size(); + } + /** * Removes a link from the list of links that represents the given share. * @@ -105,7 +112,7 @@ final class ShareLinkLayoutManager { Button btnStop = linkView.findViewById(R.id.linkBtnStop); if (this.share.getSession().getBackendVersion().isAtLeast(Constants.VERSION_COMPAT_VIEW_ID)) { Log.i("Server is compatible with individual share termination"); //NON-NLS - btnStop.setOnClickListener(new StopLinkClickListener(ShareLinkLayoutManager.this.manager, this.share)); + btnStop.setOnClickListener(new StopLinkClickListener(ShareLinkLayoutManager.this.manager, this.share, ShareLinkLayoutManager.this)); } else { Log.i("Server is not compatible with individual share termination"); //NON-NLS btnStop.setVisibility(View.GONE); diff --git a/android/app/src/main/java/info/varden/hauk/ui/listener/StopLinkClickListener.java b/android/app/src/main/java/info/varden/hauk/ui/listener/StopLinkClickListener.java index 7cee51e..a960fd2 100644 --- a/android/app/src/main/java/info/varden/hauk/ui/listener/StopLinkClickListener.java +++ b/android/app/src/main/java/info/varden/hauk/ui/listener/StopLinkClickListener.java @@ -4,6 +4,7 @@ import android.view.View; import info.varden.hauk.manager.SessionManager; import info.varden.hauk.struct.Share; +import info.varden.hauk.ui.ShareLinkLayoutManager; import info.varden.hauk.utils.Log; /** @@ -19,19 +20,32 @@ public final class StopLinkClickListener implements View.OnClickListener { */ private final SessionManager manager; + /** + * Link layout on the UI. + */ + private final ShareLinkLayoutManager layout; + /** * The share to share the link for. */ private final Share share; - public StopLinkClickListener(SessionManager manager, Share share) { + public StopLinkClickListener(SessionManager manager, Share share, ShareLinkLayoutManager layout) { this.manager = manager; this.share = share; + this.layout = layout; } @Override public void onClick(View view) { Log.i("User requested to stop sharing %s", this.share); //NON-NLS - this.manager.stopSharing(this.share); + // If there is only one share still active, stop the entire session rather than just this + // one share. + if (this.layout.getShareViewCount() == 1) { + Log.i("Stopping session because there is only one share left"); //NON-NLS + this.manager.stopSharing(); + } else { + this.manager.stopSharing(this.share); + } } } From 6fa10a14f3e11acbd7474aae9aa8e83922c10575 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 16:42:58 +0100 Subject: [PATCH 038/272] Localization cleanup --- android/app/src/main/res/values-ca/strings.xml | 2 +- android/app/src/main/res/values-de/strings.xml | 1 - android/app/src/main/res/values-eu/strings.xml | 1 - android/app/src/main/res/values-fr/strings.xml | 1 - android/app/src/main/res/values-nb-rNO/strings.xml | 2 -- android/app/src/main/res/values-nl/strings.xml | 1 - android/app/src/main/res/values-nn/strings.xml | 2 -- android/app/src/main/res/values-pl/strings.xml | 1 - android/app/src/main/res/values-ru/strings.xml | 1 - android/app/src/main/res/values-uk/strings.xml | 1 - 10 files changed, 1 insertion(+), 12 deletions(-) diff --git a/android/app/src/main/res/values-ca/strings.xml b/android/app/src/main/res/values-ca/strings.xml index 23b707a..636240e 100644 --- a/android/app/src/main/res/values-ca/strings.xml +++ b/android/app/src/main/res/values-ca/strings.xml @@ -41,7 +41,7 @@ D\'acord Cancel·la - Podeu adoptar una quota d’ubicació individual existent per incorporar-la a aquesta quota de grup. Enganxeu l\'URL o l\'identificador de la participació que vulgueu adoptar (per exemple,% s\?XXXX-XXXX) i assigneu un sobrenom a aquest recurs. + Podeu adoptar una quota d’ubicació individual existent per incorporar-la a aquesta quota de grup. Enganxeu l\'URL o l\'identificador de la participació que vulgueu adoptar (per exemple, %s\?XXXX-XXXX) i assigneu un sobrenom a aquest recurs. Podeu crear diversos enllaços per accedir a la ubicació compartida. Cada enllaç es pot aturar individualment, permetent un control minuciós sobre qui pot veure la vostra ubicació en qualsevol moment. \n \nEls enllaços addicionals que creeu d\'aquesta manera seran compartits d\'un sol usuari que només mostrin la vostra ubicació. diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index b4f9f37..bbcd41d 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -3,7 +3,6 @@ Hauk Hauk Teilen Ihrer Position mit Open Source Software - https://github.com/bilde2910/Hauk Server URL: Passwort: Status: diff --git a/android/app/src/main/res/values-eu/strings.xml b/android/app/src/main/res/values-eu/strings.xml index e94a37b..501d83b 100644 --- a/android/app/src/main/res/values-eu/strings.xml +++ b/android/app/src/main/res/values-eu/strings.xml @@ -5,7 +5,6 @@ Ados Sartu duzun zerbitzari URL-a baliogabea da. Aplikazio honek zure kokalekua atzitzeko baimena behar du funtzionatzeko, baina baimen hau ez da oraindik eman. Onartu hurrengo baimen eskaera eta gero sakatu hasi botoia berriro saiatzeko. - https://github.com/bilde2910/Hauk Zerbitzariaren URLa: Pasahitza: Partekatzearen iraupena: diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml index a5850c9..23f767b 100644 --- a/android/app/src/main/res/values-fr/strings.xml +++ b/android/app/src/main/res/values-fr/strings.xml @@ -35,7 +35,6 @@ Partager ce PIN avec les autres membres du groupe pour qu\'il rejoignent votre groupe de partage de location Hauk Hauk - https://github.com/bilde2910/Hauk Hauk vous permet de partager votre localisation pour vous-même ou pour un groupe de personnes. Si une personne crée un partage de groupe, elle obtient un code groupe que d\'autres peuvent utiliser pour rejoindre le partage de localisation. \n \nCependant, la personne qui crée le partage de groupe a également l\'option d\'ajouter un partage de localisation existant dans le groupe de partage. Cette procédure est appelée l\'ajout de partage. diff --git a/android/app/src/main/res/values-nb-rNO/strings.xml b/android/app/src/main/res/values-nb-rNO/strings.xml index 76a8439..65283b1 100644 --- a/android/app/src/main/res/values-nb-rNO/strings.xml +++ b/android/app/src/main/res/values-nb-rNO/strings.xml @@ -3,7 +3,6 @@ Passord: Varighet: Åpen løsning for posisjonsdeling - https://github.com/bilde2910/Hauk Tillat adopsjon: (Hva er dette\?) Kallenavn: @@ -119,7 +118,6 @@ <valgfritt> Brukernavn: Serveren støtter ikke ende-til-ende-kryptering. Ende-til-ende-kryptering støttes i versjon %s og høyere; serveren kjører nå versjon %s. Delingen din vil bli opprettet uten ende-til-ende-kryptering for å beholde kompatibilitet. - (for ende-til-ende-kryptering) Krypteringsnøkkel: Aktiver ende-til-ende-kryptering Krypter deling: diff --git a/android/app/src/main/res/values-nl/strings.xml b/android/app/src/main/res/values-nl/strings.xml index 0fac184..10604d7 100644 --- a/android/app/src/main/res/values-nl/strings.xml +++ b/android/app/src/main/res/values-nl/strings.xml @@ -4,7 +4,6 @@ Deel deze PIN met anderen in uw groep zodat zij uw locatie kunnen volgen De server ondersteund geen groepsdelingen. Groepsdelingen zijn ondersteund vanaf versie %s; de server draait op dit moment versie %s. Uw locatiedeling is omgezet in een individuele deling. Open source locatie deling - https://github.com/bilde2910/Hauk Server URL: Wachtwoord: Deelduratie: diff --git a/android/app/src/main/res/values-nn/strings.xml b/android/app/src/main/res/values-nn/strings.xml index 07b409a..bab469e 100644 --- a/android/app/src/main/res/values-nn/strings.xml +++ b/android/app/src/main/res/values-nn/strings.xml @@ -1,7 +1,6 @@ Open løysing for posisjonsdeling - https://github.com/bilde2910/Hauk Serveradresse: Passord: Varigheit: @@ -119,7 +118,6 @@ <valfritt> Brukarnamn: Serveren støttar ikkje ende-til-ende-kryptering. Ende-til-ende-kryptering er støtta i versjon %s og høgare; serveren køyrer no versjon %s. Delingen din vil bli oppretta utan ende-til-ende-kryptering for å behalda kompatibilitet. - (for ende-til-ende-kryptering) Krypteringsnøkkel: Aktiver ende-til-ende-kryptering Krypter deling: diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml index caefc32..8885bc0 100644 --- a/android/app/src/main/res/values-pl/strings.xml +++ b/android/app/src/main/res/values-pl/strings.xml @@ -31,7 +31,6 @@ Czas udostępniania: Hasło: URL serwera: - https://github.com/bilde2910/Hauk Otwarte oprogramowanie do udostępniania lokalizacji Hauk Hauk diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 3548ff3..d316143 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -12,7 +12,6 @@ Сервер устарел URL Сервера: Пароль: - https://github.com/bilde2910/Hauk Время жизни шары: Интервал обновления (сек): Hauk diff --git a/android/app/src/main/res/values-uk/strings.xml b/android/app/src/main/res/values-uk/strings.xml index c077ccd..a2dd76a 100644 --- a/android/app/src/main/res/values-uk/strings.xml +++ b/android/app/src/main/res/values-uk/strings.xml @@ -29,7 +29,6 @@ (Що це\?) Шарінг місцеположення активний Ділимося місцеположенням за допомогою відкритого ПЗ - https://github.com/bilde2910/Hauk URL сервера: Пароль: Час життя шари: From 405e552c4386814b4ccc77efa21d811689b50456 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 18:16:06 +0100 Subject: [PATCH 039/272] Fix errors when decrypting points with missing data E.g. missing speed or accuracy. This could happen if the client is pushing data from the network location provider, for example. --- frontend/main.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/main.js b/frontend/main.js index 85cf903..8733f86 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -462,7 +462,17 @@ function processUpdate(data, init) { algo.iv = byteArray(data.points[i][0]); var promises = []; for (var j = 1; j < data.points[i].length; j++) { - promises.push(crypto.subtle.decrypt(algo, aesKey, byteArray(data.points[i][j]))); + // Check that the array entry is not null to prevent an + // exception. If the entry is null, push a promise that returns + // null to the array to maintain indexing in the decrypted + // result. + if (data.points[i][j] !== null) { + promises.push(crypto.subtle.decrypt(algo, aesKey, byteArray(data.points[i][j]))); + } else { + promises.push(new Promise(function(resolve, reject) { + resolve(null); + })); + } } pointPromises.push(Promise.all(promises)); } @@ -476,7 +486,12 @@ function processUpdate(data, init) { var decoder = new TextDecoder("utf-8"); for (var i = 0; i < values.length; i++) { for (var j = 0; j < values[i].length; j++) { - data.points[i][j] = parseFloat(decoder.decode(values[i][j])); + // Check that the value isn't null to avoid exceptions. + if (values[i][j] !== null) { + data.points[i][j] = parseFloat(decoder.decode(values[i][j])); + } else { + data.points[i][j] = null; + } } // The IV was the first item in the array, so all items have // been shifted up once. Pop the last array element off. @@ -490,6 +505,7 @@ function processUpdate(data, init) { .catch(function(error) { // Decryption error. Most likely incorrect password. Reset the // key and prompt the user for the password again. + console.log(error); aesKey = null; processUpdate(data, init); }); From 88e54197921984dd4157658a9db6f1002a67495a Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 18:18:07 +0100 Subject: [PATCH 040/272] Fix decryption error input when polling is active This fixes an issue where the password input would constantly clear itself if there was a decryption error while polling was active. --- frontend/main.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/main.js b/frontend/main.js index 8733f86..5d27784 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -507,7 +507,11 @@ function processUpdate(data, init) { // key and prompt the user for the password again. console.log(error); aesKey = null; - processUpdate(data, init); + if (!init) { + clearInterval(fetchIntv); + clearInterval(countIntv); + } + processUpdate(data, true); }); return; From 62d28f42fe517e07aa02823442a84d2fca39cd64 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 18:52:58 +0100 Subject: [PATCH 041/272] Indicate missing backend connection; fixes #85 --- .../toast/GNSSStatusUpdateListenerImpl.java | 10 ++++ .../hauk/http/LocationUpdatePacket.java | 2 +- .../varden/hauk/http/ServerException.java | 2 +- .../manager/GNSSStatusUpdateListener.java | 10 ++++ .../varden/hauk/manager/SessionManager.java | 14 +++++ .../hauk/service/GNSSActiveHandler.java | 10 ++++ .../hauk/service/LocationPushService.java | 57 ++++++++++++++----- .../hauk/ui/GNSSStatusLabelUpdater.java | 32 +++++++++-- android/app/src/main/res/values/colors.xml | 1 + android/app/src/main/res/values/strings.xml | 1 + 10 files changed, 120 insertions(+), 19 deletions(-) diff --git a/android/app/src/main/java/info/varden/hauk/global/ui/toast/GNSSStatusUpdateListenerImpl.java b/android/app/src/main/java/info/varden/hauk/global/ui/toast/GNSSStatusUpdateListenerImpl.java index ceef0ea..0b2718a 100644 --- a/android/app/src/main/java/info/varden/hauk/global/ui/toast/GNSSStatusUpdateListenerImpl.java +++ b/android/app/src/main/java/info/varden/hauk/global/ui/toast/GNSSStatusUpdateListenerImpl.java @@ -41,4 +41,14 @@ public final class GNSSStatusUpdateListenerImpl implements GNSSStatusUpdateListe public void onAccurateLocationReceived() { Toast.makeText(this.ctx, R.string.label_status_ok, Toast.LENGTH_LONG).show(); } + + @Override + public void onServerConnectionLost() { + // Silently ignore + } + + @Override + public void onServerConnectionRestored() { + // Silently ignore + } } diff --git a/android/app/src/main/java/info/varden/hauk/http/LocationUpdatePacket.java b/android/app/src/main/java/info/varden/hauk/http/LocationUpdatePacket.java index 34dd486..9cc3174 100644 --- a/android/app/src/main/java/info/varden/hauk/http/LocationUpdatePacket.java +++ b/android/app/src/main/java/info/varden/hauk/http/LocationUpdatePacket.java @@ -78,7 +78,7 @@ public abstract class LocationUpdatePacket extends Packet { } @Override - protected final void onSuccess(String[] data, Version backendVersion) throws ServerException { + protected void onSuccess(String[] data, Version backendVersion) throws ServerException { // Somehow the data array can be empty? Check for this. if (data.length < 1) { throw new ServerException(getContext(), R.string.err_empty); diff --git a/android/app/src/main/java/info/varden/hauk/http/ServerException.java b/android/app/src/main/java/info/varden/hauk/http/ServerException.java index 8588523..e0bc78f 100644 --- a/android/app/src/main/java/info/varden/hauk/http/ServerException.java +++ b/android/app/src/main/java/info/varden/hauk/http/ServerException.java @@ -8,7 +8,7 @@ import android.content.Context; * * @author Marius Lindvall */ -class ServerException extends Exception { +public class ServerException extends Exception { private static final long serialVersionUID = 2879124634145201633L; /** diff --git a/android/app/src/main/java/info/varden/hauk/manager/GNSSStatusUpdateListener.java b/android/app/src/main/java/info/varden/hauk/manager/GNSSStatusUpdateListener.java index 121ba0c..6e71f58 100644 --- a/android/app/src/main/java/info/varden/hauk/manager/GNSSStatusUpdateListener.java +++ b/android/app/src/main/java/info/varden/hauk/manager/GNSSStatusUpdateListener.java @@ -32,4 +32,14 @@ public interface GNSSStatusUpdateListener { * adequate GNSS signal reception. */ void onAccurateLocationReceived(); + + /** + * Called if the backend server is unreachable. + */ + void onServerConnectionLost(); + + /** + * Called if the backend server was unreachable, but is now reachable again. + */ + void onServerConnectionRestored(); } diff --git a/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java b/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java index c4435d1..1453dc3 100644 --- a/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java +++ b/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java @@ -468,6 +468,20 @@ public abstract class SessionManager { } } + @Override + public void onServerConnectionLost() { + for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) { + listener.onServerConnectionLost(); + } + } + + @Override + public void onServerConnectionRestored() { + for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) { + listener.onServerConnectionRestored(); + } + } + @Override public void onShareListReceived(String linkFormat, String[] shareIDs) { List currentShares = Arrays.asList(shareIDs); diff --git a/android/app/src/main/java/info/varden/hauk/service/GNSSActiveHandler.java b/android/app/src/main/java/info/varden/hauk/service/GNSSActiveHandler.java index cd44d50..e072f53 100644 --- a/android/app/src/main/java/info/varden/hauk/service/GNSSActiveHandler.java +++ b/android/app/src/main/java/info/varden/hauk/service/GNSSActiveHandler.java @@ -16,6 +16,16 @@ public interface GNSSActiveHandler { */ void onAccurateLocationReceived(); + /** + * Called if the backend server is unreachable. + */ + void onServerConnectionLost(); + + /** + * Called if the backend server was unreachable, but is now reachable again. + */ + void onServerConnectionRestored(); + /** * Called when a list of shares the client is contributing to has been received from the server. * diff --git a/android/app/src/main/java/info/varden/hauk/service/LocationPushService.java b/android/app/src/main/java/info/varden/hauk/service/LocationPushService.java index 92f201b..2178ed3 100644 --- a/android/app/src/main/java/info/varden/hauk/service/LocationPushService.java +++ b/android/app/src/main/java/info/varden/hauk/service/LocationPushService.java @@ -14,10 +14,12 @@ import androidx.annotation.Nullable; import info.varden.hauk.Constants; import info.varden.hauk.http.LocationUpdatePacket; +import info.varden.hauk.http.ServerException; import info.varden.hauk.manager.StopSharingTask; import info.varden.hauk.notify.HaukNotification; import info.varden.hauk.notify.SharingNotification; import info.varden.hauk.struct.Share; +import info.varden.hauk.struct.Version; import info.varden.hauk.utils.Log; import info.varden.hauk.utils.ReceiverDataRegistry; @@ -75,6 +77,12 @@ public final class LocationPushService extends Service { */ private LocationListener listenCoarse; + /** + * Whether or not the last update packet was sent successfully, i.e. whether there is a + * connection to the backend server. + */ + private boolean connected = true; + @Override public void onCreate() { Log.d("Fetching location service"); //NON-NLS @@ -185,19 +193,7 @@ public final class LocationPushService extends Service { */ private void onLocationChanged(Location location) { Log.v("Sending location update packet"); //NON-NLS - new LocationUpdatePacket(this, this.share.getSession(), location) { - @Override - public void onShareListReceived(String linkFormat, String[] shares) { - Log.v("Received list of shares from server"); //NON-NLS - LocationPushService.this.gnssActiveTask.onShareListReceived(linkFormat, shares); - } - - @Override - protected void onFailure(Exception ex) { - // Errors can be due to intermittent connectivity. Ignore them. - Log.w("Failed to push location update to server", ex); //NON-NLS - } - }.send(); + new LocationUpdatePacketImpl(location).send(); } @Nullable @@ -205,4 +201,39 @@ public final class LocationPushService extends Service { public IBinder onBind(Intent intent) { return null; } + + private final class LocationUpdatePacketImpl extends LocationUpdatePacket { + private LocationUpdatePacketImpl(Location location) { + super(LocationPushService.this, LocationPushService.this.share.getSession(), location); + } + + @Override + public void onShareListReceived(String linkFormat, String[] shares) { + Log.v("Received list of shares from server"); //NON-NLS + LocationPushService.this.gnssActiveTask.onShareListReceived(linkFormat, shares); + } + + @Override + protected void onSuccess(String[] data, Version backendVersion) throws ServerException { + // Check if connection was lost previously, and notify upstream if that's the case. + Log.i("Hello " + LocationPushService.this.connected); + if (!LocationPushService.this.connected) { + LocationPushService.this.connected = true; + Log.i("Connection to the backend was restored."); //NON-NLS + LocationPushService.this.gnssActiveTask.onServerConnectionRestored(); + } + super.onSuccess(data, backendVersion); + } + + @Override + protected void onFailure(Exception ex) { + Log.w("Failed to push location update to server", ex); //NON-NLS + // Notify upstream about connectivity loss. + if (LocationPushService.this.connected) { + LocationPushService.this.connected = false; + Log.i("Connection to the backend was lost."); //NON-NLS + LocationPushService.this.gnssActiveTask.onServerConnectionLost(); + } + } + } } diff --git a/android/app/src/main/java/info/varden/hauk/ui/GNSSStatusLabelUpdater.java b/android/app/src/main/java/info/varden/hauk/ui/GNSSStatusLabelUpdater.java index 55758cb..d081eb5 100644 --- a/android/app/src/main/java/info/varden/hauk/ui/GNSSStatusLabelUpdater.java +++ b/android/app/src/main/java/info/varden/hauk/ui/GNSSStatusLabelUpdater.java @@ -22,6 +22,9 @@ final class GNSSStatusLabelUpdater implements GNSSStatusUpdateListener { */ private final TextView statusLabel; + private int lastStatus = R.string.label_status_none; + private int lastColor = R.color.statusOff; + GNSSStatusLabelUpdater(Context ctx, TextView statusLabel) { this.ctx = ctx; this.statusLabel = statusLabel; @@ -30,15 +33,19 @@ final class GNSSStatusLabelUpdater implements GNSSStatusUpdateListener { @Override public void onShutdown() { Log.d("Resetting GNSS status label"); //NON-NLS - this.statusLabel.setText(this.ctx.getString(R.string.label_status_none)); + this.statusLabel.setText(R.string.label_status_none); this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusOff)); + this.lastStatus = R.string.label_status_none; + this.lastColor = R.color.statusOff; } @Override public void onStarted() { Log.d("Set GNSS status label to initial state"); //NON-NLS - this.statusLabel.setText(this.ctx.getString(R.string.label_status_wait)); + this.statusLabel.setText(R.string.label_status_wait); this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusWait)); + this.lastStatus = R.string.label_status_wait; + this.lastColor = R.color.statusWait; } @Override @@ -46,7 +53,8 @@ final class GNSSStatusLabelUpdater implements GNSSStatusUpdateListener { // Indicate to the user that GPS data is being received when the location pusher starts // receiving GPS data. Log.i("Initial coarse location was received, awaiting high accuracy fix"); //NON-NLS - this.statusLabel.setText(this.ctx.getString(R.string.label_status_coarse)); + this.statusLabel.setText(R.string.label_status_coarse); + this.lastStatus = R.string.label_status_coarse; } @Override @@ -54,7 +62,23 @@ final class GNSSStatusLabelUpdater implements GNSSStatusUpdateListener { // Indicate to the user that GPS data is being received when the location pusher starts // receiving GPS data. Log.i("Initial high accuracy location was received, using GNSS location data for all future location updates"); //NON-NLS - this.statusLabel.setText(this.ctx.getString(R.string.label_status_ok)); + this.statusLabel.setText(R.string.label_status_ok); this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusOn)); + this.lastStatus = R.string.label_status_ok; + this.lastColor = R.color.statusOn; + } + + @Override + public void onServerConnectionLost() { + // Indicate to the user that the backend connection was lost. + this.statusLabel.setText(R.string.label_status_disconnected); + this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusDisconnected)); + } + + @Override + public void onServerConnectionRestored() { + // Restore the previous status when connection to the backend is restored. + this.statusLabel.setText(this.lastStatus); + this.statusLabel.setTextColor(this.ctx.getColor(this.lastColor)); } } diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 3d0cdf7..b4e0812 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -9,4 +9,5 @@ #D80037 #FF9C00 #007F00 + #76001E diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 4a0e7c4..f0efe28 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -52,6 +52,7 @@ Waiting for initial GNSS fix… Waiting for high accuracy fix… Location sharing active! + Unable to reach backend server YOUR GROUP PIN: Share this PIN with the others in your group to let them join your location sharing group From 58e5ac9414610fc3ee5e7b9366334a6858b89655 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Mon, 9 Dec 2019 19:30:21 +0100 Subject: [PATCH 042/272] Use server time; fixes #86 I reject your reality and substitute my own! --- backend-php/api/fetch.php | 2 ++ frontend/main.js | 15 ++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend-php/api/fetch.php b/backend-php/api/fetch.php index 2300a8d..ff037d0 100644 --- a/backend-php/api/fetch.php +++ b/backend-php/api/fetch.php @@ -28,6 +28,7 @@ if (!$share->exists()) { echo json_encode(array( "type" => $share->getType(), "expire" => $share->getExpirationTime(), + "serverTime" => microtime(true), "interval" => $session->getInterval(), "points" => $session->getPoints(), "encrypted" => $session->isEncrypted(), @@ -39,6 +40,7 @@ if (!$share->exists()) { echo json_encode(array( "type" => $share->getType(), "expire" => $share->getExpirationTime(), + "serverTime" => microtime(true), "interval" => $share->getAutoInterval(), "points" => $share->getAllPoints() )); diff --git a/frontend/main.js b/frontend/main.js index 5d27784..d12797a 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -298,13 +298,14 @@ if (passwordCancelE !== null) { var fetchIntv; var countIntv; -function setNewInterval(expire, interval) { +function setNewInterval(expire, interval, serverTime) { var countdownE = document.getElementById("countdown"); + var timeOffset = Date.now() / 1000 - serverTime; // The data contains an expiration time. Create a countdown at the top of // the map screen that ends when the share is over. countIntv = setInterval(function() { - var seconds = expire - Math.round(Date.now() / 1000); + var seconds = expire - Math.round((Date.now() - timeOffset) / 1000); if (seconds < 0) { clearInterval(countIntv); return; @@ -328,7 +329,7 @@ function setNewInterval(expire, interval) { // once per interval time. fetchIntv = setInterval(function() { // Stop the task if the share has expired. - if ((Date.now() / 1000) >= expire) { + if ((Date.now() - timeOffset) / 1000 >= expire) { clearInterval(fetchIntv); clearInterval(countIntv); if (countdownE !== null) countdownE.textContent = LANG["status_expired"]; @@ -341,7 +342,7 @@ function setNewInterval(expire, interval) { if (data.expire != expire || data.interval != interval) { clearInterval(fetchIntv); clearInterval(countIntv); - setNewInterval(data.expire, data.interval); + setNewInterval(data.expire, data.interval, data.serverTime); } processUpdate(data, false); }, function() { @@ -518,7 +519,7 @@ function processUpdate(data, init) { } // If flagged to initialize, set up polling. - if (init) setNewInterval(data.expire, data.interval); + if (init) setNewInterval(data.expire, data.interval, data.serverTime); for (var user in users) { if (!users.hasOwnProperty(user)) continue; @@ -657,13 +658,13 @@ function processUpdate(data, init) { var eLabel = document.getElementById("label-" + shares[user].id); var eLastSeen = document.getElementById("last-seen-" + shares[user].id); - if (point.time < (Date.now() / 1000) - OFFLINE_TIMEOUT) { + if (point.time < data.serverTime - OFFLINE_TIMEOUT) { eArrow.className = eArrow.className.split("live").join("dead"); if (eLabel !== null) eLabel.className = 'dead'; if (eLastSeen !== null) { // Calculate time since last update and choose an // appropriate unit. - var time = Math.round((Date.now() / 1000) - point.time); + var time = Math.round(data.serverTime - point.time); var unit = LANG["last_update_seconds"]; if (time >= 60) { time = Math.floor(time / 60); From bf4eac8d2a4f96acf9fda2675c544cb7b3142380 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Tue, 10 Dec 2019 10:38:26 +0100 Subject: [PATCH 043/272] Open settings if location services disabled; fixes #87 --- .../java/info/varden/hauk/dialog/Buttons.java | 1 + .../info/varden/hauk/ui/MainActivity.java | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/info/varden/hauk/dialog/Buttons.java b/android/app/src/main/java/info/varden/hauk/dialog/Buttons.java index 482c93a..cfe0b22 100644 --- a/android/app/src/main/java/info/varden/hauk/dialog/Buttons.java +++ b/android/app/src/main/java/info/varden/hauk/dialog/Buttons.java @@ -13,6 +13,7 @@ public enum Buttons { YES_NO (R.string.btn_yes, R.string.btn_no), CREATE_CANCEL (R.string.btn_create, R.string.btn_cancel), SETTINGS_DISMISS (R.string.btn_dismiss, R.string.btn_show_settings), + SETTINGS_OK (R.string.btn_ok, R.string.btn_show_settings), OK_SHARE (R.string.btn_ok, R.string.btn_share_short); // The dialog has one positive and one negative button. diff --git a/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java b/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java index 13bb4dd..875b681 100644 --- a/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java +++ b/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java @@ -242,7 +242,26 @@ public final class MainActivity extends AppCompatActivity { } } catch (LocationServicesDisabledException e) { Log.e("Share initiation was stopped because location services are disabled", e); //NON-NLS - this.dialogSvc.showDialog(R.string.err_client, R.string.err_location_disabled, this.uiResetTask); + this.dialogSvc.showDialog(R.string.err_client, R.string.err_location_disabled, Buttons.SETTINGS_OK, new CustomDialogBuilder() { + @Override + public void onPositive() { + // OK button + MainActivity.this.uiResetTask.run(); + } + + @Override + public void onNegative() { + // Open Settings button + MainActivity.this.uiResetTask.run(); + startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } + + @Nullable + @Override + public View createView(Context ctx) { + return null; + } + }); } catch (LocationPermissionsNotGrantedException e) { Log.w("Share initiation was stopped because the user has not granted location permissions yet", e); //NON-NLS } From 1e1d44be9fdeeba73e6924dc88fec59b277f5303 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Tue, 10 Dec 2019 09:55:33 +0000 Subject: [PATCH 044/272] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (105 of 105 strings) Translation: Hauk/Android client Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/ --- android/app/src/main/res/values-nb-rNO/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/res/values-nb-rNO/strings.xml b/android/app/src/main/res/values-nb-rNO/strings.xml index 65283b1..8c2bde1 100644 --- a/android/app/src/main/res/values-nb-rNO/strings.xml +++ b/android/app/src/main/res/values-nb-rNO/strings.xml @@ -121,4 +121,5 @@ Krypteringsnøkkel: Aktiver ende-til-ende-kryptering Krypter deling: + Serveren kan ikke nås akkurat nå \ No newline at end of file From b94dacdb6abf239a7da38a485584266aa916ab6d Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Tue, 10 Dec 2019 09:57:01 +0000 Subject: [PATCH 045/272] Translated using Weblate (Norwegian Nynorsk) Currently translated at 100.0% (105 of 105 strings) Translation: Hauk/Android client Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/ --- android/app/src/main/res/values-nn/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/res/values-nn/strings.xml b/android/app/src/main/res/values-nn/strings.xml index bab469e..ae5969c 100644 --- a/android/app/src/main/res/values-nn/strings.xml +++ b/android/app/src/main/res/values-nn/strings.xml @@ -121,4 +121,5 @@ Krypteringsnøkkel: Aktiver ende-til-ende-kryptering Krypter deling: + Kan ikkje nå servaren akkurat no \ No newline at end of file From 9c6e5e63627d17eec09cb16b355c44dfad0b31f3 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Tue, 10 Dec 2019 17:01:16 +0100 Subject: [PATCH 046/272] Update to v1.5.2 --- android/app/build.gradle | 4 ++-- backend-php/include/inc.php | 2 +- .../metadata/android/en-US/changelogs/11.txt | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/11.txt diff --git a/android/app/build.gradle b/android/app/build.gradle index 8b00ddc..4e48f0f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "info.varden.hauk" minSdkVersion 23 targetSdkVersion 29 - versionCode 10 - versionName "1.5.1" + versionCode 11 + versionName "1.5.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/backend-php/include/inc.php b/backend-php/include/inc.php index bd18f86..6546c21 100644 --- a/backend-php/include/inc.php +++ b/backend-php/include/inc.php @@ -3,7 +3,7 @@ // An include file containing constants and common functions for the Hauk // backend. It loads the configuration file and declares it as a constant. -const BACKEND_VERSION = "1.5"; +const BACKEND_VERSION = "1.5.2"; const LANGUAGES = ["ca", "de", "en", "eu", "fr", "nb_NO", "nl", "nn", "ru", "uk"]; // Create mode for create.php. Corresponds with the constants from the Android diff --git a/fastlane/metadata/android/en-US/changelogs/11.txt b/fastlane/metadata/android/en-US/changelogs/11.txt new file mode 100644 index 0000000..31a2549 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/11.txt @@ -0,0 +1,17 @@ +For full changelog, see https://github.com/bilde2910/Hauk/releases + +* Added localization for Catalan +* Sharing links can now be generated by the backend in other formats than the default XXXX-XXXX format (#73) +* Leaflet is now served locally instead of through UNPKG's CDN (#67) +* The app now indicates if the backend server is unreachable (#85) +* The prompt that warns that location services are disabled now has a button that opens the device's location settings menu (#87) +* End-to-end encryption can now be disabled without having to erase the encryption password entirely (#71) +* The map now shows how long ago the last location update was received if the person sharing goes offline or loses GPS reception (#76) +* The official Docker image is now also built for ARM-based architectures (for usage on e.g. Raspberry Pi) (#65) +* Stopping the last active shared link now properly stops the entire sharing session instead of just unbinding the link (#82) +* Sensitive information like passwords are no longer leaked in logs (#83) +* The map no longer behaves unpredictably when the browser's current time is inaccurate/out of sync (#86) +* Fixed an issue causing the map to constantly ask for the decryption password when a decryption error happened during update polling +* Fixed a related issue causing the frontend to not accept the correct password when location updates contained missing data, such as speed and accuracy data +* Fixed an issue that caused the UI to behave unpredictably and sometimes crash when resuming shares (#77, #80, #84) +* Fixed minor issue with unencrypted shares From 122304a1be2885212a74f5fb5ef312d404b9a9e7 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Tue, 10 Dec 2019 19:07:14 +0100 Subject: [PATCH 047/272] Provide tag descriptions for Docker Hub images See discussion in #90 --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 56e8406..2491c2d 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,16 @@ files manually. ## Via Docker Compose +The official Docker image on Docker Hub is `bilde2910/hauk`. It comes with several different tags: + +| Tag | Description | +| --- | ----------- | +| `latest` | Updated with each commit to this repository and always has the latest changes. | +| `stable-1.x` | The latest tagged [release](https://github.com/bilde2910/Hauk/releases) of version 1.x. | +| `X.Y.Z` | A specific release of the Hauk backend. Note that old versions are not supported and are provided for your convenience only. | + +`latest`, `stable-1.x` and all releases from `1.5.2` and up are multi-arch and compiled for x86_64, armv7l and aarch64. `1.5.1` and older are x86_64 only. You can use any of these tags for all architectures, and Docker will automatically pick the correct one. If you need the image for a specific architecture, however, you can fetch them using `*-amd64` (x86_64), `*-arm32v7` (armv7l) or `*-arm64v8` (aarch64) versions of any of the tags (e.g. `latest-arm32v7`). + **docker-compose.yml** ```yaml From a1018fa7a7f893d694dbf3804c56fb84572dbf31 Mon Sep 17 00:00:00 2001 From: dms Date: Tue, 10 Dec 2019 15:24:22 -0800 Subject: [PATCH 048/272] apache pid file deletion --- docker/start.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/start.sh b/docker/start.sh index 479d719..69a58a6 100644 --- a/docker/start.sh +++ b/docker/start.sh @@ -4,6 +4,7 @@ stop_all() { echo "Stopping apache" kill $APACHE_PID + rm -rf /run/apache2/apache2.pid echo "Stopping memcached" kill $MEMCACHED_PID From 31b5daaa12ecd4c00b99e825372ebb9e41e1e1c8 Mon Sep 17 00:00:00 2001 From: Marius Lindvall Date: Thu, 12 Dec 2019 00:21:03 +0100 Subject: [PATCH 049/272] Rework UI; see #74 --- android/app/build.gradle | 1 + .../preferences}/PreferenceTest.java | 4 +- android/app/src/main/AndroidManifest.xml | 36 +++- .../main/java/info/varden/hauk/Constants.java | 2 +- .../info/varden/hauk/global/Receiver.java | 2 +- .../InvalidPreferenceTypeException.java | 15 ++ .../preferences}/Preference.java | 30 ++- .../system/preferences/PreferenceHandler.java | 151 +++++++++++++ .../preferences}/PreferenceManager.java | 3 +- .../PreferenceNotFoundException.java | 15 ++ .../preferences/ui/SettingsActivity.java | 86 ++++++++ .../ui/listener/CascadeBindListener.java | 27 +++ .../ui/listener/HintBindListener.java | 24 +++ .../ui/listener/InputTypeBindListener.java | 24 +++ .../info/varden/hauk/ui/MainActivity.java | 107 +++------- .../EncryptionEnabledChangeListener.java | 44 ---- ...mberPasswordPreferenceChangedListener.java | 43 ---- .../hauk/utils/DeprecationMigrator.java | 1 + .../app/src/main/res/drawable/ic_settings.xml | 11 + .../app/src/main/res/layout/activity_main.xml | 202 ++---------------- .../src/main/res/layout/settings_activity.xml | 9 + android/app/src/main/res/menu/title_menu.xml | 11 + .../app/src/main/res/values-ca/strings.xml | 3 - .../app/src/main/res/values-de/strings.xml | 3 - .../app/src/main/res/values-eu/strings.xml | 2 - .../app/src/main/res/values-fr/strings.xml | 2 - .../src/main/res/values-nb-rNO/strings.xml | 3 - .../app/src/main/res/values-nl/strings.xml | 2 - .../app/src/main/res/values-nn/strings.xml | 3 - .../app/src/main/res/values-pl/strings.xml | 2 - .../app/src/main/res/values-ru/strings.xml | 2 - .../app/src/main/res/values-uk/strings.xml | 2 - android/app/src/main/res/values/strings.xml | 38 ++-- android/app/src/main/res/values/styles.xml | 9 +- .../app/src/main/res/xml/root_preferences.xml | 61 ++++++ 35 files changed, 578 insertions(+), 402 deletions(-) rename android/app/src/androidTest/java/info/varden/hauk/{utils => system/preferences}/PreferenceTest.java (96%) create mode 100644 android/app/src/main/java/info/varden/hauk/system/preferences/InvalidPreferenceTypeException.java rename android/app/src/main/java/info/varden/hauk/{utils => system/preferences}/Preference.java (89%) create mode 100644 android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceHandler.java rename android/app/src/main/java/info/varden/hauk/{utils => system/preferences}/PreferenceManager.java (96%) create mode 100644 android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceNotFoundException.java create mode 100644 android/app/src/main/java/info/varden/hauk/system/preferences/ui/SettingsActivity.java create mode 100644 android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/CascadeBindListener.java create mode 100644 android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/HintBindListener.java create mode 100644 android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/InputTypeBindListener.java delete mode 100644 android/app/src/main/java/info/varden/hauk/ui/listener/EncryptionEnabledChangeListener.java delete mode 100644 android/app/src/main/java/info/varden/hauk/ui/listener/RememberPasswordPreferenceChangedListener.java create mode 100644 android/app/src/main/res/drawable/ic_settings.xml create mode 100644 android/app/src/main/res/layout/settings_activity.xml create mode 100644 android/app/src/main/res/menu/title_menu.xml create mode 100644 android/app/src/main/res/xml/root_preferences.xml diff --git a/android/app/build.gradle b/android/app/build.gradle index 4e48f0f..164e244 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,6 +23,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.preference:preference:1.1.0-alpha05' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/android/app/src/androidTest/java/info/varden/hauk/utils/PreferenceTest.java b/android/app/src/androidTest/java/info/varden/hauk/system/preferences/PreferenceTest.java similarity index 96% rename from android/app/src/androidTest/java/info/varden/hauk/utils/PreferenceTest.java rename to android/app/src/androidTest/java/info/varden/hauk/system/preferences/PreferenceTest.java index 6cef5a7..436b572 100644 --- a/android/app/src/androidTest/java/info/varden/hauk/utils/PreferenceTest.java +++ b/android/app/src/androidTest/java/info/varden/hauk/system/preferences/PreferenceTest.java @@ -1,4 +1,4 @@ -package info.varden.hauk.utils; +package info.varden.hauk.system.preferences; import android.content.Context; import android.content.SharedPreferences; @@ -9,6 +9,8 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import info.varden.hauk.system.preferences.Preference; + import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e8a7269..cc2d97f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,17 +9,28 @@ + android:theme="@style/AppTheme"> + + + + + + android:screenOrientation="portrait" + android:theme="@style/HomeTheme"> @@ -29,7 +40,9 @@ + + - - + - + - + + - diff --git a/android/app/src/main/java/info/varden/hauk/Constants.java b/android/app/src/main/java/info/varden/hauk/Constants.java index cb16464..a73c902 100644 --- a/android/app/src/main/java/info/varden/hauk/Constants.java +++ b/android/app/src/main/java/info/varden/hauk/Constants.java @@ -1,7 +1,7 @@ package info.varden.hauk; import info.varden.hauk.struct.Version; -import info.varden.hauk.utils.Preference; +import info.varden.hauk.system.preferences.Preference; /** * Constants used in the Hauk app. diff --git a/android/app/src/main/java/info/varden/hauk/global/Receiver.java b/android/app/src/main/java/info/varden/hauk/global/Receiver.java index eef4e75..602d0bc 100644 --- a/android/app/src/main/java/info/varden/hauk/global/Receiver.java +++ b/android/app/src/main/java/info/varden/hauk/global/Receiver.java @@ -18,8 +18,8 @@ import info.varden.hauk.manager.SessionManager; import info.varden.hauk.struct.AdoptabilityPreference; import info.varden.hauk.system.LocationPermissionsNotGrantedException; import info.varden.hauk.system.LocationServicesDisabledException; +import info.varden.hauk.system.preferences.PreferenceManager; import info.varden.hauk.utils.DeprecationMigrator; -import info.varden.hauk.utils.PreferenceManager; import info.varden.hauk.utils.TimeUtils; /** diff --git a/android/app/src/main/java/info/varden/hauk/system/preferences/InvalidPreferenceTypeException.java b/android/app/src/main/java/info/varden/hauk/system/preferences/InvalidPreferenceTypeException.java new file mode 100644 index 0000000..5a2e441 --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/system/preferences/InvalidPreferenceTypeException.java @@ -0,0 +1,15 @@ +package info.varden.hauk.system.preferences; + +/** + * An exception that is thrown when attempting to read a setting that is not compatible with the + * given type on the settings screen. + * + * @author Marius Lindvall + */ +class InvalidPreferenceTypeException extends RuntimeException { + private static final long serialVersionUID = -1966346602996946755L; + + InvalidPreferenceTypeException(Object value, Class target) { + super(String.format("Cannot direct-cast %s to %s", value.toString(), target.getName())); //NON-NLS + } +} diff --git a/android/app/src/main/java/info/varden/hauk/utils/Preference.java b/android/app/src/main/java/info/varden/hauk/system/preferences/Preference.java similarity index 89% rename from android/app/src/main/java/info/varden/hauk/utils/Preference.java rename to android/app/src/main/java/info/varden/hauk/system/preferences/Preference.java index 7b20e8c..20c854b 100644 --- a/android/app/src/main/java/info/varden/hauk/utils/Preference.java +++ b/android/app/src/main/java/info/varden/hauk/system/preferences/Preference.java @@ -1,4 +1,4 @@ -package info.varden.hauk.utils; +package info.varden.hauk.system.preferences; import android.content.SharedPreferences; @@ -6,6 +6,8 @@ import info.varden.hauk.system.security.EncryptedData; import info.varden.hauk.system.security.EncryptionException; import info.varden.hauk.system.security.KeyStoreAlias; import info.varden.hauk.system.security.KeyStoreHelper; +import info.varden.hauk.utils.Log; +import info.varden.hauk.utils.StringSerializer; /** * Represents a preference key to default value mapping pair for use with storing preferences for @@ -16,6 +18,28 @@ import info.varden.hauk.system.security.KeyStoreHelper; */ public abstract class Preference { + private final java.lang.String key; + private final Class type; + + private Preference(java.lang.String key, Class type) { + this.key = key; + this.type = type; + } + + /** + * Returns the key of this preference in the {@link SharedPreferences} instance. + */ + public final java.lang.String getKey() { + return this.key; + } + + /** + * Returns the type of data this preference stores. + */ + public final Class getPreferenceType() { + return this.type; + } + /** * Gets the value of the preference from the given preference object. * @@ -59,6 +83,7 @@ public abstract class Preference { private final java.lang.String def; public String(java.lang.String key, java.lang.String def) { + super(key, java.lang.String.class); this.key = key; this.def = def; } @@ -103,6 +128,7 @@ public abstract class Preference { private final java.lang.String def; public EncryptedString(java.lang.String key, java.lang.String def) { + super(key, java.lang.String.class); this.key = key; this.def = def; } @@ -159,6 +185,7 @@ public abstract class Preference { private final int def; public Integer(java.lang.String key, int def) { + super(key, java.lang.Integer.class); this.key = key; this.def = def; } @@ -204,6 +231,7 @@ public abstract class Preference { @SuppressWarnings("BooleanParameter") public Boolean(java.lang.String key, boolean def) { + super(key, java.lang.Boolean.class); this.key = key; this.def = def; } diff --git a/android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceHandler.java b/android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceHandler.java new file mode 100644 index 0000000..fb80952 --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceHandler.java @@ -0,0 +1,151 @@ +package info.varden.hauk.system.preferences; + +import android.content.Context; + +import androidx.preference.PreferenceDataStore; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import info.varden.hauk.Constants; +import info.varden.hauk.utils.Log; + +/** + * Preference interceptor data store that redirects preference storage requests to + * {@link PreferenceManager} and {@link Preference} for proper validation and storage. This allows + * preferences to be encrypted, for example. + * + * @author Marius Lindvall + */ +public final class PreferenceHandler extends PreferenceDataStore { + /** + * Mapping between all preference keys and {@link Preference}s. + */ + private static final Map map; + + static { + // Initialize the preference map. + map = new HashMap<>(); + + // Find all Preferences declared in the Constants class and add them to the map. + Field[] fields = Constants.class.getFields(); + for (Field f : fields) { + if (f.getType().isAssignableFrom(Preference.class)) { + try { + Log.v("Found field %s of type Preference in Constants, adding to map", f.getName()); //NON-NLS + Preference p = (Preference) f.get(null); + map.put(p.getKey(), p); + } catch (IllegalAccessException e) { + Log.wtf("Failed to read constant from Constants", e); //NON-NLS + } + } + } + } + + /** + * Hauk preference manager. + */ + private final PreferenceManager manager; + + public PreferenceHandler(Context ctx) { + this.manager = new PreferenceManager(ctx); + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + if (!map.containsKey(key)) throw new PreferenceNotFoundException(key); + Object value = this.manager.get(map.get(key)); + if (value instanceof Boolean) { + return (boolean) value; + } else { + throw new InvalidPreferenceTypeException(value, Boolean.class); + } + } + + @Override + public float getFloat(String key, float defValue) { + if (!map.containsKey(key)) throw new PreferenceNotFoundException(key); + Object value = this.manager.get(map.get(key)); + if (value instanceof Float) { + return (float) value; + } else { + throw new InvalidPreferenceTypeException(value, Float.class); + } + } + + @Override + public int getInt(String key, int defValue) { + if (!map.containsKey(key)) throw new PreferenceNotFoundException(key); + Object value = this.manager.get(map.get(key)); + if (value instanceof Integer) { + return (int) value; + } else { + throw new InvalidPreferenceTypeException(value, Integer.class); + } + } + + @Override + public long getLong(String key, long defValue) { + if (!map.containsKey(key)) throw new PreferenceNotFoundException(key); + Object value = this.manager.get(map.get(key)); + if (value instanceof Long) { + return (long) value; + } else { + throw new InvalidPreferenceTypeException(value, Long.class); + } + } + + @Override + public String getString(String key, String defValue) { + if (!map.containsKey(key)) throw new PreferenceNotFoundException(key); + Object value = this.manager.get(map.get(key)); + if (value instanceof String) { + return (String) value; + } else if (value instanceof Integer || value instanceof Float || value instanceof Long) { + // EditTextPreference calls getString() instead of getInt(), getFloat() and getLong() + // because it is a text input field, despite the type of data it is set to store. This + // must be handled properly. + return String.valueOf(value); + } else { + throw new InvalidPreferenceTypeException(value, String.class); + } + } + + @Override + public void putBoolean(String key, boolean value) { + this.manager.set(map.get(key), value); + } + + @Override + public void putFloat(String key, float value) { + this.manager.set(map.get(key), value); + } + + @Override + public void putInt(String key, int value) { + this.manager.set(map.get(key), value); + } + + @Override + public void putLong(String key, long value) { + this.manager.set(map.get(key), value); + } + + @Override + public void putString(String key, String value) { + // EditTextPreferences calls putString() instead of putInt(), putFloat() and putLong() + // because it is a text input field, despite the type of data it is set to store. This + // must be handled properly. + Class type = map.get(key).getPreferenceType(); + if (type == Integer.class) { + putInt(key, Integer.valueOf(value)); + } else if (type == Float.class) { + putFloat(key, Float.valueOf(value)); + } else if (type == Long.class) { + putLong(key, Long.valueOf(value)); + } else { + this.manager.set(map.get(key), value); + } + } +} diff --git a/android/app/src/main/java/info/varden/hauk/utils/PreferenceManager.java b/android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceManager.java similarity index 96% rename from android/app/src/main/java/info/varden/hauk/utils/PreferenceManager.java rename to android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceManager.java index a3a33a9..48adfd7 100644 --- a/android/app/src/main/java/info/varden/hauk/utils/PreferenceManager.java +++ b/android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceManager.java @@ -1,9 +1,10 @@ -package info.varden.hauk.utils; +package info.varden.hauk.system.preferences; import android.content.Context; import android.content.SharedPreferences; import info.varden.hauk.Constants; +import info.varden.hauk.utils.Log; /** * Utility class that manages connection preferences in Hauk. diff --git a/android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceNotFoundException.java b/android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceNotFoundException.java new file mode 100644 index 0000000..4ca4ce6 --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/system/preferences/PreferenceNotFoundException.java @@ -0,0 +1,15 @@ +package info.varden.hauk.system.preferences; + +/** + * Exception that is thrown when {@link info.varden.hauk.system.preferences.ui.SettingsActivity} + * tries to read a setting that does not exist in Hauk. + * + * @author Marius Lindvall + */ +class PreferenceNotFoundException extends RuntimeException { + private static final long serialVersionUID = 6201186189243885309L; + + PreferenceNotFoundException(String key) { + super(String.format("Preference %s was requested but does not exist in Hauk", key)); //NON-NLS + } +} diff --git a/android/app/src/main/java/info/varden/hauk/system/preferences/ui/SettingsActivity.java b/android/app/src/main/java/info/varden/hauk/system/preferences/ui/SettingsActivity.java new file mode 100644 index 0000000..0c579e7 --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/system/preferences/ui/SettingsActivity.java @@ -0,0 +1,86 @@ +package info.varden.hauk.system.preferences.ui; + +import android.content.Context; +import android.os.Bundle; +import android.text.InputType; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.EditTextPreference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import info.varden.hauk.Constants; +import info.varden.hauk.R; +import info.varden.hauk.system.preferences.PreferenceHandler; +import info.varden.hauk.system.preferences.ui.listener.CascadeBindListener; +import info.varden.hauk.system.preferences.ui.listener.HintBindListener; +import info.varden.hauk.system.preferences.ui.listener.InputTypeBindListener; + +/** + * Settings activity that allows the user to change app preferences. + * + * @author Marius Lindvall + */ +public final class SettingsActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.settings_activity); + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.settings, new SettingsFragment(this)) + .commit(); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + } + + public static final class SettingsFragment extends PreferenceFragmentCompat { + /** + * Android application context. + */ + private final Context ctx; + + private SettingsFragment(Context ctx) { + this.ctx = ctx; + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + PreferenceManager manager = getPreferenceManager(); + + // Intercept all reads and writes so that values are properly validated and encrypted if + // required by Preference. + manager.setPreferenceDataStore(new PreferenceHandler(this.ctx)); + + // Load the preferences layout. + setPreferencesFromResource(R.xml.root_preferences, rootKey); + + // Set InputType and other attributes for text edit boxes. + ((EditTextPreference) manager.findPreference(Constants.PREF_SERVER_ENCRYPTED.getKey())).setOnBindEditTextListener(new CascadeBindListener(new EditTextPreference.OnBindEditTextListener[]{ + new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI), + new HintBindListener(R.string.label_server_hint) + })); + ((EditTextPreference) manager.findPreference(Constants.PREF_USERNAME_ENCRYPTED.getKey())).setOnBindEditTextListener(new CascadeBindListener(new EditTextPreference.OnBindEditTextListener[]{ + new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PERSON_NAME), + new HintBindListener(R.string.label_username_hint) + })); + ((EditTextPreference) manager.findPreference(Constants.PREF_PASSWORD_ENCRYPTED.getKey())).setOnBindEditTextListener( + new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD) + ); + ((EditTextPreference) manager.findPreference(Constants.PREF_E2E_PASSWORD.getKey())).setOnBindEditTextListener( + new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD) + ); + ((EditTextPreference) manager.findPreference(Constants.PREF_INTERVAL.getKey())).setOnBindEditTextListener( + new InputTypeBindListener(InputType.TYPE_CLASS_NUMBER) + ); + ((EditTextPreference) manager.findPreference(Constants.PREF_CUSTOM_ID.getKey())).setOnBindEditTextListener(new CascadeBindListener(new EditTextPreference.OnBindEditTextListener[]{ + new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE), + new HintBindListener(R.string.label_custom_id_hint) + })); + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/CascadeBindListener.java b/android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/CascadeBindListener.java new file mode 100644 index 0000000..ff8a491 --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/CascadeBindListener.java @@ -0,0 +1,27 @@ +package info.varden.hauk.system.preferences.ui.listener; + +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.preference.EditTextPreference; + +/** + * Edit text bind listener that cascades the bind event to several + * {@link androidx.preference.EditTextPreference.OnBindEditTextListener}s. + * + * @author Marius Lindvall + */ +public final class CascadeBindListener implements EditTextPreference.OnBindEditTextListener { + private final EditTextPreference.OnBindEditTextListener[] listeners; + + public CascadeBindListener(EditTextPreference.OnBindEditTextListener[] listeners) { + this.listeners = listeners.clone(); + } + + @Override + public void onBindEditText(@NonNull EditText editText) { + for (EditTextPreference.OnBindEditTextListener listener : this.listeners) { + listener.onBindEditText(editText); + } + } +} diff --git a/android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/HintBindListener.java b/android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/HintBindListener.java new file mode 100644 index 0000000..088aafa --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/HintBindListener.java @@ -0,0 +1,24 @@ +package info.varden.hauk.system.preferences.ui.listener; + +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.preference.EditTextPreference; + +/** + * Edit text bind listener that sets the hint of an {@link EditTextPreference}. + * + * @author Marius Lindvall + */ +public class HintBindListener implements EditTextPreference.OnBindEditTextListener { + private final int hintResource; + + public HintBindListener(int hintResource) { + this.hintResource = hintResource; + } + + @Override + public void onBindEditText(@NonNull EditText editText) { + editText.setHint(this.hintResource); + } +} diff --git a/android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/InputTypeBindListener.java b/android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/InputTypeBindListener.java new file mode 100644 index 0000000..23ebba1 --- /dev/null +++ b/android/app/src/main/java/info/varden/hauk/system/preferences/ui/listener/InputTypeBindListener.java @@ -0,0 +1,24 @@ +package info.varden.hauk.system.preferences.ui.listener; + +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.preference.EditTextPreference; + +/** + * Edit text bind listener that sets the input type of an {@link EditTextPreference}. + * + * @author Marius Lindvall + */ +public class InputTypeBindListener implements EditTextPreference.OnBindEditTextListener { + private final int inputType; + + public InputTypeBindListener(int inputType) { + this.inputType = inputType; + } + + @Override + public void onBindEditText(@NonNull EditText editText) { + editText.setInputType(this.inputType); + } +} diff --git a/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java b/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java index 875b681..8c0d9fe 100644 --- a/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java +++ b/android/app/src/main/java/info/varden/hauk/ui/MainActivity.java @@ -6,17 +6,18 @@ import android.content.Context; import android.content.Intent; import android.graphics.Paint; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.Checkable; -import android.widget.CompoundButton; -import android.widget.EditText; import android.widget.Spinner; import android.widget.TextView; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import info.varden.hauk.Constants; @@ -40,14 +41,13 @@ import info.varden.hauk.struct.Version; import info.varden.hauk.system.LocationPermissionsNotGrantedException; import info.varden.hauk.system.LocationServicesDisabledException; import info.varden.hauk.system.powersaving.DeviceChecker; +import info.varden.hauk.system.preferences.PreferenceManager; +import info.varden.hauk.system.preferences.ui.SettingsActivity; import info.varden.hauk.ui.listener.AddLinkClickListener; -import info.varden.hauk.ui.listener.EncryptionEnabledChangeListener; import info.varden.hauk.ui.listener.InitiateAdoptionClickListener; -import info.varden.hauk.ui.listener.RememberPasswordPreferenceChangedListener; import info.varden.hauk.ui.listener.SelectionModeChangedListener; import info.varden.hauk.utils.DeprecationMigrator; import info.varden.hauk.utils.Log; -import info.varden.hauk.utils.PreferenceManager; import info.varden.hauk.utils.TimeUtils; /** @@ -104,17 +104,13 @@ public final class MainActivity extends AppCompatActivity { Log.i("Creating main activity"); //NON-NLS setContentView(R.layout.activity_main); + setSupportActionBar((Toolbar) findViewById(R.id.mainToolbar)); + setClassVariables(); ((TextView) findViewById(R.id.labelAdoptWhatsThis)).setPaintFlags(Paint.UNDERLINE_TEXT_FLAG); Log.d("Attaching event handlers"); //NON-NLS - // Add an on checked handler to the password remember checkbox to save their encryption - // password immediately. - ((CompoundButton) findViewById(R.id.chkRemember)).setOnCheckedChangeListener( - new RememberPasswordPreferenceChangedListener(this, (EditText) findViewById(R.id.txtE2EPassword)) - ); - // Add an event handler to the sharing mode selector. //noinspection OverlyStrongTypeCast ((Spinner) findViewById(R.id.selMode)).setOnItemSelectedListener(new SelectionModeChangedListener( @@ -125,16 +121,6 @@ public final class MainActivity extends AppCompatActivity { loadPreferences(); - // Add an on checked handler to the enable E2E checkbox to toggle the E2E state. This must - // be done after loading preferences to ensure that the checkbox doesn't trigger the event - // when hidden. - ((CompoundButton) findViewById(R.id.chkUseE2E)).setOnCheckedChangeListener( - new EncryptionEnabledChangeListener(this, new View[] { - findViewById(R.id.rowE2EPassword), - findViewById(R.id.rowRemember) - }) - ); - this.manager.resumeShares(new ResumePrompt() { @Override public void promptForResumption(Context ctx, Session session, Share[] shares, PromptCallback response) { @@ -152,6 +138,24 @@ public final class MainActivity extends AppCompatActivity { new DeviceChecker(this).performCheck(); } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.title_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_settings: + startActivity(new Intent(this, SettingsActivity.class)); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + @Override protected void onDestroy() { this.uiStopTask.setActivityDestroyed(); @@ -174,14 +178,16 @@ public final class MainActivity extends AppCompatActivity { findViewById(R.id.btnShare).setEnabled(false); disableUI(); - String server = ((TextView) findViewById(R.id.txtServer)).getText().toString().trim(); - String username = ((TextView) findViewById(R.id.txtUsername)).getText().toString().trim(); - String password = ((TextView) findViewById(R.id.txtPassword)).getText().toString(); + PreferenceManager prefs = new PreferenceManager(this); + + String server = prefs.get(Constants.PREF_SERVER_ENCRYPTED).trim(); + String username = prefs.get(Constants.PREF_USERNAME_ENCRYPTED).trim(); + String password = prefs.get(Constants.PREF_PASSWORD_ENCRYPTED); int duration = Integer.parseInt(((TextView) findViewById(R.id.txtDuration)).getText().toString()); - int interval = Integer.parseInt(((TextView) findViewById(R.id.txtInterval)).getText().toString()); - String customID = ((TextView) findViewById(R.id.txtCustomID)).getText().toString().trim(); - boolean useE2E = ((Checkable) findViewById(R.id.chkUseE2E)).isChecked(); - String e2ePass = ((TextView) findViewById(R.id.txtE2EPassword)).getText().toString(); + int interval = prefs.get(Constants.PREF_INTERVAL); + String customID = prefs.get(Constants.PREF_CUSTOM_ID).trim(); + boolean useE2E = prefs.get(Constants.PREF_ENABLE_E2E); + String e2ePass = prefs.get(Constants.PREF_E2E_PASSWORD); String nickname = ((TextView) findViewById(R.id.txtNickname)).getText().toString().trim(); @SuppressWarnings("OverlyStrongTypeCast") ShareMode mode = ShareMode.fromMode(((Spinner) findViewById(R.id.selMode)).getSelectedItemPosition()); String groupPin = ((TextView) findViewById(R.id.txtGroupCode)).getText().toString(); @@ -191,24 +197,10 @@ public final class MainActivity extends AppCompatActivity { // Save connection preferences for next launch, so the user doesn't have to enter URL etc. // every time. Log.i("Updating connection preferences"); //NON-NLS - PreferenceManager prefs = new PreferenceManager(this); - prefs.set(Constants.PREF_SERVER_ENCRYPTED, server); - prefs.set(Constants.PREF_USERNAME_ENCRYPTED, username); - prefs.set(Constants.PREF_PASSWORD_ENCRYPTED, password); prefs.set(Constants.PREF_DURATION, duration); - prefs.set(Constants.PREF_INTERVAL, interval); - prefs.set(Constants.PREF_CUSTOM_ID, customID); prefs.set(Constants.PREF_DURATION_UNIT, durUnit); prefs.set(Constants.PREF_NICKNAME, nickname); prefs.set(Constants.PREF_ALLOW_ADOPTION, allowAdoption); - prefs.set(Constants.PREF_ENABLE_E2E, useE2E); - - // If password saving is enabled, save the password as well. - if (((Checkable) findViewById(R.id.chkRemember)).isChecked()) { - Log.i("Saving E2E password"); //NON-NLS - prefs.set(Constants.PREF_REMEMBER_PASSWORD, true); - prefs.set(Constants.PREF_E2E_PASSWORD, e2ePass); - } // Ignore E2E password if E2E is disabled. if (!useE2E) e2ePass = ""; @@ -286,20 +278,6 @@ public final class MainActivity extends AppCompatActivity { this.dialogSvc.showDialog(R.string.explain_adopt_title, R.string.explain_adopt_body); } - /** - * On-tap handler for the "show advanced settings" button. - */ - public void showAdvancedSettings(View view) { - view.setVisibility(View.GONE); - findViewById(R.id.rowUpdateInterval).setVisibility(View.VISIBLE); - findViewById(R.id.rowCustomID).setVisibility(View.VISIBLE); - findViewById(R.id.rowUseE2E).setVisibility(View.VISIBLE); - if (((Checkable) findViewById(R.id.chkUseE2E)).isChecked()) { - findViewById(R.id.rowE2EPassword).setVisibility(View.VISIBLE); - findViewById(R.id.rowRemember).setVisibility(View.VISIBLE); - } - } - /** * This function is called by onCreate() to initialize class-level variables for usage in this * activity. @@ -307,20 +285,13 @@ public final class MainActivity extends AppCompatActivity { private void setClassVariables() { Log.d("Setting class variables"); //NON-NLS this.lockWhileRunning = new View[] { - findViewById(R.id.txtServer), - findViewById(R.id.txtUsername), - findViewById(R.id.txtPassword), findViewById(R.id.txtDuration), - findViewById(R.id.txtInterval), - findViewById(R.id.txtCustomID), - findViewById(R.id.txtE2EPassword), findViewById(R.id.selUnit), findViewById(R.id.selMode), findViewById(R.id.txtNickname), findViewById(R.id.txtGroupCode), - findViewById(R.id.chkAllowAdopt), - findViewById(R.id.chkUseE2E) + findViewById(R.id.chkAllowAdopt) }; this.uiResetTask = new ResetTask(); @@ -351,21 +322,13 @@ public final class MainActivity extends AppCompatActivity { private void loadPreferences() { Log.i("Loading preferences..."); //NON-NLS PreferenceManager prefs = new PreferenceManager(this); - ((TextView) findViewById(R.id.txtServer)).setText(prefs.get(Constants.PREF_SERVER_ENCRYPTED)); - ((TextView) findViewById(R.id.txtUsername)).setText(prefs.get(Constants.PREF_USERNAME_ENCRYPTED)); ((TextView) findViewById(R.id.txtDuration)).setText(String.valueOf(prefs.get(Constants.PREF_DURATION))); - ((TextView) findViewById(R.id.txtInterval)).setText(String.valueOf(prefs.get(Constants.PREF_INTERVAL))); - ((TextView) findViewById(R.id.txtCustomID)).setText(prefs.get(Constants.PREF_CUSTOM_ID)); - ((TextView) findViewById(R.id.txtE2EPassword)).setText(prefs.get(Constants.PREF_E2E_PASSWORD)); - ((TextView) findViewById(R.id.txtPassword)).setText(prefs.get(Constants.PREF_PASSWORD_ENCRYPTED)); ((TextView) findViewById(R.id.txtNickname)).setText(prefs.get(Constants.PREF_NICKNAME)); // Because I can choose between an unchecked cast warning and an overly strong cast warning, // I'm going to with the latter. //noinspection OverlyStrongTypeCast ((Spinner) findViewById(R.id.selUnit)).setSelection(prefs.get(Constants.PREF_DURATION_UNIT)); - ((Checkable) findViewById(R.id.chkRemember)).setChecked(prefs.get(Constants.PREF_REMEMBER_PASSWORD)); ((Checkable) findViewById(R.id.chkAllowAdopt)).setChecked(prefs.get(Constants.PREF_ALLOW_ADOPTION)); - ((Checkable) findViewById(R.id.chkUseE2E)).setChecked(prefs.get(Constants.PREF_ENABLE_E2E)); } /** diff --git a/android/app/src/main/java/info/varden/hauk/ui/listener/EncryptionEnabledChangeListener.java b/android/app/src/main/java/info/varden/hauk/ui/listener/EncryptionEnabledChangeListener.java deleted file mode 100644 index edb3e17..0000000 --- a/android/app/src/main/java/info/varden/hauk/ui/listener/EncryptionEnabledChangeListener.java +++ /dev/null @@ -1,44 +0,0 @@ -package info.varden.hauk.ui.listener; - -import android.content.Context; -import android.view.View; -import android.widget.CompoundButton; - -import info.varden.hauk.Constants; -import info.varden.hauk.utils.Log; -import info.varden.hauk.utils.PreferenceManager; - -/** - * On-checked-change listener for the checkbox that lets users change their preference of whether or - * not they want to enable end-to-end encryption. - * - * @see info.varden.hauk.ui.MainActivity - * @author Marius Lindvall - */ -public final class EncryptionEnabledChangeListener implements CompoundButton.OnCheckedChangeListener { - /** - * Android application context. - */ - private final Context ctx; - - /** - * The list of views which should be enabled/disabled when the checkbox is changed. - */ - private final View[] e2eViews; - - public EncryptionEnabledChangeListener(Context ctx, View[] views) { - this.ctx = ctx; - this.e2eViews = views.clone(); - } - - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { - Log.i("End-to-end encryption preference changed, enabled=%s", isChecked); //NON-NLS - // Show/hide the end-to-end encryption views. - PreferenceManager prefs = new PreferenceManager(this.ctx); - for (View view : this.e2eViews) { - view.setVisibility(isChecked ? View.VISIBLE : View.GONE); - } - prefs.set(Constants.PREF_ENABLE_E2E, isChecked); - } -} diff --git a/android/app/src/main/java/info/varden/hauk/ui/listener/RememberPasswordPreferenceChangedListener.java b/android/app/src/main/java/info/varden/hauk/ui/listener/RememberPasswordPreferenceChangedListener.java deleted file mode 100644 index 5a7c2ad..0000000 --- a/android/app/src/main/java/info/varden/hauk/ui/listener/RememberPasswordPreferenceChangedListener.java +++ /dev/null @@ -1,43 +0,0 @@ -package info.varden.hauk.ui.listener; - -import android.content.Context; -import android.widget.CompoundButton; -import android.widget.EditText; - -import info.varden.hauk.Constants; -import info.varden.hauk.utils.Log; -import info.varden.hauk.utils.PreferenceManager; - -/** - * On-checked-change listener for the checkbox that lets users change their preference of whether or - * not they want the app to save their password. - * - * @see info.varden.hauk.ui.MainActivity - * @author Marius Lindvall - */ -public final class RememberPasswordPreferenceChangedListener implements CompoundButton.OnCheckedChangeListener { - /** - * Android application context. - */ - private final Context ctx; - - /** - * The text input box that contains the password. - */ - private final EditText passwordBox; - - public RememberPasswordPreferenceChangedListener(Context ctx, EditText passwordBox) { - this.ctx = ctx; - this.passwordBox = passwordBox; - } - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - Log.i("Password remember preference changed, remember=%s", isChecked); //NON-NLS - // Update the stored password immediately. Clear the password from preferences if the box - // was unchecked. - PreferenceManager prefs = new PreferenceManager(this.ctx); - prefs.set(Constants.PREF_REMEMBER_PASSWORD, isChecked); - prefs.set(Constants.PREF_E2E_PASSWORD, isChecked ? this.passwordBox.getText().toString() : ""); - } -} diff --git a/android/app/src/main/java/info/varden/hauk/utils/DeprecationMigrator.java b/android/app/src/main/java/info/varden/hauk/utils/DeprecationMigrator.java index bda90ad..ca53634 100644 --- a/android/app/src/main/java/info/varden/hauk/utils/DeprecationMigrator.java +++ b/android/app/src/main/java/info/varden/hauk/utils/DeprecationMigrator.java @@ -3,6 +3,7 @@ package info.varden.hauk.utils; import android.content.Context; import info.varden.hauk.Constants; +import info.varden.hauk.system.preferences.PreferenceManager; /** * Helper utility to migrate old, deprecated settings saved in shared preferences to modern storage. diff --git a/android/app/src/main/res/drawable/ic_settings.xml b/android/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..6057e39 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,11 @@ + + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 1381a8b..1dd1969 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -1,11 +1,21 @@ - + + - - - - - - - - - - - - - - - - - - - - - - - - - - -