Compare commits
197
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58cdc1be8d | ||
|
|
4039202268
|
||
|
|
4677cc5229 | ||
|
|
8800724aa9
|
||
|
|
56f3a9b380 | ||
|
|
a0d41c388b | ||
|
|
092cc63148 | ||
|
|
95553296bd | ||
|
|
9330227879 | ||
|
|
faaff33841 | ||
|
|
508a6899d8 | ||
|
|
be0f7b64b8 | ||
|
|
15e3248ee1 | ||
|
|
04a897c958 | ||
|
|
845d77ebda | ||
|
|
8bc3fda49b | ||
|
|
d78817180b | ||
|
|
0f810bd2ba | ||
|
|
9d1fc8b60c
|
||
|
|
dd46fb3d32
|
||
|
|
1a40e3a18d | ||
|
|
1aa5d3b43e
|
||
|
|
a418d5cf66 | ||
|
|
5cae286a5b
|
||
|
|
c5d2591371 | ||
|
|
cf59f937c9
|
||
|
|
257f59c96e | ||
|
|
c2f5806394
|
||
|
|
ac0b71fe09 | ||
|
|
19f7b50002
|
||
|
|
6342bb1e79
|
||
|
|
3abdb6910e
|
||
|
|
7d2588f7cd | ||
|
|
be40acdb2d
|
||
|
|
a2f2080959
|
||
|
|
cc49c56075 | ||
|
|
69329b9f81
|
||
|
|
20914b466e
|
||
|
|
3630637ec1
|
||
|
|
42224b074e
|
||
|
|
0e8ed14305
|
||
|
|
d3a12fb6f1 | ||
|
|
3f9d104adc
|
||
|
|
9e5d20796c
|
||
|
|
7433813337 | ||
|
|
42854fe975
|
||
|
|
71681f1e55 | ||
|
|
1cf0ab44fd
|
||
|
|
fc9eb183f1 | ||
|
|
d431a693fc
|
||
|
|
ace490b7e9 | ||
|
|
e3771d36ce
|
||
|
|
6e03476173 | ||
|
|
f3ec71d6dd
|
||
|
|
c2f0a1dd51
|
||
|
|
a417fbd760 | ||
|
|
a9cda56586
|
||
|
|
02fcbd2f9c
|
||
|
|
2f665381c3
|
||
|
|
573440868e
|
||
|
|
79490a0616
|
||
|
|
ed343f2359
|
||
|
|
563f900423
|
||
|
|
24a10c534e
|
||
|
|
0cbb7ba145
|
||
|
|
5dd2ce0d5a
|
||
|
|
e5fb725c69
|
||
|
|
fc40c18679
|
||
|
|
4fabc39d44
|
||
|
|
2120fbebbc
|
||
|
|
b9f0529dce
|
||
|
|
a3ca7638c7
|
||
|
|
8538d0a119
|
||
|
|
0cc63ef936 | ||
|
|
10d239bfb4
|
||
|
|
e2493e1b7e | ||
|
|
edd8c0f8a4
|
||
|
|
01f0f7d4bc | ||
|
|
c317a0abf4
|
||
|
|
9c4b7448a9
|
||
|
|
60e20bd92b
|
||
|
|
2c4d809d49
|
||
|
|
bf27f15709 | ||
|
|
db3094eb62
|
||
|
|
77c71ab2da | ||
|
|
8327cd975a
|
||
|
|
cc8148fcf9 | ||
|
|
2a7b5fb992
|
||
|
|
a4c77c3d32 | ||
|
|
317329ae84
|
||
|
|
234cb7f086 | ||
|
|
66c709f00d
|
||
|
|
2737c7d6d6
|
||
|
|
2ccee26424 | ||
|
|
8cbf611189
|
||
|
|
2ac9794e3f | ||
|
|
9e5f7405d1
|
||
|
|
af98d71924
|
||
|
|
39d691fd7f
|
||
|
|
405488cf4e
|
||
|
|
f2736d4d6e | ||
|
|
88b9821c61
|
||
|
|
da2c8d96a4 | ||
|
|
cd581c6fcd
|
||
|
|
f7b268e10f | ||
|
|
a8b1ab3057
|
||
|
|
8403c21ec0
|
||
|
|
0ba1cda884
|
||
|
|
b2e09c394b
|
||
|
|
0bcb0187c1
|
||
|
|
f3d1608164
|
||
|
|
b254603fb1
|
||
|
|
776561a41c | ||
|
|
27fc687043
|
||
|
|
45869321ac
|
||
|
|
b58137acc9 | ||
|
|
dd6bb5ef9a
|
||
|
|
37094e552b
|
||
|
|
32392590bd
|
||
|
|
38885849de
|
||
|
|
ef013b8b9e
|
||
|
|
ab23b37b97
|
||
|
|
7e3315fc88
|
||
|
|
6cafac1b65
|
||
|
|
631582a912 | ||
|
|
6db6208896
|
||
|
|
e17cf68007
|
||
|
|
091afb0aab | ||
|
|
954f111254
|
||
|
|
de1d7338a4
|
||
|
|
7539f637b0
|
||
|
|
67534a22f5
|
||
|
|
43aef22c4e | ||
|
|
3123643bb7
|
||
|
|
b736ce8970
|
||
|
|
4e48e6ae73
|
||
|
|
028d8a22a2
|
||
|
|
a73e60ae5a | ||
|
|
5383da81b3
|
||
|
|
e8adc9f730 | ||
|
|
149003250c
|
||
|
|
171b4278a6 | ||
|
|
091018d22f
|
||
|
|
34f0c92eb5 | ||
|
|
8ac5d5c6e2
|
||
|
|
e883d184b7
|
||
|
|
71b1e28aa9 | ||
|
|
3555be9b63
|
||
|
|
d03a3934c6 | ||
|
|
bd5b49b09b
|
||
|
|
4333240acc
|
||
|
|
4099f1d962
|
||
|
|
2fac828098
|
||
|
|
f3225ae275
|
||
|
|
48eba75cf4
|
||
|
|
a97a6f8977 | ||
|
|
2a3f891aa0
|
||
|
|
38a427416c | ||
|
|
2f14e1f4c3
|
||
|
|
6098b7bb8d | ||
|
|
596b29f841
|
||
|
|
34a4026ddc | ||
|
|
a55ece7dac
|
||
|
|
efdd5c54be
|
||
|
|
825e0744ec
|
||
|
|
18bf9aaade
|
||
|
|
5e645a1313 | ||
|
|
9c9edcbbd1
|
||
|
|
de009637de | ||
|
|
6fce002458
|
||
|
|
1287fa0bc9
|
||
|
|
bf8e5b5f4d
|
||
|
|
c8a49396c2
|
||
|
|
c2323c4c89
|
||
|
|
60cbe64718
|
||
|
|
c48bbdc3c2
|
||
|
|
ee6aa59253 | ||
|
|
e1df28123f
|
||
|
|
b4769b62dd | ||
|
|
bee991e300 | ||
|
|
ddc56426c2 | ||
|
|
c06dd1e1d2 | ||
|
|
fd657b4244 | ||
|
|
532f099ec9 | ||
|
|
ec969167e4 | ||
|
|
e4a51c139f | ||
|
|
27115e44b6 | ||
|
|
45904a98c2 | ||
|
|
b1ae7391e7 | ||
|
|
8987b563dd | ||
|
|
70d8807f38 | ||
|
|
910fdddf34 | ||
|
|
87c4c06aa6 | ||
|
|
ad24f8ac28 | ||
|
|
e2067627c4 | ||
|
|
a8419535e1 | ||
|
|
725d14400b |
No files matched your search
+5
-1
@@ -1,4 +1,4 @@
|
||||
APP_NAME=MapQuiz
|
||||
APP_NAME=MapGuesser
|
||||
DEV=1
|
||||
DB_HOST=mariadb
|
||||
DB_USER=mapguesser
|
||||
@@ -15,3 +15,7 @@ MAIL_PORT=2500
|
||||
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
|
||||
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
|
||||
GOOGLE_ANALITICS_ID=your_google_analytics_id
|
||||
MULTI_INTERNAL_HOST=multi
|
||||
MULTI_INTERNAL_PORT=5000
|
||||
MULTI_WS_URL=mapguesser-dev.ch:8090
|
||||
MULTI_WS_PORT=8090
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
.env
|
||||
installed
|
||||
unit_test_results.xml
|
||||
vendor
|
||||
node_modules
|
||||
Vendored
+8
@@ -9,6 +9,14 @@
|
||||
"pathMappings": {
|
||||
"/var/www/mapguesser": "${workspaceRoot}",
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen for NodeJS Inspector in Docker",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 9229,
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/var/www/mapguesser"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,71 @@
|
||||
# MapGuesser
|
||||
|
||||
This is the MapGuesser Application project.
|
||||
This is the MapGuesser Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications.
|
||||
|
||||
License: GNU AGPL 3.0
|
||||
## Installation
|
||||
|
||||
### Clone the Git repository
|
||||
|
||||
The first step is obviously cloning the repository to your machine:
|
||||
|
||||
```
|
||||
git clone https://gitea.e5tv.hu/esoko/mapguesser.git
|
||||
```
|
||||
|
||||
All the commands listed here should be executed from the repository root.
|
||||
|
||||
### (Optional) Setup Docker stack
|
||||
|
||||
The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`.
|
||||
|
||||
All you have to do is executing the following command:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Attach shell to the container of `mapguesser_app`. All of the following commands should be executed there.
|
||||
|
||||
**If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.**
|
||||
|
||||
### Initialize project
|
||||
|
||||
This command installes all of the Composer requirements and creates a copy of the example `.env` file.
|
||||
|
||||
```
|
||||
composer create-project
|
||||
```
|
||||
|
||||
### Set environment variables
|
||||
|
||||
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
|
||||
|
||||
**You should set here the API keys that enable playing the game. Without these API keys the application cannot work well. To get Google API keys visit this page: https://console.developers.google.com/**
|
||||
|
||||
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in produciton mode.
|
||||
|
||||
If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, etc.) are fine with the default value.
|
||||
|
||||
### (Production only) Create cron job
|
||||
|
||||
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommened to create a cron job that runs every hour:
|
||||
|
||||
```
|
||||
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1
|
||||
```
|
||||
|
||||
### Finalize installation
|
||||
|
||||
After you set the environment variables in the `.env` file, execute the following command:
|
||||
|
||||
```
|
||||
scripts/install.sh
|
||||
```
|
||||
|
||||
**And you are done!** The application is ready to use and develop.
|
||||
|
||||
If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306.
|
||||
|
||||
---
|
||||
|
||||
*License: **GNU AGPL 3.0**. Full license text can be found in file `LICENSE`.*
|
||||
@@ -13,3 +13,14 @@ pipelines:
|
||||
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
- composer install
|
||||
- vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests
|
||||
- step:
|
||||
name: Static Code Analysis
|
||||
caches:
|
||||
- composer
|
||||
artifacts:
|
||||
- static_code_analysis_results.json
|
||||
script:
|
||||
- apt-get update && apt-get install -y unzip
|
||||
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
- composer install
|
||||
- php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
+4
-2
@@ -6,10 +6,12 @@
|
||||
"require": {
|
||||
"vlucas/phpdotenv": "^4.1",
|
||||
"symfony/console": "^5.1",
|
||||
"phpmailer/phpmailer": "^6.1"
|
||||
"phpmailer/phpmailer": "^6.1",
|
||||
"fzaninotto/faker": "^1.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9"
|
||||
"phpunit/phpunit": "^9",
|
||||
"phpstan/phpstan": "^0.12.32"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
Generated
+113
-2
@@ -4,8 +4,63 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "fb37b0b958cdf02d82468cb4596613ad",
|
||||
"content-hash": "b71c0ffc0761a6b90f6242346b735a09",
|
||||
"packages": [
|
||||
{
|
||||
"name": "fzaninotto/faker",
|
||||
"version": "v1.9.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fzaninotto/Faker.git",
|
||||
"reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/fzaninotto/Faker/zipball/848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
|
||||
"reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.3.3 || ^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-intl": "*",
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7",
|
||||
"squizlabs/php_codesniffer": "^2.9.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.9-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Faker\\": "src/Faker/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "François Zaninotto"
|
||||
}
|
||||
],
|
||||
"description": "Faker is a PHP library that generates fake data for you.",
|
||||
"keywords": [
|
||||
"data",
|
||||
"faker",
|
||||
"fixtures"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/fzaninotto/Faker/issues",
|
||||
"source": "https://github.com/fzaninotto/Faker/tree/v1.9.2"
|
||||
},
|
||||
"abandoned": true,
|
||||
"time": "2020-12-11T09:56:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.1.6",
|
||||
@@ -1379,6 +1434,62 @@
|
||||
],
|
||||
"time": "2020-03-05T15:02:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "0.12.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "d03863f504c8432b3de4d1881f73f6acb8c0e67c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03863f504c8432b3de4d1881f73f6acb8c0e67c",
|
||||
"reference": "d03863f504c8432b3de4d1881f73f6acb8c0e67c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
},
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.12-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/ondrejmirtes",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/phpstan",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-07-01T11:57:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "8.0.2",
|
||||
@@ -2654,5 +2765,5 @@
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "1.1.0"
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE
|
||||
`places`
|
||||
ADD
|
||||
`pov_heading` decimal(6, 3) NOT NULL DEFAULT 0.0,
|
||||
ADD
|
||||
`pov_pitch` decimal(5, 3) NOT NULL DEFAULT 0.0,
|
||||
ADD
|
||||
`pov_zoom` decimal(5, 4) NOT NULL DEFAULT 0.0;
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE `user_password_resetters` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL,
|
||||
`token` varchar(32) CHARACTER SET ascii NOT NULL,
|
||||
`expires` timestamp NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `token` (`token`),
|
||||
CONSTRAINT `user_password_resetters_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -0,0 +1,5 @@
|
||||
UPDATE `user_confirmations` SET token=SUBSTRING(token, 1, 32);
|
||||
|
||||
ALTER TABLE `user_confirmations`
|
||||
ADD `last_sent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `token` varchar(32) CHARACTER SET ascii NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `sessions`
|
||||
MODIFY `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -0,0 +1,4 @@
|
||||
UPDATE `sessions` SET id=SUBSTRING(id, 1, 32);
|
||||
|
||||
ALTER TABLE `sessions`
|
||||
MODIFY `id` varchar(32) CHARACTER SET ascii NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `users`
|
||||
ADD `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE `multi_rooms` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`room_id` varchar(6) NOT NULL,
|
||||
`state` text NOT NULL,
|
||||
`members` text NOT NULL,
|
||||
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `room_id` (`room_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
+15
-4
@@ -1,16 +1,27 @@
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
build: ./docker
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Dockerfile-app
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- .:/var/www/mapguesser
|
||||
links:
|
||||
- 'mariadb'
|
||||
- 'mail'
|
||||
multi:
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Dockerfile-multi
|
||||
ports:
|
||||
- 5000:5000
|
||||
- 8090:8090
|
||||
- 9229:9229
|
||||
volumes:
|
||||
- .:/var/www/mapguesser
|
||||
mariadb:
|
||||
image: mariadb:10.3
|
||||
ports:
|
||||
- 3306:3306
|
||||
volumes:
|
||||
- mysql:/var/lib/mysql
|
||||
environment:
|
||||
|
||||
@@ -21,7 +21,7 @@ RUN ./install-composer.sh
|
||||
# Install Node.js and required packages
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
|
||||
RUN apt install -y nodejs
|
||||
RUN npm install -g uglify-js clean-css-cli svgo
|
||||
RUN npm install -g uglify-js clean-css-cli svgo yarn
|
||||
|
||||
EXPOSE 80
|
||||
VOLUME /var/www/mapguesser
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM ubuntu:focal
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# Install necessary packages
|
||||
RUN apt update --fix-missing
|
||||
RUN apt install -y curl build-essential
|
||||
|
||||
# Install Node.js and required packages
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
|
||||
RUN apt install -y nodejs
|
||||
|
||||
VOLUME /var/www/mapguesser
|
||||
WORKDIR /var/www/mapguesser
|
||||
|
||||
ENTRYPOINT /usr/bin/node --inspect=0.0.0.0:9229 multi
|
||||
@@ -0,0 +1,13 @@
|
||||
Hi,
|
||||
<br><br>
|
||||
You recently requested password reset on {{APP_NAME}} with this email address ({{EMAIL}}).
|
||||
To reset the password to your account, please click on the following link:<br>
|
||||
<a href="{{RESET_LINK}}" title="Reset password">{{RESET_LINK}}</a>
|
||||
<br><br>
|
||||
You can reset your password with this link util {{EXPIRES}}.
|
||||
<br><br>
|
||||
If you did not requested password reset, no further action is required, your account is not touched.
|
||||
<br><br>
|
||||
Regards,<br>
|
||||
{{APP_NAME}}<br>
|
||||
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
|
||||
+7
-3
@@ -1,10 +1,14 @@
|
||||
Hi,
|
||||
<br><br>
|
||||
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}). To activate your account, please click on the following link:<br>
|
||||
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
|
||||
To activate your account, please click on the following link:<br>
|
||||
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
|
||||
<br><br>
|
||||
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required, your email address will be deleted soon.<br>
|
||||
However if you want to immediately delete it, please click on the following link:<br>
|
||||
You can activate your account until {{ACTIVATABLE_UNTIL}}.
|
||||
If you don't activate your account, your email address will be permanently deleted after this point of time.
|
||||
<br><br>
|
||||
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
|
||||
However if you want to immediately delete your email address, please click on the following link:<br>
|
||||
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
|
||||
<br><br>
|
||||
Have fun on {{APP_NAME}}!
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
define('SCRIPT_STARTED', hrtime(true));
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
const ROOT = __DIR__;
|
||||
@@ -14,7 +16,7 @@ class Container
|
||||
{
|
||||
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
|
||||
static MapGuesser\Routing\RouteCollection $routeCollection;
|
||||
static \SessionHandlerInterface $sessionHandler;
|
||||
static MapGuesser\Interfaces\Session\ISessionHandler $sessionHandler;
|
||||
static MapGuesser\Interfaces\Request\IRequest $request;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ require 'main.php';
|
||||
|
||||
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
|
||||
|
||||
$app->add(new MapGuesser\Cli\DatabaseMigration());
|
||||
$app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
|
||||
$app->add(new MapGuesser\Cli\AddUserCommand());
|
||||
$app->add(new MapGuesser\Cli\LinkViewCommand());
|
||||
$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand());
|
||||
|
||||
$app->run();
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
'use strict';
|
||||
|
||||
process.title = 'mapguesser-multi';
|
||||
|
||||
class MultiGame {
|
||||
static ROUND_TIMEOUT_DEFAULT = 120000;
|
||||
static ROUND_TIMEOUT_MINIMUM = 15000;
|
||||
static ROUND_TIMEOUT_DIVIDER = 1.5;
|
||||
static ROUND_TIMEOUT_OFFSET = 500;
|
||||
|
||||
constructor() {
|
||||
this.rooms = new Map();
|
||||
}
|
||||
|
||||
cleanupRooms() {
|
||||
this.rooms.forEach(function (room, roomId) {
|
||||
var lastValidDate = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
if (room.updated < lastValidDate) {
|
||||
this.rooms.delete(roomId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
connectToRoom(roomId, token, connection) {
|
||||
if (!this.rooms.has(roomId) || !this.rooms.get(roomId).members.has(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId)
|
||||
var member = room.members.get(token);
|
||||
member.connection = connection;
|
||||
|
||||
this._sendInitialData(room, member, token);
|
||||
}
|
||||
|
||||
createRoom(roomId) {
|
||||
this.rooms.set(roomId, { members: new Map(), rounds: [], currentRound: -1, updated: new Date() });
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
joinRoom(roomId, token, userName) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
return { error: 'room_not_found' };
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
if (room.members.has(token)) {
|
||||
return { error: 'member_already_joined' };
|
||||
}
|
||||
|
||||
var data = { userName: userName };
|
||||
var self = this;
|
||||
room.members.forEach(function (member) {
|
||||
self._sendToMember(member, 'member_joined', data);
|
||||
});
|
||||
|
||||
room.members.set(token, { userName: userName, connection: null });
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
startGame(roomId, places) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
return { error: 'room_not_found' };
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
var rounds = [];
|
||||
places.forEach(function (place) {
|
||||
rounds.push({
|
||||
place: place,
|
||||
results: new Map(),
|
||||
timeout: MultiGame.ROUND_TIMEOUT_DEFAULT,
|
||||
timeoutStarted: null,
|
||||
timeoutHandler: null
|
||||
})
|
||||
});
|
||||
|
||||
room.rounds = rounds;
|
||||
|
||||
this.nextRound(roomId, 0);
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
guess(roomId, token, guessPosition, distance, score) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
return { error: 'room_not_found' };
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
var round = room.rounds[room.currentRound];
|
||||
if (round.results.has(token)) {
|
||||
return { error: 'already_guessed' };
|
||||
}
|
||||
|
||||
var member = room.members.get(token);
|
||||
var allResults = this._collectResultsInRound(room, round);
|
||||
|
||||
this._broadcastGuess(room, member.userName, guessPosition, distance, score);
|
||||
|
||||
round.results.set(token, { guessPosition: guessPosition, distance: distance, score: score });
|
||||
|
||||
this._setNewTimeout(room, round);
|
||||
|
||||
return { allResults: allResults };
|
||||
}
|
||||
|
||||
nextRound(roomId, currentRound) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
return { error: 'room_not_found' };
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
room.currentRound = currentRound;
|
||||
|
||||
var round = room.rounds[room.currentRound];
|
||||
|
||||
round.timeoutStarted = new Date();
|
||||
var self = this;
|
||||
round.timeoutHandler = setTimeout(function () {
|
||||
self._endRound(room, round);
|
||||
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
|
||||
|
||||
var data = {};
|
||||
data.place = { panoId: round.place.panoId, pov: round.place.pov };
|
||||
data.timeout = round.timeout;
|
||||
|
||||
var self = this;
|
||||
room.members.forEach(function (member) {
|
||||
self._sendToMember(member, 'new_round', data);
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
_setNewTimeout(room, round) {
|
||||
clearTimeout(round.timeoutHandler);
|
||||
|
||||
if (room.members.size === round.results.size) {
|
||||
round.timeout = 0;
|
||||
round.timeoutStarted = new Date();
|
||||
|
||||
this._endRound(room, round);
|
||||
} else {
|
||||
round.timeout = round.timeout - (new Date() - round.timeoutStarted);
|
||||
if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) {
|
||||
round.timeout = Math.round(round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER);
|
||||
} else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) {
|
||||
round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM;
|
||||
}
|
||||
round.timeoutStarted = new Date();
|
||||
var self = this;
|
||||
round.timeoutHandler = setTimeout(function () {
|
||||
self._endRound(room, round);
|
||||
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
|
||||
|
||||
this._broadcastTimeout(room, round);
|
||||
}
|
||||
}
|
||||
|
||||
_endRound(room, round) {
|
||||
var allResults = this._collectResultsInRound(room, round);
|
||||
var self = this;
|
||||
room.members.forEach(function (member, token) {
|
||||
var result = { guessPosition: null, distance: null, score: 0 };
|
||||
|
||||
if (round.results.has(token)) {
|
||||
result = round.results.get(token);
|
||||
} else {
|
||||
round.results.set(token, result);
|
||||
}
|
||||
|
||||
var data = { position: round.place.position, result: result, allResults: allResults };
|
||||
self._sendToMember(member, 'end_round', data);
|
||||
});
|
||||
}
|
||||
|
||||
_sendInitialData(room, member, token) {
|
||||
var data = {};
|
||||
|
||||
if (room.currentRound >= 0) {
|
||||
var round = room.rounds[room.currentRound];
|
||||
|
||||
data.place = round.place;
|
||||
data.timeout = round.timeout - (new Date() - round.timeoutStarted);
|
||||
}
|
||||
|
||||
data.history = [];
|
||||
for (var i = 0; i <= room.currentRound; ++i) {
|
||||
var round = room.rounds[i];
|
||||
|
||||
if (i === room.currentRound && !round.results.has(token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = { guessPosition: null, distance: null, score: 0 };
|
||||
var allResults = [];
|
||||
|
||||
round.results.forEach(function (currentResult, currentToken) {
|
||||
if (token === currentToken) {
|
||||
result = currentResult;
|
||||
return;
|
||||
}
|
||||
|
||||
allResults.push({ userName: room.members.get(currentToken).userName, guessPosition: currentResult.guessPosition, distance: currentResult.distance, score: currentResult.score });
|
||||
});
|
||||
|
||||
data.history.push({
|
||||
position: round.place.position,
|
||||
result: result,
|
||||
allResults: allResults
|
||||
});
|
||||
}
|
||||
|
||||
data.members = [];
|
||||
room.members.forEach(function (currentMember) {
|
||||
data.members.push({ userName: currentMember.userName, me: member === currentMember });
|
||||
});
|
||||
|
||||
data.readyToContinue = room.currentRound >= 0 && room.members.size === room.rounds[room.currentRound].results.size
|
||||
|
||||
this._sendToMember(member, 'initialize', data);
|
||||
}
|
||||
|
||||
_collectResultsInRound(room, round) {
|
||||
var results = [];
|
||||
round.results.forEach(function (result, token) {
|
||||
results.push({
|
||||
userName: room.members.get(token).userName,
|
||||
guessPosition: result.guessPosition,
|
||||
distance: result.distance,
|
||||
score: result.score
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
_broadcastTimeout(room, round) {
|
||||
var self = this;
|
||||
room.members.forEach(function (member) {
|
||||
self._sendToMember(member, 'timeout_changed', { timeout: round.timeout });
|
||||
});
|
||||
}
|
||||
|
||||
_broadcastGuess(room, userName, guessPosition, distance, score) {
|
||||
var data = { userName: userName, guessPosition: guessPosition, distance: distance, score: score };
|
||||
var round = room.rounds[room.currentRound];
|
||||
var self = this;
|
||||
room.members.forEach(function (member, token) {
|
||||
if (!round.results.has(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._sendToMember(member, 'guess', data);
|
||||
});
|
||||
}
|
||||
|
||||
_sendToMember(member, type, data) {
|
||||
if (!member.connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.connection.readyState !== ws.OPEN) {
|
||||
member.connection = null;
|
||||
return;
|
||||
}
|
||||
|
||||
member.connection.send(JSON.stringify({ type: type, data: data }));
|
||||
}
|
||||
}
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
var
|
||||
net = require('net'),
|
||||
ws = require('ws');
|
||||
|
||||
var multiGame = new MultiGame();
|
||||
|
||||
//TODO: following should be in a separate class/function
|
||||
|
||||
var tcpServer = net.createServer(function (socket) {
|
||||
socket.on('data', function (data) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error('Cannot parse data: ' + data);
|
||||
return;
|
||||
}
|
||||
|
||||
var response;
|
||||
switch (data.func) {
|
||||
case 'create_room':
|
||||
response = multiGame.createRoom(data.args.roomId);
|
||||
|
||||
break;
|
||||
|
||||
case 'join_room':
|
||||
response = multiGame.joinRoom(data.args.roomId, data.args.token, data.args.userName);
|
||||
|
||||
break;
|
||||
|
||||
case 'start_game':
|
||||
response = multiGame.startGame(data.args.roomId, data.args.places);
|
||||
|
||||
break
|
||||
|
||||
case 'guess':
|
||||
response = multiGame.guess(data.args.roomId, data.args.token, data.args.guessPosition, data.args.distance, data.args.score);
|
||||
|
||||
break;
|
||||
|
||||
case 'next_round':
|
||||
response = multiGame.nextRound(data.args.roomId, data.args.currentRound);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
socket.write(JSON.stringify(response));
|
||||
socket.end();
|
||||
});
|
||||
});
|
||||
tcpServer.on('listening', function () {
|
||||
console.log('[INFO] TCP server started');
|
||||
});
|
||||
tcpServer.listen(process.env.MULTI_INTERNAL_PORT);
|
||||
|
||||
var wsServer = new ws.Server({ port: process.env.MULTI_WS_PORT });
|
||||
wsServer.on('connection', function (connection, request) {
|
||||
console.log('[INFO] New WS connection: ' + request.connection.remoteAddress);
|
||||
|
||||
connection.on('message', function (data) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error('Cannot parse data: ' + data);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.func) {
|
||||
case 'connect_to_room':
|
||||
multiGame.connectToRoom(data.args.roomId, data.args.token, connection);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('close', function () {
|
||||
console.log('[INFO] WS connection ended: ' + request.connection.remoteAddress);
|
||||
});
|
||||
});
|
||||
wsServer.on('listening', function () {
|
||||
console.log('[INFO] WS server started');
|
||||
});
|
||||
|
||||
setInterval(function () {
|
||||
multiGame.cleanupRooms();
|
||||
}, 24 * 60 * 60 * 1000);
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "mapguesser-multi",
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "mapguesser-multi",
|
||||
"version": "",
|
||||
"description": "MapGuesser Application - Multiplayer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"dotenv": "^8.2.0",
|
||||
"ws": "^7.4.4"
|
||||
},
|
||||
"scripts": {},
|
||||
"author": "Pőcze Bence and The MapGuesser Contributors <bence@pocze.ch>",
|
||||
"license": "GNU AGPL 3.0"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
parameters:
|
||||
level: 5
|
||||
checkMissingIterableValueType: false
|
||||
paths:
|
||||
- main.php
|
||||
- mapg
|
||||
- web.php
|
||||
- src
|
||||
- tests
|
||||
+5
-3
@@ -26,8 +26,9 @@ if ($match !== null) {
|
||||
}
|
||||
|
||||
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
|
||||
$content = new MapGuesser\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 403);
|
||||
echo json_encode(['error' => 'no_valid_anti_csrf_token']);
|
||||
$content->render();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ if ($match !== null) {
|
||||
|
||||
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
|
||||
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
|
||||
echo $response->render();
|
||||
$response->render();
|
||||
|
||||
return;
|
||||
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
|
||||
@@ -47,5 +48,6 @@ if ($match !== null) {
|
||||
}
|
||||
}
|
||||
|
||||
$content = new MapGuesser\Response\HtmlContent('error/404');
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 404);
|
||||
require ROOT . '/views/error/404.php';
|
||||
$content->render();
|
||||
@@ -79,7 +79,7 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
|
||||
#distanceInfo>p:nth-child(2), #distanceInfo>p:nth-child(3), #scoreInfo>p:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -102,6 +102,53 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#startMultiGameButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#players > p {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#countdown {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 8px;
|
||||
background-color: #eeeeee;
|
||||
border: solid 1px #555555;
|
||||
border-radius: 3px;
|
||||
opacity: 0.95;
|
||||
z-index: 5;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#countdown.yellow {
|
||||
background-color: #f7c789;
|
||||
border: solid 1px #e8a349;
|
||||
}
|
||||
|
||||
#countdown.red {
|
||||
background-color: #f7a5a5;
|
||||
border: solid 1px #aa5e5e;
|
||||
}
|
||||
|
||||
#countdown p {
|
||||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
#countdown.yellow p {
|
||||
color: #9c4308;
|
||||
}
|
||||
|
||||
#countdown.red p {
|
||||
color: #701919;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
#mapName {
|
||||
display: none;
|
||||
|
||||
@@ -17,6 +17,11 @@ button::-moz-focus-inner, input::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* to be compatible with browsers that don't know <main> */
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #28a745;
|
||||
color: #ffffff;
|
||||
@@ -148,13 +153,27 @@ button, a.button {
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
button.small {
|
||||
button.small, div.inputWithButton>button {
|
||||
font-size: 14px;
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
button.small {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
div.inputWithButton>button {
|
||||
border-radius: 2px;
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
width: 75px;
|
||||
margin-left: -79px;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
|
||||
button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus {
|
||||
background-color: #29457f;
|
||||
outline: none;
|
||||
@@ -186,7 +205,7 @@ button.gray, a.button.gray {
|
||||
background-color: #808080;
|
||||
}
|
||||
|
||||
button.gray:hover, button.gray:focus, a.button.gray:hover, a.button.gray:focus {
|
||||
button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus {
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
@@ -194,7 +213,7 @@ button.red, a.button.red {
|
||||
background-color: #aa5e5e;
|
||||
}
|
||||
|
||||
button.red:hover, button.red:focus, a.button.red:hover, a.button.red:focus {
|
||||
button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus {
|
||||
background-color: #7f2929;
|
||||
}
|
||||
|
||||
@@ -202,7 +221,7 @@ button.yellow, a.button.yellow {
|
||||
background-color: #e8a349;
|
||||
}
|
||||
|
||||
button.yellow:hover, button.yellow:focus, a.button.yellow:hover, a.button.yellow:focus {
|
||||
button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus {
|
||||
background-color: #c37713;
|
||||
}
|
||||
|
||||
@@ -210,7 +229,7 @@ button.green, a.button.green {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
button.green:hover, button.green:focus, a.button.green:hover, a.button.green:focus {
|
||||
button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.button.green:focus {
|
||||
background-color: #1b7d31;
|
||||
}
|
||||
|
||||
@@ -218,22 +237,36 @@ input, select, textarea {
|
||||
background-color: #f9fafb;
|
||||
border: solid #c8d2e1 1px;
|
||||
border-radius: 2px;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
input, select {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-size: 13px;
|
||||
padding: 5px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
input.big, select.big, textarea.big {
|
||||
padding: 5px;
|
||||
input.big, select.big, textarea.big, div.inputWithButton>input {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
input.big, select.big, div.inputWithButton>input {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
textarea.big {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
input.fullWidth, select.fullWidth, textarea.fullWidth {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -248,14 +281,34 @@ input:disabled, select:disabled, textarea:disabled {
|
||||
input:focus, select:focus, textarea:focus {
|
||||
background-color: #ffffff;
|
||||
border: solid #29457f 2px;
|
||||
padding: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input.big:focus, select.big:focus, textarea.big:focus {
|
||||
input:focus, select:focus {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
input.big:focus, select.big:focus {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
div.inputWithButton>input {
|
||||
width: 100%;
|
||||
padding: 0 83px 0 6px;
|
||||
}
|
||||
|
||||
div.inputWithButton>input:focus {
|
||||
padding: 0 82px 0 5px;
|
||||
}
|
||||
|
||||
textarea.big:focus {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
div.modal {
|
||||
position: fixed;
|
||||
background-color: #ffffff;
|
||||
@@ -324,10 +377,11 @@ main {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
div.full {
|
||||
main.full {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
@@ -350,9 +404,17 @@ div.buttonContainer>button {
|
||||
}
|
||||
|
||||
#cookiesNotice {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 20px;
|
||||
background-color: #eeeeee;
|
||||
border: solid #888888 1px;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#loading {
|
||||
|
||||
@@ -18,7 +18,16 @@ div.mapItem>div.title {
|
||||
color: white;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
padding: 0 8px;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
div.mapItem>div.title>p {
|
||||
line-height: inherit;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div.mapItem>div.title>p.title {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#3183ce"
|
||||
fill-rule="evenodd"
|
||||
stroke="#19456d"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
@@ -0,0 +1,69 @@
|
||||
var Account = {
|
||||
original: null,
|
||||
countdown: null,
|
||||
|
||||
openGoogleAuthenticate: function () {
|
||||
window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600')
|
||||
},
|
||||
|
||||
authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) {
|
||||
var password = document.getElementsByTagName('form')[0].elements.password;
|
||||
var button = document.getElementById('authenticateWithGoogleButton');
|
||||
|
||||
Account.original = {
|
||||
type: password.type,
|
||||
placeholder: password.placeholder,
|
||||
required: password.required,
|
||||
disabled: password.disabled
|
||||
};
|
||||
|
||||
password.type = 'text';
|
||||
password.placeholder = 'Authenticated!'
|
||||
password.value = '';
|
||||
password.required = false;
|
||||
password.disabled = true;
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
Account.countdown = setInterval(function () {
|
||||
var timeLeft = Math.ceil((authenticatedWithGoogleUntil.getTime() - new Date().getTime()) / 1000);
|
||||
|
||||
if (timeLeft > 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
Account.resetGoogleAuthentication();
|
||||
return;
|
||||
}
|
||||
|
||||
password.placeholder = 'Authenticated! ' + timeLeft + ' seconds left...';
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
resetGoogleAuthentication: function () {
|
||||
if (Account.countdown !== null) {
|
||||
clearInterval(Account.countdown);
|
||||
}
|
||||
|
||||
var password = document.getElementsByTagName('form')[0].elements.password;
|
||||
var button = document.getElementById('authenticateWithGoogleButton');
|
||||
|
||||
password.type = Account.original.type;
|
||||
password.placeholder = Account.original.placeholder
|
||||
password.required = Account.original.required;
|
||||
password.disabled = Account.original.disabled;
|
||||
|
||||
button.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
(function () {
|
||||
document.getElementById('authenticateWithGoogleButton').onclick = function () {
|
||||
Account.openGoogleAuthenticate();
|
||||
};
|
||||
|
||||
document.getElementsByTagName('form')[0].onreset = function () {
|
||||
Account.resetGoogleAuthentication();
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,10 @@
|
||||
(function () {
|
||||
if (success) {
|
||||
window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil);
|
||||
window.close();
|
||||
} else {
|
||||
document.getElementById('closeWindowButton').onclick = function () {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
})();
|
||||
+569
-97
@@ -5,49 +5,291 @@
|
||||
NUMBER_OF_ROUNDS: 5,
|
||||
MAX_SCORE: 1000,
|
||||
|
||||
mapBounds: null,
|
||||
multi: { token: null, owner: false },
|
||||
rounds: [],
|
||||
scoreSum: 0,
|
||||
panoId: null,
|
||||
pov: null,
|
||||
panorama: null,
|
||||
map: null,
|
||||
guessMarker: null,
|
||||
adaptGuess: false,
|
||||
googleLink: null,
|
||||
|
||||
initialize: function () {
|
||||
readyToContinue: false,
|
||||
timeoutEnd: null,
|
||||
countdownHandler: null,
|
||||
|
||||
MultiConnector: {
|
||||
connection: null,
|
||||
reconnectCounter: 0,
|
||||
|
||||
connect: function () {
|
||||
if (Game.MultiConnector.connection && Game.MultiConnector.connection.readyState !== WebSocket.CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
Game.MultiConnector.connection = new WebSocket((MapGuesser.isSecure ? 'wss' : 'ws') + '://' + multiUrl);
|
||||
|
||||
Game.MultiConnector.connection.onopen = function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
Game.MultiConnector.reconnectCounter = 0;
|
||||
Game.MultiConnector.connection.send(JSON.stringify({ func: 'connect_to_room', args: { roomId: roomId, token: Game.multi.token } }));
|
||||
};
|
||||
|
||||
Game.MultiConnector.connection.onclose = Game.MultiConnector.noConnection;
|
||||
|
||||
Game.MultiConnector.connection.onerror = function (event) {
|
||||
console.error('WebSocket error in Game.MultiConnector:', event);
|
||||
};
|
||||
|
||||
Game.MultiConnector.connection.onmessage = function (message) {
|
||||
var json;
|
||||
|
||||
try {
|
||||
json = JSON.parse(message.data);
|
||||
} catch (e) {
|
||||
console.error('Cannot parse message!');
|
||||
console.error(message.data);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (json.type) {
|
||||
case 'initialize':
|
||||
Game.MultiConnector.initialize(json.data);
|
||||
break;
|
||||
|
||||
case 'member_joined':
|
||||
Game.MultiConnector.memberJoined(json.data);
|
||||
break;
|
||||
|
||||
case 'new_round':
|
||||
Game.MultiConnector.newRound(json.data);
|
||||
break;
|
||||
|
||||
case 'guess':
|
||||
Game.MultiConnector.guess(json.data);
|
||||
break;
|
||||
|
||||
case 'timeout_changed':
|
||||
Game.MultiConnector.timeoutChanged(json.data);
|
||||
break;
|
||||
|
||||
case 'end_round':
|
||||
Game.MultiConnector.endRound(json.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
noConnection: function () {
|
||||
if (Game.MultiConnector.reconnectCounter === 2) {
|
||||
console.error('Could not reconnect WebSocket for Game.MultiConnector...')
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
Game.MultiConnector.reconnectCounter++;
|
||||
|
||||
console.log('Reconnecting WebSocket for Game.MultiConnector... ' + Game.MultiConnector.reconnectCounter);
|
||||
Game.MultiConnector.connect();
|
||||
}, 1000 + Math.min(Game.MultiConnector.reconnectCounter * 500, 9000));
|
||||
},
|
||||
|
||||
initialize: function (data) {
|
||||
if (data.history.length === 0 && !data.place) {
|
||||
var div = document.getElementById('players');
|
||||
|
||||
for (var i = 0; i < data.members.length; ++i) {
|
||||
var member = data.members[i];
|
||||
|
||||
var p = document.createElement('p');
|
||||
p.innerHTML = member.userName + (member.me ? ' (me)' : '');
|
||||
div.appendChild(p);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.history.length; ++i) {
|
||||
var round = data.history[i];
|
||||
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
|
||||
Game.addPositionToResultMap(true);
|
||||
if (round.result.guessPosition) {
|
||||
Game.addGuessPositionToResultMap(round.result.guessPosition, null, true);
|
||||
}
|
||||
Game.scoreSum += round.result.score;
|
||||
|
||||
for (var j = 0; j < round.allResults.length; ++j) {
|
||||
var result = round.allResults[j];
|
||||
if (result.guessPosition) {
|
||||
Game.addGuessPositionToResultMap(result.guessPosition, result, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
|
||||
|
||||
if (data.timeout) {
|
||||
Game.startCountdown(data.timeout);
|
||||
}
|
||||
|
||||
if (data.place) {
|
||||
Game.readyToContinue = data.readyToContinue;
|
||||
Game.panoId = data.place.panoId;
|
||||
Game.pov = data.place.pov;
|
||||
|
||||
document.getElementById('multi').style.visibility = 'hidden';
|
||||
|
||||
if (Game.rounds.length === 0 || !Game.rounds[Game.rounds.length - 1].position) {
|
||||
document.getElementById('panoCover').style.visibility = 'hidden';
|
||||
Game.startNewRound();
|
||||
} else {
|
||||
Game.loadPano(Game.panoId, Game.pov);
|
||||
Game.showResultFromHistory(data.history[data.history.length - 1].result);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
},
|
||||
|
||||
memberJoined: function (data) {
|
||||
var div = document.getElementById('players');
|
||||
|
||||
var p = document.createElement('p');
|
||||
p.innerHTML = data.userName;
|
||||
div.appendChild(p);
|
||||
},
|
||||
|
||||
newRound: function (data) {
|
||||
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
|
||||
Game.reset();
|
||||
}
|
||||
|
||||
Game.readyToContinue = false;
|
||||
Game.panoId = data.place.panoId;
|
||||
Game.pov = data.place.pov;
|
||||
|
||||
document.getElementById('multi').style.visibility = 'hidden';
|
||||
Game.resetRound();
|
||||
Game.startNewRound();
|
||||
|
||||
Game.startCountdown(data.timeout);
|
||||
},
|
||||
|
||||
guess: function (data) {
|
||||
var resultBounds = Game.map.getBounds();
|
||||
|
||||
Game.addGuessPositionToResultMap(data.guessPosition, data);
|
||||
resultBounds.extend(data.guessPosition);
|
||||
|
||||
Game.map.fitBounds(resultBounds);
|
||||
},
|
||||
|
||||
timeoutChanged: function (data) {
|
||||
Game.startCountdown(data.timeout);
|
||||
},
|
||||
|
||||
endRound: function (data) {
|
||||
Game.readyToContinue = true;
|
||||
Game.startCountdown(0);
|
||||
|
||||
if (Game.rounds[Game.rounds.length - 1].guessPosition || Game.rounds[Game.rounds.length - 1].position) {
|
||||
if (Game.multi.owner) {
|
||||
document.getElementById('continueButton').disabled = false;
|
||||
document.getElementById('startNewGameButton').disabled = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('guessButton').disabled = true;
|
||||
document.getElementById('panoCover').style.visibility = 'visible';
|
||||
|
||||
Game.receiveResult(data.position, data.result.guessPosition, { distance: data.result.distance, score: data.result.score }, data.allResults);
|
||||
}
|
||||
},
|
||||
|
||||
prepare: function () {
|
||||
var data = new FormData();
|
||||
var userNames;
|
||||
|
||||
if (roomId) {
|
||||
var userNames = localStorage.userNames ? JSON.parse(localStorage.userNames) : {};
|
||||
if (!userNames.hasOwnProperty(roomId)) {
|
||||
userNames[roomId] = prompt('Your name: ');
|
||||
localStorage.userNames = JSON.stringify(userNames);
|
||||
}
|
||||
|
||||
data.append('userName', userNames[roomId]);
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
var url = roomId ? '/multiGame/' + roomId + '/prepare.json' : '/game/' + mapId + '/prepare.json';
|
||||
MapGuesser.httpRequest('POST', url, function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('mapName').innerHTML = this.response.mapName;
|
||||
Game.mapBounds = this.response.bounds;
|
||||
|
||||
Game.initialize();
|
||||
|
||||
if (roomId) {
|
||||
Game.multi.token = this.response.token;
|
||||
Game.multi.owner = this.response.owner;
|
||||
|
||||
document.getElementById('multi').style.visibility = 'visible';
|
||||
if (Game.multi.owner) {
|
||||
document.getElementById('startMultiGameButton').style.display = 'block';
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
Game.MultiConnector.connect();
|
||||
}
|
||||
}, data);
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
document.getElementById('panoCover').style.visibility = 'visible';
|
||||
document.getElementById('currentRound').innerHTML = '1/' + String(Game.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentScoreSum').innerHTML = '0/0';
|
||||
|
||||
Game.map.setOptions({
|
||||
draggableCursor: 'crosshair'
|
||||
});
|
||||
Game.map.fitBounds(mapBounds);
|
||||
Game.map.fitBounds(Game.mapBounds);
|
||||
|
||||
MapGuesser.httpRequest('GET', '/game/' + mapId + '/newPlace.json', function () {
|
||||
if (roomId) {
|
||||
// if it is multiplayer mode, data is sent via WS
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
MapGuesser.httpRequest('POST', '/game/' + mapId + '/initialData.json', function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
document.getElementById('panoCover').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
//TODO: handle this error
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
Game.panoId = this.response.panoId;
|
||||
Game.panoId = this.response.place.panoId;
|
||||
Game.pov = this.response.place.pov;
|
||||
|
||||
if (this.response.history) {
|
||||
for (var i = 0; i < this.response.history.length; ++i) {
|
||||
var round = this.response.history[i];
|
||||
Game.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null });
|
||||
Game.addRealGuessPair(round.position, round.guessPosition, true);
|
||||
Game.scoreSum += round.score;
|
||||
}
|
||||
|
||||
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
|
||||
for (var i = 0; i < this.response.history.length; ++i) {
|
||||
var round = this.response.history[i];
|
||||
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
|
||||
Game.addPositionToResultMap(true);
|
||||
Game.addGuessPositionToResultMap(round.result.guessPosition, null, true);
|
||||
Game.scoreSum += round.result.score;
|
||||
}
|
||||
|
||||
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
|
||||
|
||||
Game.startNewRound();
|
||||
});
|
||||
},
|
||||
@@ -61,10 +303,16 @@
|
||||
for (var i = 0; i < Game.rounds.length; ++i) {
|
||||
var round = Game.rounds[i];
|
||||
|
||||
if (round.realMarker && round.guessMarker && round.line) {
|
||||
if (round.realMarker) {
|
||||
round.realMarker.setMap(null);
|
||||
round.guessMarker.setMap(null);
|
||||
round.line.setMap(null);
|
||||
}
|
||||
for (var j = 0; j < round.guessMarkers.length; ++j) {
|
||||
var guessMarker = round.guessMarkers[j];
|
||||
guessMarker.marker.setMap(null);
|
||||
guessMarker.line.setMap(null);
|
||||
if (guessMarker.info) {
|
||||
guessMarker.info.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +322,12 @@
|
||||
var distanceInfo = document.getElementById('distanceInfo');
|
||||
distanceInfo.children[0].style.display = null;
|
||||
distanceInfo.children[1].style.display = null;
|
||||
distanceInfo.children[2].style.display = null;
|
||||
var scoreInfo = document.getElementById('scoreInfo');
|
||||
scoreInfo.children[0].style.display = null;
|
||||
scoreInfo.children[1].style.display = null;
|
||||
document.getElementById('continueButton').style.display = null;
|
||||
document.getElementById('showSummaryButton').style.display = null;
|
||||
document.getElementById('startNewGameButton').style.display = null;
|
||||
|
||||
document.getElementById('showGuessButton').style.visibility = null;
|
||||
@@ -94,8 +344,15 @@
|
||||
var lastRound = Game.rounds[Game.rounds.length - 1];
|
||||
|
||||
lastRound.realMarker.setVisible(false);
|
||||
lastRound.guessMarker.setVisible(false);
|
||||
lastRound.line.setVisible(false);
|
||||
for (var i = 0; i < lastRound.guessMarkers.length; ++i) {
|
||||
var guessMarker = lastRound.guessMarkers[i];
|
||||
guessMarker.marker.setVisible(false);
|
||||
guessMarker.line.setVisible(false);
|
||||
if (guessMarker.info) {
|
||||
guessMarker.info.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.getElementById('panoCover').style.visibility = 'hidden';
|
||||
@@ -106,40 +363,157 @@
|
||||
Game.map.setOptions({
|
||||
draggableCursor: 'crosshair'
|
||||
});
|
||||
Game.map.fitBounds(mapBounds);
|
||||
Game.map.fitBounds(Game.mapBounds);
|
||||
|
||||
if (roomId) {
|
||||
// if it is multiplayer mode, data is sent via WS
|
||||
return;
|
||||
}
|
||||
|
||||
Game.startNewRound();
|
||||
},
|
||||
|
||||
startNewRound: function () {
|
||||
Game.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarker: null, line: null });
|
||||
Game.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarkers: [] });
|
||||
|
||||
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
|
||||
|
||||
Game.loadPano(Game.panoId);
|
||||
Game.loadPano(Game.panoId, Game.pov);
|
||||
},
|
||||
|
||||
handleErrorResponse: function (error) {
|
||||
// for the time being we only handle the "no_session_found" error and reset the game
|
||||
switch (error) {
|
||||
case 'no_session_found':
|
||||
MapGuesser.showModalWithContent('Error', 'Your session is invalid!', [{
|
||||
type: 'button',
|
||||
classNames: [],
|
||||
text: 'Restart game',
|
||||
onclick: function () {
|
||||
window.location.reload();
|
||||
}
|
||||
}]);
|
||||
break;
|
||||
|
||||
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
|
||||
mapBounds = this.response.bounds;
|
||||
case 'game_already_started':
|
||||
MapGuesser.showModalWithContent('Error', 'This game is already started, you cannot join.');
|
||||
break;
|
||||
|
||||
Game.reset();
|
||||
});
|
||||
default:
|
||||
MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\'');
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
loadPano: function (panoId) {
|
||||
loadPano: function (panoId, pov) {
|
||||
if (Game.adaptGuess) {
|
||||
document.getElementById('guess').classList.add('adapt');
|
||||
}
|
||||
|
||||
Game.panorama.setPov({ heading: 0, pitch: 0 });
|
||||
Game.panorama.setZoom(0);
|
||||
Game.panorama.setPov({ heading: pov.heading, pitch: pov.pitch });
|
||||
Game.panorama.setZoom(pov.zoom);
|
||||
Game.panorama.setPano(panoId);
|
||||
},
|
||||
|
||||
evaluateGuess: function () {
|
||||
receiveResult: function (position, guessPosition, result, allResults) {
|
||||
Game.scoreSum += result.score;
|
||||
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
|
||||
|
||||
var resultBounds = new google.maps.LatLngBounds();
|
||||
|
||||
Game.rounds[Game.rounds.length - 1].position = position;
|
||||
Game.addPositionToResultMap();
|
||||
resultBounds.extend(position);
|
||||
|
||||
if (guessPosition) {
|
||||
Game.addGuessPositionToResultMap(guessPosition);
|
||||
resultBounds.extend(guessPosition);
|
||||
}
|
||||
|
||||
if (allResults) {
|
||||
for (var i = 0; i < allResults.length; ++i) {
|
||||
var currentResult = allResults[i];
|
||||
if (currentResult.guessPosition) {
|
||||
Game.addGuessPositionToResultMap(currentResult.guessPosition, currentResult);
|
||||
resultBounds.extend(currentResult.guessPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Game.showResultMap(result, resultBounds);
|
||||
},
|
||||
|
||||
showResultFromHistory: function (result) {
|
||||
var round = Game.rounds[Game.rounds.length - 1];
|
||||
var resultBounds = new google.maps.LatLngBounds();
|
||||
|
||||
round.realMarker.setVisible(true);
|
||||
resultBounds.extend(round.position);
|
||||
|
||||
for (var j = 0; j < round.guessMarkers.length; ++j) {
|
||||
var guessMarker = round.guessMarkers[j];
|
||||
guessMarker.marker.setVisible(true);
|
||||
guessMarker.line.setVisible(true);
|
||||
|
||||
resultBounds.extend(guessMarker.marker.getPosition());
|
||||
}
|
||||
|
||||
Game.showResultMap(result, resultBounds);
|
||||
},
|
||||
|
||||
showResultMap: function (result, resultBounds) {
|
||||
// TODO: refactor - it is necessary for mobile
|
||||
if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') {
|
||||
document.getElementById('showGuessButton').click();
|
||||
}
|
||||
|
||||
if (Game.adaptGuess) {
|
||||
document.getElementById('guess').classList.remove('adapt');
|
||||
}
|
||||
|
||||
if (Game.guessMarker) {
|
||||
Game.guessMarker.setMap(null);
|
||||
Game.guessMarker = null;
|
||||
}
|
||||
|
||||
document.getElementById('guess').classList.add('result');
|
||||
|
||||
Game.map.setOptions({
|
||||
draggableCursor: 'grab'
|
||||
});
|
||||
Game.map.fitBounds(resultBounds);
|
||||
|
||||
var distanceInfo = document.getElementById('distanceInfo');
|
||||
if (result.distance === null) {
|
||||
distanceInfo.children[0].style.display = 'none';
|
||||
distanceInfo.children[1].style.display = 'block';
|
||||
} else {
|
||||
distanceInfo.children[0].style.display = 'block';
|
||||
distanceInfo.children[1].style.display = 'none';
|
||||
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(result.distance);
|
||||
}
|
||||
|
||||
document.getElementById('score').innerHTML = result.score;
|
||||
|
||||
var scoreBarProperties = Game.calculateScoreBarProperties(result.score, Game.MAX_SCORE);
|
||||
var scoreBar = document.getElementById('scoreBar');
|
||||
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
|
||||
scoreBar.style.width = scoreBarProperties.width;
|
||||
|
||||
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
|
||||
document.getElementById('continueButton').style.display = 'none';
|
||||
document.getElementById('showSummaryButton').style.display = 'block';
|
||||
} else if (roomId) {
|
||||
if (Game.multi.owner) {
|
||||
if (!Game.readyToContinue) {
|
||||
document.getElementById('continueButton').disabled = true;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('continueButton').style.display = 'none';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
guess: function () {
|
||||
if (!Game.guessMarker) {
|
||||
return;
|
||||
}
|
||||
@@ -148,62 +522,34 @@
|
||||
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
|
||||
|
||||
document.getElementById('guessButton').disabled = true;
|
||||
if (Game.adaptGuess) {
|
||||
document.getElementById('guess').classList.remove('adapt');
|
||||
}
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
document.getElementById('panoCover').style.visibility = 'visible';
|
||||
|
||||
var data = new FormData();
|
||||
data.append('lat', String(guessPosition.lat));
|
||||
data.append('lng', String(guessPosition.lng));
|
||||
|
||||
MapGuesser.httpRequest('POST', '/game/' + mapId + '/guess.json', function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
var url = roomId ? '/multiGame/' + roomId + '/guess.json' : '/game/' + mapId + '/guess.json';
|
||||
MapGuesser.httpRequest('POST', url, function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
Game.guessMarker.setMap(null);
|
||||
Game.guessMarker = null;
|
||||
Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults);
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
document.getElementById('guess').classList.add('result');
|
||||
|
||||
Game.scoreSum += this.response.result.score;
|
||||
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
|
||||
|
||||
Game.rounds[Game.rounds.length - 1].position = this.response.result.position;
|
||||
Game.addRealGuessPair(this.response.result.position, guessPosition);
|
||||
|
||||
var resultBounds = new google.maps.LatLngBounds();
|
||||
resultBounds.extend(this.response.result.position);
|
||||
resultBounds.extend(guessPosition);
|
||||
|
||||
Game.map.setOptions({
|
||||
draggableCursor: 'grab'
|
||||
});
|
||||
Game.map.fitBounds(resultBounds);
|
||||
|
||||
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(this.response.result.distance);
|
||||
document.getElementById('score').innerHTML = this.response.result.score;
|
||||
|
||||
var scoreBarProperties = Game.calculateScoreBarProperties(this.response.result.score, Game.MAX_SCORE);
|
||||
var scoreBar = document.getElementById('scoreBar');
|
||||
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
|
||||
scoreBar.style.width = scoreBarProperties.width;
|
||||
|
||||
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
|
||||
document.getElementById('continueButton').style.display = 'none';
|
||||
document.getElementById('showSummaryButton').style.display = 'block';
|
||||
if (this.response.place) {
|
||||
Game.panoId = this.response.place.panoId;
|
||||
Game.pov = this.response.place.pov;
|
||||
}
|
||||
|
||||
Game.panoId = this.response.panoId;
|
||||
}, data);
|
||||
},
|
||||
|
||||
addRealGuessPair: function (position, guessPosition, hidden) {
|
||||
addPositionToResultMap: function (hidden) {
|
||||
var round = Game.rounds[Game.rounds.length - 1];
|
||||
var position = round.position;
|
||||
|
||||
round.realMarker = new google.maps.Marker({
|
||||
map: Game.map,
|
||||
@@ -224,16 +570,25 @@
|
||||
round.realMarker.addListener('click', function () {
|
||||
window.open('https://www.google.com/maps/search/?api=1&query=' + this.getPosition().toUrlValue(), '_blank');
|
||||
});
|
||||
},
|
||||
|
||||
round.guessMarker = new google.maps.Marker({
|
||||
addGuessPositionToResultMap: function (guessPosition, result, hidden) {
|
||||
var round = Game.rounds[Game.rounds.length - 1];
|
||||
var position = round.position;
|
||||
|
||||
var guessMarker = { marker: null, line: null, info: null };
|
||||
var markerSvg = result ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg';
|
||||
var markerLabel = result ? result.userName.charAt(0).toUpperCase() : '?';
|
||||
|
||||
guessMarker.marker = new google.maps.Marker({
|
||||
map: Game.map,
|
||||
visible: !hidden,
|
||||
position: guessPosition,
|
||||
zIndex: Game.rounds.length,
|
||||
clickable: false,
|
||||
clickable: !!result,
|
||||
draggable: false,
|
||||
icon: {
|
||||
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION,
|
||||
url: STATIC_ROOT + '/img/markers/' + markerSvg + '?rev=' + REVISION,
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32),
|
||||
@@ -244,11 +599,11 @@
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
text: '?'
|
||||
text: markerLabel
|
||||
}
|
||||
});
|
||||
|
||||
round.line = new google.maps.Polyline({
|
||||
guessMarker.line = new google.maps.Polyline({
|
||||
map: Game.map,
|
||||
visible: !hidden,
|
||||
path: [
|
||||
@@ -271,6 +626,19 @@
|
||||
draggable: false,
|
||||
editable: false
|
||||
});
|
||||
|
||||
if (result) {
|
||||
guessMarker.info = new google.maps.InfoWindow({
|
||||
content: '<p class="small bold">' + result.userName + '</p>' +
|
||||
'<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>',
|
||||
});
|
||||
|
||||
guessMarker.marker.addListener('click', function () {
|
||||
guessMarker.info.open(Game.map, this);
|
||||
});
|
||||
}
|
||||
|
||||
round.guessMarkers.push(guessMarker);
|
||||
},
|
||||
|
||||
calculateScoreBarProperties: function (score, maxScore) {
|
||||
@@ -291,12 +659,19 @@
|
||||
showSummary: function () {
|
||||
var distanceInfo = document.getElementById('distanceInfo');
|
||||
distanceInfo.children[0].style.display = 'none';
|
||||
distanceInfo.children[1].style.display = 'block';
|
||||
distanceInfo.children[1].style.display = 'none';
|
||||
distanceInfo.children[2].style.display = 'block';
|
||||
var scoreInfo = document.getElementById('scoreInfo');
|
||||
scoreInfo.children[0].style.display = 'none';
|
||||
scoreInfo.children[1].style.display = 'block';
|
||||
document.getElementById('showSummaryButton').style.display = null;
|
||||
document.getElementById('startNewGameButton').style.display = 'block';
|
||||
|
||||
if (!roomId || Game.multi.owner) {
|
||||
document.getElementById('startNewGameButton').style.display = 'block';
|
||||
if (!Game.readyToContinue) {
|
||||
document.getElementById('startNewGameButton').disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
var resultBounds = new google.maps.LatLngBounds();
|
||||
|
||||
@@ -317,12 +692,17 @@
|
||||
fontWeight: '500',
|
||||
text: String(i + 1)
|
||||
});
|
||||
round.realMarker.setVisible(true);
|
||||
round.guessMarker.setVisible(true);
|
||||
round.line.setVisible(true);
|
||||
|
||||
round.realMarker.setVisible(true);
|
||||
resultBounds.extend(round.position);
|
||||
resultBounds.extend(round.guessPosition);
|
||||
|
||||
for (var j = 0; j < round.guessMarkers.length; ++j) {
|
||||
var guessMarker = round.guessMarkers[j];
|
||||
guessMarker.marker.setVisible(true);
|
||||
guessMarker.line.setVisible(true);
|
||||
|
||||
resultBounds.extend(guessMarker.marker.getPosition());
|
||||
}
|
||||
}
|
||||
|
||||
Game.map.fitBounds(resultBounds);
|
||||
@@ -353,6 +733,52 @@
|
||||
Game.googleLink.href = 'https://maps.google.com/maps';
|
||||
}
|
||||
}, 1);
|
||||
},
|
||||
|
||||
startCountdown: function (timeout) {
|
||||
if (Game.countdownHandler) {
|
||||
clearInterval(Game.countdownHandler);
|
||||
}
|
||||
|
||||
Game.countdownElement = document.getElementById('countdown');
|
||||
Game.countdownTimeElement = document.getElementById('countdownTime');
|
||||
|
||||
Game.setCountdownTime(Math.round(timeout / 1000));
|
||||
|
||||
if (timeout <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Game.timeoutEnd = new Date(new Date().getTime() + timeout);
|
||||
|
||||
Game.countdownHandler = setInterval(function () {
|
||||
var timeLeft = Math.round((Game.timeoutEnd - new Date()) / 1000);
|
||||
|
||||
Game.setCountdownTime(timeLeft);
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
document.getElementById('panoCover').style.visibility = 'visible';
|
||||
clearInterval(Game.countdownHandler);
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
setCountdownTime: function (time) {
|
||||
if (time <= 0) {
|
||||
Game.countdownElement.style.visibility = 'hidden';
|
||||
return;
|
||||
}
|
||||
|
||||
if (time <= 15) {
|
||||
Game.countdownElement.className = 'red';
|
||||
} else if (time <= 30) {
|
||||
Game.countdownElement.className = 'yellow';
|
||||
} else {
|
||||
Game.countdownElement.className = '';
|
||||
}
|
||||
|
||||
Game.countdownElement.style.visibility = 'visible';
|
||||
Game.countdownTimeElement.innerHTML = time;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -370,13 +796,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
MapGuesser.sessionAvailableHooks.reinitializeGame = function () {
|
||||
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
|
||||
mapBounds = this.response.bounds;
|
||||
|
||||
Game.initialize();
|
||||
});
|
||||
};
|
||||
MapGuesser.sessionAvailableHooks.reinitializeGame = Game.prepare;
|
||||
|
||||
if (!('ontouchstart' in document.documentElement)) {
|
||||
Game.adaptGuess = true;
|
||||
@@ -389,7 +809,7 @@
|
||||
});
|
||||
|
||||
Game.map.addListener('click', function (e) {
|
||||
if (Game.rounds[Game.rounds.length - 1].guessPosition) {
|
||||
if (Game.rounds[Game.rounds.length - 1].guessPosition || Game.rounds[Game.rounds.length - 1].position) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -404,7 +824,7 @@
|
||||
clickable: false,
|
||||
draggable: true,
|
||||
icon: {
|
||||
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION,
|
||||
url: STATIC_ROOT + '/img/markers/marker-blue-empty.svg?rev=' + REVISION,
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32),
|
||||
@@ -437,7 +857,9 @@
|
||||
Game.rewriteGoogleLink();
|
||||
});
|
||||
|
||||
Game.initialize();
|
||||
if (COOKIES_CONSENT) {
|
||||
Game.prepare();
|
||||
}
|
||||
|
||||
document.getElementById('showGuessButton').onclick = function () {
|
||||
this.style.visibility = 'hidden';
|
||||
@@ -450,11 +872,27 @@
|
||||
}
|
||||
|
||||
document.getElementById('guessButton').onclick = function () {
|
||||
Game.evaluateGuess();
|
||||
Game.guess();
|
||||
}
|
||||
|
||||
document.getElementById('continueButton').onclick = function () {
|
||||
Game.resetRound();
|
||||
if (roomId) {
|
||||
if (!Game.multi.owner || !Game.readyToContinue) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/nextRound.json', function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Game.resetRound();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('showSummaryButton').onclick = function () {
|
||||
@@ -462,6 +900,40 @@
|
||||
}
|
||||
|
||||
document.getElementById('startNewGameButton').onclick = function () {
|
||||
Game.reset();
|
||||
if (roomId) {
|
||||
if (!Game.multi.owner) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/initialData.json', function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Game.reset();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('startMultiGameButton').onclick = function () {
|
||||
if (!roomId || !Game.multi.owner) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('multi').style.visibility = 'hidden';
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/initialData.json', function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -1,43 +0,0 @@
|
||||
(function () {
|
||||
var form = document.getElementById('loginForm');
|
||||
|
||||
form.onsubmit = function (e) {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
var formData = new FormData(form);
|
||||
|
||||
MapGuesser.httpRequest('POST', form.action, function () {
|
||||
if (this.response.error) {
|
||||
if (this.response.error === 'user_not_found') {
|
||||
window.location.replace('/signup');
|
||||
return;
|
||||
}
|
||||
|
||||
var errorText;
|
||||
switch (this.response.error) {
|
||||
case 'password_too_short':
|
||||
errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!'
|
||||
break;
|
||||
case 'user_not_active':
|
||||
errorText = 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!';
|
||||
break;
|
||||
case 'password_not_match':
|
||||
errorText = 'The given password is wrong.'
|
||||
break;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
var loginFormError = document.getElementById('loginFormError');
|
||||
loginFormError.style.display = 'block';
|
||||
loginFormError.innerHTML = errorText;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.replace('/');
|
||||
}, formData);
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,9 @@
|
||||
(function () {
|
||||
document.getElementById('cancelGoogleSignupButton').onclick = function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/signup/google/reset', function () {
|
||||
window.location.replace('/signup');
|
||||
});
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,12 @@
|
||||
(function () {
|
||||
var resetSignupButton = document.getElementById('resetSignupButton');
|
||||
if (resetSignupButton) {
|
||||
resetSignupButton.onclick = function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/signup/reset', function () {
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -310,6 +310,21 @@
|
||||
var placeId = addedPlaces[i].id;
|
||||
places[tempId].id = placeId;
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: check whether marker is already existing on the map for the coordinates
|
||||
// or alternatively block saving for matching coordinates
|
||||
placeMarker: function (latlng) {
|
||||
var marker = L.marker(latlng, {
|
||||
icon: IconCollection.iconBlue,
|
||||
zIndexOffset: 2000
|
||||
})
|
||||
.addTo(MapEditor.map)
|
||||
.on('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
|
||||
MapEditor.select(marker);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -340,6 +355,32 @@
|
||||
} else {
|
||||
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
|
||||
}
|
||||
},
|
||||
|
||||
extractCoordinates: function (coordinatesStr) {
|
||||
var coordinates = { valid: false, latlng: { lat: 0., lng: 0. } };
|
||||
var delimiters = [',', ' ', ';'];
|
||||
|
||||
coordinatesStr = coordinatesStr.trim();
|
||||
if (coordinatesStr.length == 0) {
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
for (var delimiter of delimiters) {
|
||||
if (coordinatesStr.indexOf(delimiter) != -1) {
|
||||
|
||||
var coordinatesArr = coordinatesStr.split(delimiter);
|
||||
coordinates.latlng.lat = parseFloat(coordinatesArr[0]);
|
||||
coordinates.latlng.lng = parseFloat(coordinatesArr[1]);
|
||||
|
||||
if (!isNaN(coordinates.latlng.lat) && !isNaN(coordinates.latlng.lng)) {
|
||||
coordinates.valid = true;
|
||||
return coordinates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return coordinates;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -348,16 +389,7 @@
|
||||
});
|
||||
|
||||
MapEditor.map.on('click', function (e) {
|
||||
var marker = L.marker(e.latlng, {
|
||||
icon: IconCollection.iconBlue,
|
||||
zIndexOffset: 2000
|
||||
})
|
||||
.addTo(MapEditor.map)
|
||||
.on('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
|
||||
MapEditor.select(marker);
|
||||
MapEditor.placeMarker(e.latlng);
|
||||
});
|
||||
|
||||
var highResData = Util.getHighResData();
|
||||
@@ -440,4 +472,31 @@
|
||||
document.getElementById('deleteButton').onclick = function () {
|
||||
MapEditor.deletePlace();
|
||||
};
|
||||
|
||||
document.getElementById('jumpButton').onclick = function (e) {
|
||||
var coordinatesStr = document.getElementById("jumpCoordinates").value;
|
||||
var coordinates = Util.extractCoordinates(coordinatesStr);
|
||||
|
||||
if (coordinates.valid) {
|
||||
MapEditor.placeMarker(coordinates.latlng);
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('jumpCoordinates').onkeyup = function (e) {
|
||||
var coordinatesStr = document.getElementById("jumpCoordinates").value;
|
||||
var coordinates = Util.extractCoordinates(coordinatesStr);
|
||||
var jumpButton = document.getElementById("jumpButton");
|
||||
|
||||
if (coordinates.valid) {
|
||||
jumpButton.disabled = false;
|
||||
|
||||
if (e.key == 'Enter') {
|
||||
MapEditor.placeMarker(coordinates.latlng);
|
||||
}
|
||||
}
|
||||
else {
|
||||
jumpButton.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
@@ -1,4 +1,5 @@
|
||||
var MapGuesser = {
|
||||
isSecure: window.location.protocol === 'https:',
|
||||
cookiesAgreed: false,
|
||||
sessionAvailableHooks: {},
|
||||
|
||||
@@ -15,7 +16,7 @@ var MapGuesser = {
|
||||
document.head.appendChild(script);
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
function gtag() { dataLayer.push(arguments); }
|
||||
gtag('js', new Date());
|
||||
gtag('config', GOOGLE_ANALITICS_ID);
|
||||
},
|
||||
@@ -66,6 +67,52 @@ var MapGuesser = {
|
||||
}
|
||||
},
|
||||
|
||||
setOnsubmitForForm: function (form) {
|
||||
form.onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
var formData = new FormData(form);
|
||||
var formError = form.getElementsByClassName('formError')[0];
|
||||
var pageLeaveOnSuccess = form.dataset.redirectOnSuccess || form.dataset.reloadOnSuccess;
|
||||
|
||||
MapGuesser.httpRequest('POST', form.action, function () {
|
||||
if (!pageLeaveOnSuccess) {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
if (this.response.error) {
|
||||
if (pageLeaveOnSuccess) {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
formError.style.display = 'block';
|
||||
formError.innerHTML = this.response.error.errorText;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.response.redirect) {
|
||||
window.location.replace(this.response.redirect.target);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pageLeaveOnSuccess) {
|
||||
formError.style.display = 'none';
|
||||
form.reset();
|
||||
} else {
|
||||
if (form.dataset.redirectOnSuccess) {
|
||||
window.location.replace(form.dataset.redirectOnSuccess);
|
||||
} else if (form.dataset.reloadOnSuccess) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}, formData);
|
||||
}
|
||||
},
|
||||
|
||||
showModal: function (id) {
|
||||
document.getElementById(id).style.visibility = 'visible';
|
||||
document.getElementById('cover').style.visibility = 'visible';
|
||||
@@ -136,11 +183,35 @@ var MapGuesser = {
|
||||
document.getElementById('cover').style.visibility = 'hidden';
|
||||
},
|
||||
|
||||
toggleDisableOnChange: function (input, button) {
|
||||
observeInput: function (input, buttonToToggle) {
|
||||
if (input.defaultValue !== input.value) {
|
||||
button.disabled = false;
|
||||
buttonToToggle.disabled = false;
|
||||
} else {
|
||||
button.disabled = true;
|
||||
buttonToToggle.disabled = true;
|
||||
}
|
||||
},
|
||||
|
||||
observeInputsInForm: function (form, observedInputs) {
|
||||
for (var i = 0; i < observedInputs.length; i++) {
|
||||
var input = form.elements[observedInputs[i]];
|
||||
|
||||
switch (input.tagName) {
|
||||
case 'INPUT':
|
||||
case 'TEXTAREA':
|
||||
input.oninput = function () {
|
||||
MapGuesser.observeInput(this, form.elements.submit);
|
||||
};
|
||||
break;
|
||||
case 'SELECT':
|
||||
input.onchange = function () {
|
||||
MapGuesser.observeInput(this, form.elements.submit);
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
form.onreset = function () {
|
||||
form.elements.submit.disabled = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -156,7 +227,41 @@ var MapGuesser = {
|
||||
}
|
||||
}
|
||||
|
||||
var forms = document.getElementsByTagName('form');
|
||||
for (var i = 0; i < forms.length; i++) {
|
||||
var form = forms[i];
|
||||
|
||||
if (form.dataset.noSubmit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MapGuesser.setOnsubmitForForm(form);
|
||||
|
||||
if (form.dataset.observeInputs) {
|
||||
MapGuesser.observeInputsInForm(form, form.dataset.observeInputs.split(','));
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('cover').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
if (COOKIES_CONSENT) {
|
||||
MapGuesser.initGoogleAnalitics();
|
||||
} else {
|
||||
// we don't want user to agree cookies when clicking on the notice itself
|
||||
document.getElementById('cookiesNotice').onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
document.getElementById('agreeCookiesButton').onclick = function () {
|
||||
MapGuesser.agreeCookies();
|
||||
|
||||
document.getElementById('cookiesNotice').style.display = 'none';
|
||||
};
|
||||
|
||||
window.onclick = function () {
|
||||
MapGuesser.agreeCookies();
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,112 @@
|
||||
(function () {
|
||||
var Maps = {
|
||||
descriptionDivs: null,
|
||||
|
||||
addStaticMaps: function () {
|
||||
var imgContainers = document.getElementById('mapContainer').getElementsByClassName('imgContainer');
|
||||
for (var i = 0; i < imgContainers.length; i++) {
|
||||
var imgContainer = imgContainers[i];
|
||||
|
||||
var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' +
|
||||
'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&' +
|
||||
'visible=' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
|
||||
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng +
|
||||
'&key=' + GOOGLE_MAPS_JS_API_KEY;
|
||||
|
||||
imgContainer.style.backgroundImage = 'url("' + imgSrc + '")';
|
||||
}
|
||||
},
|
||||
|
||||
initializeDescriptionDivs: function () {
|
||||
Maps.descriptionDivs = document.getElementById('mapContainer').getElementsByClassName('description');
|
||||
|
||||
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
|
||||
var description = Maps.descriptionDivs[i];
|
||||
var boundingClientRect = description.getBoundingClientRect();
|
||||
|
||||
description.defaultHeight = boundingClientRect.height;
|
||||
}
|
||||
},
|
||||
|
||||
calculateDescriptionDivHeights: function () {
|
||||
var currentY;
|
||||
var rows = [];
|
||||
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
|
||||
var description = Maps.descriptionDivs[i];
|
||||
var boundingClientRect = description.getBoundingClientRect();
|
||||
|
||||
if (currentY !== boundingClientRect.y) {
|
||||
rows.push([]);
|
||||
}
|
||||
|
||||
rows[rows.length - 1].push(description);
|
||||
|
||||
currentY = boundingClientRect.y;
|
||||
}
|
||||
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var row = rows[i];
|
||||
|
||||
var maxHeight = 0;
|
||||
for (var j = 0; j < row.length; j++) {
|
||||
var description = row[j];
|
||||
|
||||
if (description.defaultHeight > maxHeight) {
|
||||
maxHeight = description.defaultHeight;
|
||||
}
|
||||
}
|
||||
|
||||
for (var j = 0; j < row.length; j++) {
|
||||
var description = row[j];
|
||||
|
||||
description.style.height = maxHeight + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Maps.addStaticMaps();
|
||||
|
||||
Maps.initializeDescriptionDivs();
|
||||
Maps.calculateDescriptionDivHeights();
|
||||
|
||||
window.onresize = function () {
|
||||
Maps.calculateDescriptionDivHeights();
|
||||
};
|
||||
|
||||
document.getElementById('multiForm').onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var roomId = this.elements.roomId.value;
|
||||
if (roomId.length !== 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = '/multiGame/' + this.elements.roomId.value;
|
||||
};
|
||||
|
||||
document.getElementById('multiButton').onclick = function () {
|
||||
MapGuesser.showModal('multi');
|
||||
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
|
||||
document.getElementById('multiForm').elements.roomId.select();
|
||||
}
|
||||
|
||||
document.getElementById('closePlayModeButton').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
document.getElementById('closeMultiButton').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
var button = buttons[i];
|
||||
|
||||
button.onclick = function () {
|
||||
MapGuesser.showModal('playMode');
|
||||
document.getElementById('singleButton').href = '/game/' + this.dataset.mapId;
|
||||
document.getElementById('multiButton').dataset.mapId = this.dataset.mapId;
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,34 @@
|
||||
(function () {
|
||||
Maps = {
|
||||
deleteMap: function (mapId, mapName) {
|
||||
MapGuesser.showModalWithContent('Delete map', 'Are you sure you want to delete map \'' + mapName + '\'?', [{
|
||||
type: 'button',
|
||||
classNames: ['red'],
|
||||
text: 'Delete',
|
||||
onclick: function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/admin/deleteMap/' + mapId, function () {
|
||||
if (this.response.error) {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
//TODO: handle this error
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
var buttons = document.getElementById('mapContainer').getElementsByClassName('deleteButton');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
var button = buttons[i];
|
||||
|
||||
button.onclick = function () {
|
||||
Maps.deleteMap(this.dataset.mapId, this.dataset.mapName);
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -1,48 +0,0 @@
|
||||
(function () {
|
||||
var form = document.getElementById('profileForm');
|
||||
|
||||
form.elements.password_new.onkeyup = function () {
|
||||
MapGuesser.toggleDisableOnChange(this, form.elements.save);
|
||||
};
|
||||
|
||||
form.elements.password_new_confirm.onkeyup = function () {
|
||||
MapGuesser.toggleDisableOnChange(this, form.elements.save);
|
||||
};
|
||||
|
||||
form.onsubmit = function (e) {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
var formData = new FormData(form);
|
||||
|
||||
MapGuesser.httpRequest('POST', form.action, function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
var errorText;
|
||||
switch (this.response.error) {
|
||||
case 'password_not_match':
|
||||
errorText = 'The given current password is wrong.'
|
||||
break;
|
||||
case 'password_too_short':
|
||||
errorText = 'The given new password is too short. Please choose a password that is at least 6 characters long!'
|
||||
break;
|
||||
case 'passwords_not_match':
|
||||
errorText = 'The given new passwords do not match.'
|
||||
break;
|
||||
}
|
||||
|
||||
var profileFormError = document.getElementById('profileFormError');
|
||||
profileFormError.style.display = 'block';
|
||||
profileFormError.innerHTML = errorText;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('profileFormError').style.display = 'none';
|
||||
form.reset();
|
||||
form.elements.save.disabled = true;
|
||||
}, formData);
|
||||
};
|
||||
})();
|
||||
@@ -1,60 +0,0 @@
|
||||
(function () {
|
||||
var form = document.getElementById('signupForm');
|
||||
|
||||
form.onsubmit = function (e) {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
var formData = new FormData(form);
|
||||
|
||||
MapGuesser.httpRequest('POST', form.action, function () {
|
||||
if (this.response.error) {
|
||||
if (this.response.error === 'user_found') {
|
||||
window.location.replace('/');
|
||||
return;
|
||||
}
|
||||
|
||||
var errorText;
|
||||
switch (this.response.error) {
|
||||
case 'email_not_valid':
|
||||
errorText = 'The given email address is not valid.'
|
||||
break;
|
||||
case 'password_too_short':
|
||||
errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!'
|
||||
break;
|
||||
case 'passwords_not_match':
|
||||
errorText = 'The given passwords do not match.'
|
||||
break;
|
||||
case 'user_found_user_not_active':
|
||||
errorText = 'There is a user already registered with the given email address. Please check your email and click on the activation link!';
|
||||
break;
|
||||
case 'user_found_password_not_match':
|
||||
errorText = 'There is a user already registered with the given email address, but the given password is wrong.'
|
||||
break;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
var signupFormError = document.getElementById('signupFormError');
|
||||
signupFormError.style.display = 'block';
|
||||
signupFormError.innerHTML = errorText;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.replace('/signup/success');
|
||||
}, formData);
|
||||
};
|
||||
|
||||
var resetSignupButton = document.getElementById('resetSignupButton');
|
||||
if (resetSignupButton) {
|
||||
resetSignupButton.onclick = function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/signup/reset', function () {
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -59,10 +59,25 @@ def getRevisionForRef(ref):
|
||||
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
|
||||
|
||||
def getLatestReleaseTag():
|
||||
return subprocess.check_output(["git", "for-each-ref", "refs/tags/Release*", "--count=1", "--sort=-creatordate", "--format=%(refname:short)"], cwd=REPO).decode().strip()
|
||||
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
|
||||
|
||||
for line in process.stdout:
|
||||
tag = line.decode().rstrip()
|
||||
|
||||
if isTagVerified(tag):
|
||||
return tag
|
||||
|
||||
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
|
||||
|
||||
raise Exception("No verified 'Release*' tag found!")
|
||||
|
||||
def isTagVerified(tag):
|
||||
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
|
||||
|
||||
return process.returncode == 0
|
||||
|
||||
def updateRepoFromRemote():
|
||||
subprocess.call(["git", "fetch", "origin", "--prune"], cwd=REPO)
|
||||
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
|
||||
|
||||
def checkoutWorktree(worktreePath, ref):
|
||||
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
|
||||
|
||||
+8
-2
@@ -9,18 +9,24 @@ if [ -f ${ROOT_DIR}/installed ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing NPM packages..."
|
||||
(cd ${ROOT_DIR}/multi && npm install)
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
echo "Installing MapGuesser DB..."
|
||||
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
|
||||
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/mapguesser.sql
|
||||
|
||||
echo "Migrating DB..."
|
||||
(cd ${ROOT_DIR} && ./mapg migrate)
|
||||
(cd ${ROOT_DIR} && ./mapg db:migrate)
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
|
||||
echo "Linking view files..."
|
||||
(cd ${ROOT_DIR} && ./mapg view:link)
|
||||
fi
|
||||
|
||||
touch ${ROOT_DIR}/installed
|
||||
+12
-2
@@ -5,15 +5,25 @@ ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
echo "Installing Composer packages..."
|
||||
(cd ${ROOT_DIR} && composer install --no-dev)
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
(cd ${ROOT_DIR} && composer install --no-dev)
|
||||
else
|
||||
(cd ${ROOT_DIR} && composer install --dev)
|
||||
fi
|
||||
|
||||
echo "Installing NPM packages..."
|
||||
(cd ${ROOT_DIR}/multi && npm install)
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
echo "Migrating DB..."
|
||||
(cd ${ROOT_DIR} && ./mapg migrate)
|
||||
(cd ${ROOT_DIR} && ./mapg db:migrate)
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
|
||||
echo "Linking view files..."
|
||||
(cd ${ROOT_DIR} && ./mapg view:link)
|
||||
fi
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php namespace MapGuesser\Cli;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -9,7 +10,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class AddUserCommand extends Command
|
||||
{
|
||||
public function configure()
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('user:add')
|
||||
->setDescription('Adding of user.')
|
||||
@@ -23,6 +24,8 @@ class AddUserCommand extends Command
|
||||
$user = new User();
|
||||
$user->setEmail($input->getArgument('email'));
|
||||
$user->setPlainPassword($input->getArgument('password'));
|
||||
$user->setActive(true);
|
||||
$user->setCreatedDate(new DateTime());
|
||||
|
||||
if ($input->hasArgument('type')) {
|
||||
$user->setType($input->getArgument('type'));
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php namespace MapGuesser\Cli;
|
||||
|
||||
use FilesystemIterator;
|
||||
use MapGuesser\View\Linker;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class LinkViewCommand extends Command
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('view:link')
|
||||
->setDescription('Linking of views.')
|
||||
->addArgument('view', InputArgument::OPTIONAL, 'View file to be linked.');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$views = [];
|
||||
|
||||
$view = $input->getArgument('view');
|
||||
|
||||
if ($view !== null) {
|
||||
$views[] = $view;
|
||||
} else {
|
||||
$folder = ROOT . '/views';
|
||||
$folderLength = strlen($folder) + 1;
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php');
|
||||
|
||||
if (strpos($view, 'templates') === 0 || strpos($view, 'tests') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$views[] = $view;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($views as $view) {
|
||||
$generator = new Linker($view);
|
||||
$generator->generate();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Linking view(s) failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>View(s) successfully linked!</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php namespace MapGuesser\Cli;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Repository\UserConfirmationRepository;
|
||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||
use MapGuesser\Repository\UserRepository;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MaintainDatabaseCommand extends Command
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
private UserConfirmationRepository $userConfirmationRepository;
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('db:maintain')
|
||||
->setDescription('Maintain database.');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
try {
|
||||
$this->deleteInactiveExpiredUsers();
|
||||
$this->deleteExpiredPasswordResetters();
|
||||
$this->deleteExpiredRooms();
|
||||
$this->deleteExpiredSessions();
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Maintenance failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>Maintenance was successful!</info>');
|
||||
$output->writeln('');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function deleteInactiveExpiredUsers(): void
|
||||
{
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
foreach ($this->userRepository->getAllInactiveExpired() as $user) {
|
||||
//TODO: these can be in some wrapper class
|
||||
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
|
||||
if ($userConfirmation !== null) {
|
||||
$this->pdm->deleteFromDb($userConfirmation);
|
||||
}
|
||||
|
||||
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
|
||||
if ($userPasswordResetter !== null) {
|
||||
$this->pdm->deleteFromDb($userPasswordResetter);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($user);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
}
|
||||
|
||||
private function deleteExpiredPasswordResetters(): void
|
||||
{
|
||||
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
|
||||
$this->pdm->deleteFromDb($passwordResetter);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteExpiredRooms(): void
|
||||
{
|
||||
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
|
||||
$this->pdm->deleteFromDb($multiRoom);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteExpiredSessions(): void
|
||||
{
|
||||
//TODO: model may be used for sessions too
|
||||
$select = new Select(\Container::$dbConnection, 'sessions');
|
||||
$select->columns(['id']);
|
||||
$select->where('updated', '<', (new DateTime('-7 days'))->format('Y-m-d H:i:s'));
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$modify = new Modify(\Container::$dbConnection, 'sessions');
|
||||
$modify->setId($session['id']);
|
||||
$modify->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class DatabaseMigration extends Command
|
||||
class MigrateDatabaseCommand extends Command
|
||||
{
|
||||
public function configure()
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('migrate')
|
||||
$this->setName('db:migrate')
|
||||
->setDescription('Migration of database changes.');
|
||||
}
|
||||
|
||||
@@ -88,6 +88,10 @@ class DatabaseMigration extends Command
|
||||
$path = ROOT . '/database/migrations/' . $type;
|
||||
$dir = opendir($path);
|
||||
|
||||
if ($dir === false) {
|
||||
throw new \Exception('Cannot open dir: ' . $path);
|
||||
}
|
||||
|
||||
$files = [];
|
||||
while ($file = readdir($dir)) {
|
||||
$filePath = $path . '/' . $file;
|
||||
@@ -1,51 +1,169 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use Faker\Factory;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Interfaces\Response\IRedirect;
|
||||
use MapGuesser\Multi\MultiConnector;
|
||||
use MapGuesser\PersistentData\Model\MultiRoom;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\MapRepository;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Response\Redirect;
|
||||
|
||||
class GameController
|
||||
{
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private MultiConnector $multiConnector;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
private MapRepository $mapRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->multiConnector = new MultiConnector();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
$this->mapRepository = new MapRepository();
|
||||
}
|
||||
|
||||
public function getGame(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$data = $this->prepareGame($mapId);
|
||||
return new HtmlContent('game', $data);
|
||||
|
||||
return new HtmlContent('game', ['mapId' => $mapId]);
|
||||
}
|
||||
|
||||
public function getGameJson(): IContent
|
||||
public function getNewMultiGame(): IRedirect
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$data = $this->prepareGame($mapId);
|
||||
return new JsonContent($data);
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$roomId = bin2hex(random_bytes(3));
|
||||
$token = $this->getMultiToken($roomId);
|
||||
|
||||
$room = new MultiRoom();
|
||||
$room->setRoomId($roomId);
|
||||
$room->setStateArray([
|
||||
'mapId' => $mapId,
|
||||
'area' => $map->getArea(),
|
||||
'rounds' => [],
|
||||
'currentRound' => -1
|
||||
]);
|
||||
$room->setMembersArray(['owner' => $token, 'all' => []]);
|
||||
$room->setUpdatedDate(new DateTime());
|
||||
|
||||
$this->pdm->saveToDb($room);
|
||||
|
||||
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
|
||||
|
||||
return new Redirect(
|
||||
\Container::$routeCollection
|
||||
->getRoute('multiGame')
|
||||
->generateLink(['roomId' => $roomId]),
|
||||
IRedirect::TEMPORARY
|
||||
);
|
||||
}
|
||||
|
||||
private function prepareGame(int $mapId)
|
||||
public function getMultiGame(): IContent
|
||||
{
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$roomId = $this->request->query('roomId');
|
||||
|
||||
return new HtmlContent('game', ['roomId' => $roomId]);
|
||||
}
|
||||
|
||||
public function prepareGame(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
$session->set('state', [
|
||||
'mapId' => $mapId,
|
||||
'area' => $map->getArea(),
|
||||
'rounds' => []
|
||||
'rounds' => [],
|
||||
'currentRound' => -1
|
||||
]);
|
||||
} else { // update the area of the map in the session in any case
|
||||
$state['area'] = $map->getArea();
|
||||
$session->set('state', $state);
|
||||
}
|
||||
|
||||
return ['mapId' => $mapId, 'mapName' => $map->getName(), 'bounds' => $map->getBounds()->toArray()];
|
||||
return new JsonContent([
|
||||
'mapId' => $mapId,
|
||||
'mapName' => $map->getName(),
|
||||
'bounds' => $map->getBounds()->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
public function prepareMultiGame(): IContent
|
||||
{
|
||||
$roomId = $this->request->query('roomId');
|
||||
$userName = $this->request->post('userName');
|
||||
if (empty($userName)) {
|
||||
$faker = Factory::create();
|
||||
$userName = $faker->userName;
|
||||
}
|
||||
|
||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||
$state = $room->getStateArray();
|
||||
$map = $this->mapRepository->getById($state['mapId']);
|
||||
$token = $this->getMultiToken($roomId);
|
||||
|
||||
$members = $room->getMembersArray();
|
||||
|
||||
if (!in_array($token, $members['all'])) {
|
||||
if ($state['currentRound'] >= 0) {
|
||||
return new JsonContent(['error' => 'game_already_started']);
|
||||
}
|
||||
|
||||
$members['all'][] = $token;
|
||||
}
|
||||
|
||||
$room->setMembersArray($members);
|
||||
$room->setUpdatedDate(new DateTime());
|
||||
|
||||
$this->pdm->saveToDb($room);
|
||||
|
||||
$this->multiConnector->sendMessage('join_room', [
|
||||
'roomId' => $roomId,
|
||||
'token' => $token,
|
||||
'userName' => $userName
|
||||
]);
|
||||
|
||||
return new JsonContent([
|
||||
'roomId' => $roomId,
|
||||
'token' => $token,
|
||||
'owner' => $members['owner'] == $token,
|
||||
'mapId' => $state['mapId'],
|
||||
'mapName' => $map->getName(),
|
||||
'bounds' => $map->getBounds()->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
private function getMultiToken(string $roomId): string
|
||||
{
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
|
||||
$token = bin2hex(random_bytes(16));
|
||||
|
||||
$session->set('multiState', [
|
||||
'roomId' => $roomId,
|
||||
'token' => $token
|
||||
]);
|
||||
} else {
|
||||
$token = $multiState['token'];
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\Multi\MultiConnector;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Repository\PlaceRepository;
|
||||
|
||||
class GameFlowController
|
||||
@@ -14,122 +17,226 @@ class GameFlowController
|
||||
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private MultiConnector $multiConnector;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->multiConnector = new MultiConnector();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
$this->placeRepository = new PlaceRepository();
|
||||
}
|
||||
|
||||
public function getNewPlace(): IContent
|
||||
public function initialData(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
$data = ['error' => 'no_session_found'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
if (count($state['rounds']) === 0) {
|
||||
$placesWithoutPano = [];
|
||||
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, [], $placesWithoutPano);
|
||||
|
||||
$this->addNewRoundToState($state, $place, $placesWithoutPano);
|
||||
|
||||
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
|
||||
$this->startNewGame($state, $mapId);
|
||||
$session->set('state', $state);
|
||||
}
|
||||
|
||||
$data = ['panoId' => $place->getPanoIdCached()];
|
||||
} else {
|
||||
$rounds = count($state['rounds']);
|
||||
$last = $state['rounds'][$rounds - 1];
|
||||
$response = [];
|
||||
|
||||
$history = [];
|
||||
for ($i = 0; $i < $rounds - 1; ++$i) {
|
||||
$round = $state['rounds'][$i];
|
||||
$history[] = [
|
||||
'position' => $round['position']->toArray(),
|
||||
$last = $state['rounds'][$state['currentRound']];
|
||||
$response['place'] = [
|
||||
'panoId' => $last['panoId'],
|
||||
'pov' => $last['pov']->toArray()
|
||||
];
|
||||
|
||||
$response['history'] = [];
|
||||
for ($i = 0; $i < $state['currentRound']; ++$i) {
|
||||
$round = $state['rounds'][$i];
|
||||
$response['history'][] = [
|
||||
'position' => $round['position']->toArray(),
|
||||
'result' => [
|
||||
'guessPosition' => $round['guessPosition']->toArray(),
|
||||
'distance' => $round['distance'],
|
||||
'score' => $round['score']
|
||||
];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'history' => $history,
|
||||
'panoId' => $last['panoId']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonContent($data);
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
public function evaluateGuess(): IContent
|
||||
public function multiInitialData(): IContent
|
||||
{
|
||||
$roomId = $this->request->query('roomId');
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||
$state = $room->getStateArray();
|
||||
$members = $room->getMembersArray();
|
||||
|
||||
if ($members['owner'] !== $multiState['token']) {
|
||||
return new JsonContent(['error' => 'not_owner_of_room']);
|
||||
}
|
||||
|
||||
if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
|
||||
$this->startNewGame($state, $state['mapId']);
|
||||
$room->setStateArray($state);
|
||||
$room->setUpdatedDate(new DateTime());
|
||||
$this->pdm->saveToDb($room);
|
||||
}
|
||||
|
||||
$places = [];
|
||||
foreach ($state['rounds'] as $round) {
|
||||
$places[] = [
|
||||
'position' => $round['position']->toArray(),
|
||||
'panoId' => $round['panoId'],
|
||||
'pov' => $round['pov']->toArray()
|
||||
];
|
||||
}
|
||||
|
||||
$this->multiConnector->sendMessage('start_game', ['roomId' => $roomId, 'places' => $places]);
|
||||
|
||||
return new JsonContent(['ok' => true]);
|
||||
}
|
||||
|
||||
public function guess(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
$data = ['error' => 'no_session_found'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
$last = $state['rounds'][count($state['rounds']) - 1];
|
||||
|
||||
$position = $last['position'];
|
||||
$last = $state['rounds'][$state['currentRound']];
|
||||
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
|
||||
|
||||
$distance = $this->calculateDistance($position, $guessPosition);
|
||||
$score = $this->calculateScore($distance, $state['area']);
|
||||
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']);
|
||||
|
||||
$last['guessPosition'] = $guessPosition;
|
||||
$last['distance'] = $distance;
|
||||
$last['score'] = $score;
|
||||
$state['rounds'][count($state['rounds']) - 1] = $last;
|
||||
$last['distance'] = $result['distance'];
|
||||
$last['score'] = $result['score'];
|
||||
|
||||
if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
|
||||
$exclude = [];
|
||||
$response = [
|
||||
'position' => $last['position']->toArray(),
|
||||
'result' => $result
|
||||
];
|
||||
|
||||
foreach ($state['rounds'] as $round) {
|
||||
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
|
||||
}
|
||||
$state['rounds'][$state['currentRound']] = $last;
|
||||
$state['currentRound'] += 1;
|
||||
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
|
||||
$next = $state['rounds'][$state['currentRound']];
|
||||
|
||||
$placesWithoutPano = [];
|
||||
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, $exclude, $placesWithoutPano);
|
||||
|
||||
$this->addNewRoundToState($state, $place, $placesWithoutPano);
|
||||
|
||||
$panoId = $place->getPanoIdCached();
|
||||
} else {
|
||||
$state['rounds'] = [];
|
||||
|
||||
$panoId = null;
|
||||
$response['place'] = [
|
||||
'panoId' => $next['panoId'],
|
||||
'pov' => $next['pov']->toArray()
|
||||
];
|
||||
}
|
||||
|
||||
$session->set('state', $state);
|
||||
|
||||
$data = [
|
||||
'result' => [
|
||||
'position' => $position->toArray(),
|
||||
'distance' => $distance,
|
||||
'score' => $score
|
||||
],
|
||||
'panoId' => $panoId
|
||||
];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
private function addNewRoundToState(&$state, Place $place, array $placesWithoutPano): void
|
||||
public function multiGuess(): IContent
|
||||
{
|
||||
$state['rounds'][] = [
|
||||
'placesWithoutPano' => $placesWithoutPano,
|
||||
'placeId' => $place->getId(),
|
||||
'position' => $place->getPosition(),
|
||||
'panoId' => $place->getPanoIdCached()
|
||||
$roomId = $this->request->query('roomId');
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||
$state = $room->getStateArray();
|
||||
|
||||
$last = $state['rounds'][$state['currentRound']];
|
||||
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
|
||||
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']);
|
||||
|
||||
$responseFromMulti = $this->multiConnector->sendMessage('guess', [
|
||||
'roomId' => $roomId,
|
||||
'token' => $multiState['token'],
|
||||
'guessPosition' => $guessPosition->toArray(),
|
||||
'distance' => $result['distance'],
|
||||
'score' => $result['score']
|
||||
]);
|
||||
|
||||
if (isset($responseFromMulti['error'])) {
|
||||
return new JsonContent(['error' => $responseFromMulti['error']]);
|
||||
}
|
||||
|
||||
$response = [
|
||||
'position' => $last['position']->toArray(),
|
||||
'result' => $result,
|
||||
'allResults' => $responseFromMulti['allResults']
|
||||
];
|
||||
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
public function multiNextRound(): IContent
|
||||
{
|
||||
$roomId = $this->request->query('roomId');
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||
$state = $room->getStateArray();
|
||||
$members = $room->getMembersArray();
|
||||
|
||||
if ($members['owner'] !== $multiState['token']) {
|
||||
return new JsonContent(['error' => 'not_owner_of_room']);
|
||||
}
|
||||
|
||||
$state['currentRound'] += 1;
|
||||
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
|
||||
$this->multiConnector->sendMessage('next_round', ['roomId' => $roomId, 'currentRound' => $state['currentRound']]);
|
||||
}
|
||||
|
||||
$room->setStateArray($state);
|
||||
$room->setUpdatedDate(new DateTime());
|
||||
$this->pdm->saveToDb($room);
|
||||
|
||||
return new JsonContent(['ok' => true]);
|
||||
}
|
||||
|
||||
private function evalueteGuess(Position $realPosition, Position $guessPosition, float $area)
|
||||
{
|
||||
$distance = $this->calculateDistance($realPosition, $guessPosition);
|
||||
$score = $this->calculateScore($distance, $area);
|
||||
|
||||
return ['distance' => $distance, 'score' => $score];
|
||||
}
|
||||
|
||||
private function startNewGame(array &$state, int $mapId): void
|
||||
{
|
||||
$places = $this->placeRepository->getRandomNForMapWithValidPano($mapId, static::NUMBER_OF_ROUNDS);
|
||||
|
||||
$state['rounds'] = [];
|
||||
$state['currentRound'] = 0;
|
||||
|
||||
foreach ($places as $place) {
|
||||
$state['rounds'][] = [
|
||||
'placeId' => $place->getId(),
|
||||
'position' => $place->getPosition(),
|
||||
'panoId' => $place->getPanoIdCached(),
|
||||
'pov' => $place->getPov()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateDistance(Position $realPosition, Position $guessPosition): float
|
||||
|
||||
@@ -24,7 +24,6 @@ class HomeController
|
||||
{
|
||||
// session starts with the request, this method just sends valid data to the client
|
||||
|
||||
$data = ['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')]);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Interfaces\Response\IRedirect;
|
||||
@@ -7,8 +10,10 @@ use MapGuesser\Mailing\Mail;
|
||||
use MapGuesser\OAuth\GoogleOAuth;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\UserConfirmation;
|
||||
use MapGuesser\PersistentData\Model\UserPasswordResetter;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\UserConfirmationRepository;
|
||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||
use MapGuesser\Repository\UserRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
@@ -25,12 +30,15 @@ class LoginController
|
||||
|
||||
private UserConfirmationRepository $userConfirmationRepository;
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
}
|
||||
|
||||
public function getLoginForm()
|
||||
@@ -39,18 +47,23 @@ class LoginController
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
return new HtmlContent('login/login', $data);
|
||||
return new HtmlContent('login/login');
|
||||
}
|
||||
|
||||
public function getGoogleLoginRedirect(): IRedirect
|
||||
{
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
|
||||
$this->request->session()->set('oauth_state', $state);
|
||||
$this->request->session()->set('oauth_nonce', $nonce);
|
||||
|
||||
$oAuth = new GoogleOAuth();
|
||||
$url = $oAuth->getDialogUrl($state, $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink());
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$url = $oAuth->getDialogUrl(
|
||||
$state,
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
|
||||
$nonce
|
||||
);
|
||||
|
||||
return new Redirect($url, IRedirect::TEMPORARY);
|
||||
}
|
||||
@@ -74,8 +87,7 @@ class LoginController
|
||||
|
||||
public function getSignupSuccess(): IContent
|
||||
{
|
||||
$data = [];
|
||||
return new HtmlContent('login/signup_success', $data);
|
||||
return new HtmlContent('login/signup_success');
|
||||
}
|
||||
|
||||
public function getSignupWithGoogleForm()
|
||||
@@ -92,48 +104,96 @@ class LoginController
|
||||
|
||||
$user = $this->userRepository->getByEmail($userData['email']);
|
||||
|
||||
$data = ['found' => $user !== null, 'email' => $userData['email']];
|
||||
return new HtmlContent('login/google_signup', $data);
|
||||
return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email']]);
|
||||
}
|
||||
|
||||
public function getRequestPasswordResetForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
|
||||
}
|
||||
|
||||
public function getRequestPasswordResetSuccess(): IContent
|
||||
{
|
||||
return new HtmlContent('login/password_reset_request_success');
|
||||
}
|
||||
|
||||
public function getResetPasswordForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$token = $this->request->query('token');
|
||||
$resetter = $this->userPasswordResetterRepository->getByToken($token);
|
||||
|
||||
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
|
||||
return new HtmlContent('login/reset_password', ['success' => false]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getById($resetter->getUserId());
|
||||
|
||||
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]);
|
||||
}
|
||||
|
||||
public function login(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByEmail($this->request->post('email'));
|
||||
|
||||
if ($user === null) {
|
||||
if (strlen($this->request->post('password')) < 6) {
|
||||
$data = ['error' => 'password_too_short'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$tmpUser = new User();
|
||||
$tmpUser->setPlainPassword($this->request->post('password'));
|
||||
|
||||
$this->request->session()->set('tmp_user_data', ['email' => $this->request->post('email'), 'password_hashed' => $tmpUser->getPassword()]);
|
||||
$this->request->session()->set('tmp_user_data', [
|
||||
'email' => $this->request->post('email'),
|
||||
'password_hashed' => $tmpUser->getPassword()
|
||||
]);
|
||||
|
||||
$data = ['error' => 'user_not_found'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$user->getActive()) {
|
||||
$data = ['error' => 'user_not_active'];
|
||||
return new JsonContent($data);
|
||||
$this->resendConfirmationEmail($user);
|
||||
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'User found with the given email address, but the account is not activated. ' .
|
||||
'Please check your email and click on the activation link!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$user->checkPassword($this->request->post('password'))) {
|
||||
$data = ['error' => 'password_not_match'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' .
|
||||
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$this->request->setUser($user);
|
||||
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function loginWithGoogle()
|
||||
@@ -143,30 +203,34 @@ class LoginController
|
||||
}
|
||||
|
||||
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
|
||||
$data = [];
|
||||
return new HtmlContent('login/google_login', $data);
|
||||
return new HtmlContent('login/google_login');
|
||||
}
|
||||
|
||||
$oAuth = new GoogleOAuth();
|
||||
$tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink());
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$tokenData = $oAuth->getToken(
|
||||
$this->request->query('code'),
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
|
||||
);
|
||||
|
||||
if (!isset($tokenData['id_token'])) {
|
||||
$data = [];
|
||||
return new HtmlContent('login/google_login', $data);
|
||||
return new HtmlContent('login/google_login');
|
||||
}
|
||||
|
||||
$jwtParser = new JwtParser($tokenData['id_token']);
|
||||
$userData = $jwtParser->getPayload();
|
||||
$idToken = $jwtParser->getPayload();
|
||||
|
||||
if (!$userData['email_verified']) {
|
||||
$data = [];
|
||||
return new HtmlContent('login/google_login', $data);
|
||||
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
|
||||
return new HtmlContent('login/google_login');
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByGoogleSub($userData['sub']);
|
||||
if (!$idToken['email_verified']) {
|
||||
return new HtmlContent('login/google_login');
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByGoogleSub($idToken['sub']);
|
||||
|
||||
if ($user === null) {
|
||||
$this->request->session()->set('google_user_data', $userData);
|
||||
$this->request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
|
||||
|
||||
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
@@ -186,8 +250,7 @@ class LoginController
|
||||
public function signup(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
$data = ['error' => 'logged_in'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByEmail($this->request->post('email'));
|
||||
@@ -195,22 +258,31 @@ class LoginController
|
||||
if ($user !== null) {
|
||||
if ($user->getActive()) {
|
||||
if (!$user->checkPassword($this->request->post('password'))) {
|
||||
$data = ['error' => 'user_found_password_not_match'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'There is a user already registered with the given email address, ' .
|
||||
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
|
||||
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$this->request->setUser($user);
|
||||
|
||||
$data = ['error' => 'user_found'];
|
||||
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]];
|
||||
} else {
|
||||
$data = ['error' => 'user_found_user_not_active'];
|
||||
$data = [
|
||||
'error' => [
|
||||
'errorText' => 'There is a user already registered with the given email address. ' .
|
||||
'Please check your email and click on the activation link!'
|
||||
]
|
||||
];
|
||||
}
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
|
||||
$data = ['error' => 'email_not_valid'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
|
||||
}
|
||||
|
||||
if ($this->request->session()->has('tmp_user_data')) {
|
||||
@@ -220,52 +292,53 @@ class LoginController
|
||||
$tmpUser->setPassword($tmpUserData['password_hashed']);
|
||||
|
||||
if (!$tmpUser->checkPassword($this->request->post('password'))) {
|
||||
$data = ['error' => 'passwords_not_match'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
|
||||
}
|
||||
} else {
|
||||
if (strlen($this->request->post('password')) < 6) {
|
||||
$data = ['error' => 'password_too_short'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
|
||||
$data = ['error' => 'passwords_not_match'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
|
||||
}
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($this->request->post('email'));
|
||||
$user->setPlainPassword($this->request->post('password'));
|
||||
$user->setCreatedDate(new DateTime());
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
|
||||
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime());
|
||||
$token = bin2hex(random_bytes(16));
|
||||
|
||||
$confirmation = new UserConfirmation();
|
||||
$confirmation->setUser($user);
|
||||
$confirmation->setToken($token);
|
||||
$confirmation->setLastSentDate(new DateTime());
|
||||
|
||||
$this->pdm->saveToDb($confirmation);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->sendConfirmationEmail($user->getEmail(), $token);
|
||||
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
|
||||
|
||||
$this->request->session()->delete('tmp_user_data');
|
||||
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function signupWithGoogle(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
$userData = $this->request->session()->get('google_user_data');
|
||||
@@ -277,6 +350,7 @@ class LoginController
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($userData['email']);
|
||||
$user->setCreatedDate(new DateTime());
|
||||
} else {
|
||||
$sendWelcomeEmail = false;
|
||||
}
|
||||
@@ -293,24 +367,21 @@ class LoginController
|
||||
$this->request->session()->delete('google_user_data');
|
||||
$this->request->setUser($user);
|
||||
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function resetSignup(): IContent
|
||||
{
|
||||
$this->request->session()->delete('tmp_user_data');
|
||||
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function resetGoogleSignup(): IContent
|
||||
{
|
||||
$this->request->session()->delete('google_user_data');
|
||||
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function activate()
|
||||
@@ -319,11 +390,10 @@ class LoginController
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
|
||||
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
|
||||
|
||||
if ($confirmation === null) {
|
||||
$data = [];
|
||||
return new HtmlContent('login/activate', $data);
|
||||
return new HtmlContent('login/activate');
|
||||
}
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
@@ -348,11 +418,10 @@ class LoginController
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
|
||||
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
|
||||
|
||||
if ($confirmation === null) {
|
||||
$data = ['success' => false];
|
||||
return new HtmlContent('login/cancel', $data);
|
||||
return new HtmlContent('login/cancel', ['success' => false]);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
@@ -365,23 +434,156 @@ class LoginController
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$data = ['success' => true];
|
||||
return new HtmlContent('login/cancel', $data);
|
||||
return new HtmlContent('login/cancel', ['success' => true]);
|
||||
}
|
||||
|
||||
private function sendConfirmationEmail(string $email, string $token): void
|
||||
public function requestPasswordReset(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByEmail($this->request->post('email'));
|
||||
|
||||
if ($user === null) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$user->getActive()) {
|
||||
$this->resendConfirmationEmail($user);
|
||||
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'User found with the given email address, but the account is not activated. ' .
|
||||
'Please check your email and click on the activation link!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$existingResetter = $this->userPasswordResetterRepository->getByUser($user);
|
||||
|
||||
if ($existingResetter !== null && $existingResetter->getExpiresDate() > new DateTime()) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'Password reset was recently requested for this account. Please check your email, or try again later!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(16));
|
||||
$expires = new DateTime('+1 hour');
|
||||
|
||||
$passwordResetter = new UserPasswordResetter();
|
||||
$passwordResetter->setUser($user);
|
||||
$passwordResetter->setToken($token);
|
||||
$passwordResetter->setExpiresDate($expires);
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
if ($existingResetter !== null) {
|
||||
$this->pdm->deleteFromDb($existingResetter);
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($passwordResetter);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
|
||||
public function resetPassword(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$token = $this->request->query('token');
|
||||
$resetter = $this->userPasswordResetterRepository->getByToken($token);
|
||||
|
||||
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (strlen($this->request->post('password')) < 6) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
|
||||
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$this->pdm->deleteFromDb($resetter);
|
||||
|
||||
$user = $this->userRepository->getById($resetter->getUserId());
|
||||
$user->setPlainPassword($this->request->post('password'));
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->request->setUser($user);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function sendConfirmationEmail(string $email, string $token, DateTime $created): void
|
||||
{
|
||||
$mail = new Mail();
|
||||
$mail->addRecipient($email);
|
||||
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
|
||||
$mail->setBodyFromTemplate('signup', [
|
||||
'EMAIL' => $email,
|
||||
'ACTIVATE_LINK' => $this->request->getBase() . '/'. \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
|
||||
'CANCEL_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
|
||||
'ACTIVATE_LINK' => $this->request->getBase() . '/' .
|
||||
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
|
||||
'CANCEL_LINK' => $this->request->getBase() . '/' .
|
||||
\Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
|
||||
'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T')
|
||||
]);
|
||||
$mail->send();
|
||||
}
|
||||
|
||||
private function resendConfirmationEmail(User $user): bool
|
||||
{
|
||||
$confirmation = $this->userConfirmationRepository->getByUser($user);
|
||||
|
||||
if ($confirmation === null || (clone $confirmation->getLastSentDate())->add(new DateInterval('PT1H')) > new DateTime()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$confirmation->setLastSentDate(new DateTime());
|
||||
|
||||
$this->pdm->saveToDb($confirmation);
|
||||
|
||||
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function sendWelcomeEmail(string $email): void
|
||||
{
|
||||
$mail = new Mail();
|
||||
@@ -392,4 +594,18 @@ class LoginController
|
||||
]);
|
||||
$mail->send();
|
||||
}
|
||||
|
||||
private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void
|
||||
{
|
||||
$mail = new Mail();
|
||||
$mail->addRecipient($email);
|
||||
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
|
||||
$mail->setBodyFromTemplate('password-reset', [
|
||||
'EMAIL' => $email,
|
||||
'RESET_LINK' => $this->request->getBase() . '/' .
|
||||
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
|
||||
'EXPIRES' => $expires->format('Y-m-d H:i T')
|
||||
]);
|
||||
$mail->send();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Authentication\IUser;
|
||||
use MapGuesser\Interfaces\Authorization\ISecured;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\PersistentData\Model\Map;
|
||||
@@ -15,7 +13,7 @@ use MapGuesser\Repository\PlaceRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
use MapGuesser\Util\Panorama\Pov;
|
||||
|
||||
class MapAdminController implements ISecured
|
||||
{
|
||||
@@ -57,8 +55,13 @@ class MapAdminController implements ISecured
|
||||
$places = [];
|
||||
}
|
||||
|
||||
$data = ['mapId' => $mapId, 'mapName' => $map->getName(), 'mapDescription' => str_replace('<br>', "\n", $map->getDescription()), 'bounds' => $map->getBounds()->toArray(), 'places' => &$places];
|
||||
return new HtmlContent('admin/map_editor', $data);
|
||||
return new HtmlContent('admin/map_editor', [
|
||||
'mapId' => $mapId,
|
||||
'mapName' => $map->getName(),
|
||||
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
|
||||
'bounds' => $map->getBounds()->toArray(),
|
||||
'places' => &$places
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPlace(): IContent
|
||||
@@ -67,8 +70,7 @@ class MapAdminController implements ISecured
|
||||
|
||||
$place = $this->placeRepository->getById($placeId);
|
||||
|
||||
$data = ['panoId' => $place->getFreshPanoId()];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['panoId' => $place->getFreshPanoId()]);
|
||||
}
|
||||
|
||||
public function saveMap(): IContent
|
||||
@@ -94,6 +96,11 @@ class MapAdminController implements ISecured
|
||||
$place->setMap($map);
|
||||
$place->setLat((float) $placeRaw['lat']);
|
||||
$place->setLng((float) $placeRaw['lng']);
|
||||
$place->setPov(new Pov(
|
||||
(float) $placeRaw['pov']['heading'],
|
||||
(float) $placeRaw['pov']['pitch'],
|
||||
(float) $placeRaw['pov']['zoom']
|
||||
));
|
||||
|
||||
if ($placeRaw['panoId'] === -1) {
|
||||
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
|
||||
@@ -114,6 +121,11 @@ class MapAdminController implements ISecured
|
||||
$place = $this->placeRepository->getById((int) $placeRaw['id']);
|
||||
$place->setLat((float) $placeRaw['lat']);
|
||||
$place->setLng((float) $placeRaw['lng']);
|
||||
$place->setPov(new Pov(
|
||||
(float) $placeRaw['pov']['heading'],
|
||||
(float) $placeRaw['pov']['pitch'],
|
||||
(float) $placeRaw['pov']['zoom']
|
||||
));
|
||||
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
|
||||
|
||||
$this->pdm->saveToDb($place);
|
||||
@@ -146,11 +158,11 @@ class MapAdminController implements ISecured
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$data = ['mapId' => $map->getId(), 'added' => $addedIds];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
|
||||
}
|
||||
|
||||
public function deleteMap() {
|
||||
public function deleteMap(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
@@ -163,40 +175,22 @@ class MapAdminController implements ISecured
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function deletePlaces(Map $map): void
|
||||
{
|
||||
//TODO: relations?
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(Place::getFields());
|
||||
$select->where('map_id', '=', $map->getId());
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($placeData = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$place = new Place();
|
||||
|
||||
$this->pdm->fillWithData($placeData, $place);
|
||||
|
||||
foreach ($this->placeRepository->getAllForMap($map) as $place) {
|
||||
$this->pdm->deleteFromDb($place);
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateMapBounds(Map $map): Bounds
|
||||
{
|
||||
//TODO: from repository or relations
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['lat', 'lng']);
|
||||
$select->where('map_id', '=', $map->getId());
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
$bounds = new Bounds();
|
||||
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$bounds->extend(new Position($place['lat'], $place['lng']));
|
||||
|
||||
foreach ($this->placeRepository->getAllForMap($map) as $place) {
|
||||
$bounds->extend($place->getPosition());
|
||||
}
|
||||
|
||||
return $bounds;
|
||||
@@ -204,26 +198,19 @@ class MapAdminController implements ISecured
|
||||
|
||||
private function &getPlaces(Map $map): array
|
||||
{
|
||||
//TODO: from repository or relations
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
|
||||
$select->where('map_id', '=', $map->getId());
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
$places = [];
|
||||
|
||||
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
//$panoId = ???
|
||||
//$pov = ???
|
||||
$noPano = $place['pano_id_cached_timestamp'] && $place['pano_id_cached'] === null;
|
||||
foreach ($this->placeRepository->getAllForMap($map) as $place) {
|
||||
$noPano = $place->getPanoIdCachedTimestampDate() !== null && $place->getPanoIdCached() === null;
|
||||
|
||||
$places[$place['id']] = [
|
||||
'id' => $place['id'],
|
||||
'lat' => $place['lat'],
|
||||
'lng' => $place['lng'],
|
||||
$placeId = $place->getId();
|
||||
|
||||
$places[$placeId] = [
|
||||
'id' => $placeId,
|
||||
'lat' => $place->getLat(),
|
||||
'lng' => $place->getLng(),
|
||||
'panoId' => null,
|
||||
'pov' => ['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
|
||||
'pov' => $place->getPov()->toArray(),
|
||||
'noPano' => $noPano
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class MapsController
|
||||
|
||||
public function getMaps(): IContent
|
||||
{
|
||||
//TODO: from repository
|
||||
//TODO: from repository - count should be added
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns([
|
||||
['maps', 'id'],
|
||||
@@ -46,8 +46,10 @@ class MapsController
|
||||
}
|
||||
|
||||
$user = $this->request->user();
|
||||
$data = ['maps' => $maps, 'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)];
|
||||
return new HtmlContent('maps', $data);
|
||||
return new HtmlContent('maps', [
|
||||
'maps' => $maps,
|
||||
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
|
||||
]);
|
||||
}
|
||||
|
||||
private function formatMapAreaForHuman(float $area): array
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\Interfaces\Authorization\ISecured;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Interfaces\Response\IRedirect;
|
||||
use MapGuesser\OAuth\GoogleOAuth;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\Repository\UserConfirmationRepository;
|
||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Response\Redirect;
|
||||
use MapGuesser\Util\JwtParser;
|
||||
|
||||
class UserController implements ISecured
|
||||
{
|
||||
@@ -14,10 +22,16 @@ class UserController implements ISecured
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private UserConfirmationRepository $userConfirmationRepository;
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
@@ -27,38 +41,126 @@ class UserController implements ISecured
|
||||
return $user !== null;
|
||||
}
|
||||
|
||||
public function getProfile(): IContent
|
||||
public function getAccount(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
$data = ['user' => $user->toArray()];
|
||||
return new HtmlContent('profile', $data);
|
||||
return new HtmlContent('account/account', ['user' => $user->toArray()]);
|
||||
}
|
||||
|
||||
public function saveProfile(): IContent
|
||||
public function getGoogleAuthenticateRedirect(): IRedirect
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
if (!$user->checkPassword($this->request->post('password'))) {
|
||||
$data = ['error' => 'password_not_match'];
|
||||
return new JsonContent($data);
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
|
||||
$this->request->session()->set('oauth_state', $state);
|
||||
$this->request->session()->set('oauth_nonce', $nonce);
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
|
||||
$url = $oAuth->getDialogUrl(
|
||||
$state,
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
|
||||
$nonce,
|
||||
$user->getEmail()
|
||||
);
|
||||
|
||||
return new Redirect($url, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function authenticateWithGoogle(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
|
||||
return new HtmlContent('account/google_authenticate', ['success' => false]);
|
||||
}
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$tokenData = $oAuth->getToken(
|
||||
$this->request->query('code'),
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
|
||||
);
|
||||
|
||||
if (!isset($tokenData['id_token'])) {
|
||||
return new HtmlContent('account/google_authenticate', ['success' => false]);
|
||||
}
|
||||
|
||||
$jwtParser = new JwtParser($tokenData['id_token']);
|
||||
$idToken = $jwtParser->getPayload();
|
||||
|
||||
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
|
||||
return new HtmlContent('account/google_authenticate', ['success' => false]);
|
||||
}
|
||||
|
||||
if ($idToken['sub'] !== $user->getGoogleSub()) {
|
||||
return new HtmlContent('account/google_authenticate', [
|
||||
'success' => false,
|
||||
'errorText' => 'This Google account is not linked to your account.'
|
||||
]);
|
||||
}
|
||||
|
||||
$authenticatedWithGoogleUntil = new DateTime('+45 seconds');
|
||||
$this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
|
||||
|
||||
return new HtmlContent('account/google_authenticate', [
|
||||
'success' => true,
|
||||
'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil
|
||||
]);
|
||||
}
|
||||
|
||||
public function getDeleteAccount(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
return new HtmlContent('account/delete', ['user' => $user->toArray()]);
|
||||
}
|
||||
|
||||
public function saveAccount(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
if (!$this->confirmUserIdentity(
|
||||
$user,
|
||||
$this->request->session()->get('authenticated_with_google_until'),
|
||||
$this->request->post('password'),
|
||||
$error
|
||||
)) {
|
||||
return new JsonContent(['error' => ['errorText' => $error]]);
|
||||
}
|
||||
|
||||
if (strlen($this->request->post('password_new')) > 0) {
|
||||
if (strlen($this->request->post('password_new')) < 6) {
|
||||
$data = ['error' => 'password_too_short'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
|
||||
$data = ['error' => 'passwords_not_match'];
|
||||
return new JsonContent($data);
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given new passwords do not match.'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$user->setPlainPassword($this->request->post('password_new'));
|
||||
@@ -66,7 +168,64 @@ class UserController implements ISecured
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
$this->request->session()->delete('authenticated_with_google_until');
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function deleteAccount(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
if (!$this->confirmUserIdentity(
|
||||
$user,
|
||||
$this->request->session()->get('authenticated_with_google_until'),
|
||||
$this->request->post('password'),
|
||||
$error
|
||||
)) {
|
||||
return new JsonContent(['error' => ['errorText' => $error]]);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
|
||||
if ($userConfirmation !== null) {
|
||||
$this->pdm->deleteFromDb($userConfirmation);
|
||||
}
|
||||
|
||||
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
|
||||
if ($userPasswordResetter !== null) {
|
||||
$this->pdm->deleteFromDb($userPasswordResetter);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($user);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->request->session()->delete('authenticated_with_google_until');
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, string &$error): bool
|
||||
{
|
||||
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($password !== null) {
|
||||
if ($user->checkPassword($password)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$error = 'The given current password is wrong.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$error = 'Could not confirm your identity. Please try again!';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,11 @@ class Connection implements IConnection
|
||||
public function __construct(string $host, string $user, string $password, string $db, int $port = -1, string $socket = null)
|
||||
{
|
||||
if ($port < 0) {
|
||||
$port = ini_get('mysqli.default_port');
|
||||
$port = (int) ini_get('mysqli.default_port');
|
||||
}
|
||||
|
||||
if ($socket === null) {
|
||||
$socket = ini_get('mysqli.default_socket');
|
||||
$socket = (string) ini_get('mysqli.default_socket');
|
||||
}
|
||||
|
||||
$this->connection = new mysqli($host, $user, $password, $db, $port, $socket);
|
||||
|
||||
@@ -135,6 +135,6 @@ class Modify
|
||||
|
||||
private function generateKey(): string
|
||||
{
|
||||
return substr(hash('sha256', serialize($this->attributes) . random_bytes(10) . microtime()), 0, 7);
|
||||
return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class Select
|
||||
|
||||
private array $orders = [];
|
||||
|
||||
private array $limit;
|
||||
private ?array $limit;
|
||||
|
||||
public function __construct(IConnection $connection, ?string $table = null)
|
||||
{
|
||||
@@ -144,7 +144,7 @@ class Select
|
||||
$this->limit = null;
|
||||
}
|
||||
|
||||
public function paginate(int $page, int $itemsPerPage)
|
||||
public function paginate(int $page, int $itemsPerPage): Select
|
||||
{
|
||||
$this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
|
||||
|
||||
@@ -308,7 +308,7 @@ class Select
|
||||
return implode(' ', $joins);
|
||||
}
|
||||
|
||||
private function generateConditions(string $type): array
|
||||
private function generateConditions(int $type): array
|
||||
{
|
||||
$conditions = '';
|
||||
$params = [];
|
||||
@@ -376,9 +376,9 @@ class Select
|
||||
return [$conditionsString, $params];
|
||||
}
|
||||
|
||||
private function generateComplexConditionFragment(string $type, Closure $conditionCallback): array
|
||||
private function generateComplexConditionFragment(int $type, Closure $conditionCallback): array
|
||||
{
|
||||
$instance = new static($this->connection, $this->table);
|
||||
$instance = new self($this->connection, $this->table);
|
||||
$instance->tableAliases = $this->tableAliases;
|
||||
|
||||
$conditionCallback($instance);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php namespace MapGuesser\Database;
|
||||
|
||||
class Utils {
|
||||
public static function backtick(string $name) {
|
||||
public static function backtick(string $name): string
|
||||
{
|
||||
return '`' . $name . '`';
|
||||
}
|
||||
}
|
||||
+22
-13
@@ -1,11 +1,10 @@
|
||||
<?php namespace MapGuesser\Http;
|
||||
|
||||
class Request
|
||||
use MapGuesser\Interfaces\Http\IRequest;
|
||||
use MapGuesser\Interfaces\Http\IResponse;
|
||||
|
||||
class Request implements IRequest
|
||||
{
|
||||
const HTTP_GET = 0;
|
||||
|
||||
const HTTP_POST = 1;
|
||||
|
||||
private string $url;
|
||||
|
||||
private int $method;
|
||||
@@ -14,13 +13,23 @@ class Request
|
||||
|
||||
private array $headers = [];
|
||||
|
||||
public function __construct(string $url, int $method = self::HTTP_GET)
|
||||
public function __construct(string $url = '', int $method = self::HTTP_GET)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
public function setQuery($query)
|
||||
public function setUrl(string $url): void
|
||||
{
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function setMethod(int $method): void
|
||||
{
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
public function setQuery($query): void
|
||||
{
|
||||
if (is_string($query)) {
|
||||
$this->query = $query;
|
||||
@@ -29,22 +38,22 @@ class Request
|
||||
}
|
||||
}
|
||||
|
||||
public function setHeaders(array $headers)
|
||||
public function setHeaders(array $headers): void
|
||||
{
|
||||
$this->headers = array_merge($this->headers, $headers);
|
||||
}
|
||||
|
||||
public function send(): Response
|
||||
public function send(): IResponse
|
||||
{
|
||||
$ch = curl_init();
|
||||
|
||||
if ($this->method === self::HTTP_GET) {
|
||||
$url = $this->url . '?' . $this->query;
|
||||
} elseif ($this->method === self::HTTP_POST) {
|
||||
$url = $this->url;
|
||||
if ($this->method === self::HTTP_POST) {
|
||||
$url = $this->url;
|
||||
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query);
|
||||
} else {
|
||||
$url = $this->url . '?' . $this->query;
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php namespace MapGuesser\Http;
|
||||
|
||||
class Response
|
||||
use MapGuesser\Interfaces\Http\IResponse;
|
||||
|
||||
class Response implements IResponse
|
||||
{
|
||||
private string $body;
|
||||
|
||||
@@ -12,12 +14,12 @@ class Response
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
public function getBody()
|
||||
public function getBody(): string
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function getHeaders()
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php namespace MapGuesser\Interfaces\Http;
|
||||
|
||||
interface IRequest
|
||||
{
|
||||
const HTTP_GET = 0;
|
||||
|
||||
const HTTP_POST = 1;
|
||||
|
||||
public function setUrl(string $url): void;
|
||||
|
||||
public function setMethod(int $method): void;
|
||||
|
||||
public function setQuery($query): void;
|
||||
|
||||
public function setHeaders(array $headers): void;
|
||||
|
||||
public function send(): IResponse;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php namespace MapGuesser\Interfaces\Http;
|
||||
|
||||
interface IResponse
|
||||
{
|
||||
public function getBody(): string;
|
||||
|
||||
public function getHeaders(): array;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use MapGuesser\Interfaces\Authentication\IUser;
|
||||
|
||||
interface IRequest
|
||||
{
|
||||
public function setParsedRouteParams(array &$routeParams);
|
||||
public function setParsedRouteParams(array &$routeParams): void;
|
||||
|
||||
public function getBase(): string;
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
interface IContent
|
||||
{
|
||||
public function &getData(): array;
|
||||
public function setData(array $data): void;
|
||||
|
||||
public function &render(): string;
|
||||
public function getData(): array;
|
||||
|
||||
public function render(): void;
|
||||
|
||||
public function getContentType(): string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php namespace MapGuesser\Interfaces\Session;
|
||||
|
||||
use SessionHandlerInterface;
|
||||
use SessionIdInterface;
|
||||
use SessionUpdateTimestampHandlerInterface;
|
||||
|
||||
interface ISessionHandler extends SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
|
||||
{
|
||||
}
|
||||
@@ -77,7 +77,7 @@ class Mail
|
||||
}
|
||||
}
|
||||
|
||||
private function sendMail(PHPMailer $mailer, array $recipient)
|
||||
private function sendMail(PHPMailer $mailer, array $recipient): void
|
||||
{
|
||||
$mailer->clearAddresses();
|
||||
$mailer->addAddress($recipient[0], $recipient[1]);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php namespace MapGuesser\Multi;
|
||||
|
||||
class MultiConnector
|
||||
{
|
||||
public function sendMessage(string $func, array $args = [])
|
||||
{
|
||||
$message = json_encode([
|
||||
'func' => $func,
|
||||
'args' => $args
|
||||
]);
|
||||
|
||||
$connection = fsockopen($_ENV['MULTI_INTERNAL_HOST'], $_ENV['MULTI_INTERNAL_PORT']);
|
||||
fwrite($connection, $message);
|
||||
$response = '';
|
||||
while (!feof($connection)) {
|
||||
$response .= fgets($connection);
|
||||
}
|
||||
fclose($connection);
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
<?php namespace MapGuesser\OAuth;
|
||||
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\Interfaces\Http\IRequest;
|
||||
|
||||
class GoogleOAuth
|
||||
{
|
||||
private static $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
private static string $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
|
||||
private static $tokenUrlBase = 'https://oauth2.googleapis.com/token';
|
||||
private static string $tokenUrlBase = 'https://oauth2.googleapis.com/token';
|
||||
|
||||
public function getDialogUrl(string $state, string $redirectUrl): string
|
||||
private IRequest $request;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function getDialogUrl(string $state, string $redirectUrl, ?string $nonce = null, ?string $loginHint = null): string
|
||||
{
|
||||
$oauthParams = [
|
||||
'response_type' => 'code',
|
||||
@@ -16,13 +23,20 @@ class GoogleOAuth
|
||||
'scope' => 'openid email',
|
||||
'redirect_uri' => $redirectUrl,
|
||||
'state' => $state,
|
||||
'nonce' => hash('sha256', random_bytes(10) . microtime()),
|
||||
];
|
||||
|
||||
if ($nonce !== null) {
|
||||
$oauthParams['nonce'] = $nonce;
|
||||
}
|
||||
|
||||
if ($loginHint !== null) {
|
||||
$oauthParams['login_hint'] = $loginHint;
|
||||
}
|
||||
|
||||
return self::$dialogUrlBase . '?' . http_build_query($oauthParams);
|
||||
}
|
||||
|
||||
public function getToken(string $code, string $redirectUrl)
|
||||
public function getToken(string $code, string $redirectUrl): array
|
||||
{
|
||||
$tokenParams = [
|
||||
'code' => $code,
|
||||
@@ -32,9 +46,10 @@ class GoogleOAuth
|
||||
'grant_type' => 'authorization_code',
|
||||
];
|
||||
|
||||
$request = new Request(self::$tokenUrlBase, Request::HTTP_POST);
|
||||
$request->setQuery($tokenParams);
|
||||
$response = $request->send();
|
||||
$this->request->setUrl(self::$tokenUrlBase);
|
||||
$this->request->setMethod(IRequest::HTTP_POST);
|
||||
$this->request->setQuery($tokenParams);
|
||||
$response = $this->request->send();
|
||||
|
||||
return json_decode($response->getBody(), true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class MultiRoom extends Model
|
||||
{
|
||||
protected static string $table = 'multi_rooms';
|
||||
|
||||
protected static array $fields = ['room_id', 'state', 'members', 'updated'];
|
||||
|
||||
private string $roomId = '';
|
||||
|
||||
private array $state = [];
|
||||
|
||||
private array $members = [];
|
||||
|
||||
private DateTime $updated;
|
||||
|
||||
public function setRoomId(string $roomId): void
|
||||
{
|
||||
$this->roomId = $roomId;
|
||||
}
|
||||
|
||||
public function setStateArray(array $state): void
|
||||
{
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
public function setMembersArray(array $members): void
|
||||
{
|
||||
$this->members = $members;
|
||||
}
|
||||
|
||||
public function setState(string $state): void
|
||||
{
|
||||
$this->state = unserialize($state);
|
||||
}
|
||||
|
||||
public function setMembers(string $members): void
|
||||
{
|
||||
$this->members = unserialize($members);
|
||||
}
|
||||
|
||||
public function setUpdatedDate(DateTime $updated): void
|
||||
{
|
||||
$this->updated = $updated;
|
||||
}
|
||||
|
||||
public function setUpdated(string $updated): void
|
||||
{
|
||||
$this->updated = new DateTime($updated);
|
||||
}
|
||||
|
||||
public function getRoomId(): string
|
||||
{
|
||||
return $this->roomId;
|
||||
}
|
||||
|
||||
public function getStateArray(): array
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function getState(): string
|
||||
{
|
||||
return serialize($this->state);
|
||||
}
|
||||
|
||||
public function getMembersArray(): array
|
||||
{
|
||||
return $this->members;
|
||||
}
|
||||
|
||||
public function getMembers(): string
|
||||
{
|
||||
return serialize($this->members);
|
||||
}
|
||||
|
||||
public function getUpdatedDate(): DateTime
|
||||
{
|
||||
return $this->updated;
|
||||
}
|
||||
|
||||
public function getUpdated(): string
|
||||
{
|
||||
return $this->updated->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,13 @@ use DateTime;
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
use MapGuesser\Util\Panorama\Pov;
|
||||
|
||||
class Place extends Model
|
||||
{
|
||||
protected static string $table = 'places';
|
||||
|
||||
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp'];
|
||||
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp', 'pov_heading', 'pov_pitch', 'pov_zoom'];
|
||||
|
||||
protected static array $relations = ['map' => Map::class];
|
||||
|
||||
@@ -24,9 +25,12 @@ class Place extends Model
|
||||
|
||||
private ?DateTime $panoIdCachedTimestamp = null;
|
||||
|
||||
private Pov $pov;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->position = new Position(0.0, 0.0);
|
||||
$this->pov = new Pov(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
public function setMap(Map $map): void
|
||||
@@ -54,6 +58,26 @@ class Place extends Model
|
||||
$this->position->setLng($lng);
|
||||
}
|
||||
|
||||
public function setPov(Pov $pov): void
|
||||
{
|
||||
$this->pov = $pov;
|
||||
}
|
||||
|
||||
public function setPovHeading(float $heading): void
|
||||
{
|
||||
$this->pov->setHeading($heading);
|
||||
}
|
||||
|
||||
public function setPovPitch(float $pitch): void
|
||||
{
|
||||
$this->pov->setPitch($pitch);
|
||||
}
|
||||
|
||||
public function setPovZoom(float $zoom): void
|
||||
{
|
||||
$this->pov->setZoom($zoom);
|
||||
}
|
||||
|
||||
public function setPanoIdCached(?string $panoIdCached): void
|
||||
{
|
||||
$this->panoIdCached = $panoIdCached;
|
||||
@@ -96,6 +120,26 @@ class Place extends Model
|
||||
return $this->position->getLng();
|
||||
}
|
||||
|
||||
public function getPov(): Pov
|
||||
{
|
||||
return $this->pov;
|
||||
}
|
||||
|
||||
public function getPovHeading(): float
|
||||
{
|
||||
return $this->pov->getHeading();
|
||||
}
|
||||
|
||||
public function getPovPitch(): float
|
||||
{
|
||||
return $this->pov->getPitch();
|
||||
}
|
||||
|
||||
public function getPovZoom(): float
|
||||
{
|
||||
return $this->pov->getZoom();
|
||||
}
|
||||
|
||||
public function getFreshPanoId(bool $canBeIndoor = false): ?string
|
||||
{
|
||||
if (
|
||||
@@ -122,7 +166,7 @@ class Place extends Model
|
||||
}
|
||||
|
||||
$this->panoIdCached = $panoId;
|
||||
$this->panoIdCachedTimestamp = new DateTime('now');
|
||||
$this->panoIdCachedTimestamp = new DateTime();
|
||||
|
||||
(new PersistentDataManager())->saveToDb($this);
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Interfaces\Authentication\IUser;
|
||||
|
||||
class User extends Model implements IUser
|
||||
{
|
||||
protected static string $table = 'users';
|
||||
|
||||
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub'];
|
||||
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub', 'created'];
|
||||
|
||||
private static array $types = ['user', 'admin'];
|
||||
|
||||
@@ -20,6 +21,8 @@ class User extends Model implements IUser
|
||||
|
||||
private ?string $googleSub = null;
|
||||
|
||||
private DateTime $created;
|
||||
|
||||
public function setEmail(string $email): void
|
||||
{
|
||||
$this->email = $email;
|
||||
@@ -42,9 +45,9 @@ class User extends Model implements IUser
|
||||
}
|
||||
}
|
||||
|
||||
public function setActive($active): void
|
||||
public function setActive(bool $active): void
|
||||
{
|
||||
$this->active = (bool) $active;
|
||||
$this->active = $active;
|
||||
}
|
||||
|
||||
public function setGoogleSub(?string $googleSub): void
|
||||
@@ -52,6 +55,16 @@ class User extends Model implements IUser
|
||||
$this->googleSub = $googleSub;
|
||||
}
|
||||
|
||||
public function setCreatedDate(DateTime $created): void
|
||||
{
|
||||
$this->created = $created;
|
||||
}
|
||||
|
||||
public function setCreated(string $created): void
|
||||
{
|
||||
$this->created = new DateTime($created);
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
@@ -77,15 +90,25 @@ class User extends Model implements IUser
|
||||
return $this->googleSub;
|
||||
}
|
||||
|
||||
public function getCreatedDate(): DateTime
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
public function getCreated(): string
|
||||
{
|
||||
return $this->created->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function hasPermission(int $permission): bool
|
||||
{
|
||||
switch ($permission) {
|
||||
case IUser::PERMISSION_NORMAL:
|
||||
return true;
|
||||
break;
|
||||
case IUser::PERMISSION_ADMIN:
|
||||
return $this->type === 'admin';
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Permission does not exist: ' . $permission);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +124,10 @@ class User extends Model implements IUser
|
||||
|
||||
public function checkPassword(string $password): bool
|
||||
{
|
||||
if ($this->password === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return password_verify($password, $this->password);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class UserConfirmation extends Model
|
||||
{
|
||||
protected static string $table = 'user_confirmations';
|
||||
|
||||
protected static array $fields = ['user_id', 'token'];
|
||||
protected static array $fields = ['user_id', 'token', 'last_sent'];
|
||||
|
||||
protected static array $relations = ['user' => User::class];
|
||||
|
||||
@@ -14,6 +16,8 @@ class UserConfirmation extends Model
|
||||
|
||||
private string $token = '';
|
||||
|
||||
private DateTime $lastSent;
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
@@ -29,6 +33,16 @@ class UserConfirmation extends Model
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function setLastSentDate(DateTime $lastSent): void
|
||||
{
|
||||
$this->lastSent = $lastSent;
|
||||
}
|
||||
|
||||
public function setLastSent(string $lastSent): void
|
||||
{
|
||||
$this->lastSent = new DateTime($lastSent);
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
@@ -43,4 +57,14 @@ class UserConfirmation extends Model
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getLastSentDate(): DateTime
|
||||
{
|
||||
return $this->lastSent;
|
||||
}
|
||||
|
||||
public function getLastSent(): string
|
||||
{
|
||||
return $this->lastSent->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class UserPasswordResetter extends Model
|
||||
{
|
||||
protected static string $table = 'user_password_resetters';
|
||||
|
||||
protected static array $fields = ['user_id', 'token', 'expires'];
|
||||
|
||||
protected static array $relations = ['user' => User::class];
|
||||
|
||||
private ?User $user = null;
|
||||
|
||||
private ?int $userId = null;
|
||||
|
||||
private string $token = '';
|
||||
|
||||
private DateTime $expires;
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function setToken(string $token): void
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function setExpiresDate(DateTime $expires): void
|
||||
{
|
||||
$this->expires = $expires;
|
||||
}
|
||||
|
||||
public function setExpires(string $expires): void
|
||||
{
|
||||
$this->expires = new DateTime($expires);
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getToken(): string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getExpiresDate(): DateTime
|
||||
{
|
||||
return $this->expires;
|
||||
}
|
||||
|
||||
public function getExpires(): string
|
||||
{
|
||||
return $this->expires->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php namespace MapGuesser\PersistentData;
|
||||
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
@@ -7,32 +8,10 @@ use MapGuesser\PersistentData\Model\Model;
|
||||
|
||||
class PersistentDataManager
|
||||
{
|
||||
public function selectFromDb(Select $select, string $type, bool $withRelations = false): ?Model
|
||||
public function selectFromDb(Select $select, string $type, bool $withRelations = false)
|
||||
{
|
||||
$table = call_user_func([$type, 'getTable']);
|
||||
$fields = call_user_func([$type, 'getFields']);
|
||||
$select = $this->createSelect($select, $type, $withRelations);
|
||||
|
||||
$select->from($table);
|
||||
|
||||
//TODO: only with some relations?
|
||||
if ($withRelations) {
|
||||
$relations = call_user_func([$type, 'getRelations']);
|
||||
|
||||
$columns = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$columns[] = [$table, $field];
|
||||
}
|
||||
|
||||
$columns = array_merge($columns, $this->getRelationColumns($relations));
|
||||
|
||||
$this->leftJoinRelations($select, $table, $relations);
|
||||
$select->columns($columns);
|
||||
} else {
|
||||
$select->columns($fields);
|
||||
}
|
||||
|
||||
//TODO: return with array?
|
||||
$data = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
if ($data === null) {
|
||||
@@ -45,7 +24,20 @@ class PersistentDataManager
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function selectFromDbById($id, string $type, bool $withRelations = false): ?Model
|
||||
public function selectMultipleFromDb(Select $select, string $type, bool $withRelations = false): Generator
|
||||
{
|
||||
$select = $this->createSelect($select, $type, $withRelations);
|
||||
$result = $select->execute();
|
||||
|
||||
while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$model = new $type();
|
||||
$this->fillWithData($data, $model);
|
||||
|
||||
yield $model;
|
||||
}
|
||||
}
|
||||
|
||||
public function selectFromDbById($id, string $type, bool $withRelations = false)
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->whereId($id);
|
||||
@@ -136,6 +128,34 @@ class PersistentDataManager
|
||||
$model->resetSnapshot();
|
||||
}
|
||||
|
||||
private function createSelect(Select $select, string $type, bool $withRelations = false): Select
|
||||
{
|
||||
$table = call_user_func([$type, 'getTable']);
|
||||
$fields = call_user_func([$type, 'getFields']);
|
||||
|
||||
$select->from($table);
|
||||
|
||||
//TODO: only with some relations?
|
||||
if ($withRelations) {
|
||||
$relations = call_user_func([$type, 'getRelations']);
|
||||
|
||||
$columns = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$columns[] = [$table, $field];
|
||||
}
|
||||
|
||||
$columns = array_merge($columns, $this->getRelationColumns($relations));
|
||||
|
||||
$this->leftJoinRelations($select, $table, $relations);
|
||||
$select->columns($columns);
|
||||
} else {
|
||||
$select->columns($fields);
|
||||
}
|
||||
|
||||
return $select;
|
||||
}
|
||||
|
||||
private function getRelationColumns(array $relations): array
|
||||
{
|
||||
$columns = [];
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use DateTime;
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\MultiRoom;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
class MultiRoomRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getById(int $id): ?MultiRoom
|
||||
{
|
||||
return $this->pdm->selectFromDbById($id, MultiRoom::class);
|
||||
}
|
||||
|
||||
public function getByRoomId(string $roomId): ?MultiRoom
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('room_id', '=', $roomId);
|
||||
|
||||
return $this->pdm->selectFromDb($select, MultiRoom::class);
|
||||
}
|
||||
|
||||
public function getAllExpired(): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('updated', '<', (new DateTime('-7 day'))->format('Y-m-d H:i:s'));
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, MultiRoom::class);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\Map;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
@@ -18,30 +20,58 @@ class PlaceRepository
|
||||
return $this->pdm->selectFromDbById($placeId, Place::class);
|
||||
}
|
||||
|
||||
public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place
|
||||
public function getAllForMap(Map $map): Generator
|
||||
{
|
||||
$placesWithoutPano = [];
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('map_id', '=', $map->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Place::class);
|
||||
}
|
||||
|
||||
//TODO: use Map instead of id
|
||||
public function getRandomNForMapWithValidPano(int $mapId, int $n, array $exclude = []): array
|
||||
{
|
||||
$places = [];
|
||||
|
||||
for ($i = 1; $i <= $n; ++$i) {
|
||||
$place = $this->getRandomForMapWithValidPano($mapId, $exclude);
|
||||
|
||||
$places[] = $place;
|
||||
$exclude[] = $place->getId();
|
||||
}
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
//TODO: use Map instead of id
|
||||
public function getRandomForMapWithValidPano(int $mapId, array $exclude = []): Place
|
||||
{
|
||||
do {
|
||||
$place = $this->selectRandomFromDbForMap($mapId, $exclude);
|
||||
|
||||
$panoId = $place->getFreshPanoId();
|
||||
|
||||
if ($panoId === null) {
|
||||
$placesWithoutPano[] = $exclude[] = $place->getId();
|
||||
$exclude[] = $place->getId();
|
||||
}
|
||||
} while ($panoId === null);
|
||||
|
||||
return $place;
|
||||
}
|
||||
|
||||
private function selectRandomFromDbForMap(int $mapId, array $exclude): ?Place
|
||||
private function selectRandomFromDbForMap(int $mapId, array $exclude): Place
|
||||
{
|
||||
//TODO: omit table name here
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->where('id', 'NOT IN', $exclude);
|
||||
$select->where('map_id', '=', $mapId);
|
||||
|
||||
$numberOfPlaces = $select->count(); // TODO: what if 0
|
||||
$numberOfPlaces = $select->count();
|
||||
|
||||
//TODO: prevent this
|
||||
if ($numberOfPlaces === 0) {
|
||||
throw new \Exception('There is no selectable place (count was 0).');
|
||||
}
|
||||
|
||||
$randomOffset = random_int(0, $numberOfPlaces - 1);
|
||||
|
||||
$select->orderBy('id');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\UserConfirmation;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
@@ -25,4 +26,12 @@ class UserConfirmationRepository
|
||||
|
||||
return $this->pdm->selectFromDb($select, UserConfirmation::class);
|
||||
}
|
||||
|
||||
public function getByUser(User $user): ?UserConfirmation
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
return $this->pdm->selectFromDb($select, UserConfirmation::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use DateTime;
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\UserPasswordResetter;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
class UserPasswordResetterRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getById(int $userConfirmationId): ?UserPasswordResetter
|
||||
{
|
||||
return $this->pdm->selectFromDbById($userConfirmationId, UserPasswordResetter::class);
|
||||
}
|
||||
|
||||
public function getByToken(string $token): ?UserPasswordResetter
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('token', '=', $token);
|
||||
|
||||
return $this->pdm->selectFromDb($select, UserPasswordResetter::class);
|
||||
}
|
||||
|
||||
public function getByUser(User $user): ?UserPasswordResetter
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
return $this->pdm->selectFromDb($select, UserPasswordResetter::class);
|
||||
}
|
||||
|
||||
public function getAllExpired(): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, UserPasswordResetter::class);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use DateTime;
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
@@ -33,4 +35,13 @@ class UserRepository
|
||||
|
||||
return $this->pdm->selectFromDb($select, User::class);
|
||||
}
|
||||
|
||||
public function getAllInactiveExpired(): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('active', '=', false);
|
||||
$select->where('created', '<', (new DateTime('-1 day'))->format('Y-m-d H:i:s'));
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, User::class);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class Request implements IRequest
|
||||
}
|
||||
}
|
||||
|
||||
public function setParsedRouteParams(array &$routeParams)
|
||||
public function setParsedRouteParams(array &$routeParams): void
|
||||
{
|
||||
$this->routeParams = &$routeParams;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ class Session implements ISession
|
||||
$this->data = &$data;
|
||||
}
|
||||
|
||||
public function has($key): bool
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
public function get($key)
|
||||
public function get(string $key)
|
||||
{
|
||||
if (isset($this->data[$key])) {
|
||||
return $this->data[$key];
|
||||
@@ -25,12 +25,12 @@ class Session implements ISession
|
||||
return null;
|
||||
}
|
||||
|
||||
public function set($key, $value): void
|
||||
public function set(string $key, $value): void
|
||||
{
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
public function delete($key): void
|
||||
public function delete(string $key): void
|
||||
{
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,17 @@ abstract class ContentBase implements IContent
|
||||
{
|
||||
protected array $data;
|
||||
|
||||
public function &getData(): array
|
||||
public function setData(array $data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
abstract public function &render(): string;
|
||||
abstract public function render(): void;
|
||||
|
||||
abstract public function getContentType(): string;
|
||||
}
|
||||
@@ -1,25 +1,30 @@
|
||||
<?php namespace MapGuesser\Response;
|
||||
|
||||
use MapGuesser\View\Linker;
|
||||
|
||||
class HtmlContent extends ContentBase
|
||||
{
|
||||
private string $template;
|
||||
private string $view;
|
||||
|
||||
public function __construct(string $template, array &$data = [])
|
||||
public function __construct(string $view, array $data = [])
|
||||
{
|
||||
$this->template = $template;
|
||||
$this->data = &$data;
|
||||
$this->view = $view;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function &render(): string
|
||||
public function render(): void
|
||||
{
|
||||
if (!empty($_ENV['DEV'])) {
|
||||
$generator = new Linker($this->view);
|
||||
$generator->generate();
|
||||
}
|
||||
|
||||
extract($this->data);
|
||||
|
||||
ob_start();
|
||||
require ROOT . '/views/' . $this->template . '.php';
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
require ROOT . '/cache/views/' . $this->view . '.php';
|
||||
|
||||
return $content;
|
||||
// @phpstan-ignore-next-line - SCRIPT_STARTED is defined in main.php
|
||||
echo '<!-- __debug__runtime: ' . round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1) . ' -->';
|
||||
}
|
||||
|
||||
public function getContentType(): string
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
class JsonContent extends ContentBase
|
||||
{
|
||||
public function __construct(array &$data = [])
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
$this->data = &$data;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function &render(): string
|
||||
public function render(): void
|
||||
{
|
||||
$content = json_encode($this->data);
|
||||
// @phpstan-ignore-next-line - SCRIPT_STARTED is defined in main.php
|
||||
$this->data['__debug__runtime'] = round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1);
|
||||
|
||||
return $content;
|
||||
echo json_encode($this->data);
|
||||
}
|
||||
|
||||
public function getContentType(): string
|
||||
|
||||
@@ -16,7 +16,7 @@ class Redirect implements IRedirect
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
if (preg_match('/^http(s)?/', $this->target)) {
|
||||
if (preg_match('/^http(s)?/', $this->target) === 1) {
|
||||
$link = $this->target;
|
||||
} else {
|
||||
$link = \Container::$request->getBase() . '/' . $this->target;
|
||||
|
||||
@@ -30,7 +30,7 @@ class Route
|
||||
$link = [];
|
||||
|
||||
foreach ($this->pattern as $fragment) {
|
||||
if (preg_match('/^{(\\w+)(\\?)?}$/', $fragment, $matches)) {
|
||||
if (preg_match('/^{(\\w+)(\\?)?}$/', $fragment, $matches) === 1) {
|
||||
if (isset($parameters[$matches[1]])) {
|
||||
$link[] = $parameters[$matches[1]];
|
||||
unset($parameters[$matches[1]]);
|
||||
@@ -61,7 +61,7 @@ class Route
|
||||
$parameters = [];
|
||||
|
||||
foreach ($path as $i => $fragment) {
|
||||
if (preg_match('/^{(\\w+)(?:\\?)?}$/', $this->pattern[$i], $matches)) {
|
||||
if (preg_match('/^{(\\w+)(?:\\?)?}$/', $this->pattern[$i], $matches) === 1) {
|
||||
$parameters[$matches[1]] = $fragment;
|
||||
} elseif ($fragment != $this->pattern[$i]) {
|
||||
return null;
|
||||
|
||||
@@ -76,7 +76,7 @@ class RouteCollection
|
||||
|
||||
$this->searchTable[$method][$groupNumber][] = $route;
|
||||
|
||||
while (preg_match('/^{\\w+\\?}$/', end($pattern))) {
|
||||
while (preg_match('/^{\\w+\\?}$/', end($pattern)) === 1) {
|
||||
$groupNumber--;
|
||||
array_pop($pattern);
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ use DateTime;
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use SessionHandlerInterface;
|
||||
use SessionIdInterface;
|
||||
use SessionUpdateTimestampHandlerInterface;
|
||||
use MapGuesser\Interfaces\Session\ISessionHandler;
|
||||
|
||||
class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
|
||||
class DatabaseSessionHandler implements ISessionHandler
|
||||
{
|
||||
private bool $exists = false;
|
||||
|
||||
@@ -28,7 +26,7 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'sessions');
|
||||
$select->columns(['data']);
|
||||
$select->whereId($id);
|
||||
$select->whereId(substr($id, 0, 32));
|
||||
|
||||
$result = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
@@ -46,16 +44,16 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
|
||||
$modify = new Modify(\Container::$dbConnection, 'sessions');
|
||||
|
||||
if ($this->exists) {
|
||||
$modify->setId($id);
|
||||
$modify->setId(substr($id, 0, 32));
|
||||
} else {
|
||||
$modify->setExternalId($id);
|
||||
$modify->setExternalId(substr($id, 0, 32));
|
||||
}
|
||||
|
||||
$modify->set('data', $data);
|
||||
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
|
||||
$modify->save();
|
||||
|
||||
$written = true;
|
||||
$this->written = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -63,37 +61,30 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
|
||||
public function destroy($id): bool
|
||||
{
|
||||
$modify = new Modify(\Container::$dbConnection, 'sessions');
|
||||
$modify->setId($id);
|
||||
$modify->setId(substr($id, 0, 32));
|
||||
$modify->delete();
|
||||
|
||||
$this->exists = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function gc($maxlifetime): int
|
||||
public function gc($maxlifetime): bool
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'sessions');
|
||||
$select->columns(['id']);
|
||||
$select->where('updated', '<', (new DateTime('-' . $maxlifetime . ' seconds'))->format('Y-m-d H:i:s'));
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$modify = new Modify(\Container::$dbConnection, 'sessions');
|
||||
$modify->setId($session['id']);
|
||||
$modify->delete();
|
||||
}
|
||||
// empty on purpose
|
||||
// old sessions are deleted by MaintainDatabaseCommand
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function create_sid(): string
|
||||
{
|
||||
return hash('sha256', random_bytes(10) . microtime());
|
||||
return bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
public function validateId($id): bool
|
||||
{
|
||||
return preg_match('/^[a-f0-9]{64}$/', $id);
|
||||
return preg_match('/^[a-f0-9]{32}$/', $id) === 1;
|
||||
}
|
||||
|
||||
public function updateTimestamp($id, $data): bool
|
||||
@@ -104,7 +95,7 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
|
||||
|
||||
$modify = new Modify(\Container::$dbConnection, 'sessions');
|
||||
|
||||
$modify->setId($id);
|
||||
$modify->setId(substr($id, 0, 32));
|
||||
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
|
||||
$modify->save();
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class Bounds
|
||||
|
||||
public static function createDirectly(float $southLat, float $westLng, float $northLat, float $eastLng): Bounds
|
||||
{
|
||||
$instance = new static();
|
||||
$instance = new self();
|
||||
|
||||
$instance->southLat = $southLat;
|
||||
$instance->westLng = $westLng;
|
||||
|
||||
@@ -11,7 +11,7 @@ class JwtParser
|
||||
}
|
||||
}
|
||||
|
||||
public function setToken(string $token)
|
||||
public function setToken(string $token): void
|
||||
{
|
||||
$this->token = explode('.', str_replace(['_', '-'], ['/', '+'], $token));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php namespace MapGuesser\Util\Panorama;
|
||||
|
||||
class Pov
|
||||
{
|
||||
private float $heading;
|
||||
|
||||
private float $pitch;
|
||||
|
||||
private float $zoom;
|
||||
|
||||
public function __construct(float $heading, float $pitch, float $zoom)
|
||||
{
|
||||
$this->heading = $heading;
|
||||
$this->pitch = $pitch;
|
||||
$this->zoom = $zoom;
|
||||
}
|
||||
|
||||
public function setHeading(float $heading): void
|
||||
{
|
||||
$this->heading = $heading;
|
||||
}
|
||||
|
||||
public function setPitch(float $pitch): void
|
||||
{
|
||||
$this->pitch = $pitch;
|
||||
}
|
||||
|
||||
public function setZoom(float $zoom): void
|
||||
{
|
||||
$this->zoom = $zoom;
|
||||
}
|
||||
|
||||
public function getHeading(): float
|
||||
{
|
||||
return $this->heading;
|
||||
}
|
||||
|
||||
public function getPitch(): float
|
||||
{
|
||||
return $this->pitch;
|
||||
}
|
||||
|
||||
public function getZoom(): float
|
||||
{
|
||||
return $this->zoom;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'heading' => $this->heading,
|
||||
'pitch' => $this->pitch,
|
||||
'zoom' => $this->zoom
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php namespace MapGuesser\View;
|
||||
|
||||
class Linker
|
||||
{
|
||||
const INLINE_ASSET_LIMIT = 2000;
|
||||
|
||||
private string $view;
|
||||
|
||||
public function __construct(string $view)
|
||||
{
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
public function generate(): void
|
||||
{
|
||||
$input = ROOT . '/views/' . $this->view . '.php';
|
||||
|
||||
$temporaryFiles = [];
|
||||
$sections = ['externalCss' => '', 'inlineCss' => '', 'externalJs' => '', 'inlineJs' => ''];
|
||||
$extra = ['', ''];
|
||||
|
||||
do {
|
||||
$parser = new Parser($input);
|
||||
$fragment = $parser->parse();
|
||||
|
||||
$extends = $fragment->getExtends();
|
||||
|
||||
$this->generateAssets($fragment, $sections);
|
||||
|
||||
$sections = array_merge($sections, $fragment->getSections()); //TODO: detect if section defined multiple times
|
||||
$extra[0] = $fragment->getExtra()[0] . $extra[0];
|
||||
$extra[1] = $extra[1] . $fragment->getExtra()[1];
|
||||
|
||||
if ($extends === null) {
|
||||
$this->writeFinal($extra, $input, ROOT . '/cache/views/' . $this->view . '.php');
|
||||
break;
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'mapg-view-');
|
||||
$temporaryFiles[] = $tmpFile;
|
||||
|
||||
$this->extendTemplate($sections, ROOT . '/views/' . $extends . '.php', $tmpFile);
|
||||
|
||||
$input = $tmpFile;
|
||||
} while (true);
|
||||
|
||||
foreach ($temporaryFiles as $tmpFile) {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
private function extendTemplate(array $sections, string $file, string $output): void
|
||||
{
|
||||
$inputFileHandle = fopen($file, 'r');
|
||||
if (!$inputFileHandle) {
|
||||
throw new \Exception('Cannot open file ' . $file);
|
||||
}
|
||||
|
||||
$outputFileHandle = fopen($output, 'w');
|
||||
if (!$outputFileHandle) {
|
||||
throw new \Exception('Cannot open file ' . $output . 'for writing.');
|
||||
}
|
||||
|
||||
$lineNumber = 0;
|
||||
while (($line = fgets($inputFileHandle)) !== false) {
|
||||
++$lineNumber;
|
||||
|
||||
if (preg_match('/^\s*@yields\(\'([\w\/]+)\'\)\s*$/', $line, $matches) === 1) {
|
||||
if (isset($sections[$matches[1]])) {
|
||||
fwrite($outputFileHandle, $sections[$matches[1]]);
|
||||
}
|
||||
} else {
|
||||
fwrite($outputFileHandle, $line);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($inputFileHandle);
|
||||
fclose($outputFileHandle);
|
||||
}
|
||||
|
||||
private function writeFinal(array $extra, string $file, string $output): void
|
||||
{
|
||||
$dirname = pathinfo($output, PATHINFO_DIRNAME);
|
||||
if (!is_dir($dirname)) {
|
||||
mkdir($dirname, 0755, true);
|
||||
}
|
||||
|
||||
$inputFileHandle = fopen($file, 'r');
|
||||
if (!$inputFileHandle) {
|
||||
throw new \Exception('Cannot open file ' . $file);
|
||||
}
|
||||
|
||||
$outputFileHandle = fopen($output, 'w');
|
||||
if (!$outputFileHandle) {
|
||||
throw new \Exception('Cannot open file ' . $output . 'for writing.');
|
||||
}
|
||||
|
||||
fwrite($outputFileHandle, $extra[0]);
|
||||
while (($line = fgets($inputFileHandle)) !== false) {
|
||||
fwrite($outputFileHandle, $line);
|
||||
}
|
||||
fwrite($outputFileHandle, $extra[1]);
|
||||
|
||||
fclose($inputFileHandle);
|
||||
fclose($outputFileHandle);
|
||||
}
|
||||
|
||||
private function generateAssets(ParsedFragment $fragment, array &$sections): void
|
||||
{
|
||||
foreach ($fragment->getCss() as $cssFile) {
|
||||
$asset = $this->parseAsset($cssFile);
|
||||
if (isset($asset['code'])) {
|
||||
$sections['inlineCss'] .= '<style>' . PHP_EOL;
|
||||
$sections['inlineCss'] .= $asset['code'];
|
||||
$sections['inlineCss'] .= '</style>' . PHP_EOL;
|
||||
} elseif (isset($asset['file'])) {
|
||||
$sections['externalCss'] .= '<link href="' . $asset['file'] . '" rel="stylesheet">' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($fragment->getJs() as $jsFile) {
|
||||
$asset = $this->parseAsset($jsFile);
|
||||
if (isset($asset['code'])) {
|
||||
$sections['inlineJs'] .= '<script>' . PHP_EOL;
|
||||
$sections['inlineJs'] .= $asset['code'];
|
||||
$sections['inlineJs'] .= '</script>' . PHP_EOL;
|
||||
} elseif (isset($asset['file'])) {
|
||||
$sections['externalJs'] .= '<script src="' . $asset['file'] . '"></script>' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseAsset(string $asset): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
if (preg_match('/^[\w\/\.]+$/', $asset) === 1) {
|
||||
if (
|
||||
empty($_ENV['DEV']) &&
|
||||
filesize(ROOT . '/public/static/' . $asset) < self::INLINE_ASSET_LIMIT
|
||||
) {
|
||||
$output['code'] = file_get_contents(ROOT . '/public/static/' . $asset);
|
||||
} else {
|
||||
$output['file'] = '<?= $_ENV[\'STATIC_ROOT\'] ?>/' . $asset . '?rev=<?= REVISION ?>';
|
||||
}
|
||||
} else {
|
||||
$output['file'] = $asset;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php namespace MapGuesser\View;
|
||||
|
||||
class ParsedFragment
|
||||
{
|
||||
private ?string $extends;
|
||||
|
||||
private array $css;
|
||||
|
||||
private array $js;
|
||||
|
||||
private array $sections;
|
||||
|
||||
private array $extra;
|
||||
|
||||
public function __construct(?string $extends, array $css, array $js, array $sections, array $extra)
|
||||
{
|
||||
$this->extends = $extends;
|
||||
$this->css = $css;
|
||||
$this->js = $js;
|
||||
$this->sections = $sections;
|
||||
$this->extra = $extra;
|
||||
}
|
||||
|
||||
public function getExtends(): ?string
|
||||
{
|
||||
return $this->extends;
|
||||
}
|
||||
|
||||
public function getCss(): array
|
||||
{
|
||||
return $this->css;
|
||||
}
|
||||
|
||||
public function getJs(): array
|
||||
{
|
||||
return $this->js;
|
||||
}
|
||||
|
||||
public function getSections(): array
|
||||
{
|
||||
return $this->sections;
|
||||
}
|
||||
|
||||
public function getExtra(): array
|
||||
{
|
||||
return $this->extra;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php namespace MapGuesser\View;
|
||||
|
||||
class Parser
|
||||
{
|
||||
private string $file;
|
||||
|
||||
public function __construct(string $file)
|
||||
{
|
||||
$this->file = $file;
|
||||
}
|
||||
|
||||
public function parse(): ParsedFragment
|
||||
{
|
||||
$sectionOpen = null;
|
||||
$extraOpen = false;
|
||||
|
||||
$extends = null;
|
||||
$js = [];
|
||||
$css = [];
|
||||
$sections = [];
|
||||
$extra = ['', ''];
|
||||
|
||||
$fileHandle = fopen($this->file, 'r');
|
||||
if (!$fileHandle) {
|
||||
throw new \Exception('Cannot open file ' . $this->file);
|
||||
}
|
||||
|
||||
$lineNumber = 0;
|
||||
while (($line = fgets($fileHandle)) !== false) {
|
||||
++$lineNumber;
|
||||
|
||||
if (($cssMatched = $this->matchCss($line)) !== null) {
|
||||
$css[] = $cssMatched;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($jsMatched = $this->matchJs($line)) !== null) {
|
||||
$js[] = $jsMatched;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($extendsMatched = $this->matchExtends($line)) !== null) {
|
||||
if ($extends !== null) {
|
||||
throw new \Exception('Error in file ' . $this->file . ' in line ' . $lineNumber . ' - There is already an \'@extends\' declared.');
|
||||
}
|
||||
|
||||
$extends = $extendsMatched;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($sectionMatched = $this->matchSection($line)) !== null) {
|
||||
if ($extends === null) {
|
||||
throw new \Exception('Error in file ' . $this->file . ' in line ' . $lineNumber . ' - \'@section\' has no meaning if view extends nothing.');
|
||||
}
|
||||
if ($sectionOpen !== null) {
|
||||
throw new \Exception('Parse error in file ' . $this->file . ' in line ' . $lineNumber . ' - A \'@section\' is already open (no \'@endsection\' found).');
|
||||
}
|
||||
|
||||
$sectionOpen = $sectionMatched;
|
||||
$sections[$sectionOpen] = '';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->matchEndSection($line)) {
|
||||
if ($sectionOpen === null) {
|
||||
throw new \Exception('Parse error in file ' . $this->file . ' in line ' . $lineNumber . ' - Cannot end section until no \'@section\' is open.');
|
||||
}
|
||||
|
||||
$sectionOpen = null;
|
||||
}
|
||||
|
||||
if ($this->matchExtra($line)) {
|
||||
if ($extraOpen) {
|
||||
throw new \Exception('Parse error in file ' . $this->file . ' in line ' . $lineNumber . ' - An \'@extra\' is already open (no \'@endextra\' found).');
|
||||
}
|
||||
|
||||
$extraOpen = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->matchEndExtra($line)) {
|
||||
if (!$extraOpen) {
|
||||
throw new \Exception('Parse error in file ' . $this->file . ' in line ' . $lineNumber . ' - Cannot end extra until no \'@extra\' is open.');
|
||||
}
|
||||
|
||||
$extraOpen = false;
|
||||
}
|
||||
|
||||
if ($sectionOpen !== null) {
|
||||
$sections[$sectionOpen] .= $line;
|
||||
}
|
||||
|
||||
if ($extraOpen) {
|
||||
$offset = $extends === null ? 0 : 1;
|
||||
$extra[$offset] .= $line;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($fileHandle);
|
||||
|
||||
return new ParsedFragment($extends, $css, $js, $sections, $extra);
|
||||
}
|
||||
|
||||
private function matchCss(string $line): ?string
|
||||
{
|
||||
if (preg_match('/^\s*@css\((.*)\)\s*$/', $line, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function matchJs(string $line): ?string
|
||||
{
|
||||
if (preg_match('/^\s*@js\((.*)\)\s*$/', $line, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function matchExtends(string $line): ?string
|
||||
{
|
||||
if (preg_match('/^\s*@extends\(([\w\/]+)\)\s*$/', $line, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function matchSection(string $line): ?string
|
||||
{
|
||||
if (preg_match('/^\s*@section\((\w+)\)\s*$/', $line, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function matchEndSection(string $line): bool
|
||||
{
|
||||
return preg_match('/^\s*@endsection(?:\(\))?\s*$/', $line) === 1;
|
||||
}
|
||||
|
||||
private function matchExtra(string $line): bool
|
||||
{
|
||||
return preg_match('/^\s*@extra(?:\(\))?\s*$/', $line) === 1;
|
||||
}
|
||||
|
||||
private function matchEndExtra(string $line): bool
|
||||
{
|
||||
return preg_match('/^\s*@endextra(?:\(\))?\s*$/', $line) === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace MapGuesser\Tests\OAuth;
|
||||
|
||||
use MapGuesser\Interfaces\Http\IRequest;
|
||||
use MapGuesser\Interfaces\Http\IResponse;
|
||||
use MapGuesser\OAuth\GoogleOAuth;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class GoogleOAuthTest extends TestCase
|
||||
{
|
||||
public function testCanCreateDialogUrl(): void
|
||||
{
|
||||
$_ENV['GOOGLE_OAUTH_CLIENT_ID'] = 'xyz';
|
||||
$state = 'random_state_string';
|
||||
$nonce = 'random_nonce_string';
|
||||
$redirectUrl = 'http://example.com/oauth';
|
||||
|
||||
$requestMock = $this->getMockBuilder(IRequest::class)
|
||||
->setMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send'])
|
||||
->getMock();
|
||||
$googleOAuth = new GoogleOAuth($requestMock);
|
||||
|
||||
$dialogUrl = $googleOAuth->getDialogUrl($state, $redirectUrl, $nonce);
|
||||
$dialogUrlParsed = explode('?', $dialogUrl);
|
||||
|
||||
$this->assertEquals('https://accounts.google.com/o/oauth2/v2/auth', $dialogUrlParsed[0]);
|
||||
|
||||
parse_str($dialogUrlParsed[1], $dialogUrlQueryParams);
|
||||
|
||||
$expectedQueryParams = [
|
||||
'response_type' => 'code',
|
||||
'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'],
|
||||
'scope' => 'openid email',
|
||||
'redirect_uri' => $redirectUrl,
|
||||
'state' => $state,
|
||||
'nonce' => $nonce,
|
||||
];
|
||||
|
||||
$this->assertEquals($expectedQueryParams, $dialogUrlQueryParams);
|
||||
}
|
||||
|
||||
public function testCanRequestToken(): void
|
||||
{
|
||||
$_ENV['GOOGLE_OAUTH_CLIENT_ID'] = 'abc';
|
||||
$_ENV['GOOGLE_OAUTH_CLIENT_SECRET'] = 'xxx';
|
||||
$code = 'code_from_google';
|
||||
$redirectUrl = 'http://example.com/oauth';
|
||||
|
||||
$requestMock = $this->getMockBuilder(IRequest::class)
|
||||
->setMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send'])
|
||||
->getMock();
|
||||
$responseMock = $this->getMockBuilder(IResponse::class)
|
||||
->setMethods(['getBody', 'getHeaders'])
|
||||
->getMock();
|
||||
$googleOAuth = new GoogleOAuth($requestMock);
|
||||
|
||||
$expectedQueryParams = [
|
||||
'code' => $code,
|
||||
'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'],
|
||||
'client_secret' => $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'],
|
||||
'redirect_uri' => $redirectUrl,
|
||||
'grant_type' => 'authorization_code',
|
||||
];
|
||||
|
||||
$requestMock->expects($this->once())
|
||||
->method('setUrl')
|
||||
->with($this->equalTo('https://oauth2.googleapis.com/token'));
|
||||
$requestMock->expects($this->once())
|
||||
->method('setMethod')
|
||||
->with($this->equalTo(IRequest::HTTP_POST));
|
||||
$requestMock->expects($this->once())
|
||||
->method('setQuery')
|
||||
->with($this->equalTo($expectedQueryParams));
|
||||
$requestMock->expects($this->once())
|
||||
->method('send')
|
||||
->will($this->returnValue($responseMock));
|
||||
$responseMock->expects($this->once())
|
||||
->method('getBody')
|
||||
->will($this->returnValue('{"test":"json"}'));
|
||||
|
||||
$token = $googleOAuth->getToken($code, $redirectUrl);
|
||||
|
||||
$this->assertEquals(['test' => 'json'], $token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php namespace MapGuesser\Tests\Util;
|
||||
|
||||
use MapGuesser\PersistentData\Model\Model;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class OtherModel
|
||||
{
|
||||
}
|
||||
|
||||
class DummyModel extends Model
|
||||
{
|
||||
protected static string $table = 'test_table';
|
||||
|
||||
protected static array $fields = ['name', 'valid'];
|
||||
|
||||
protected static array $relations = ['other_model' => OtherModel::class];
|
||||
|
||||
private string $name;
|
||||
|
||||
private bool $valid;
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function setValid(bool $valid): void
|
||||
{
|
||||
$this->valid = $valid;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getValid(): bool
|
||||
{
|
||||
return $this->valid;
|
||||
}
|
||||
}
|
||||
|
||||
final class ModelTest extends TestCase
|
||||
{
|
||||
public function testCanReturnTable(): void
|
||||
{
|
||||
$this->assertEquals('test_table', DummyModel::getTable());
|
||||
}
|
||||
|
||||
public function testCanReturnFields(): void
|
||||
{
|
||||
$this->assertEquals(['id', 'name', 'valid'], DummyModel::getFields());
|
||||
}
|
||||
|
||||
public function testCanReturnRelations(): void
|
||||
{
|
||||
$this->assertEquals(['other_model' => OtherModel::class], DummyModel::getRelations());
|
||||
}
|
||||
|
||||
public function testCanBeConvertedToArray(): void
|
||||
{
|
||||
$model = new DummyModel();
|
||||
$model->setId(123);
|
||||
$model->setName('John');
|
||||
$model->setValid(true);
|
||||
|
||||
$this->assertEquals([
|
||||
'id' => 123,
|
||||
'name' => 'John',
|
||||
'valid' => true
|
||||
], $model->toArray());
|
||||
}
|
||||
|
||||
public function testCanSaveAndResetSnapshot(): void
|
||||
{
|
||||
$model = new DummyModel();
|
||||
$model->setId(123);
|
||||
$model->setName('John');
|
||||
$model->setValid(true);
|
||||
|
||||
$model->saveSnapshot();
|
||||
|
||||
$this->assertEquals([
|
||||
'id' => 123,
|
||||
'name' => 'John',
|
||||
'valid' => true
|
||||
], $model->getSnapshot());
|
||||
|
||||
$model->resetSnapshot();
|
||||
|
||||
$this->assertEquals([], $model->getSnapshot());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php namespace MapGuesser\Tests\Util;
|
||||
|
||||
use MapGuesser\Util\JwtParser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class JwtParserTest extends TestCase
|
||||
{
|
||||
private JwtParser $jwtParser;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->jwtParser = new JwtParser(
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
|
||||
);
|
||||
}
|
||||
|
||||
public function testSettingTokenIsTheSameAsCreatingWithToken(): void
|
||||
{
|
||||
$jwtParser2 = new JwtParser();
|
||||
$jwtParser2->setToken(
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
|
||||
);
|
||||
|
||||
$this->assertEquals($this->jwtParser, $jwtParser2);
|
||||
}
|
||||
|
||||
public function testCanParseTokenHeader(): void
|
||||
{
|
||||
$this->assertEquals([
|
||||
'alg' => 'HS256',
|
||||
'typ' => 'JWT'
|
||||
], $this->jwtParser->getHeader());
|
||||
}
|
||||
|
||||
public function testCanParseTokenPayload(): void
|
||||
{
|
||||
$this->assertEquals([
|
||||
'sub' => '1234567890',
|
||||
'name' => 'John Doe',
|
||||
'iat' => 1516239022
|
||||
], $this->jwtParser->getPayload());
|
||||
}
|
||||
|
||||
public function testCanParseTokenSignature(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
'49f94ac7044948c78a285d904f87f0a4c7897f7e8f3a4eb2255fda750b2cc397',
|
||||
bin2hex($this->jwtParser->getSignature())
|
||||
);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user