Compare commits

...

555 Commits

Author SHA1 Message Date
9b2ffa8b2c
Merge pull request 'do not recreate docker runner group and user' (!84) from do-no-recreate-group-and-user into develop
All checks were successful
mapguesser/pipeline/tag This commit looks good
mapguesser/pipeline/head This commit looks good
Reviewed-on: #84
2024-03-11 00:14:35 +01:00
9bafc52626
do not recreate docker runner group and user
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2024-03-10 23:52:14 +01:00
18ddaa1da4
Merge pull request 'feature/update-to-ubuntu-2204' (!83) from feature/update-to-ubuntu-2204 into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
mapguesser/pipeline/tag This commit looks good
Reviewed-on: #83
2023-09-27 22:21:33 +02:00
be2105a284
update soko-web
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-27 22:12:49 +02:00
5481cc67a0
generate composer.lock 2023-09-27 22:12:49 +02:00
a547fbb631
update phpunit 2023-09-27 22:12:49 +02:00
0e3f943f1e
remove not used dependency fzaninotto/faker 2023-09-27 22:12:49 +02:00
ec42479304
update ubuntu to 22.04 and php to 8.1 2023-09-27 22:12:49 +02:00
dbb7c1c0fc
Merge pull request 'update package-lock.json to new format' (!82) from feature/update-package-lock-to-new-format into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
mapguesser/pipeline/tag This commit looks good
Reviewed-on: #82
2023-09-27 00:32:53 +02:00
410bba4966
Merge pull request 'update soko-web' (!81) from feature/log-handler-errors into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #81
2023-09-27 00:31:51 +02:00
17aee22400
update package-lock.json to new format
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-27 00:31:23 +02:00
aaf220dce2
update soko-web
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-27 00:29:50 +02:00
276a289ca7
Merge pull request 'error reporting should always be E_ALL' (!80) from bugfix/fix-error-reporting into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #80
2023-09-27 00:26:38 +02:00
e684365612
error reporting should always be E_ALL
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-26 23:49:36 +02:00
105cc96963
Merge pull request 'build docker image for multi arch' (!78) from feature/multiarch-docker-image into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #78
2023-09-26 23:48:28 +02:00
bdd62aadf5
build docker image for multi arch
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-26 22:20:49 +02:00
390c13608a
Merge pull request 'set runner user of web service' (!79) from feature/set-runner-user-of-web-service into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #79
2023-09-26 22:07:40 +02:00
345cf31bb3
build and push docker image in one step
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-26 22:00:18 +02:00
c7f5ea0d85
set runner user of web service 2023-09-26 21:56:29 +02:00
fc6141e2b9
Merge pull request 'feature/add-git-link-to-footer' (!76) from feature/add-git-link-to-footer into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
mapguesser/pipeline/tag This commit looks good
Reviewed-on: #76
2023-09-26 01:01:30 +02:00
5049a01d2a
links should look good in footer
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-26 00:48:05 +02:00
45d0c9fa80
add link to git repo to footer 2023-09-26 00:48:05 +02:00
1e4b982430
Merge pull request 'feature/accept-email-and-username-in-forms' (!77) from feature/accept-email-and-username-in-forms into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #77
2023-09-26 00:44:56 +02:00
0a7d248a3e
check email/username syntax before sending it to db
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-26 00:43:14 +02:00
2177dfd893
fill email or username on failed-login-signup 2023-09-26 00:37:39 +02:00
173b50fa6c
accept email and username on login and passord reset request page 2023-09-26 00:36:39 +02:00
d7338b84d3
Merge pull request 'delete orphaned data migration' (!75) from bugfix/delete-orphan-migration into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #75
2023-09-26 00:16:19 +02:00
0216861579
delete orphaned data migration
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-26 00:11:09 +02:00
7be04f128a
Merge pull request 'use username for multi games' (!74) from feature/use-username-for-multi into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
mapguesser/pipeline/tag This commit looks good
Reviewed-on: #74
2023-09-26 00:05:55 +02:00
c4dd60e0de
always archive test results
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-26 00:04:18 +02:00
752ea12810
use username for multi games 2023-09-26 00:03:44 +02:00
751a86c823
Merge pull request 'bugfix/fix-build-badge' (!73) from bugfix/fix-build-badge into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #73
2023-09-25 23:52:37 +02:00
52873fc759
fix cli user creation usage
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-25 21:27:19 +02:00
0882a67019
fix build badge link 2023-09-25 21:27:19 +02:00
49069f4a52
Merge pull request 'username should be an argument of user:add' (!71) from bugfix/fix-user-creation-cli into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #71
2023-09-25 21:21:54 +02:00
4bba7599e1
Merge pull request 'bugfix/username-validation-fixes' (!72) from bugfix/username-validation-fixes into develop
Some checks are pending
mapguesser/pipeline/head Build queued...
Reviewed-on: #72
2023-09-25 21:21:48 +02:00
7fb75c9f25
reset grecaptcha in case of error
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-25 21:19:32 +02:00
5d367d5b35
check if username is used during signup 2023-09-25 21:08:34 +02:00
a2d6376e81
check if username is empty in usercontroller 2023-09-25 20:55:21 +02:00
f3c3aa69eb
username should be an argument of user:add
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-25 20:44:34 +02:00
467399c81b
Merge pull request 'feature/username-for-users' (!70) from feature/username-for-users into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #70
2023-09-25 20:04:49 +02:00
84e848506f
user display name is the username
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-25 19:56:57 +02:00
e18ed3a034
add google connect and disconnect to account 2023-09-25 19:56:57 +02:00
ea8b46ab91
fix observeInput function 2023-09-25 19:56:57 +02:00
a1b0f5e9fb
identify user by username as well 2023-09-25 19:56:57 +02:00
b1ed28f4b5
merge signup and google signup handling with username support 2023-09-25 19:56:57 +02:00
36f4b6b4d0
make it possible to change email and username 2023-09-25 19:56:57 +02:00
2c706cc7f3
add usernamegenerator 2023-09-25 19:56:57 +02:00
77c6e6c4e6
add new getters to userrepository 2023-09-25 19:56:36 +02:00
c25ba2dd28
add username migration 2023-09-25 19:56:36 +02:00
5b045335a1
add username to user model 2023-09-24 00:50:04 +02:00
703966f8f7
Merge pull request 'process scheme from reverse proxy correctly' (!69) from bugfix/process-scheme-correctly into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
mapguesser/pipeline/tag This commit looks good
Reviewed-on: #69
2023-09-17 11:02:57 +02:00
76570a76c5
process scheme from reverse proxy correctly
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-17 01:53:29 +02:00
383bb9d4fb
Merge pull request 'feature/create-working-docker-images' (!68) from feature/create-working-docker-images into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
mapguesser/pipeline/tag This commit looks good
Reviewed-on: #68
2023-09-17 00:10:54 +02:00
716494f614
update readme
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-17 00:04:16 +02:00
7c61491653
update soko-web 2023-09-17 00:04:16 +02:00
7a1da55ceb
update docker-compose 2023-09-17 00:04:16 +02:00
cb5e90c0f7
add docker release stage to pipeline 2023-09-17 00:04:16 +02:00
32e9fc2cb0
use the new dockerfile in pipeline 2023-09-17 00:04:13 +02:00
c180d7a012
delete deprecated dockerfiles 2023-09-17 00:04:10 +02:00
fea4211930
add new dockerfile with dev and release stages 2023-09-17 00:04:10 +02:00
4f6e18f83a
add entry point for dev docker 2023-09-17 00:04:06 +02:00
57a24f8c90
add entry point for release docker 2023-09-17 00:03:20 +02:00
f5950658df
install base database in MigrateDatabaseCommand 2023-09-17 00:03:20 +02:00
cd17f44de0
merge migration of migrations to base db 2023-09-17 00:03:20 +02:00
a11a879a4f
remove deprecated scripts 2023-09-17 00:03:20 +02:00
0048fb9b1d
add cron for db:maintain 2023-09-17 00:03:20 +02:00
cede0e7985
add release generator script 2023-09-16 23:07:51 +02:00
80501b7f0a
add nodejs installer script 2023-09-16 15:05:34 +02:00
a255180a4d
Merge pull request 'fix unambiguous where and orderBy statements in PlaceRepository' (!67) from fix-selects-in-placerepository into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #67
2023-09-15 01:40:16 +02:00
af09da8782
fix unambiguous where and orderBy statements in PlaceRepository
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-09-15 01:38:26 +02:00
ddfea0530d
Merge pull request 'fix syntax for password input in login form' (!66) from bugfix/fix-login-password-layout into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #66
2023-05-16 18:08:37 +02:00
9d7d19899d
fix syntax for password input in login form
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-05-16 18:06:30 +02:00
10f032a9a5
Merge pull request 'bugfix/fix-ambiguous-sql-field-names' (!65) from bugfix/fix-ambiguous-sql-field-names into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #65
2023-05-02 18:12:04 +02:00
c02e35580f
use sql field names with tables where join happens
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-05-02 18:10:22 +02:00
afd58bb91b
update soko-web to 0.10.1 2023-05-02 18:08:53 +02:00
fc6792ccfc
Merge pull request 'add autocomplete values for username and password fields' (!64) from feature/add-autocompletes-for-usernames-and-passwords into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #64
2023-05-02 14:14:55 +02:00
a5ba8e71e4
add autocomplete values for username and password fields
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-05-02 14:10:10 +02:00
184f63585c
Merge pull request 'feature/session-handler-changes' (!63) from feature/session-handler-changes into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #63
2023-05-02 13:20:07 +02:00
d666593fde
session should be valid for a session
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-05-02 13:18:37 +02:00
75fad05362
adapt to new soko-web interfaces 2023-05-02 13:18:14 +02:00
45b34cb514
update to soko-web 0.10 2023-05-02 13:18:03 +02:00
a6ba53764b
Merge pull request 'feature/update-soko-web-to-0.7' (!62) from feature/update-soko-web-to-0.7 into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #62
2023-04-30 21:07:08 +02:00
e70f8a9965
adapt $withRelations usage to soko-web 0.7
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-04-30 21:01:15 +02:00
4aadcab2d2
update soko-web to 0.7 2023-04-30 20:29:43 +02:00
9aea195f85
Merge pull request 'feature/update-soko-web' (!61) from feature/update-soko-web into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #61
2023-04-20 00:41:25 +02:00
eafcf2315e
use IRouteCollection in app container
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-04-20 00:39:10 +02:00
e52300b9e5
adapt Container usage to new soko-web 2023-04-20 00:39:10 +02:00
ee4a9f2ce3
remove unnecessary startTransaction and commit calls 2023-04-20 00:39:10 +02:00
8141845ae9
add error view 500 to app config 2023-04-20 00:39:10 +02:00
69f54c861a
add view for error 500 2023-04-20 00:39:10 +02:00
c9ab7c31b7
pass dbConnection to HttpResponse 2023-04-20 00:30:01 +02:00
82279f11d3
update soko-web to 0.6.1 2023-04-20 00:30:01 +02:00
6cb4e06acc
Merge pull request 'remove unnecessary use in web.php' (!60) from bugfix/remove-unnecessary-use into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #60
2023-04-16 22:02:22 +02:00
ee33fc1913
remove unnecessary use in web.php
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-04-16 22:00:25 +02:00
4fb1831763
Merge pull request 'feature/adapt-to-new-soko-web' (!59) from feature/adapt-to-new-soko-web into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #59
2023-04-16 21:46:07 +02:00
9dee8ba988
use classes at beginning of web.php
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-04-16 21:44:11 +02:00
82562117b2
adapt code to soko-web 0.4 2023-04-16 21:38:25 +02:00
317a4de5c0
update soko-web to 0.4 2023-04-16 21:37:18 +02:00
bf6b5f39a5
Merge pull request 'feature/use-new-features-from-soko-web' (!58) from feature/use-new-features-from-soko-web into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #58
2023-04-16 17:49:41 +02:00
ed80fe544f
add adminer to docker-compose
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-04-16 17:48:12 +02:00
3e4c24bbc1
implement redirect after login 2023-04-16 17:48:12 +02:00
54a4ee4d60
use ISecured/IAuthenticationRequired correctly in containers 2023-04-16 17:14:27 +02:00
62e0261a0b
implement redirect to login for controllers for authentication is required 2023-04-16 17:09:46 +02:00
9d1bb41f02
adapt to soko-web 0.2 2023-04-16 17:01:16 +02:00
59527a1898
update soko-web to 0.3 2023-04-16 17:00:54 +02:00
525d740409
Merge pull request 'MAPG-243 delete assets for view testing (these are in soko-web now)' (!57) from feature/MAPG-243-delete-further-unused-files into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #57
2023-04-08 10:56:46 +02:00
0f7828e486
MAPG-243 delete assets for view testing (these are in soko-web now)
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-04-08 10:53:04 +02:00
4b02f00399
Merge pull request 'MAPG-243 use soko-web framework' (!56) from feature/MAPG-243-use-soko-web-framework into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: #56
2023-04-07 21:26:39 +02:00
f127ad3c22
MAPG-243 delete unused class members
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2023-04-07 21:12:27 +02:00
8bcb2c2486
MAPG-243 set COMPOSER_HOME to the workspace 2023-04-07 21:12:26 +02:00
b04df4411b
MAPG-243 change base image of Dockerfile-test to the same as Dockerfile-app 2023-04-07 21:12:09 +02:00
c565fc2b65
MAPG-243 replace and adapt to soko-web 2023-04-07 21:10:14 +02:00
0bfebec8ca
MAPG-243 install soko-web by composer 2023-04-07 21:10:14 +02:00
de78084ad5
MAPG-243 delete classes which come from soko-web now 2023-04-07 20:26:52 +02:00
d30ec3a3a0
Merge pull request 'feature/MAPG-242-add-captcha-for-signup-and-password-reset' (!54) from feature/MAPG-242-add-captcha-for-signup-and-password-reset into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/54
2022-05-26 18:47:46 +02:00
cc19d454fa
MAPG-242 add captcha validation for password reset
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2022-05-26 18:45:16 +02:00
241d2f2b30
MAPG-242 add captcha validation for signup 2022-05-26 18:41:26 +02:00
d0751017db
MAPG-242 add possibility to captcha validation 2022-05-26 18:39:33 +02:00
6014e4517a
Merge pull request 'MAPG-241 use class 'text' for text inputs' (!53) from bugfix/MAPG-241-fix-text-input-styles into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/53
2022-05-26 15:58:22 +02:00
81a8153866
MAPG-241 use class 'text' for text inputs
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2022-05-26 15:55:45 +02:00
e435ef6be0
Merge pull request 'feature/MAPG-231-make-it-possible-to-create-unlisted-maps' (!52) from feature/MAPG-231-make-it-possible-to-create-unlisted-maps into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/52
2022-05-26 14:34:48 +02:00
90528e61e5
MAPG-231 cleanup input styles
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
2022-05-26 14:23:29 +02:00
d02aa4abe0
MAPG-231 introduce unlisted maps 2022-05-26 14:23:08 +02:00
4f45e213c3
Merge pull request 'add badge for passing builds' (!51) from feature/add-badge-for-builds into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/51
2021-12-22 02:26:03 +01:00
d877d2fe03
add badge for passing builds
All checks were successful
mapguesser/pipeline/head This commit looks good
mapguesser/pipeline/pr-develop This commit looks good
2021-12-22 02:21:22 +01:00
66240c2d65
Merge pull request 'add Jenkinsfile and Dockerfile-test' (!50) from feature/add-jenkins-support into develop
All checks were successful
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/50
2021-12-22 02:14:44 +01:00
0654b65940
add Jenkinsfile and Dockerfile-test
All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
default-pipeline default-pipeline #18
2021-12-22 01:59:31 +01:00
272ec3568d
Merge pull request 'bugfix/MAPG-240-fix-static-code-analysis-errors' (!49) from bugfix/MAPG-240-fix-static-code-analysis-errors into develop
All checks were successful
default-pipeline default-pipeline #11
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/49
Reviewed-by: Balázs Vigh <balazs@vigh.eu>
2021-05-29 10:22:20 +02:00
be4a2038e5
MAPG-240 add use for PlaceInChallenge (used in MapAdminController::deleteChallenge)
All checks were successful
default-pipeline default-pipeline #9
2021-05-29 00:57:59 +02:00
f305c97513
MAPG-240 UserInChallengeRepository::isUserParticipatingInChallenge should return bool 2021-05-29 00:56:43 +02:00
784037de6f Merge pull request 'feature/MAPG-235-basic-challenge-mode' (#48) from feature/MAPG-235-basic-challenge-mode into develop
Some checks failed
default-pipeline default-pipeline #8
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/48
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-28 20:41:08 +02:00
6f27450423 MAPG-235 replaced explicit margin-top and margin-bottom with marginTop and marginBottom classes 2021-05-28 08:36:45 +02:00
bbaa2fe1eb MAPG-235 removed font-family and font-weight css attributes for restrictions and now handled on higher level 2021-05-28 08:19:35 +02:00
28165d76d3 MAPG-235 refactored challenge token generation and check 2021-05-28 08:07:02 +02:00
1c1e5f051d MAPG-235 simplified highscore calculation 2021-05-26 08:16:32 +02:00
283c214c50 MAPG-235 noZoom label fix 2021-05-26 07:49:05 +02:00
7970927654 MAPG-235 refactored whitespaces 2021-05-22 21:17:49 +02:00
567602c749 MAPG-235 hide restrictions information if there are no restrictions for the challenge 2021-05-22 21:00:54 +02:00
a5238234d2 MAPG-235 information of the current restrictions displayed on the ribbon in the top 2021-05-22 20:54:58 +02:00
c965713c9c Merge branch 'develop' of gitea.e5tv.hu:esoko/mapguesser into feature/MAPG-235-basic-challenge-mode 2021-05-22 16:20:59 +02:00
b00fe4ebe3 MAPG-235 highscores added to the end of challenge 2021-05-22 11:34:55 +02:00
599d8bf153 MAPG-235 fix unwanted time limit after page reload 2021-05-21 15:41:17 +02:00
636c47366a MAPG-235 fix loading other players' history and calculating final score 2021-05-21 15:40:43 +02:00
4520d11559 MAPG-235 fix calculating timelimit when 0 sec time was left from the last round 2021-05-21 14:45:40 +02:00
5f0b639fe7 MAPG-235 distance and score info added also to the player's own markers 2021-05-21 14:44:28 +02:00
d4a279d2f4 MAPG-235 refactored human readable time format 2021-05-21 11:51:47 +02:00
aadeda05af MAPG-235 timeLimit restored after reload of the page. Time limit selector usability improved 2021-05-21 11:30:30 +02:00
b78d564f6f MAPG-235 time limit restriction for the whole game feature added 2021-05-20 21:32:01 +02:00
c2df9b7713 MAPG-235 timelimit restriction per round functionality implemented for challenges 2021-05-20 20:55:44 +02:00
bbb66ca979 MAPG-235 challenge token related error handling 2021-05-20 08:31:52 +02:00
3b98570f6d MAPG-235 noMove, noZoom, noPan restrictions implemented 2021-05-19 21:16:59 +02:00
77ff175794 MAPG-235 added radio button for time limit type and fixed saving restrictions for challenge 2021-05-19 17:55:04 +02:00
ae7fd98256 MAPG-235 allow only logged in users to start a challenge 2021-05-19 17:11:49 +02:00
517d758e8e MAPG-235 fixed place/map deletion 2021-05-19 16:33:18 +02:00
2a1b51b014 MAPG-235 added time limit type field to the challenge 2021-05-19 14:52:49 +02:00
c085a5a1ca MAPG-235 fix user deletion bug 2021-05-19 13:52:19 +02:00
a7cfc3c19f MAPG-235 queries with selected relations enabled; reduced size of some of the queries 2021-05-19 13:06:46 +02:00
7898c52328 MAPG-235 go back to start button added to the end of the challenge 2021-05-18 20:22:43 +02:00
62251b1062 MAPG-235 created error handling for challenge access without login 2021-05-18 19:55:05 +02:00
069c6b37c8 MAPG-235 fixed bugs with displaying score and round 2021-05-18 18:06:02 +02:00
30f4b7ad19 MAPG-235 fixed sending history to initializer and fixed pov data format 2021-05-17 21:30:07 +02:00
45ddb7f56a MAPG-235 reduced number of queries for init and guess in challenge 2021-05-16 20:46:53 +02:00
7792f1c3ff MAPG-235 refactor: renamed fields in challenge related tables 2021-05-16 13:14:00 +02:00
7a1674fdd0 Merge branch 'feature/MAPG-235-basic-challenge-mode' of gitea.e5tv.hu:esoko/mapguesser into feature/MAPG-235-basic-challenge-mode 2021-05-15 11:43:47 +02:00
44df94eb98 MAPG-235 queries with relations on any recursion level implemented 2021-05-15 11:43:00 +02:00
00afd65d75 MAP-235 queries with relations on any recursion level implemented 2021-05-15 11:32:41 +02:00
21e41b7c36 MAP-235 fixed overlapping markers of history and user's guesses 2021-05-13 19:55:32 +02:00
69964acfb2 MAPG-235 results of other players shown 2021-05-13 19:48:25 +02:00
e7869d67f7 MAPG-235 guesses of other players in the round are sent after guess 2021-05-13 13:06:24 +02:00
93f8fc3f34 MAPG-235 basic gameflow works 2021-05-12 21:01:41 +02:00
5daed10036 MAPG-235 challengeInitialData implemented and first round is loaded 2021-05-12 15:34:15 +02:00
d7147b30d6 MAPG-235 new challenge can be created and is prepared 2021-05-12 13:51:06 +02:00
c02f595606
Merge pull request 'MAPG-143 make it possible to disable game for guests (default is disable)' (#47) from feature/MAPG-143-allow-game-only-for-users into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/47
2021-05-11 18:29:55 +02:00
99244e93d2 MAPG-235 updated structure and created models 2021-05-11 11:16:39 +02:00
313a3568aa MAPG-235 extended database structure for challenges 2021-05-10 21:22:07 +02:00
54bc9c31db
MAPG-143 make it possible to disable game for guests (default is disable) 2021-05-10 19:10:09 +02:00
75bea7c05c
Merge pull request 'MAPG-229 make .controlItem relative so it's children can be sized more properly' (#45) from bugfix/MAPG-229-circle-controls-won-t-show-up-in-firefox into develop
All checks were successful
default-pipeline default-pipeline #204
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/45
2021-05-09 17:22:13 +02:00
1c3238e64e
Merge pull request 'MAPG-230 renew session cookie if it already exists' (#44) from bugfix/MAPG-230-session-s-cookie-is-not-renewed into develop
All checks were successful
default-pipeline default-pipeline #203
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/44
2021-05-09 17:21:46 +02:00
6cb9e17ddf
Merge pull request 'give example for LEAFLET_TILESERVER_ATTRIBUTION' (#46) from feature/extend-leaflet-env-examples into develop
All checks were successful
default-pipeline default-pipeline #202
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/46
2021-05-09 17:17:18 +02:00
88ad6e9d61
give example for LEAFLET_TILESERVER_ATTRIBUTION
All checks were successful
default-pipeline default-pipeline #197
2021-05-09 17:11:51 +02:00
2fb807c479
MAPG-229 make .controlItem relative so it's children can be sized more properly
All checks were successful
default-pipeline default-pipeline #196
2021-05-09 16:15:29 +02:00
8e9e5b08f9
MAPG-230 renew session cookie if it already exists
All checks were successful
default-pipeline default-pipeline #194
2021-05-09 15:49:27 +02:00
94624d5b2c Merge pull request 'reverted removal of preference for outdoor panorama in the front end' (#43) from feature/preference-for-outdoor-panorama-in-the-frontend into develop
All checks were successful
default-pipeline default-pipeline #195
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/43
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-09 13:34:36 +02:00
71c728d56c reverted removal of preference for outdoor panorama in the front end
All checks were successful
default-pipeline default-pipeline #190
2021-05-09 11:46:03 +02:00
216d30329f Merge pull request 'feature/avoid-repeating-places-in-game' (#38) from feature/avoid-repeating-places-in-game into develop
All checks were successful
default-pipeline default-pipeline #189
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/38
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-09 10:58:53 +02:00
3f8311d708 implemented review findings
All checks were successful
default-pipeline default-pipeline #188
2021-05-08 23:37:36 +02:00
7f8c1eb291 Merge pull request 'added error handling when requesting to join non existing room' (#41) from bugfix/unhandled-error-when-trying-to-join-non-existing-room into develop
All checks were successful
default-pipeline default-pipeline #187
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/41
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-08 20:59:29 +02:00
d57a4c63e0 Merge pull request 'check the user type if it has valid value' (#40) from bugfix/cli-user-creation-crashes-when-no-type-is-given into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/40
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-08 20:59:09 +02:00
ed8e1773c4 Merge pull request 'allow error string to be null in case the password was correct' (#39) from bugfix/account-cannot-be-deleted into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/39
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-08 20:57:37 +02:00
2963bfe739 Merge pull request 'added leaflet subdomains to the environment variables, which are used now for initializing a leaflet map' (#42) from feature/leaflet-subdomains-in-environment-variables into develop
All checks were successful
default-pipeline default-pipeline #184
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/42
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-08 20:57:07 +02:00
7232b322ab extended readme
All checks were successful
default-pipeline default-pipeline #183
2021-05-08 18:30:37 +02:00
e6819b6dd3 added leaflet subdomains to the environment variables, which are used now for initializing a leaflet map
All checks were successful
default-pipeline default-pipeline #182
2021-05-08 18:20:54 +02:00
01a4fdf5e5 added error handling when requesting to join non existing room
All checks were successful
default-pipeline default-pipeline #181
2021-05-08 18:03:17 +02:00
1cc3294ee7 check the user type if it has valid value
All checks were successful
default-pipeline default-pipeline #180
2021-05-08 16:19:49 +02:00
ad0b8d078f allow error string to be null in case the password was correct
All checks were successful
default-pipeline default-pipeline #178
2021-05-08 09:34:34 +02:00
899817a853 removed unnecessary comment
All checks were successful
default-pipeline default-pipeline #177
2021-05-06 20:41:23 +02:00
c626e36bbb specified explicit type in function parameter
All checks were successful
default-pipeline default-pipeline #176
2021-05-06 20:39:00 +02:00
8d8074977b fixing comments
All checks were successful
default-pipeline default-pipeline #175
2021-05-06 20:25:48 +02:00
8b3c95bdc7 handling of deleting places or user updated
All checks were successful
default-pipeline default-pipeline #174
2021-05-06 20:09:05 +02:00
b2535ad78a refactored function and variable names, and replaced variables in inner scope
All checks were successful
default-pipeline default-pipeline #173
2021-05-06 17:12:18 +02:00
886bd02f88 removed mistakenly added parameter
All checks were successful
default-pipeline default-pipeline #172
2021-05-06 10:09:28 +02:00
7143c7ec63 moved saving to UserPlayedPlace to seperate function
Some checks failed
default-pipeline default-pipeline #171
2021-05-06 10:05:23 +02:00
fb7c0e7a5c renamed the random int generator function
All checks were successful
default-pipeline default-pipeline #170
2021-05-06 09:36:49 +02:00
8136273b33 replaced left join with inner join for older places search
All checks were successful
default-pipeline default-pipeline #169
2021-05-05 21:58:19 +02:00
3c6a7a3c5f selection with preference for older places; refactored PlaceRepository 2021-05-05 21:50:23 +02:00
01f3c7c8cf added preference for places not seen by user in the selection of places for the game 2021-05-05 19:19:38 +02:00
66c871fbc2 Extended Select functionality with handling of derived tables 2021-05-05 19:18:07 +02:00
807b4d024f extended database with UserPlayedPlace table for tracking when and what place a User played in game 2021-05-05 19:16:54 +02:00
430b32d0c6
Merge pull request 'do not exclude indoor panoramas' (#37) from feature/pano-could-be-indoor into develop
All checks were successful
default-pipeline default-pipeline #179
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/37
2021-05-02 18:44:40 +02:00
11eee64895
do not exclude indoor panoramas
All checks were successful
default-pipeline default-pipeline #166
2021-05-02 18:38:29 +02:00
cc24f96395 Merge pull request 'radius calculation for new panorama search based on latitude and mercator projection for google maps' (#36) from feature/more-accurate-new-selection-in-mapeditor-on-google-maps into develop
All checks were successful
default-pipeline default-pipeline #165
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/36
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-02 18:02:28 +02:00
c3e5a6db1c radius calculation for new panorama search based on latitude and mercator projection for google maps
All checks were successful
default-pipeline default-pipeline #164
2021-05-02 16:57:47 +02:00
398204c5d5
Merge pull request 'feature/place-streetview-cover-to-the-map' (#35) from feature/place-streetview-cover-to-the-map into develop
All checks were successful
default-pipeline default-pipeline #162
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/35
2021-05-02 16:27:41 +02:00
aaa5a1daba
Merge pull request 'bugfix/streetviewcontrol-is-still-visible' (#34) from bugfix/streetviewcontrol-is-still-visible into develop
All checks were successful
default-pipeline default-pipeline #161
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/34
2021-05-02 16:27:34 +02:00
c112a25409
move street view cover button to the map as circleControl
All checks were successful
default-pipeline default-pipeline #160
2021-05-02 15:04:14 +02:00
35cab17c21
make circle control items more general 2021-05-02 15:04:14 +02:00
f9c1829a00
enable zoomControl for Leaflet map to be consistent with Google map
All checks were successful
default-pipeline default-pipeline #157
2021-05-02 13:45:34 +02:00
e03850ce05
switch off streetViewControl explicitly 2021-05-02 13:42:46 +02:00
55ec226407 Merge pull request 'feature/google-maps-in-mapeditor-added' (#33) from feature/google-maps-in-mapeditor-added into develop
All checks were successful
default-pipeline default-pipeline #155
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/33
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-02 13:38:17 +02:00
6ef04fcd4e
fit both google and leaflet map to bounds if map already exists
All checks were successful
default-pipeline default-pipeline #154
2021-05-02 13:29:48 +02:00
3045d8acbb
remove builtin streetViewControl from google map 2021-05-02 13:13:11 +02:00
19a1b79b0a toggling of street view cover added
All checks were successful
default-pipeline default-pipeline #153
2021-05-02 12:56:25 +02:00
d432a5b584 cleaned up the comment 2021-05-02 12:34:57 +02:00
8b9345eb36 removed unused css declarations; renamed invalidateSize to resize 2021-05-02 12:34:57 +02:00
e1eb0077b1 hide the map selection and jump to location elements on mobile view 2021-05-02 12:34:57 +02:00
745bda11c0 refactoring and bug fixing related to the map switching 2021-05-02 12:34:57 +02:00
0bb31cbe14 added switching functionality between maps in mapeditor 2021-05-02 12:34:57 +02:00
ef8ad69506 organised Leaflet map and Google Maps into wrappers 2021-05-02 12:23:37 +02:00
f10ad37a74
Merge pull request 'update readme' (#32) from feature/update-readme into develop
All checks were successful
default-pipeline default-pipeline #152
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/32
2021-04-29 20:03:19 +02:00
70b60d2fb8
create the first user during installation in development mode 2021-04-29 20:02:00 +02:00
7e68580db6
update readme 2021-04-29 20:01:42 +02:00
8218883faf Merge pull request 'Feature In-Game Compass' (#27) from feature/in-game-compass into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/27
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 19:15:44 +02:00
145cb5f637 Merge branch 'develop' into feature/in-game-compass
All checks were successful
default-pipeline default-pipeline #151
2021-04-29 19:00:11 +02:00
6d3f74f911 increased the left margin for mobile view
All checks were successful
default-pipeline default-pipeline #150
2021-04-29 18:59:24 +02:00
3d663fb5d6 Merge pull request 'Quickfix for single player game cannot be restarted' (#30) from bugfix/quickfix-for-single-play-cannot-be-restarted into develop
All checks were successful
default-pipeline default-pipeline #149
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/30
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:58 +02:00
759f654a0d Merge pull request 'feature/return-to-starting-point-button' (#26) from feature/return-to-starting-point-button into develop
All checks were successful
default-pipeline default-pipeline #148
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/26
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:22 +02:00
c09cce3c97
Merge pull request 'MAPG-167 set background color' (#31) from feature/MAPG-167-set-background-color into develop
All checks were successful
default-pipeline default-pipeline #147
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/31
2021-04-29 18:46:26 +02:00
276e331a47
MAPG-167 set background color
All checks were successful
default-pipeline default-pipeline #146
2021-04-29 18:43:30 +02:00
62f4fd7b84 readyToContinue is true by default and set to false only in the multigame initializing
All checks were successful
default-pipeline default-pipeline #145
2021-04-29 13:57:32 +02:00
b22f13887f clicking on compass sets the heading direction to north
All checks were successful
default-pipeline default-pipeline #144
2021-04-29 12:39:44 +02:00
2a3f2212a7 replaced span with div for the compass item
All checks were successful
default-pipeline default-pipeline #143
2021-04-29 12:12:17 +02:00
98df08e5fa Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 12:10:02 +02:00
8f0cfd4489 replaced spans with divs again and fixed the css issue
All checks were successful
default-pipeline default-pipeline #142
2021-04-29 12:08:04 +02:00
d42280eb10 only the image is rotated, not the whole div
All checks were successful
default-pipeline default-pipeline #141
2021-04-29 09:18:24 +02:00
0739c65961 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 09:11:32 +02:00
3122696364 added the visibility reset to resetRound and reverted the span div replacement in the view
All checks were successful
default-pipeline default-pipeline #140
2021-04-29 09:07:44 +02:00
462f6cde80 Merge branch 'feature/return-to-starting-point-button' of gitea.e5tv.hu:esoko/mapguesser into feature/return-to-starting-point-button
All checks were successful
default-pipeline default-pipeline #139
2021-04-29 09:05:32 +02:00
3f4fa83d87 reverted replacing spans with divs 2021-04-29 09:04:50 +02:00
ea599809b8 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 08:34:39 +02:00
f0b0de405c Merge branch 'develop' into feature/return-to-starting-point-button
All checks were successful
default-pipeline default-pipeline #138
2021-04-29 08:25:52 +02:00
ffc4aaf110 fixed findings
All checks were successful
default-pipeline default-pipeline #137
2021-04-29 08:24:20 +02:00
58cdc1be8d
Merge pull request 'fix typo' (#29) from feature/modify-author into develop
All checks were successful
default-pipeline default-pipeline #134
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/29
2021-04-29 00:58:57 +02:00
4039202268
fix typo
All checks were successful
default-pipeline default-pipeline #133
2021-04-29 00:58:20 +02:00
4677cc5229
Merge pull request 'generalize author attribution' (#28) from feature/modify-author into develop
All checks were successful
default-pipeline default-pipeline #131
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/28
2021-04-29 00:53:26 +02:00
8800724aa9
generalize author attribution
All checks were successful
default-pipeline default-pipeline #130
2021-04-29 00:52:46 +02:00
08c38c6374 reverted style tailoring for guess
All checks were successful
default-pipeline default-pipeline #129
2021-04-28 20:46:52 +02:00
854775011c tailored the style for the navigation buttons
All checks were successful
default-pipeline default-pipeline #128
2021-04-28 20:42:42 +02:00
e33d8c02e9 added compass button and coupled its transform:rotate style attribute with the POV heading value 2021-04-28 20:33:17 +02:00
3d52c967b6 reformatted: added new line breaks to the game.css
All checks were successful
default-pipeline default-pipeline #127
2021-04-28 18:30:58 +02:00
5c8f3d6fb9 returns to start when clicked on home symbol
All checks were successful
default-pipeline default-pipeline #126
2021-04-28 18:26:46 +02:00
b9927f79fc added a new button to the view for the return to starting point feature 2021-04-28 17:58:48 +02:00
56f3a9b380 Merge pull request 'Stuck scoring at zero fixed by regular map area update in session' (#24) from bugfix/scoring-stuck-at-zero into develop
All checks were successful
default-pipeline default-pipeline #124
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/24
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 20:36:41 +02:00
a0d41c388b Merge branch 'develop' into bugfix/scoring-stuck-at-zero
All checks were successful
default-pipeline default-pipeline #123
2021-04-27 17:56:52 +02:00
092cc63148 Merge pull request 'jump to coordinate feature added to the header stripe' (#25) from feature/jump-to-specific-coordinates-in-mapeditor into develop
All checks were successful
default-pipeline default-pipeline #122
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/25
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 17:56:35 +02:00
95553296bd removed extra line break
All checks were successful
default-pipeline default-pipeline #121
2021-04-27 17:54:47 +02:00
9330227879 Merge branch 'feature/jump-to-specific-coordinates-in-mapeditor' of gitea.e5tv.hu:esoko/mapguesser into feature/jump-to-specific-coordinates-in-mapeditor
All checks were successful
default-pipeline default-pipeline #120
2021-04-27 17:50:10 +02:00
faaff33841 removed #jumpForm and reformatted the code 2021-04-27 17:48:45 +02:00
508a6899d8 Merge branch 'develop' into bugfix/scoring-stuck-at-zero
All checks were successful
default-pipeline default-pipeline #119
2021-04-27 17:36:18 +02:00
be0f7b64b8 refactored comment
All checks were successful
default-pipeline default-pipeline #118
2021-04-27 17:35:06 +02:00
15e3248ee1 Merge branch 'develop' into feature/jump-to-specific-coordinates-in-mapeditor
All checks were successful
default-pipeline default-pipeline #117
2021-04-27 17:26:44 +02:00
04a897c958 Merge pull request 'folder name db changed to database in the path' (#23) from bugfix/change-path-to-renamed-folder into develop
All checks were successful
default-pipeline default-pipeline #116
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/23
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 17:25:54 +02:00
845d77ebda area of map is updated in the session at each game preparation to avoid outdated map area and wrong score calculation
All checks were successful
default-pipeline default-pipeline #115
2021-04-27 16:46:40 +02:00
8bc3fda49b jump to coordinate feature added to the header stripe
All checks were successful
default-pipeline default-pipeline #114
2021-04-27 10:18:28 +02:00
d78817180b folder name db changed to database in the path
All checks were successful
default-pipeline default-pipeline #113
2021-04-26 20:21:21 +02:00
0f810bd2ba
Merge pull request 'feature/MAPG-228-deploy-signed-and-verified-tags-only' (#22) from feature/MAPG-228-deploy-signed-and-verified-tags-only into develop
All checks were successful
default-pipeline default-pipeline #111
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/22
2021-04-25 15:16:46 +02:00
9d1fc8b60c
MAPG-228 prune local tags when fetching
All checks were successful
default-pipeline default-pipeline #107
2021-04-25 15:11:49 +02:00
dd46fb3d32
MAPG-228 check if the Release* tag is signed and verified 2021-04-25 15:11:20 +02:00
1a40e3a18d
Merge pull request 'MAPG-226 change remaining time calculation' (#21) from feature/MAPG-226-change-timeout-algorithm into develop
All checks were successful
default-pipeline default-pipeline #106
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/21
2021-04-10 19:00:20 +02:00
1aa5d3b43e
MAPG-226 change remaining time calculation 2021-04-10 18:59:27 +02:00
a418d5cf66
Merge pull request 'MAPG-213 move visibility workaround to Game.showResultMap' (#20) from bugfix/MAPG-213-result-map-not-show-on-mobile-after-reload into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/20
2021-04-04 23:41:05 +02:00
5cae286a5b
MAPG-213 move visibility workaround to Game.showResultMap 2021-04-04 23:40:27 +02:00
c5d2591371
Merge pull request 'MAPG-223 send error if member already guessed' (#19) from feature/MAPG-223-user-should-not-be-able-to-guess-after-round-is-over into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/19
2021-04-04 23:29:04 +02:00
cf59f937c9
MAPG-223 send error if member already guessed
All checks were successful
default-pipeline default-pipeline #104
2021-04-04 23:28:44 +02:00
257f59c96e
Merge pull request 'MAPG-222 fixes: skip invalid guessPosition, hide showSummaryButton, handle countdown time if timeout is 0' (#18) from bugfix/MAPG-222-show-summary-can-be-clicked-when-game-is-restarted into develop
All checks were successful
default-pipeline default-pipeline #98
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/18
2021-04-04 23:05:47 +02:00
c2f5806394
MAPG-222 fixes: skip invalid guessPosition, hide showSummaryButton, handle countdown time if timeout is 0
All checks were successful
default-pipeline default-pipeline #96
2021-04-04 23:04:07 +02:00
ac0b71fe09
Merge pull request 'feature/MAPG-213-user-shouldn-t-be-able-to-re-guess-when-page-is-reloaded' (#17) from feature/MAPG-213-user-shouldn-t-be-able-to-re-guess-when-page-is-reloaded into develop
All checks were successful
default-pipeline default-pipeline #94
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/17
2021-04-04 22:31:29 +02:00
19f7b50002
MAPG-213 show results immediately if user already guessed
All checks were successful
default-pipeline default-pipeline #91
2021-04-04 22:24:40 +02:00
6342bb1e79
MAPG-213 send round data at initialization if user guessed 2021-04-04 22:18:00 +02:00
3abdb6910e
MAPG-213 clean up init
All checks were successful
default-pipeline default-pipeline #90
2021-04-04 20:52:28 +02:00
7d2588f7cd
Merge pull request 'feature/MAPG-219-disable-continue-if-timeout-is-not-reached' (#16) from feature/MAPG-219-disable-continue-if-timeout-is-not-reached into develop
All checks were successful
default-pipeline default-pipeline #88
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/16
2021-04-04 19:23:08 +02:00
be40acdb2d
MAPG-219 disable continue button until round is over
All checks were successful
default-pipeline default-pipeline #84
2021-04-04 18:48:31 +02:00
a2f2080959
MAPG-219 send 'end_round' when all members are finished
All checks were successful
default-pipeline default-pipeline #83
2021-04-04 17:43:37 +02:00
cc49c56075
Merge pull request 'feature/MAPG-205-set-timeout-in-multiplayer-rooms' (#15) from feature/MAPG-205-set-timeout-in-multiplayer-rooms into develop
All checks were successful
default-pipeline default-pipeline #82
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/15
2021-04-04 14:22:59 +02:00
69329b9f81
MAPG-205 show countdown time
All checks were successful
default-pipeline default-pipeline #80
2021-04-04 14:22:10 +02:00
20914b466e
MAPG-205 add elements for showing countdown 2021-04-04 14:20:12 +02:00
3630637ec1
MAPG-205 handle round timeout on frontend 2021-04-04 14:20:12 +02:00
42224b074e
MAPG-205 set timeout for rounds 2021-04-04 14:20:12 +02:00
0e8ed14305
MAPG-205 delete unnecessary empty lines 2021-04-03 23:39:20 +02:00
d3a12fb6f1
Merge pull request 'feature/MAPG-204-show-guesses-from-others' (#14) from feature/MAPG-204-show-guesses-from-others into develop
All checks were successful
default-pipeline default-pipeline #76
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/14
2021-04-03 22:13:52 +02:00
3f9d104adc
MAPG-204 show guesses from others
All checks were successful
default-pipeline default-pipeline #72
2021-04-03 22:10:21 +02:00
9e5d20796c
MAPG-204 clean up result sending 2021-04-03 18:24:43 +02:00
7433813337
Merge pull request 'MAPG-208 error handling adaptations' (#13) from bugfix/MAPG-208-error-messages into develop
All checks were successful
default-pipeline default-pipeline #75
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/13
2021-03-21 16:39:43 +01:00
42854fe975
MAPG-208 error handling adaptations
All checks were successful
default-pipeline default-pipeline #53
2021-03-21 14:37:08 +01:00
71681f1e55
Merge pull request 'MAPG-207 don't use MapGuesser.showmodal for modal 'multi'' (#12) from bugfix/MAPG-207-prevent-closing-multi-modal into develop
All checks were successful
default-pipeline default-pipeline #50
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/12
2021-03-21 12:47:35 +01:00
1cf0ab44fd
MAPG-207 don't use MapGuesser.showmodal for modal 'multi'
All checks were successful
default-pipeline default-pipeline #49
2021-03-21 12:45:57 +01:00
fc9eb183f1
Merge pull request 'MAPG-203 fix wrong variable name' (#11) from bugfix/MAPG-203-fix-condition-in-js into develop
All checks were successful
default-pipeline default-pipeline #46
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/11
2021-03-20 22:26:33 +01:00
d431a693fc
MAPG-203 fix wrong variable name
All checks were successful
default-pipeline default-pipeline #45
2021-03-20 22:24:47 +01:00
ace490b7e9
Merge pull request 'MAPG-203 use MULTI_WS_URL environment variable' (#10) from feature/MAPG-203-fine-tune-env-variables into develop
All checks were successful
default-pipeline default-pipeline #43
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/10
2021-03-20 21:24:20 +01:00
e3771d36ce
MAPG-203 use MULTI_WS_URL environment variable
All checks were successful
default-pipeline default-pipeline #42
2021-03-20 21:21:40 +01:00
6e03476173
Merge pull request 'bugfix/MAPG-203-fix-singleplayer' (#9) from bugfix/MAPG-203-fix-singleplayer into develop
All checks were successful
default-pipeline default-pipeline #39
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/9
2021-03-20 21:00:14 +01:00
f3ec71d6dd
MAPG-203 fix condition in game.js
All checks were successful
default-pipeline default-pipeline #38
2021-03-20 20:59:10 +01:00
c2f0a1dd51
MAPG-203 fix signatures in GameController 2021-03-20 20:58:57 +01:00
a417fbd760
Merge pull request 'feature/MAPG-203-initial-multiplayer-implementation' (#8) from feature/MAPG-203-initial-multiplayer-implementation into develop
All checks were successful
default-pipeline default-pipeline #37
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/8
2021-03-20 20:46:36 +01:00
a9cda56586
MAPG-203 install NPM packages on install and update
All checks were successful
default-pipeline default-pipeline #36
2021-03-20 19:45:21 +01:00
02fcbd2f9c
MAPG-203 prepare GameFlowController for multiplayer 2021-03-20 19:45:21 +01:00
2f665381c3
MAPG-203 prepare GameController for multiplayer 2021-03-20 19:45:21 +01:00
573440868e
MAPG-203 add new routes for multiplayer 2021-03-20 19:45:21 +01:00
79490a0616
MAPG-203 add class for multiplayer internal connection (PHP-NodeJS TCP) 2021-03-20 19:45:21 +01:00
ed343f2359
MAPG-203 add new environment variables 2021-03-20 19:45:20 +01:00
563f900423
MAPG-203 implement game mode selection UI 2021-03-20 19:45:20 +01:00
24a10c534e
MAPG-203 frontend adaptations for multiplayer 2021-03-20 19:44:48 +01:00
0cbb7ba145
MAPG-203 add debugger config for NodeJS 2021-03-20 19:44:48 +01:00
5dd2ce0d5a
MAPG-203 add cleanup for multi_rooms to db:maintain 2021-03-20 19:44:48 +01:00
e5fb725c69
MAPG-203 add new table and model for multi_room 2021-03-20 19:44:48 +01:00
fc40c18679
MAPG-203 multiplayer handler server (NodeJS) 2021-03-20 19:44:48 +01:00
4fabc39d44
MAPG-203 restructure Docker stack 2021-03-20 19:44:48 +01:00
2120fbebbc
MAPG-203 add Dockerfile for multiplayer (NodeJS) 2021-03-20 19:44:47 +01:00
b9f0529dce
MAPG-203 add new Composer packages 2021-03-20 19:44:47 +01:00
a3ca7638c7
MAPG-203 add NPM packages for multiplayer 2021-03-20 15:21:07 +01:00
8538d0a119
MAPG-203 add node_modules to gitignore 2021-03-17 23:09:07 +01:00
0cc63ef936
Merge pull request 'MAPG-203 this.response.newPlace -> this.response.place' (#7) from bugfix/MAPG-203-fix-wrong-object-key into develop
All checks were successful
default-pipeline default-pipeline #28
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/7
2021-03-17 22:54:45 +01:00
10d239bfb4
MAPG-203 this.response.newPlace -> this.response.place
All checks were successful
default-pipeline default-pipeline #25
2021-03-17 22:54:06 +01:00
e2493e1b7e
Merge pull request 'MAPG-203 select places at the beginning of the game' (#6) from feature/MAPG-203-initial-multiplayer-implementation into develop
All checks were successful
default-pipeline default-pipeline #24
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/6
2021-03-17 22:48:43 +01:00
edd8c0f8a4
MAPG-203 select places at the beginning of the game
All checks were successful
default-pipeline default-pipeline #21
2021-03-17 22:47:11 +01:00
01f0f7d4bc
Merge pull request 'feature/MAPG-203-initial-multiplayer-implementation' (#5) from feature/MAPG-203-initial-multiplayer-implementation into develop
All checks were successful
default-pipeline default-pipeline #17
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/5
2021-03-15 12:32:54 +01:00
c317a0abf4
MAPG-203 adapt game.js to the new endpoints
All checks were successful
default-pipeline default-pipeline #15
2021-03-15 12:28:25 +01:00
9c4b7448a9
MAPG-203 refactor GameFlowController 2021-03-15 12:28:06 +01:00
60e20bd92b
MAPG-203 rename game/newPlace endpoint 2021-03-15 12:26:15 +01:00
2c4d809d49
MAPG-203 remove placesWithoutPano logic from PlaceRepository 2021-03-15 12:25:49 +01:00
bf27f15709
Merge pull request 'MAPG-2020 run PHPStan with 1G memory limit' (#4) from bugfix/MAPG-2020-phpstan-needs-bigger-memory-limit into develop
All checks were successful
default-pipeline default-pipeline #12
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/4
2021-01-01 22:36:56 +01:00
db3094eb62
MAPG-2020 run PHPStan with 1G memory limit
All checks were successful
default-pipeline default-pipeline #11
2021-01-01 22:34:32 +01:00
77c71ab2da
Merge pull request 'MAPG-201 update Git link to gitea.e5tv.hu' (#2) from feature/MAPG-201-migrate-to-own-git-server into develop
Some checks reported warnings
Check 2 Check 2 was successful
Check 3 Check 3 was not successful
Check 4 Check 4 was successful
Check 5 Check 5 was successful
Check Check was successful
Pipeline Pipeline #1
test_pipeline test_pipeline #3
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/2
2020-12-27 22:12:33 +00:00
8327cd975a
MAPG-201 update Git link to gitea.e5tv.hu 2020-12-27 23:08:30 +01:00
cc8148fcf9 Merged in bugfix/MAPG-196-fix-remote-static-root-detection (pull request #181)
MAPG-196 fix regex that recognizes remote static root
2020-07-06 20:00:49 +00:00
2a7b5fb992
MAPG-196 fix regex that recognizes remote static root 2020-07-06 21:59:25 +02:00
a4c77c3d32 Merged in bugfix/MAPG-196-fix-compatibility-issues (pull request #180)
MAPG-196 revert link preload to plain stylesheet because it is not supported everywhere
2020-07-06 19:15:32 +00:00
317329ae84
MAPG-196 revert link preload to plain stylesheet because it is not supported everywhere 2020-07-06 21:12:56 +02:00
234cb7f086 Merged in feature/MAPG-196-fix-issues-reported-by-lighthouse (pull request #179)
Feature/MAPG-196 fix issues reported by lighthouse
2020-07-06 18:05:51 +00:00
66c709f00d
MAPG-196 reposition of cookie notice 2020-07-06 20:04:39 +02:00
2737c7d6d6
MAPG-196 speed up asset loading, add missing description, add missing image alts 2020-07-06 20:04:39 +02:00
2ccee26424 Merged in feature/MAPG-199-static-analyser-save-artifact (pull request #178)
MAPG-199 save static code analysis results as artifact
2020-07-06 16:52:21 +00:00
8cbf611189
MAPG-199 save static code analysis results as artifact 2020-07-06 18:50:17 +02:00
2ac9794e3f Merged in feature/MAPG-199-introduce-static-php-analyser (pull request #177)
Feature/MAPG-199 introduce static php analyser
2020-07-05 21:23:24 +00:00
9e5f7405d1
MAPG-199 move test views to another folder to prevent code analysis errors 2020-07-05 23:18:57 +02:00
af98d71924
MAPG-199 fix issues found by PHPStan 2020-07-05 23:18:56 +02:00
39d691fd7f
MAPG-199 add static code analysis step to pipeline 2020-07-05 22:51:36 +02:00
405488cf4e
MAPG-199 add PHPStan to require-dev 2020-07-05 21:35:49 +02:00
f2736d4d6e Merged in hotfix/MAPG-198-fix-not-working-game (pull request #176)
MAPG-198 re-add table name to Select
2020-07-05 19:19:53 +00:00
88b9821c61
MAPG-198 re-add table name to Select
(table name is used when counting)
2020-07-05 21:17:29 +02:00
da2c8d96a4 Merged in bugfix/MAPG-142-fix-timestamp-onupdate (pull request #175)
MAPG-142 fix timestamp onupdate issue
2020-07-05 16:24:17 +00:00
cd581c6fcd
MAPG-142 fix timestamp onupdate issue 2020-07-05 18:23:02 +02:00
f7b268e10f Merged in feature/MAPG-185-create-cron-script (pull request #174)
Feature/MAPG-185 create cron script
2020-07-05 15:56:12 +00:00
a8b1ab3057
MAPG-185 adapt README 2020-07-05 17:52:13 +02:00
8403c21ec0
MAPG-185 consolidation of email templates 2020-07-05 17:37:02 +02:00
0ba1cda884
MAPG-185 fix getter of User 2020-07-05 17:36:27 +02:00
b2e09c394b
MAPG-185 switch off builtin session gc 2020-07-05 16:48:37 +02:00
0bcb0187c1
MAPG-185 rename migrate database command 2020-07-05 16:45:38 +02:00
f3d1608164
MAPG-185 add maintain database command 2020-07-05 16:43:36 +02:00
b254603fb1
MAPG-185 add getter for expired users and password resetters 2020-07-05 16:37:37 +02:00
776561a41c Merged in feature/MAPG-193-replace-direct-selects-to-repository-calls (pull request #173)
Feature/MAPG-193 replace direct selects to repository calls
2020-07-05 13:20:08 +00:00
27fc687043
MAPG-193 replace Select usage to repository calls where it is easily possible 2020-07-05 15:17:28 +02:00
45869321ac
MAPG-193 fix PersistentDataManager::selectMultipleFromDb 2020-07-05 15:15:29 +02:00
b58137acc9 Merged in feature/MAPG-142-resend-activation-link-functionality (pull request #172)
Feature/MAPG-142 resend activation link functionality
2020-07-05 12:32:32 +00:00
dd6bb5ef9a
MAPG-142 limit password reset query if the existing is not expired 2020-07-05 14:24:06 +02:00
37094e552b
MAPG-142 modify UserPasswordResetterRepository to return only one resetter for user
(it is not possible for an user to have multiple)
2020-07-05 14:24:06 +02:00
32392590bd
MAPG-142 save user creation date 2020-07-05 14:24:06 +02:00
38885849de
MAPG-142 remove unnecessary 'now' from DateTime constructor 2020-07-05 14:24:05 +02:00
ef013b8b9e
MAPG-142 rename app to MapGuesser in .env.example 2020-07-05 14:24:05 +02:00
ab23b37b97
MAPG-142 redefine tokens and increase OAuth security with nonce 2020-07-05 14:24:05 +02:00
7e3315fc88
MAPG-142 implemenet confirmation mail resend 2020-07-05 13:39:56 +02:00
6cafac1b65
MAPG-142 modify UserConfirmationRepository to return only one confirmation for user
(it is not possible for an user to have multiple)
2020-07-05 13:33:02 +02:00
631582a912 Merged in feature/MAPG-191-get-rid-of-unnecessary-passing-by-reference (pull request #171)
Feature/MAPG-191 get rid of unnecessary passing by reference
2020-07-04 23:02:57 +00:00
6db6208896
MAPG-191 don't create separate variable for data where not necessary 2020-07-05 00:58:03 +02:00
e17cf68007
MAPG-191 don't pass data by reference to IContent 2020-07-05 00:25:34 +02:00
091afb0aab Merged in feature/MAPG-141-password-forgotten-functionality (pull request #170)
Feature/MAPG-141 password forgotten functionality
2020-07-04 22:15:50 +00:00
954f111254
MAPG-141 delete password resetters when deleting account 2020-07-05 00:09:45 +02:00
de1d7338a4
MAPG-141 add reset password functionality 2020-07-05 00:09:11 +02:00
7539f637b0
MAPG-141 add and fix some links to login/signup 2020-07-05 00:04:35 +02:00
67534a22f5
MAPG-141 add password resetter table, model and repository 2020-07-05 00:03:56 +02:00
43aef22c4e Merged in feature/MAPG-44-possibility-to-add-pov-data-to-locations (pull request #169)
Feature/MAPG-44 possibility to add pov data to locations
2020-07-03 23:12:56 +00:00
3123643bb7
MAPG-44 modify game to use pov of place 2020-07-04 01:11:04 +02:00
b736ce8970
MAPG-44 modify MapAdminController to get/save place pov data 2020-07-04 01:10:03 +02:00
4e48e6ae73
MAPG-44 add Pov class and adjust Place model to store pov 2020-07-04 01:09:00 +02:00
028d8a22a2
MAPG-44 modify DB structure to store pov data 2020-07-04 01:06:37 +02:00
a73e60ae5a Merged in bugfix/MAPG-189-fix-inputwithbuttons-input-padding (pull request #168)
MAPG-189 fix div.inputWithButton>input's padding when it is focused
2020-07-03 21:51:38 +00:00
5383da81b3
MAPG-189 fix div.inputWithButton>input's padding when it is focused 2020-07-03 23:50:22 +02:00
e8adc9f730 Merged in feature/MAPG-177-create-unit-tests-for-all-units (pull request #167)
MAPG-177 add tests for View\Parser
2020-06-28 21:13:32 +00:00
149003250c
MAPG-177 add tests for View\Parser 2020-06-28 22:55:15 +02:00
171b4278a6 Merged in feature/MAPG-175-unify-styles (pull request #166)
MAPG-175 make map title one liner
2020-06-28 18:16:23 +00:00
091018d22f
MAPG-175 make map title one liner 2020-06-28 20:15:11 +02:00
34f0c92eb5 Merged in bugfix/MAPG-187-dont-evaluate-asset-paths (pull request #165)
Bugfix/MAPG-187 dont evaluate asset paths
2020-06-28 17:38:03 +00:00
8ac5d5c6e2
MAPG-187 adapt views to the new parser 2020-06-28 19:35:14 +02:00
e883d184b7
MAPG-187 refactor view parsing and linking 2020-06-28 19:35:14 +02:00
71b1e28aa9 Merged in feature/MAPG-188-pagespeed-improvements (pull request #164)
MAPG-188 get rid of inline js
2020-06-28 16:13:16 +00:00
3555be9b63
MAPG-188 get rid of inline js 2020-06-28 17:56:47 +02:00
d03a3934c6 Merged in feature/MAPG-181-handle-the-case-when-user-has-no-password (pull request #163)
Feature/MAPG-181 handle the case when user has no password
2020-06-28 08:42:10 +00:00
bd5b49b09b
MAPG-181 set user active when added from CLI 2020-06-28 03:19:07 +02:00
4333240acc
MAPG-181 add stuff to authenticate without password 2020-06-28 03:18:51 +02:00
4099f1d962
MAPG-181 add new layout type (minimal) 2020-06-28 03:16:48 +02:00
2fac828098
MAPG-181 text fixes 2020-06-28 03:16:28 +02:00
f3225ae275
MAPG-181 ability to add login_hint to dialog url 2020-06-28 03:15:20 +02:00
48eba75cf4
MAPG-181 fix and unify styles for buttons and inputs 2020-06-28 03:12:16 +02:00
a97a6f8977 Merged in bugfix/MAPG-180-dont-echo-void (pull request #162)
MAPG-180 don't echo void
2020-06-27 12:26:57 +00:00
2a3f891aa0
MAPG-180 don't echo void 2020-06-27 14:26:19 +02:00
38a427416c Merged in bugfix/MAPG-180-unify-inline-js (pull request #161)
MAPG-180 don't parse REVISION in view linking time
2020-06-27 11:21:02 +00:00
2f14e1f4c3
MAPG-180 don't parse REVISION in view linking time 2020-06-27 13:20:11 +02:00
6098b7bb8d Merged in feature/MAPG-175-unify-styles (pull request #160)
MAPG-175 add theme color
2020-06-27 11:11:49 +00:00
596b29f841
MAPG-175 add theme color 2020-06-27 13:09:24 +02:00
34a4026ddc Merged in feature/MAPG-180-inline-js-css-automatically (pull request #159)
Feature/MAPG-180 inline js css automatically
2020-06-27 11:02:21 +00:00
a55ece7dac
MAPG-180 don't buffer content output to variable 2020-06-27 12:56:47 +02:00
efdd5c54be
MAPG-180 ability to inline CSS and JS assets 2020-06-27 12:51:00 +02:00
825e0744ec
MAPG-180 rename pagescript to pageScript 2020-06-27 12:47:13 +02:00
18bf9aaade
MAPG-180 declare CSS and JS files 2020-06-27 02:25:54 +02:00
5e645a1313 Merged in hotfix/MAPG-184-show-runtime (pull request #158)
MAPG-184 measure runtime and show in web output
2020-06-26 23:21:02 +00:00
9c9edcbbd1
MAPG-184 measure runtime and show in web output 2020-06-27 01:19:17 +02:00
de009637de Merged in feature/MAPG-89-create-a-better-templating-engine (pull request #156)
Feature/MAPG-89 create a better templating engine
2020-06-26 22:50:14 +00:00
6fce002458
MAPG-89 unify styles 2020-06-27 00:47:14 +02:00
1287fa0bc9
MAPG-89 rewrite views and view templates to the new concept 2020-06-27 00:47:14 +02:00
bf8e5b5f4d
MAPG-89 adaptations in HtmlContent and index.php 2020-06-27 00:44:38 +02:00
c8a49396c2
MAPG-89 install and update sh scripts to link views in non-DEV mode 2020-06-27 00:44:38 +02:00
c2323c4c89
MAPG-89 add command that can link views from command line 2020-06-27 00:44:38 +02:00
60cbe64718
MAPG-89 add cache folder that can hold cache files 2020-06-27 00:44:38 +02:00
c48bbdc3c2
MAPG-89 add classes that can link views into one PHP 2020-06-27 00:44:38 +02:00
ee6aa59253 Merged in bugfix/MAPG-183-tooling-fixes (pull request #157)
MAPG-183 tooling fixes
2020-06-26 22:40:14 +00:00
e1df28123f
MAPG-183 tooling fixes 2020-06-27 00:38:19 +02:00
b4769b62dd Merged in feature/MAPG-177-create-unit-tests-for-all-units (pull request #155)
Feature/MAPG-177 create unit tests for all units
2020-06-25 22:33:56 +00:00
bee991e300 MAPG-177 add test for Model 2020-06-26 00:28:37 +02:00
ddc56426c2 MAPG-177 add test for GoogleOAuth 2020-06-26 00:28:37 +02:00
c06dd1e1d2 MAPG-177 refactor GoogleOAuth, Http\Request, Http\Response to be more testable 2020-06-26 00:28:37 +02:00
fd657b4244 MAPG-177 add test for JwtParser 2020-06-26 00:28:37 +02:00
532f099ec9 Merged in feature/MAPG-180-unify-inline-js (pull request #154)
Feature/MAPG-180 unify inline js
2020-06-25 20:41:47 +00:00
ec969167e4 MAPG-180 standardize form submit with AJAX 2020-06-25 22:35:04 +02:00
e4a51c139f MAPG-180 improve form functionality 2020-06-25 22:34:32 +02:00
27115e44b6 MAPG-180 extract all inline JS into separate JS file 2020-06-25 20:42:12 +02:00
45904a98c2 Merged in feature/MAPG-156-delete-account-functionality (pull request #153)
Feature/MAPG-156 delete account functionality
2020-06-25 18:30:42 +00:00
b1ae7391e7 MAPG-156 implement user deletion 2020-06-25 20:26:51 +02:00
8987b563dd MAPG-156 add getByUser to UserConfirmationRepository 2020-06-25 20:26:51 +02:00
70d8807f38 MAPG-156 extend PDM to be able to return with Generator 2020-06-25 20:26:51 +02:00
910fdddf34 MAPG-156 adapt account's JS to the new helper functions 2020-06-25 20:26:51 +02:00
87c4c06aa6 MAPG-156 add some form helper functions 2020-06-25 20:26:51 +02:00
ad24f8ac28 MAPG-156 rename profile to account 2020-06-25 16:44:34 +02:00
e2067627c4 Merged in feature/MAPG-179-extend-the-readme (pull request #152)
MAPG-179 add basic stuff to the README
2020-06-25 13:33:27 +00:00
a8419535e1 MAPG-179 export MariaDB server port 2020-06-25 15:30:38 +02:00
725d14400b MAPG-179 add basic stuff to the README 2020-06-25 15:30:21 +02:00
b75eb16b3d Merged in feature/MAPG-178-add-google-analytics (pull request #151)
Feature/MAPG-178 add google analytics
2020-06-25 12:06:43 +00:00
f8a570d0d5 MAPG-178 add Google Analytics
style adaptations
cookie notice improvements
2020-06-25 14:02:39 +02:00
e7ee7bbe8e MAPG-178 add endpoint that can send valid data based on started session 2020-06-25 14:01:17 +02:00
2f4f66cc94 MAPG-178 make style compatible with cookie notice on the top of the page 2020-06-25 14:00:38 +02:00
4afbc046ff MAPG-178 add GOOGLE_ANALITICS_ID to .env.example 2020-06-25 13:57:05 +02:00
79bd5b98d5 Merged in feature/MAPG-94-unittest-output (pull request #150)
MAPG-94 save test results and save only the result xml as artifact
2020-06-25 08:17:45 +00:00
c1cdeba1d3 MAPG-94 save test results and save only the result xml as artifact 2020-06-25 10:16:28 +02:00
255168ffb6 Merged in bugfix/MAPG-94-introduce-automatic-unit-testing (pull request #147)
MAPG-94 fix possible syntax error in yml
2020-06-24 22:45:01 +00:00
ca0b8da733 MAPG-94 fix possible syntax error in yml 2020-06-25 00:44:24 +02:00
34206cac12 Merged in feature/MAPG-94-introduce-automatic-unit-testing (pull request #146)
Feature/MAPG-94 introduce automatic unit testing
2020-06-24 22:38:36 +00:00
47a2f15fc5 MAPG-94 don't install Composer dev packages during update 2020-06-25 00:37:48 +02:00
17658f70ba MAPG-94 create Bitbucket pipeline for unit testing 2020-06-25 00:35:36 +02:00
500ec958cb MAPG-94 add tests for Util\Geo 2020-06-25 00:35:36 +02:00
d2fa2050ac MAPG-94 install php-xml to Docker because it is a dependency of PHPUnit
install unzip as well because it is sometimes needed
2020-06-25 00:35:36 +02:00
e8be433ee6 MAPG-94 add PHPUnit to composer.json 2020-06-25 00:27:01 +02:00
506072017c Merged in hotfix/MAPG-172-create-cookie-notice (pull request #145)
MAPG-172 make variable local
2020-06-24 21:22:05 +00:00
4561c825a4 MAPG-172 make variable local 2020-06-24 23:21:14 +02:00
693bddbcf0 Merged in hotfix/MAPG-172-create-cookie-notice (pull request #144)
MAPG-172 don't close the cookie DIV if not the agree button was clicked
2020-06-24 21:01:34 +00:00
eae7cefae1 MAPG-172 don't close the cookie DIV if not the agree button was clicked 2020-06-24 23:00:26 +02:00
212728e138 Merged in bugfix/MAPG-172-create-cookie-notice (pull request #143)
MAPG-172 quote I agree
2020-06-24 20:55:36 +00:00
0d627f297e MAPG-172 quote I agree 2020-06-24 22:55:13 +02:00
f07ae979a2 Merged in feature/MAPG-175-unify-styles (pull request #142)
MAPG-175 align noPano middle and center as other elements
2020-06-24 20:37:59 +00:00
8bdbbafcff MAPG-175 align noPano middle and center as other elements 2020-06-24 22:37:33 +02:00
3856ccb576 Merged in feature/MAPG-174-adjust-map-description-style (pull request #141)
MAPG-174 create separate div for description and align description vertically middle
2020-06-24 20:34:00 +00:00
9f839d3f5d MAPG-174 create separate div for description and align description vertically middle 2020-06-24 22:32:54 +02:00
721b611cdb Merged in feature/MAPG-174-adjust-map-description-style (pull request #140)
MAPG-174 align description center
2020-06-24 20:11:08 +00:00
70fb30f673 MAPG-174 align description center 2020-06-24 22:10:24 +02:00
e200dab2a4 Merged in feature/MAPG-172-create-cookie-notice (pull request #139)
Feature/MAPG-172 create cookie notice
2020-06-24 20:03:29 +00:00
e8c246f002 MAPG-172 add cookie consent DIV and JS handler 2020-06-24 21:57:41 +02:00
251bf1be3d MAPG-172 add new styles 2020-06-24 21:57:40 +02:00
0be20a6097 MAPG-172 don't send session if user didn't consent the cookies 2020-06-24 21:57:40 +02:00
d5d3563ddc Merged in feature/MAPG-169-make-map-boxes-height-identical (pull request #138)
Feature/MAPG-169 make map boxes height identical
2020-06-23 21:44:58 +00:00
19aa4098e5 MAPG-169 set mapContainer's inners' height dynamically to be identical in a row 2020-06-23 23:36:19 +02:00
394c8b8830 MAPG-169 make static map background instead of img 2020-06-23 23:36:19 +02:00
dc4ba4b5a4 Merged in feature/MAPG-168-prevent-home-redirection (pull request #137)
Feature/MAPG-168 prevent home redirection
2020-06-23 18:48:24 +00:00
eda4e00378 MAPG-168 remove headers until a new home page is created 2020-06-23 20:44:26 +02:00
dfc74375d7 MAPG-168 make maps as home temporarily to prevent redirections 2020-06-23 20:41:28 +02:00
dc1aedd28f Merged in hotfix/MAPG-166-response-to-head-requests (pull request #136)
MAPG-166 response to HEAD request with the GET content
2020-06-23 13:27:48 +00:00
787427b211 MAPG-166 response to HEAD request with the GET content 2020-06-23 15:26:53 +02:00
05b1a84168 Merged in bugfix/MAPG-164-fix-mail-sender (pull request #134)
MAPG-164 use env variable for mail sender instead of hardcoded value
2020-06-22 21:37:40 +00:00
e2a0da3dd2 MAPG-164 use env variable for mail sender instead of hardcoded value 2020-06-22 21:37:17 +00:00
e751fdaa3e Merged in feature/MAPG-165-make-version-number-more-nice (pull request #133)
MAPG-165 don't write 'Release_' into footer
2020-06-22 19:17:38 +00:00
675d7e60d0 MAPG-165 don't write 'Release_' into footer 2020-06-22 21:17:02 +02:00
0d097aacb5 Merged in feature/MAPG-164-make-possible-to-rename-application (pull request #132)
MAPG-164 show application's name from env variable
2020-06-22 19:07:39 +00:00
98cd15e91f MAPG-164 show application's name from env variable 2020-06-22 21:06:15 +02:00
4f680db8f3 Merged in bugfix/MAPG-163-fix-modal-when-height-is-low (pull request #131)
MAPG-163 optimize modal height
2020-06-21 21:09:08 +00:00
ea98af0681 MAPG-163 optimize modal sizes 2020-06-21 23:07:47 +02:00
05934d6245 Merged in feature/MAPG-87-create-footer-with-author-copyright (pull request #130)
Feature/MAPG-87 create footer with author copyright
2020-06-21 20:27:40 +00:00
3ab8ac5fe6 MAPG-87 use HTML5 semantic elements, new styles, new footer 2020-06-21 22:25:29 +02:00
94e60e4ac5 MAPG-87 unify some styles 2020-06-21 22:20:04 +02:00
9df5028c20 MAPG-87 introduce footer 2020-06-21 22:18:35 +02:00
49bd03f303 Merged in feature/MAPG-162-add-script-that-can-run-sql-on-db (pull request #129)
MAPG-162 add script that can run external SQLs on the DB
2020-06-21 17:45:23 +00:00
7210e12656 MAPG-162 add script that can run external SQLs on the DB 2020-06-21 19:44:02 +02:00
8320b94210 Merged in feature/MAPG-161-make-static-maps-look-good-on-mobile (pull request #128)
MAPG-161 load static maps with JS and set scale based on devicePixelRatio
2020-06-21 17:37:35 +00:00
28aa762153 MAPG-161 load static maps with JS and set scale based on devicePixelRatio 2020-06-21 19:32:30 +02:00
33eb154112 Merged in feature/MAPG-160-replace-space-with-margin (pull request #127)
MAPG-160 replace space with margin-right next to inline images
2020-06-21 15:37:14 +00:00
a2ca7888f4 MAPG-160 replace space with margin-right next to inline images 2020-06-21 17:36:08 +02:00
e0395132d6 Merged in feature/MAPG-159-update-docker-composer-to-mariadb-103 (pull request #126)
MAPG-159 update MariaDB to 10.3 in docker-compose.yml
2020-06-21 14:07:49 +00:00
aeaf0fe8d3 MAPG-159 update MariaDB to 10.3 in docker-compose.yml 2020-06-21 16:05:14 +02:00
d96ac938f8 Merged in bugfix/MAPG-69-dont-resend-welcome-email (pull request #125)
MAPG-69 welcome email shouldn't be sent to existing user
2020-06-21 14:03:28 +00:00
cb5229a5e8 MAPG-69 welcome email shouldn't be sent to existing user 2020-06-21 16:01:46 +02:00
5a4fe48a9e Merged in bugfix/MAPG-69-add-env-example (pull request #124)
MAPG-69 add Google OAuth environment variables to .env.example
2020-06-21 14:01:12 +00:00
02d6298d95 MAPG-69 add Google OAuth environment variables to .env.example 2020-06-21 15:51:52 +02:00
a56ce849c0 Merged in feature/MAPG-69-implement-google-registration-login (pull request #123)
Feature/MAPG-69 implement google registration login
2020-06-21 13:48:28 +00:00
87d476065d MAPG-69 fix routing to handle emptry string properly 2020-06-21 15:44:40 +02:00
df60efbb8c MAPG-69 group routes to better overview 2020-06-21 15:43:49 +02:00
6338e35cfb MAPG-69 ability to cancel Google signup 2020-06-21 15:29:02 +02:00
1cfbef418e MAPG-69 add missing return types in LoginController 2020-06-21 15:19:36 +02:00
a8777b897b MAPG-69 ability to reset signup filled after login attempt 2020-06-21 15:12:45 +02:00
285f2dd0ac MAPG-69 login user when trying to sign up 2020-06-21 15:12:45 +02:00
10b7766458 MAPG-69 redirect to sign up when user not found during login 2020-06-21 15:12:45 +02:00
9697163457 MAPG-69 merge SignupController to LoginController and introduce Google login/sign up 2020-06-21 15:12:45 +02:00
d1c9e221f7 MAPG-69 add Google OAuth handler 2020-06-21 15:12:44 +02:00
2d2e218002 MAPG-69 add simple JWT parser 2020-06-21 15:12:44 +02:00
7ba11f34cc MAPG-69 add new field to User 2020-06-21 15:12:44 +02:00
99c72d99be MAPG-69 make Redirect able to redirect to external sources 2020-06-21 15:12:44 +02:00
295570a28d MAPG-69 fix missing relations in UserConfirmation 2020-06-21 01:21:42 +02:00
e612d0d93f Merged in feature/MAPG-155-adjust-minzoom-and-maxzoom-to-z (pull request #122)
MAPG-155 minZoom and maxZoom should depend on zoomOffset
2020-06-20 00:15:18 +00:00
a2d2b94cc6 MAPG-155 minZoom and maxZoom should depend on zoomOffset 2020-06-20 02:14:17 +02:00
80b850fdd7 Merged in feature/MAPG-140-solve-persistent-model-anomalies (pull request #121)
Feature/MAPG-140 solve persistent model anomalies
2020-06-20 00:10:48 +00:00
0bf6e900fe MAPG-140 add UserConfirmation model and repository and adapt SignupController 2020-06-20 02:03:11 +02:00
bd46809d3a MAPG-140 adapt PlaceRepository and classes that use it to use persistent model 2020-06-20 02:03:11 +02:00
821a9d80c0 MAPG-140 adapt MapRepository and classes that use it to use persistent model 2020-06-20 02:03:11 +02:00
eafa98571e MAPG-140 add Map persistent model 2020-06-20 02:03:11 +02:00
316e39f2f2 MAPG-140 add Place persistent model 2020-06-20 02:03:11 +02:00
5ff720e3c6 MAPG-140 MAPG-154 store user in session based on id 2020-06-20 02:03:11 +02:00
a9eec05288 MAPG-140 refactor user handling to use the PersistentDataManager 2020-06-20 02:03:11 +02:00
d6750777c2 MAPG-140 add PersistentDataManager and refactor of models 2020-06-20 01:48:20 +02:00
1a4d928143 Merged in bugfix/MAPG-149-fix-cover (pull request #120)
MAPG-149 fix z-indexes
2020-06-18 14:38:03 +00:00
b35668c2fb MAPG-149 fix z-indexes 2020-06-18 16:37:32 +02:00
5814620b4f Merged in bugfix/MAPG-149-fix-new-client-side-bugs (pull request #119)
Bugfix/MAPG-149 fix new client side bugs
2020-06-18 14:28:44 +00:00
64b6197bd4 MAPG-149 omit focusing/selecting input elements after processing 2020-06-18 16:28:01 +02:00
1be51b3e07 MAPG-149 make current password the first input element 2020-06-18 16:23:30 +02:00
43775c8f4f Merged in bugfix/MAPG-148-fix-xmlhttp-responsetype-error (pull request #118)
MAPG-148 set xhr responseType after xhr open() because it is accepted by all browsers
2020-06-18 14:21:30 +00:00
95eb956403 Merged in bugfix/MAPG-152-save-user-to-session-after-modi (pull request #117)
MAPG-152 save modified user to session
2020-06-18 14:21:23 +00:00
ce96e15911 MAPG-148 set xhr responseType after xhr open() because it is accepted by all browsers 2020-06-18 16:20:45 +02:00
3726573f4c MAPG-152 save modified user to session 2020-06-18 16:18:41 +02:00
a8913b0d44 Merged in bugfix/MAPG-151-check-email-address-validity-on (pull request #116)
Bugfix/MAPG-151 check email address validity on
2020-06-18 14:16:32 +00:00
e014bf9dc1 MAPG-151 add some client side input checks 2020-06-18 16:15:03 +02:00
50c7e3972c MAPG-151 check email address validity on server side 2020-06-18 16:12:40 +02:00
181 changed files with 10813 additions and 3756 deletions

View File

@ -1,3 +1,5 @@
APP_NAME=MapGuesser
APP_URL=mapguesser.dev
DEV=1
DB_HOST=mariadb
DB_USER=mapguesser
@ -6,8 +8,19 @@ DB_NAME=mapguesser
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url
LEAFLET_TILESERVER_SUBDOMAINS=list_of_subdomains_for_the_tileserver_without_separators
LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles
STATIC_ROOT=/static
MAIL_FROM=mapguesser@mapguesser-dev.ch
MAIL_HOST=mail
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
ENABLE_GAME_FOR_GUESTS=0
RECAPTCHA_SITEKEY=your_recaptcha_sitekey
RECAPTCHA_SECRET=your_recaptcha_secret

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.env
installed
vendor
node_modules

8
.vscode/launch.json vendored
View File

@ -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"
}
]
}

106
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,106 @@
pipeline {
agent {
node {
label 'mapguesser'
customWorkspace 'workspace/mapguesser'
}
}
stages {
stage('Install composer') {
environment {
COMPOSER_HOME="${WORKSPACE}/.composer"
}
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
steps {
sh 'composer install'
}
}
stage('Unit Testing') {
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
steps {
sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests'
}
post {
always {
archiveArtifacts 'unit_test_results.xml'
}
}
}
stage('Static Code Analysis') {
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
steps {
sh 'php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json'
}
post {
always {
archiveArtifacts 'static_code_analysis_results.json'
}
}
}
stage('Prepare Docker release') {
environment {
COMPOSER_HOME="${WORKSPACE}/.composer"
npm_config_cache="${WORKSPACE}/.npm"
}
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
steps {
script {
sh script: 'git clean -ffdx', label: 'Clean repository'
env.VERSION = sh(script: 'git describe --tags --always --match "Release_*" HEAD', returnStdout: true).trim()
sh script: 'docker/scripts/release.sh', label: 'Release script'
sh script: "rm -rf ${env.COMPOSER_HOME} ${env.npm_config_cache}"
}
}
}
stage('Release Docker image') {
steps {
script {
withDockerRegistry([credentialsId: 'gitea-system-user', url: 'https://git.esoko.eu/']) {
sh script: 'docker buildx create --use --bootstrap --platform=linux/arm64,linux/amd64 --name multi-platform-builder'
sh script: """docker buildx build \
--platform linux/amd64,linux/arm64 \
-f docker/Dockerfile \
--target mapg_release \
-t git.esoko.eu/esoko/mapguesser:${env.VERSION} \
--push \
.""",
label: 'Build Docker image'
}
}
}
}
}
}

104
README.md
View File

@ -1,5 +1,105 @@
# MapGuesser
This is the MapGuesser Application project.
[![Build Status](https://ci.esoko.eu/job/mapguesser/job/develop/badge/icon)](https://ci.esoko.eu/job/mapguesser/job/develop/)
License: GNU AGPL 3.0
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.
## Installation
### 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. Check `.env.example` for reference.
**Important: `DEV` should NOT be set for production! See section Development if you want to use the application in development mode.**
#### API keys
**You should set 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/**
Required Google APIs:
* **Maps JavaScript API**: for the interactive maps and street views
* **Maps Static API**: for the static map images
* **Street View Static API**: for the backend metadata requests
Required API keys:
* **GOOGLE_MAPS_SERVER_API_KEY**: this it used by the backend and should have access to **Street View Static API**
* **GOOGLE_MAPS_JS_API_KEY**: this is used by the frontend and should have access to **Maps JavaScript API** and **Maps Static API**
Additionally, a tile provider is also needed for map editor. This should be configured by `LEAFLET_TILESERVER_URL`, `LEAFLET_TILESERVER_SUBDOMAINS` and `LEAFLET_TILESERVER_ATTRIBUTION`. You can find some providers here: https://wiki.openstreetmap.org/wiki/Tile_servers. OpenStreetMap's tile server is fine for testing.
Example:
```
LEAFLET_TILESERVER_URL=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
LEAFLET_TILESERVER_SUBDOMAINS=abc
LEAFLET_TILESERVER_ATTRIBUTION="&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
```
### Docker Compose
Create a `docker-compose.yml` file. The example code below assumes that `.env` is placed in the same folder.
```yml
version: '3'
services:
app:
image: git.esoko.eu/esoko/mapguesser:latest
depends_on:
mariadb:
condition: service_healthy
ports:
- 80:80
- 8090:8090
volumes:
- .env:/var/www/mapguesser/.env
mariadb:
image: mariadb:10.3
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: 'root'
MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser'
MYSQL_PASSWORD: 'mapguesser'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
volumes:
mysql:
```
Execute the following command:
```bash
docker compose up -d
```
**And you are done!** The application is ready to use. You can create the first administrative user with the following command after attaching to the `app` container:
```
./mapg user:add EMAIL USERNAME PASSWORD admin
```
## Development
### Set environment variables
`.env.example` should be copied to `.env` into the repo root. Only the variables for external dependencies (API keys, map attribution, etc.) should be adapted in. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value. **`DEV=1` should be set for development!**
### Docker Compose
Execute the following command from the repo root:
```bash
docker compose up -d
```
**And you are done!** You can reach the application 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, or you can use Adminer web interface on http://localhost:9090
You might have to attach to the `app` container, e.g. for creating users, `composer update`, etc.
---
*License: **GNU AGPL 3.0**. Full license text can be found in file `LICENSE`.*

26
bitbucket-pipelines.yml Normal file
View File

@ -0,0 +1,26 @@
image: php:7.4.7-cli-buster
pipelines:
default:
- step:
name: Unit Testing
caches:
- composer
artifacts:
- unit_test_results.xml
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
- 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

2
cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -3,12 +3,19 @@
"type": "project",
"description": "MapGuesser Application",
"license": "GNU GPL 3.0",
"repositories": [
{
"url": "https://git.esoko.eu/esoko/soko-web.git",
"type": "git"
}
],
"require": {
"vlucas/phpdotenv": "^4.1",
"symfony/console": "^5.1",
"phpmailer/phpmailer": "^6.1"
"esoko/soko-web": "0.15"
},
"require-dev": {
"phpunit/phpunit": "^10.3",
"phpstan/phpstan": "^1.10"
},
"require-dev": {},
"autoload": {
"psr-4": {
"MapGuesser\\": "src"

2426
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,14 @@ CREATE TABLE `maps` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `migrations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) NOT NULL,
`type` enum('structure', 'data') NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
DROP TABLE IF EXISTS `places`;
CREATE TABLE `places` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,

View File

@ -1 +0,0 @@
<?php //empty on purpose

View File

@ -1,8 +1,8 @@
<?php
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
use MapGuesser\Util\Geo\Bounds;
$select = new Select(\Container::$dbConnection, 'maps');

View File

@ -1,8 +1,8 @@
<?php
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(['id']);

View File

@ -0,0 +1,20 @@
<?php
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Util\UsernameGenerator;
use SokoWeb\Database\Query\Select;
$select = new Select(Container::$dbConnection);
$users = Container::$persistentDataManager->selectMultipleFromDb($select, User::class);
$userRepository = new UserRepository();
$usernameGenerator = new UsernameGenerator();
foreach ($users as $user) {
do {
$username = $usernameGenerator->generate();
} while ($userRepository->getByUsername($username));
$user->setUsername($username);
Container::$persistentDataManager->saveToDb($user);
}

View File

@ -1,6 +0,0 @@
CREATE TABLE `migrations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) NOT NULL,
`type` enum('structure', 'data') NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,8 @@
ALTER TABLE
`users`
ADD
`google_sub` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NULL DEFAULT NULL,
ADD
UNIQUE `google_sub` (`google_sub`),
MODIFY
`password` varchar(60) NULL DEFAULT NULL;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,2 @@
ALTER TABLE `sessions`
MODIFY `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@ -0,0 +1,4 @@
UPDATE `sessions` SET id=SUBSTRING(id, 1, 32);
ALTER TABLE `sessions`
MODIFY `id` varchar(32) CHARACTER SET ascii NOT NULL;

View File

@ -0,0 +1,2 @@
ALTER TABLE `users`
ADD `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@ -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;

View File

@ -0,0 +1,12 @@
CREATE TABLE `user_played_place` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`place_id` int(10) unsigned NOT NULL,
`last_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`occurrences` int(10) NOT NULL DEFAULT 1,
PRIMARY KEY(`id`),
KEY `user_id` (`user_id`),
KEY `place_id` (`place_id`),
CONSTRAINT `user_played_place_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `user_played_place_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,54 @@
CREATE TABLE `challenges` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`token` int(10) unsigned NOT NULL,
`time_limit` int(10) unsigned,
`time_limit_type` enum('game', 'round') NOT NULL DEFAULT 'game',
`no_move` tinyint(1) NOT NULL DEFAULT 0,
`no_pan` tinyint(1) NOT NULL DEFAULT 0,
`no_zoom` tinyint(1) NOT NULL DEFAULT 0,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `user_in_challenge` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`challenge_id` int(10) unsigned NOT NULL,
`current_round` smallint(5) signed NOT NULL DEFAULT 0,
`time_left` int(10) unsigned,
`is_owner` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `challenge_id` (`challenge_id`),
CONSTRAINT `user_in_challenge_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `user_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `place_in_challenge` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`place_id` int(10) unsigned NOT NULL,
`challenge_id` int(10) unsigned NOT NULL,
`round` smallint(5) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `place_id` (`place_id`),
KEY `challenge_id` (`challenge_id`),
CONSTRAINT `place_in_challenge_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`),
CONSTRAINT `place_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`),
CONSTRAINT `unique_order_in_challenge` UNIQUE (`round`, `challenge_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `guesses` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`place_in_challenge_id` int(10) unsigned NOT NULL,
`lat` decimal(8,6) NOT NULL,
`lng` decimal(9,6) NOT NULL,
`score` int(10) NOT NULL,
`distance` int(10) NOT NULL,
`time_spent` int(10),
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `place_in_challenge_id` (`place_in_challenge_id`),
CONSTRAINT `guesses_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `guesses_place_in_challenge_id` FOREIGN KEY (`place_in_challenge_id`) REFERENCES `place_in_challenge` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,2 @@
ALTER TABLE `maps`
ADD `unlisted` TINYINT(1) NOT NULL DEFAULT 0;

View File

@ -0,0 +1,3 @@
ALTER TABLE `users`
ADD `username` VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL AFTER `email`,
ADD UNIQUE `username` (`username`);

View File

@ -1,16 +1,25 @@
version: '3'
services:
app:
build: ./docker
build:
context: .
dockerfile: docker/Dockerfile
target: mapg_dev
depends_on:
mariadb:
condition: service_healthy
ports:
- 80:80
- 5000:5000
- 8090:8090
- 9229:9229
volumes:
- .:/var/www/mapguesser
links:
- 'mariadb'
- 'mail'
working_dir: /var/www/mapguesser
mariadb:
image: mariadb:10.1
image: mariadb:10.3
ports:
- 3306:3306
volumes:
- mysql:/var/lib/mysql
environment:
@ -18,6 +27,19 @@ services:
MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser'
MYSQL_PASSWORD: 'mapguesser'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
adminer:
image: adminer:4.8.1-standalone
ports:
- 9090:8080
environment:
- ADMINER_DEFAULT_SERVER=mariadb
mail:
image: marcopas/docker-mailslurper:latest
ports:

View File

@ -1,30 +1,44 @@
FROM ubuntu:focal
FROM ubuntu:22.04 AS mapg_base
ENV DEBIAN_FRONTEND noninteractive
# Install Nginx, PHP and further necessary packages
RUN apt update --fix-missing
RUN apt install -y curl git mariadb-client nginx \
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip
RUN apt update --fix-missing && apt install -y sudo curl git unzip mariadb-client nginx \
php-apcu php8.1-cli php8.1-curl php8.1-fpm php8.1-mbstring php8.1-mysql php8.1-zip php8.1-xml
# Configure Nginx with PHP
RUN mkdir -p /run/php
COPY configs/nginx.conf /etc/nginx/sites-available/default
RUN echo "xdebug.remote_enable = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_autostart = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_connect_back = 1" >> /etc/php/7.4/mods-available/xdebug.ini
COPY docker/configs/nginx.conf /etc/nginx/sites-available/default
# Install Composer
COPY scripts/install-composer.sh install-composer.sh
COPY docker/scripts/install-composer.sh install-composer.sh
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
COPY docker/scripts/install-nodejs.sh install-nodejs.sh
RUN ./install-nodejs.sh
RUN npm install -g uglify-js clean-css-cli svgo yarn
FROM mapg_base AS mapg_dev
RUN apt update --fix-missing && apt install -y php-xdebug
RUN echo "xdebug.remote_enable = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_autostart = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_connect_back = 1" >> /etc/php/8.1/mods-available/xdebug.ini
EXPOSE 80
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
EXPOSE 5000
EXPOSE 8090
EXPOSE 9229
ENTRYPOINT docker/scripts/entry-point-dev.sh
ENTRYPOINT /usr/sbin/php-fpm7.4 -F & /usr/sbin/nginx -g 'daemon off;'
FROM mapg_base AS mapg_release
RUN apt update --fix-missing && apt install -y cron
WORKDIR /var/www/mapguesser
COPY ./ /var/www/mapguesser
RUN rm -rf /var/www/mapguesser/.git
EXPOSE 80
EXPOSE 8090
ENTRYPOINT docker/scripts/entry-point.sh

View File

@ -1,11 +1,15 @@
map $http_x_forwarded_proto $forwarded_scheme {
default $scheme;
http http;
https https;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/mapguesser/public;
index index.php index.html index.htm index.nginx-debian.html;
server_name mapguesser-dev.ch;
location / {
@ -14,7 +18,8 @@ server {
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param REQUEST_SCHEME $forwarded_scheme;
}
location ~ /\.ht {

1
docker/scripts/cron Normal file
View File

@ -0,0 +1 @@
0 * * * * /var/www/mapguesser/mapg db:maintain

View File

@ -0,0 +1,40 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
if [ -f .env ]; then
composer install
else
composer create-project
fi
echo "Installing NPM packages..."
(cd multi && npm install)
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Migrating DB..."
./mapg db:migrate
echo "Set runner user based on owner of .env..."
if ! getent group mapg; then
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID mapg
fi
if ! id -u mapg; then
USER_UID=$(stat -c "%u" .env)
useradd --uid $USER_UID --gid $USER_GID mapg
fi
sed -i -e "s/^user = .*$/user = mapg/g" -e "s/^group = .*$/group = mapg/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
sudo -u mapg -g mapg /usr/bin/node --inspect=0.0.0.0:9229 multi &
wait -n
exit $?

32
docker/scripts/entry-point.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
set -e
echo "Migrating DB..."
./mapg db:migrate
echo "Installing crontab..."
/usr/bin/crontab docker/scripts/cron
echo "Set runner user based on owner of .env..."
if ! getent group mapg; then
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID mapg
fi
if ! id -u mapg; then
USER_UID=$(stat -c "%u" .env)
useradd --uid $USER_UID --gid $USER_GID mapg
fi
chown mapg:mapg cache
sed -i -e "s/^user = .*$/user = mapg/g" -e "s/^group = .*$/group = mapg/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/cron -f &
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
sudo -u mapg -g mapg /usr/bin/node multi &
wait -n
exit $?

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
apt update
apt install -y ca-certificates curl gnupg
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=18
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
apt update
apt install -y nodejs

30
docker/scripts/release.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
composer create-project --no-dev
echo "Installing NPM packages..."
(cd multi && npm install)
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php
echo "Minifying JS, CSS and SVG files..."
find public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
find public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
find public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
echo "Linking view files..."
./mapg view:link
rm .env

13
mail/password-reset.html Normal file
View File

@ -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>

View File

@ -0,0 +1,9 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this Google account ({{EMAIL}}).
<br><br>
Have fun on {{APP_NAME}}!
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>

View File

@ -1,13 +1,18 @@
Hi,
<br><br>
You recently signed up on MapGuesser 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 MapGuesser 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 MapGuesser!
Have fun on {{APP_NAME}}!
<br><br>
Regards,<br>
MapGuesser
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>

View File

@ -1,5 +1,7 @@
<?php
define('SCRIPT_STARTED', hrtime(true));
require 'vendor/autoload.php';
const ROOT = __DIR__;
@ -12,10 +14,12 @@ $dotenv->load();
class Container
{
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
static MapGuesser\Routing\RouteCollection $routeCollection;
static \SessionHandlerInterface $sessionHandler;
static MapGuesser\Interfaces\Request\IRequest $request;
static SokoWeb\Interfaces\Database\IConnection $dbConnection;
static SokoWeb\Interfaces\PersistentData\IPersistentDataManager $persistentDataManager;
static SokoWeb\Interfaces\Routing\IRouteCollection $routeCollection;
static SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler;
static SokoWeb\Interfaces\Request\IRequest $request;
}
Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
Container::$dbConnection = new SokoWeb\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
Container::$persistentDataManager = new SokoWeb\PersistentData\PersistentDataManager(Container::$dbConnection);

4
mapg
View File

@ -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
multi/index.js Normal file
View File

@ -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);

43
multi/package-lock.json generated Normal file
View File

@ -0,0 +1,43 @@
{
"name": "mapguesser-multi",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mapguesser-multi",
"license": "GNU AGPL 3.0",
"dependencies": {
"dotenv": "^8.2.0",
"ws": "^7.4.4"
}
},
"node_modules/dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"engines": {
"node": ">=8"
}
},
"node_modules/ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

13
multi/package.json Normal file
View File

@ -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"
}

9
phpstan.neon Normal file
View File

@ -0,0 +1,9 @@
parameters:
level: 5
checkMissingIterableValueType: false
paths:
- main.php
- mapg
- web.php
- src
- tests

View File

@ -1,51 +1,3 @@
<?php
require '../web.php';
$method = strtolower($_SERVER['REQUEST_METHOD']);
$url = substr($_SERVER['REQUEST_URI'], strlen('/'));
if (($pos = strpos($url, '?')) !== false) {
$url = substr($url, 0, $pos);
}
$url = rawurldecode($url);
$match = Container::$routeCollection->match($method, explode('/', $url));
if ($match !== null) {
list($route, $params) = $match;
Container::$request->setParsedRouteParams($params);
$handler = $route->getHandler();
$controller = new $handler[0](Container::$request);
if ($controller instanceof MapGuesser\Interfaces\Authorization\ISecured) {
$authorized = $controller->authorize();
} else {
$authorized = true;
}
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
header('Content-Type: text/html; charset=UTF-8', true, 403);
echo json_encode(['error' => 'no_valid_anti_csrf_token']);
return;
}
if ($authorized) {
$response = call_user_func([$controller, $handler[1]]);
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
echo $response->render();
return;
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
header('Location: ' . Container::$request->getBase() . '/' . $response->getUrl(), true, $response->getHttpCode());
return;
}
}
}
header('Content-Type: text/html; charset=UTF-8', true, 404);
require ROOT . '/views/error/404.php';

View File

@ -1,17 +1,31 @@
#panorama {
width: 100%;
height: calc(100% - 40px);
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
#guessCover {
#panoCover {
position: absolute;
top: 40px;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: #000000;
opacity: 0.5;
z-index: 4;
}
#panningBlockerCover {
display: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
opacity: 0;
z-index: 3;
}
@ -19,7 +33,7 @@
position: absolute;
bottom: 30px;
right: 20px;
z-index: 2;
z-index: 3;
}
#guess.result {
@ -76,7 +90,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;
}
@ -99,19 +113,111 @@
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;
}
#navigation {
z-index: 2;
}
#goToStart {
display: none;
}
#highscoresTable {
margin: 1em;
border-collapse: collapse;
width: 90%;
}
#highscoresTable td, #highscoresTable th {
border: 1px solid #ddd;
padding: 8px;
}
#highscoresTable tr:nth-child(even) {
background-color: #f2f2f2;
}
#highscoresTable tr:hover {
background-color: #ddd;
}
#highscoresTable th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #e8a349;
color: white;
}
#highscoresTable tr.ownPlayer {
font-weight: 500;
}
@media screen and (max-width: 899px) {
.hideOnNarrowScreen {
display: none;
}
}
@media screen and (max-width: 599px) {
#mapName {
display: none;
}
#showGuessButtonContainer {
position: absolute;
left: 20px;
left: 65px;
bottom: 30px;
right: 20px;
z-index: 2;
}
#guess {
top: 50px;
top: 10px;
left: 20px;
opacity: 0.95;
visibility: hidden;
@ -122,6 +228,10 @@
#scoreBarBase {
width: 100%;
}
#navigation {
bottom: 25px;
left: 10px;
}
}
@media screen and (min-width: 600px) {
@ -157,7 +267,7 @@
#guess.result {
width: initial;
height: initial;
top: 50px;
top: 10px;
left: 50px;
right: 50px;
bottom: 50px;
@ -165,13 +275,17 @@
#scoreBarBase {
width: 60%;
}
#navigation {
bottom: 50px;
left: 20px;
}
@media screen and (max-height: 424px) {
#guess {
top: 50px;
top: 10px;
height: initial;
}
#guess.adapt:hover {
top: 50px;
top: 10px;
height: initial;
}
#guess.result {
@ -179,5 +293,9 @@
right: 20px;
bottom: 30px;
}
#navigation {
bottom: 30px;
left: 10px;
}
}
}

View File

@ -1,9 +1,20 @@
#map {
width: 100%;
height: calc(100% - 40px);
.map {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
#mapSelection img {
display: inline;
width: 1em;
height: 1em;
vertical-align: -0.15em;
}
/* modify the cursor for the Leaflet map */
.leaflet-container {
cursor: crosshair;
}
@ -16,6 +27,8 @@
#noPano {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 2;
visibility: hidden;
@ -23,12 +36,12 @@
}
#noPano>p {
margin: auto;
text-align: center;
}
#control {
position: absolute;
top: 50px;
top: 10px;
right: 10px;
width: 125px;
z-index: 3;
@ -42,37 +55,49 @@
visibility: hidden;
}
#overlayControl {
bottom: 20px;
right: 10px;
z-index: 3;
}
#deleteButton {
display: none;
}
@media screen and (max-width: 999px) and (min-height: 600px) {
#map.selected {
height: calc(50% - 20px);
height: 50%;
}
#panorama, #noPano {
left: 0;
bottom: 0;
right: 0;
height: calc(50% - 20px);
height: 50%;
}
#placeControl {
top: calc(50% + 30px);
top: calc(50% + 10px);
}
.hideOnMobile {
display: none;
}
}
@media screen and (min-width: 1000px), (max-height: 599px) {
#map.selected {
top: 0;
bottom: 0;
left: 0;
width: 50%;
}
#panorama, #noPano {
top: 40px;
top: 0;
bottom: 0;
right: 0;
width: 50%;
}
#placeControl {
top: 50px;
top: 10px;
}
#modified.selected {
right: calc(50% + 10px);

View File

@ -12,21 +12,30 @@ html, body {
padding: 0;
}
body {
background-color: #cccccc;
}
button::-moz-focus-inner, input::-moz-focus-inner {
padding: 0;
border: 0;
}
/* to be compatible with browsers that don't know <main> */
main {
display: block;
}
::selection {
background-color: #28a745;
color: #ffffff;
}
p, h1, h2, input, textarea, select, button, a {
p, h1, h2, h3, input, textarea, select, button, a, table, label {
font-family: 'Roboto', sans-serif;
}
h1, h2 {
h1, h2, h3 {
font-weight: 500;
}
@ -42,11 +51,15 @@ h1>a:hover, h1>a:focus {
text-decoration: none;
}
h2, div.header.small h1 {
h2, header.small h1 {
font-size: 24px;
}
p, h2 {
h3 {
font-size: 18px;
}
p, h2, h3 {
line-height: 150%;
}
@ -83,8 +96,8 @@ hr {
font-weight: 500;
}
.small {
font-size: 12px;
p.small, span.small {
font-size: 14px;
}
.justify {
@ -107,6 +120,10 @@ hr {
margin-right: 10px;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
@ -115,6 +132,7 @@ svg.inline, img.inline {
display: inline;
width: 1em;
height: 1em;
margin-right: 0.3em;
vertical-align: -0.15em;
}
@ -143,6 +161,27 @@ button, a.button {
line-height: 35px;
}
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;
@ -174,7 +213,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;
}
@ -182,7 +221,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;
}
@ -190,7 +229,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;
}
@ -198,60 +237,97 @@ 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;
}
input, select, textarea {
input.text, 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.text, select {
height: 30px;
line-height: 30px;
padding: 0 5px;
}
input[type=checkbox], input[type=radio] {
margin-right: 0.5em;
}
textarea {
font-size: 13px;
padding: 5px;
resize: none;
}
input.big, select.big, textarea.big {
padding: 5px;
input.text.big, select.big, textarea.big, div.inputWithButton>input.text {
font-size: 18px;
}
input.text.big, select.big, div.inputWithButton>input.text {
height: 35px;
line-height: 35px;
padding: 0 6px;
}
textarea.big {
padding: 6px;
}
input.fullWidth, select.fullWidth, textarea.fullWidth {
display: block;
width: 100%;
}
input:disabled, select:disabled, textarea:disabled {
input.text:disabled, select:disabled, textarea:disabled {
background-color: #dfdfdf;
border: solid #dfdfdf 1px;
color: #000000;
}
input:focus, select:focus, textarea:focus {
input.text: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.text:focus, select:focus {
padding: 0 4px;
}
textarea:focus {
padding: 4px;
}
input.text.big:focus, select.big:focus {
padding: 0 5px;
}
div.inputWithButton>input.text {
width: 100%;
padding: 0 83px 0 6px;
}
div.inputWithButton>input.text:focus {
padding: 0 82px 0 5px;
}
textarea.big:focus {
padding: 5px;
}
div.modal {
position: fixed;
top: 100px;
background-color: #ffffff;
border-radius: 3px;
padding: 20px;
box-sizing: border-box;
z-index: 3;
overflow-y: auto;
z-index: 6;
visibility: hidden;
}
@ -263,7 +339,7 @@ div.modal {
right: 0;
background-color: #000000;
opacity: 0.5;
z-index: 2;
z-index: 5;
visibility: hidden;
}
@ -276,7 +352,7 @@ p.formError {
display: none;
}
div.header {
header {
display: grid;
grid-template-columns: auto auto;
background-color: #333333;
@ -286,33 +362,52 @@ div.header {
color: white;
}
div.header.small {
header.small {
height: 40px;
line-height: 40px;
}
div.header>p.header {
header>p {
line-height: inherit;
text-align: right;
}
div.header>p.header>span {
header>p>span {
padding-left: 6px;
}
div.header>p.header>span>a:link, div.header>p.header>span>a:visited {
header>p>span>a:link, header>p>span>a:visited, footer>p>a:link, footer>p>a:visited {
color: inherit;
}
div.header>p.header>span:not(:last-child) {
header>p>span:not(:last-child) {
border-right: solid white 1px;
padding-right: 6px;
}
div.main {
main {
background-color: #ffffff;
padding: 6px 12px;
}
main.full {
position: relative;
width: 100%;
height: calc(100% - 40px);
padding: 0;
}
footer {
background-color: #444444;
padding: 6px 12px;
color: white;
text-align: center;
}
footer>p {
font-size: 13px;
}
div.buttonContainer {
height: 35px;
}
@ -321,6 +416,20 @@ div.buttonContainer>button {
margin: 0 auto;
}
#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 {
position: fixed;
width: 64px;
@ -329,7 +438,7 @@ div.buttonContainer>button {
left: 50%;
margin-top: -32px;
margin-left: -32px;
z-index: 5;
z-index: 7;
visibility: hidden;
}
@ -342,27 +451,128 @@ div.box {
box-sizing: border-box;
}
.circleControl {
position: absolute;
width: 60px;
bottom: 20px;
right: 10px;
}
.circleControl .controlItem {
position: relative;
height: 60px;
margin-top: 10px;
opacity: 70%;
cursor: pointer;
}
.circleControl .controlItem:hover {
opacity: 100%;
}
.circleControl .controlItem div {
position: absolute;
width: 100%;
height: 100%;
}
.circleControl .controlBackground {
width: 100%;
height: 100%;
opacity: 50%;
}
.circleControl .controlIcon {
width: 75%;
height: 75%;
margin: auto;
margin-top: 50%;
transform: translateY(-50%);
}
@media screen and (max-width: 599px) {
div.header h1 span {
header h1 span {
display: none;
}
footer>p:not(:first-child) {
margin-top: 4px;
}
button, a.button {
padding: 0;
width: 100%;
}
button.marginLeft, a.button.marginLeft {
margin-left: 0;
}
button.marginRight, a.button.marginRight {
margin-right: 0;
}
div.modal {
left: 20px;
right: 20px;
padding-left: 15px;
padding-right: 15px;
}
div.box {
width: initial;
}
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
}
@media screen and (min-width: 600px) {
footer>p {
display: inline;
}
footer>p:not(:first-child) {
padding-left: 6px;
}
footer>p:not(:last-child) {
border-right: solid white 1px;
padding-right: 6px;
}
div.modal {
width: 540px;
left: 50%;
margin-left: -270px;
padding-left: 20px;
padding-right: 20px;
}
}
@media screen and (max-height: 399px) {
div.modal {
top: 20px;
max-height: calc(100% - 40px);
padding-top: 10px;
padding-bottom: 10px;
}
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
}
@media screen and (min-height: 400px) and (max-height: 499px) {
div.modal {
top: 50px;
max-height: calc(100% - 100px);
padding-top: 15px;
padding-bottom: 15px;
}
}
@media screen and (min-height: 500px) {
div.modal {
top: 75px;
max-height: calc(100% - 150px);
padding-top: 15px;
padding-bottom: 15px;
}
}

View File

@ -13,12 +13,25 @@ div.mapItem.new {
align-items: center;
}
div.mapItem.unlisted {
opacity: 0.6;
}
div.mapItem>div.title {
background-color: #28a745;
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 {
@ -29,17 +42,16 @@ div.mapItem>div.title>p.title {
div.mapItem>div.imgContainer {
width: 100%;
padding-top: 50%;
background: #cccccc;
}
div.mapItem>div.imgContainer>img {
width: 100%;
margin-top: -50%
background-color: #cccccc;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
div.mapItem>div.inner {
background-color: #eeeeee;
padding: 10px 8px;
box-sizing: border-box;
}
div.mapItem>div.inner>div.info {
@ -47,6 +59,12 @@ div.mapItem>div.inner>div.info {
grid-template-columns: auto auto;
}
div.mapItem>div.inner>div.description {
display: flex;
justify-content: center;
align-items: center;
}
div.mapItem>div.inner>div.info>p:nth-child(1) {
text-align: left;
}
@ -61,6 +79,10 @@ div.mapItem>div.buttonContainer {
grid-auto-flow: column;
}
#timeLimitType {
margin-left: 1.5em;
}
@media screen and (min-width: 1504px) {
#mapContainer {
grid-template-columns: auto auto auto auto;

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 16 16">
<circle cx="50%" cy="50%" r="50%" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 146 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon id="needleNorth" points="50,0 33,50 66,50" style="fill:red;stroke:black;stroke-width:1" />
<polygon id="needleSouth" points="50,100 33,50 66,50" style="fill:white;stroke:black;stroke-width:1" />
Sorry, your browser does not support inline SVG.
</svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@ -0,0 +1,5 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="#3183ce" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293l6-6zm5-.793V6l-2-2V2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5z"/>
<path fill-rule="evenodd" d="M7.293 1.5a1 1 0 0 1 1.414 0l6.647 6.646a.5.5 0 0 1-.708.708L8 2.207 1.354 8.854a.5.5 0 1 1-.708-.708L7.293 1.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 556 B

View File

@ -0,0 +1,4 @@
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103zM10 1.91l-4-.8v12.98l4 .8V1.91zm1 12.98 4-.8V1.11l-4 .8v12.98zm-6-.8V1.11l-4 .8v12.98l4-.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 530 B

BIN
public/static/img/markers/m1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/static/img/markers/m2.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/static/img/markers/m3.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/static/img/markers/m4.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/static/img/markers/m5.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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

View File

@ -0,0 +1,5 @@
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
<path d="m14.12 10.163 1.715.858c.22.11.22.424 0 .534L8.267 15.34a.598.598 0 0 1-.534 0L.165 11.555a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.66zM7.733.063a.598.598 0 0 1 .534 0l7.568 3.784a.3.3 0 0 1 0 .535L8.267 8.165a.598.598 0 0 1-.534 0L.165 4.382a.299.299 0 0 1 0-.535L7.733.063z"/>
<path d="m14.12 6.576 1.715.858c.22.11.22.424 0 .534l-7.568 3.784a.598.598 0 0 1-.534 0L.165 7.968a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.659z"/>
</svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@ -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();
};
})();

View File

@ -0,0 +1,10 @@
(function () {
if (success) {
window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil);
window.close();
} else {
document.getElementById('closeWindowButton').onclick = function () {
window.close();
}
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +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) {
var errorText;
switch (this.response.error) {
case 'user_not_found':
errorText = 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up here</a>!';
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;
form.elements.email.select();
return;
}
window.location.replace('/');
}, formData);
};
})();

View File

@ -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');
});
};
})();

View File

@ -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();
});
};
}
})();

View File

@ -7,7 +7,6 @@
description: null
},
map: null,
markers: null,
panorama: null,
selectedMarker: null,
added: {},
@ -19,6 +18,7 @@
MapEditor.metadata.name = form.elements.name.value;
MapEditor.metadata.description = form.elements.description.value;
MapEditor.metadata.unlisted = form.elements.unlisted.checked;
document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
@ -87,7 +87,7 @@
sv.getPanorama({
location: location,
preference: google.maps.StreetViewPreference.NEAREST,
radius: 100,
radius: MapEditor.map.getSearchRadius(location),
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
}, function (data, status) {
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
@ -118,16 +118,13 @@
MapEditor.resetSelected();
MapEditor.selectedMarker = marker;
MapEditor.map.invalidateSize(true);
MapEditor.map.resize();
MapEditor.map.panTo(marker.getLatLng());
MapEditor.panorama.setVisible(false);
if (marker.placeId) {
MapEditor.markers.removeLayer(MapEditor.selectedMarker);
MapEditor.map.addLayer(MapEditor.selectedMarker);
marker.setIcon(IconCollection.iconBlue);
marker.setZIndexOffset(2000);
MapEditor.map.changeMarkerIcon(marker, MapEditor.map.iconCollection.iconBlue);
document.getElementById('deleteButton').style.display = 'block';
@ -165,13 +162,13 @@
var placeId = MapEditor.selectedMarker.placeId
if (places[placeId].id && !del) {
MapEditor.map.removeLayer(MapEditor.selectedMarker);
MapEditor.markers.addLayer(MapEditor.selectedMarker);
MapEditor.selectedMarker.setIcon(places[placeId].noPano ? IconCollection.iconRed : IconCollection.iconGreen);
MapEditor.selectedMarker.setZIndexOffset(1000);
MapEditor.map.changeMarkerIcon(
MapEditor.selectedMarker,
places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen
);
} else {
delete places[placeId];
MapEditor.map.removeLayer(MapEditor.selectedMarker);
MapEditor.map.removeMarker(MapEditor.selectedMarker);
}
document.getElementById('deleteButton').style.display = 'none';
@ -223,7 +220,7 @@
MapEditor.resetSelected(del);
MapEditor.selectedMarker = null;
MapEditor.map.invalidateSize(true);
MapEditor.map.resize();
},
deletePlace: function () {
@ -239,6 +236,7 @@
delete MapEditor.added[placeId];
delete MapEditor.edited[placeId];
delete places[placeId];
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
@ -257,6 +255,9 @@
if (MapEditor.metadata.description !== null) {
data.append('description', MapEditor.metadata.description);
}
if (MapEditor.metadata.unlisted !== null) {
data.append('unlisted', MapEditor.metadata.unlisted);
}
for (var placeId in MapEditor.added) {
if (!MapEditor.added.hasOwnProperty(placeId)) {
@ -313,91 +314,346 @@
}
};
var IconCollection = {
iconGreen: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconRed: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconBlue: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
};
var Util = {
getHighResData: function () {
if (window.devicePixelRatio >= 4) {
return { ppi: 320, tileSize: 128, zoomOffset: 1 };
return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 };
} else if (window.devicePixelRatio >= 2) {
return { ppi: 250, tileSize: 256, zoomOffset: 0 };
return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 };
} else {
return { ppi: 72, tileSize: 512, zoomOffset: -1 };
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;
}
};
MapEditor.map = L.map('map', {
zoomControl: false
});
var LMapWrapper = {
map: null,
markers: null,
divId: null,
iconCollection: {
iconGreen: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconRed: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconBlue: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
})
},
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);
});
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
MapEditor.select(marker);
});
if (!LMapWrapper.map) {
LMapWrapper.divId = divId;
LMapWrapper.map = L.map(LMapWrapper.divId, {
center: { lat: 0., lng: 0. },
zoom: 2
});
var highResData = Util.getHighResData();
LMapWrapper.map.on('click', function (e) {
LMapWrapper.placeMarker(e.latlng);
});
L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: '1234',
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: 2,
maxZoom: 20
}).addTo(MapEditor.map);
var highResData = Util.getHighResData();
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: tileSubdomains,
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(LMapWrapper.map);
MapEditor.markers = L.markerClusterGroup({
maxClusterRadius: 50
});
if (mapId) {
LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
}
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
LMapWrapper.loadMarkers(places);
document.getElementById('streetViewCoverSelector').style.display = 'none';
},
hide: function () {
document.getElementById(LMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!LMapWrapper.markers) {
LMapWrapper.markers = L.markerClusterGroup({
maxClusterRadius: 50
});
} else {
LMapWrapper.markers.clearLayers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? LMapWrapper.iconCollection.iconRed : LMapWrapper.iconCollection.iconGreen,
zIndexOffset: 1000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
}
LMapWrapper.map.addLayer(LMapWrapper.markers);
},
// 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: LMapWrapper.iconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
},
panTo: function (latLng) {
LMapWrapper.map.panTo(latLng);
},
resize: function () {
LMapWrapper.map.invalidateSize(true);
},
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
marker.setZIndexOffset(2000);
},
removeMarker: function (marker) {
LMapWrapper.markers.removeLayer(marker);
},
toggleStreetViewCover: function () { },
getSearchRadius: function (location) {
return 100;
}
};
var place = places[placeId];
var GMapWrapper = {
map: null,
markers: null,
divId: null,
streetViewCover: null,
streetViewCoverOn: false,
iconCollection: {
iconGreen: {
url: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconRed: {
url: STATIC_ROOT + '/img/markers/marker-red.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconBlue: {
url: STATIC_ROOT + '/img/markers/marker-blue.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
}
},
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? IconCollection.iconRed : IconCollection.iconGreen,
zIndexOffset: 1000
})
.addTo(MapEditor.markers)
.on('click', function () {
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
if (!GMapWrapper.map) {
GMapWrapper.divId = divId;
GMapWrapper.map = new google.maps.Map(document.getElementById(GMapWrapper.divId), {
center: { lat: 0., lng: 0. },
zoom: 2,
fullscreenControl: false,
zoomControl: true,
zoomControlOptions: {
position: google.maps.ControlPosition.LEFT_BOTTOM
},
streetViewControl: false,
draggableCursor: 'crosshair'
});
GMapWrapper.streetViewCover = new google.maps.StreetViewCoverageLayer();
GMapWrapper.map.addListener('click', function (mapsMouseEvent) {
GMapWrapper.placeMarker({
lat: mapsMouseEvent.latLng.lat(),
lng: mapsMouseEvent.latLng.lng()
});
});
if (mapId) {
GMapWrapper.map.fitBounds({ south: mapBounds.south, west: mapBounds.west, north: mapBounds.north, east: mapBounds.east });
}
}
GMapWrapper.loadMarkers(places);
document.getElementById('streetViewCoverSelector').style.display = 'block'
},
hide: function () {
document.getElementById(GMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!GMapWrapper.markers) {
GMapWrapper.markers = new MarkerClusterer(GMapWrapper.map, [], {
imagePath: STATIC_ROOT + '/img/markers/m',
imageExtension: 'png?rev' + REVISION
});
} else {
GMapWrapper.markers.clearMarkers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = new google.maps.Marker({
position: {
lat: place.lat,
lng: place.lng
},
icon: place.noPano ? GMapWrapper.iconCollection.iconRed : GMapWrapper.iconCollection.iconGreen
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
GMapWrapper.markers.addMarker(marker);
}
},
// 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 = new google.maps.Marker({
map: GMapWrapper.map,
position: latLng,
icon: GMapWrapper.iconCollection.iconBlue,
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
marker.placeId = place.id;
}
GMapWrapper.markers.addMarker(marker);
MapEditor.map.addLayer(MapEditor.markers);
MapEditor.select(marker);
},
panTo: function (latLng) {
GMapWrapper.map.panTo(latLng);
},
resize: function () {
google.maps.event.trigger(GMapWrapper.map, 'resize');
},
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
},
removeMarker: function (marker) {
GMapWrapper.markers.removeMarker(marker);
},
toggleStreetViewCover: function () {
if (GMapWrapper.streetViewCoverOn) {
GMapWrapper.streetViewCover.setMap(null);
GMapWrapper.streetViewCoverOn = false;
} else {
GMapWrapper.streetViewCover.setMap(GMapWrapper.map);
GMapWrapper.streetViewCoverOn = true;
}
},
getSearchRadius: function (location) {
// source: https://www.yorku.ca/mack/CHI01.htm
var movementOffset = 4;
// source: https://groups.google.com/g/google-maps-js-api-v3/c/hDRO4oHVSeM/m/osOYQYXg2oUJ?pli=1
var metersPerPixel = 156543.03392 * Math.cos(location.lat * Math.PI / 180) / Math.pow(2, GMapWrapper.map.getZoom());
var minSearchRadius = 5;
var searchRadius = Math.max(minSearchRadius, Math.round(movementOffset * metersPerPixel));
return searchRadius;
}
};
// initialize content of #map with google maps
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
// switch off fullscreenControl because positioning doesn't work
@ -440,4 +696,48 @@
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.map.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.map.placeMarker(coordinates.latlng);
}
}
else {
jumpButton.disabled = true;
}
};
document.getElementById('mapSelector').onclick = function () {
MapEditor.closePlace();
MapEditor.map.hide();
if (MapEditor.map === GMapWrapper) {
MapEditor.map = LMapWrapper;
MapEditor.map.init('lmap', places);
} else {
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
}
}
document.getElementById('streetViewCoverSelector').onclick = function () {
MapEditor.map.toggleStreetViewCover();
}
})();

View File

@ -1,12 +1,59 @@
var MapGuesser = {
isSecure: window.location.protocol === 'https:',
cookiesAgreed: false,
sessionAvailableHooks: {},
initGoogleAnalitics: function () {
if (typeof GOOGLE_ANALITICS_ID === 'undefined') {
return;
}
// Global site tag (gtag.js) - Google Analytics
var script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + GOOGLE_ANALITICS_ID;
script.async = true;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', GOOGLE_ANALITICS_ID);
},
agreeCookies: function () {
if (MapGuesser.cookiesAgreed) {
return;
}
var expirationDate = new Date(new Date().getTime() + 20 * 365 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = 'COOKIES_CONSENT=1; expires=' + expirationDate + '; path=/';
MapGuesser.initGoogleAnalitics();
MapGuesser.httpRequest('GET', '/startSession.json', function () {
ANTI_CSRF_TOKEN = this.response.antiCsrfToken;
for (var hookId in MapGuesser.sessionAvailableHooks) {
if (!MapGuesser.sessionAvailableHooks.hasOwnProperty(hookId)) {
continue;
}
MapGuesser.sessionAvailableHooks[hookId]();
}
});
MapGuesser.cookiesAgreed = true;
},
httpRequest: function (method, url, callback, data) {
var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onload = callback;
xhr.open(method, url, true);
xhr.responseType = 'json';
if (method === 'POST') {
if (typeof data === 'undefined') {
data = new FormData();
@ -20,6 +67,55 @@ 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;
if (typeof grecaptcha !== 'undefined') {
grecaptcha.reset();
}
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';
@ -90,11 +186,46 @@ var MapGuesser = {
document.getElementById('cover').style.visibility = 'hidden';
},
toggleDisableOnChange: function (input, button) {
if (input.defaultValue !== input.value) {
button.disabled = false;
} else {
button.disabled = true;
observeInput: function (form, observedInputs) {
var anyChanged = false;
for (var i = 0; i < observedInputs.length; i++) {
var input = form.elements[observedInputs[i]];
if (input.type === 'checkbox') {
if (input.defaultChecked !== input.checked) {
anyChanged = true;
}
} else {
if (input.defaultValue !== input.value) {
anyChanged = true;
}
}
}
form.elements['submit_button'].disabled = !anyChanged;
},
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(form, observedInputs);
};
break;
case 'SELECT':
input.onchange = function () {
MapGuesser.observeInput(form, observedInputs);
};
break;
}
}
form.onreset = function () {
form.elements['submit_button'].disabled = true;
}
}
};
@ -110,7 +241,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();
};
}
})();

181
public/static/js/maps.js Normal file
View File

@ -0,0 +1,181 @@
(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';
}
}
}
};
var Util = {
printTimeForHuman: function (time) {
const minutes = Math.floor(time / 60);
const seconds = time % 60;
var time_str = '';
if (minutes == 1) {
time_str += '1 minute';
} else if (minutes > 1) {
time_str += minutes + ' minutes';
}
if (minutes > 0 && seconds > 0) {
time_str += ' and ';
}
if (seconds == 1) {
time_str += '1 second';
} else if (seconds > 1) {
time_str += seconds + ' seconds';
}
return time_str;
}
};
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('challengeForm').onsubmit = function (e) {
e.preventDefault();
var url = '/challenge/create.json';
var formData = new FormData(this);
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', url, function() {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
window.location.href = '/challenge/' + this.response.challengeToken;
}, formData);
};
if (document.getElementById('multiButton')) {
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('playMode').style.visibility = 'hidden';
}
}
if (document.getElementById('challengeButton')) {
document.getElementById('challengeButton').onclick = function () {
MapGuesser.showModal('challenge');
document.getElementById('playMode').style.visibility = 'hidden';
var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
};
}
document.getElementById('closePlayModeButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeMultiButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeChallengeButton').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;
document.getElementById('challengeMapId').value = this.dataset.mapId;
};
}
document.getElementById('timeLimit').oninput = function () {
var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
document.getElementById('timerEnabled').checked = true;
}
})();

View File

@ -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);
};
}
})();

View File

@ -1,51 +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 'passwords_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;
form.elements.password_new.select();
return;
}
document.getElementById('profileFormError').style.display = 'none';
form.reset();
form.elements.save.disabled = true;
form.elements.password_new.focus();
}, formData);
};
})();

View File

@ -1,47 +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 () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
var errorText;
switch (this.response.error) {
case 'passwords_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':
errorText = 'There is a user already registered with the given email address. Please <a href="/login" title="Login">login here</a>!';
break;
case 'not_active_user_found':
errorText = 'There is a user already registered with the given email address. Please check your email and click on the activation link!';
break;
}
var signupFormError = document.getElementById('signupFormError');
signupFormError.style.display = 'block';
signupFormError.innerHTML = errorText;
form.elements.email.select();
return;
}
document.getElementById('signupFormError').style.display = 'none';
form.reset();
form.elements.email.focus();
MapGuesser.showModalWithContent('Sign up successful', 'Sign up was successful. Please check your email and click on the activation link to activate your account!');
}, formData);
};
})();

View File

@ -1,147 +0,0 @@
#!/usr/bin/python3
# Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH
import sys
import os
import subprocess
import re
WORKTREE_REGEX = r"^worktree (.*)\nHEAD ([a-f0-9]*)\n(?:branch refs\/heads\/(.*)|detached)$"
if len(sys.argv) < 4:
print("Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH")
exit(1)
REPO = os.path.abspath(sys.argv[1])
WORKTREE_DEVELOPMENT = os.path.abspath(sys.argv[2])
WORKTREE_PRODUCTION = os.path.abspath(sys.argv[3])
class Worktree:
def __init__(self, path, branch, revision, version):
self.path = path
self.branch = branch
self.revision = revision
self.version = version
self.newRevision = None
self.newVersion = None
def getDataForWorktrees():
ret = subprocess.check_output(["git", "worktree", "list", "--porcelain"], cwd=REPO).decode().strip()
blocks = ret.split("\n\n")
worktrees = []
for block in blocks:
matches = re.search(WORKTREE_REGEX, block)
if matches:
path = matches.group(1)
revision = matches.group(2)
branch = matches.group(3)
version = getVersion(revision)
worktrees.append(Worktree(path, branch, revision, version))
return worktrees
def findWorktree(path):
for worktree in worktrees:
if worktree.path == path:
return worktree
return None
def getVersion(branch):
return subprocess.check_output(["git", "describe", "--tags", "--always", "--match", "Release_*", branch], cwd=REPO).decode().strip()
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()
def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune"], cwd=REPO)
def checkoutWorktree(worktreePath, ref):
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
def cleanWorktree(worktreePath):
subprocess.call(["git", "clean", "-f", "-d"], cwd=worktreePath)
def updateAppInWorktree(worktreePath):
subprocess.call([worktreePath + "/scripts/update.sh"], cwd=worktreePath)
def updateAppVersionInWorktree(worktreePath):
subprocess.call([worktreePath + "/scripts/update-version.sh"], cwd=worktreePath)
worktrees = getDataForWorktrees()
updateRepoFromRemote()
print("Repo is updated from origin")
print("----------------------------------------------")
print("----------------------------------------------")
developmentWorktree = findWorktree(WORKTREE_DEVELOPMENT)
developmentWorktree.newRevision = getRevisionForRef(developmentWorktree.branch)
developmentWorktree.newVersion = getVersion(developmentWorktree.revision)
print("DEVELOPMENT (" + developmentWorktree.path + ") is on branch " + developmentWorktree.branch)
print(developmentWorktree.revision + " = " + developmentWorktree.branch + " (" + developmentWorktree.version + ")")
print(developmentWorktree.newRevision + " = origin/" + developmentWorktree.branch + " (" + developmentWorktree.newVersion + ")")
if developmentWorktree.revision != developmentWorktree.newRevision:
print("-> DEVELOPMENT (" + developmentWorktree.path + ") will be UPDATED")
print("----------------------------------------------")
checkoutWorktree(developmentWorktree.path, developmentWorktree.branch)
cleanWorktree(developmentWorktree.path)
print(developmentWorktree.path + " is checked out to " + developmentWorktree.branch + " and cleaned")
updateAppInWorktree(developmentWorktree.path)
updateAppVersionInWorktree(developmentWorktree.path)
print("MapGuesser is updated in " + developmentWorktree.path)
elif developmentWorktree.version != developmentWorktree.newVersion:
print("-> DEVELOPMENT " + developmentWorktree.path + "'s version info will be UPDATED")
updateAppVersionInWorktree(developmentWorktree.path)
print("MapGuesser version is updated in " + developmentWorktree.path)
else:
print("-> DEVELOPMENT (" + developmentWorktree.path + ") WON'T be updated")
print("----------------------------------------------")
print("----------------------------------------------")
productionWorktree = findWorktree(WORKTREE_PRODUCTION)
productionWorktree.newVersion = getLatestReleaseTag()
productionWorktree.newRevision = getRevisionForRef(productionWorktree.newVersion)
print("PRODUCTION (" + productionWorktree.path + ")")
print(productionWorktree.revision + " = " + productionWorktree.version)
print(productionWorktree.newRevision + " = " + productionWorktree.newVersion)
if productionWorktree.revision != productionWorktree.newRevision:
print("-> PRODUCTION (" + productionWorktree.path + ") will be UPDATED")
checkoutWorktree(productionWorktree.path, productionWorktree.newRevision)
cleanWorktree(productionWorktree.path)
print(productionWorktree.path + " is checked out to " + productionWorktree.newRevision + " and cleaned")
updateAppInWorktree(productionWorktree.path)
updateAppVersionInWorktree(productionWorktree.path)
print("MapGuesser is updated in " + productionWorktree.path)
else:
print("-> PRODUCTION (" + productionWorktree.path + ") WON'T be updated")
print("----------------------------------------------")
print("----------------------------------------------")

View File

@ -1,26 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -f ${ROOT_DIR}/installed ]; then
echo "MapGuesser is already installed! To force reinstall, delete file 'installed' from the root directory!"
exit 1
fi
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
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
fi
touch ${ROOT_DIR}/installed

View File

@ -1,11 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
find ${ROOT_DIR}/public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
find ${ROOT_DIR}/public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
find ${ROOT_DIR}/public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;

20
scripts/run-sql.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <sql_file>"
exit 1
fi
SQL_FILE=$1
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "This script can only be used in DEV mode!"
exit 1
fi
echo "Running SQL on DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${SQL_FILE}

View File

@ -1,17 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
cd ${ROOT_DIR}
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php

View File

@ -1,19 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
echo "Installing Composer packages..."
(cd ${ROOT_DIR} && composer install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
fi

View File

@ -1,7 +1,7 @@
<?php namespace MapGuesser\Cli;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Model\User;
use DateTime;
use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@ -9,31 +9,31 @@ 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.')
->addArgument('email', InputArgument::REQUIRED, 'Email of user')
->addArgument('username', InputArgument::REQUIRED, 'Username of user')
->addArgument('password', InputArgument::REQUIRED, 'Password of user')
->addArgument('type', InputArgument::OPTIONAL, 'Type of user');;
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$user = new User([
'email' => $input->getArgument('email'),
]);
$user = new User();
$user->setEmail($input->getArgument('email'));
$user->setUsername($input->getArgument('username'));
$user->setPlainPassword($input->getArgument('password'));
$user->setActive(true);
$user->setCreatedDate(new DateTime());
if ($input->hasArgument('type')) {
if ($input->hasArgument('type') && $input->getArgument('type') !== null) {
$user->setType($input->getArgument('type'));
}
try {
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
\Container::$persistentDataManager->saveToDb($user);
} catch (\Exception $e) {
$output->writeln('<error>Adding user failed!</error>');
$output->writeln('');

View File

@ -0,0 +1,69 @@
<?php namespace MapGuesser\Cli;
use FilesystemIterator;
use SokoWeb\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;
}
}

View File

@ -0,0 +1,123 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
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 UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private MultiRoomRepository $multiRoomRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct()
{
parent::__construct();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->multiRoomRepository = new MultiRoomRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
}
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) {
\Container::$persistentDataManager->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
\Container::$persistentDataManager->deleteFromDb($userPasswordResetter);
}
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
\Container::$persistentDataManager->deleteFromDb($user);
}
\Container::$dbConnection->commit();
}
private function deleteExpiredPasswordResetters(): void
{
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
\Container::$persistentDataManager->deleteFromDb($passwordResetter);
}
}
private function deleteExpiredRooms(): void
{
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
\Container::$persistentDataManager->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();
}
}
}

View File

@ -1,17 +1,17 @@
<?php namespace MapGuesser\Cli;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
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.');
}
@ -19,6 +19,8 @@ class DatabaseMigration extends Command
{
$db = \Container::$dbConnection;
$this->createBaseDb();
$db->startTransaction();
$success = [];
@ -62,10 +64,8 @@ class DatabaseMigration extends Command
return 0;
}
private function readDir(string $type): array
private function createBaseDb()
{
$done = [];
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
FROM information_schema.tables
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
@ -73,21 +73,34 @@ class DatabaseMigration extends Command
->fetch(IResultSet::FETCH_NUM)[0];
if ($migrationTableExists != 0) {
$select = new Select(\Container::$dbConnection, 'migrations');
$select->columns(['migration']);
$select->where('type', '=', $type);
$select->orderBy('migration');
return;
}
$result = $select->execute();
\Container::$dbConnection->multiQuery(file_get_contents(ROOT . '/database/mapguesser.sql'));
}
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
$done[] = $migration['migration'];
}
private function readDir(string $type): array
{
$done = [];
$select = new Select(\Container::$dbConnection, 'migrations');
$select->columns(['migration']);
$select->where('type', '=', $type);
$select->orderBy('migration');
$result = $select->execute();
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
$done[] = $migration['migration'];
}
$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;

View File

@ -1,54 +1,298 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
use DateTime;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Response\IRedirect;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use SokoWeb\Response\Redirect;
class GameController
class GameController implements IAuthenticationRequired
{
private IRequest $request;
const NUMBER_OF_ROUNDS = 5;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private MapRepository $mapRepository;
public function __construct(IRequest $request)
private PlaceRepository $placeRepository;
private ChallengeRepository $challengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
public function __construct()
{
$this->request = $request;
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository();
$this->challengeRepository = new ChallengeRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
}
public function isAuthenticationRequired(): bool
{
return empty($_ENV['ENABLE_GAME_FOR_GUESTS']);
}
public function getGame(): IContent
{
$mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new HtmlContent('game', $data);
$mapId = (int) \Container::$request->query('mapId');
return new HtmlContent('game', ['mapId' => $mapId]);
}
public function getGameJson(): IContent
{
$mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new JsonContent($data);
}
private function prepareGame(int $mapId)
public function getNewMultiGame(): IRedirect
{
$mapId = (int) \Container::$request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$roomId = bin2hex(random_bytes(3));
$token = $this->getMultiToken($roomId);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$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());
$session = $this->request->session();
\Container::$persistentDataManager->saveToDb($room);
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
return new Redirect(
\Container::$routeCollection
->getRoute('multiGame')
->generateLink(['roomId' => $roomId]),
IRedirect::TEMPORARY
);
}
public function getMultiGame(): IContent
{
$roomId = \Container::$request->query('roomId');
return new HtmlContent('game', ['roomId' => $roomId]);
}
public function getChallenge(): IContent
{
$challengeToken = \Container::$request->query('challengeToken');
return new HtmlContent('game', ['challengeToken' => $challengeToken]);
}
public function createNewChallenge(): IContent
{
// create Challenge
do {
// initiliaze or if a challenge with the same token already exists
$challengeToken = mt_rand();
} while ($this->challengeRepository->getByToken($challengeToken));
$challenge = new Challenge();
$challenge->setToken($challengeToken);
$challenge->setCreatedDate(new DateTime());
if (\Container::$request->post('timerEnabled') !== null && \Container::$request->post('timeLimit') !== null) {
$challenge->setTimeLimit(\Container::$request->post('timeLimit'));
}
if (\Container::$request->post('timeLimitType') !== null) {
$challenge->setTimeLimitType(\Container::$request->post('timeLimitType'));
}
if (\Container::$request->post('noMove') !== null) {
$challenge->setNoMove(true);
}
if (\Container::$request->post('noPan') !== null) {
$challenge->setNoPan(true);
}
if (\Container::$request->post('noZoom') !== null) {
$challenge->setNoZoom(true);
}
\Container::$persistentDataManager->saveToDb($challenge);
// save owner/creator
$session = \Container::$request->session();
$userId = $session->get('userId');
$userInChallenge = new UserInChallenge();
$userInChallenge->setUserId($userId);
$userInChallenge->setChallenge($challenge);
$userInChallenge->setTimeLeft($challenge->getTimeLimit());
$userInChallenge->setIsOwner(true);
\Container::$persistentDataManager->saveToDb($userInChallenge);
// select places
$mapId = (int) \Container::$request->post('mapId');
// $map = $this->mapRepository->getById($mapId);
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
$round = 0;
foreach ($places as $place) {
$placeInChallenge = new PlaceInChallenge();
$placeInChallenge->setPlace($place);
$placeInChallenge->setChallenge($challenge);
$placeInChallenge->setRound($round++);
\Container::$persistentDataManager->saveToDb($placeInChallenge);
}
return new JsonContent(['challengeToken' => dechex($challengeToken)]);
}
public function prepareGame(): IContent
{
$mapId = (int) \Container::$request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [
'mapId' => $mapId,
'area' => $map['area'],
'rounds' => []
'area' => $map->getArea(),
'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['name'], 'bounds' => $bounds->toArray()];
return new JsonContent([
'mapId' => $mapId,
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
public function prepareMultiGame(): IContent
{
/**
* @var User|null $user
*/
$user = \Container::$request->user();
if ($user === null)
{
return new JsonContent(['error' => 'anonymous_user']);
}
$roomId = \Container::$request->query('roomId');
$room = $this->multiRoomRepository->getByRoomId($roomId);
if (!isset($room)) {
return new JsonContent(['error' => 'game_not_found']);
}
$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());
\Container::$persistentDataManager->saveToDb($room);
$this->multiConnector->sendMessage('join_room', [
'roomId' => $roomId,
'token' => $token,
'userName' => $user->getDisplayName()
]);
return new JsonContent([
'roomId' => $roomId,
'token' => $token,
'owner' => $members['owner'] == $token,
'mapId' => $state['mapId'],
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
public function prepareChallenge(): IContent
{
$challengeToken_str = \Container::$request->query('challengeToken');
$session = \Container::$request->session();
$userId = $session->get('userId');
if (!isset($userId))
{
return new JsonContent(['error' => 'anonymous_user']);
}
$challenge = $this->challengeRepository->getByTokenStr($challengeToken_str);
if (!isset($challenge))
{
return new JsonContent(['error' => 'game_not_found']);
}
if (!$this->userInChallengeRepository->isUserParticipatingInChallenge($userId, $challenge)) {
// new player is joining
$userInChallenge = new UserInChallenge();
$userInChallenge->setUserId($userId);
$userInChallenge->setChallenge($challenge);
$userInChallenge->setTimeLeft($challenge->getTimeLimit());
\Container::$persistentDataManager->saveToDb($userInChallenge);
}
$map = $this->mapRepository->getByChallenge($challenge);
return new JsonContent([
'mapId' => $map->getId(),
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
private function getMultiToken(string $roomId): string
{
$session = \Container::$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;
}
}

View File

@ -1,119 +1,456 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Interfaces\Request\IRequest;
use DateTime;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent;
use SokoWeb\Interfaces\Response\IContent;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Guess;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserPlayedPlace;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
class GameFlowController
class GameFlowController implements IAuthenticationRequired
{
const NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000;
private IRequest $request;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private PlaceRepository $placeRepository;
public function __construct(IRequest $request)
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private UserInChallengeRepository $userInChallengeRepository;
private PlaceInChallengeRepository $placeInChallengeRepository;
private GuessRepository $guessRepository;
public function __construct()
{
$this->request = $request;
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->placeRepository = new PlaceRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
$this->guessRepository = new GuessRepository();
}
public function getNewPlace(): IContent
public function isAuthenticationRequired(): bool
{
$mapId = (int) $this->request->query('mapId');
return empty($_ENV['ENABLE_GAME_FOR_GUESTS']);
}
$session = $this->request->session();
public function initialData(): IContent
{
$mapId = (int) \Container::$request->query('mapId');
$session = \Container::$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) {
$place = $this->placeRepository->getForMapWithValidPano($mapId);
$state['rounds'][] = $place;
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
$this->startNewGame($state, $mapId);
$session->set('state', $state);
}
$data = ['panoId' => $place['panoId']];
} 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
{
$mapId = (int) $this->request->query('mapId');
$roomId = \Container::$request->query('roomId');
$session = \Container::$request->session();
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$last = $state['rounds'][count($state['rounds']) - 1];
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
$position = $last['position'];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
$distance = $this->calculateDistance($position, $guessPosition);
$score = $this->calculateScore($distance, $state['area']);
if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
$this->startNewGame($state, $state['mapId']);
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($room);
}
$last['guessPosition'] = $guessPosition;
$last['distance'] = $distance;
$last['score'] = $score;
$state['rounds'][count($state['rounds']) - 1] = $last;
$places = [];
foreach ($state['rounds'] as $round) {
$places[] = [
'position' => $round['position']->toArray(),
'panoId' => $round['panoId'],
'pov' => $round['pov']->toArray()
];
}
if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
$exclude = [];
$this->multiConnector->sendMessage('start_game', ['roomId' => $roomId, 'places' => $places]);
foreach ($state['rounds'] as $round) {
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
return new JsonContent(['ok' => true]);
}
private function prepareChallengeResponse(int $userId, Challenge $challenge, int $currentRound, bool $withHistory = false): array
{
$currentPlace = $this->placeRepository->getByRoundInChallenge($challenge, $currentRound);
// if the last round was played ($currentPlace == null) or history is explicitly requested (for initializing)
if (!isset($currentPlace) || $withHistory) {
$withRelations = ['user', 'place_in_challange', 'place'];
foreach ($this->guessRepository->getAllInChallenge($challenge, $withRelations) as $guess) {
$round = $guess->getPlaceInChallenge()->getRound();
if ($guess->getUser()->getId() === $userId) {
$response['history'][$round]['position'] =
$guess->getPlaceInChallenge()->getPlace()->getPosition()->toArray();
$response['history'][$round]['result'] = [
'guessPosition' => $guess->getPosition()->toArray(),
'distance' => $guess->getDistance(),
'score' => $guess->getScore()
];
} else {
$response['history'][$round]['allResults'][] = [
'userName' => $guess->getUser()->getDisplayName(),
'guessPosition' => $guess->getPosition()->toArray(),
'distance' => $guess->getDistance(),
'score' => $guess->getScore()
];
}
}
$place = $this->placeRepository->getForMapWithValidPano($mapId, $exclude);
$state['rounds'][] = $place;
$session->set('state', $state);
// setting default values for rounds without guesses (because of timeout)
for ($i = 0; $i < $currentRound; ++$i) {
if (!isset($response['history'][$i]) || !isset($response['history'][$i]['result'])) {
$response['history'][$i]['result'] = [
'guessPosition' => null,
'distance' => null,
'score' => 0
];
$panoId = $place['panoId'];
} else {
$state['rounds'] = [];
$session->set('state', $state);
$response['history'][$i]['position'] =
$this->placeRepository->getByRoundInChallenge($challenge, $i)->getPosition()->toArray();
}
}
$panoId = null;
$response['history']['length'] = $currentRound;
}
$data = [
'result' => [
'position' => $position->toArray(),
'distance' => $distance,
'score' => $score
],
'panoId' => $panoId
if (!isset($currentPlace)) { // game finished
$response['finished'] = true;
} else { // continue game
$response['place'] = [
'panoId' => $currentPlace->getPanoIdCached(),
'pov' => $currentPlace->getPov()->toArray()
];
$prevRound = $currentRound - 1;
if ($prevRound >= 0) {
foreach ($this->guessRepository->getAllInChallengeByRound($prevRound, $challenge, ['user']) as $guess) {
if ($guess->getUser()->getId() != $userId) {
$response['allResults'][] = [
'userName' => $guess->getUser()->getDisplayName(),
'guessPosition' => $guess->getPosition()->toArray(),
'distance' => $guess->getDistance(),
'score' => $guess->getScore()
];
}
}
}
}
$response['restrictions'] = [
'timeLimit' => $challenge->getTimeLimit() * 1000,
'timeLimitType' => $challenge->getTimeLimitType(),
'noMove' => $challenge->getNoMove(),
'noPan' => $challenge->getNoPan(),
'noZoom' => $challenge->getNoZoom()
];
return new JsonContent($data);
return $response;
}
public function challengeInitialData(): IContent
{
$session = \Container::$request->session();
$userId = $session->get('userId');
$challengeToken_str = \Container::$request->query('challengeToken');
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, ['challenge']);
if (!isset($userInChallenge)) {
return new JsonContent(['error' => 'game_not_found']);
}
$challenge = $userInChallenge->getChallenge();
$currentRound = $userInChallenge->getCurrentRound();
$response = $this->prepareChallengeResponse($userId, $challenge, $currentRound, true);
if ($challenge->getTimeLimitType() === 'game' && $challenge->getTimeLimit() !== null && $userInChallenge->getCurrentRound() > 0) {
$timeLimit = max(10, $userInChallenge->getTimeLeft());
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
}
return new JsonContent($response);
}
public function guess(): IContent
{
$mapId = (int) \Container::$request->query('mapId');
$session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']);
}
$last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$last['guessPosition'] = $guessPosition;
$last['distance'] = $result['distance'];
$last['score'] = $result['score'];
$response = [
'position' => $last['position']->toArray(),
'result' => $result
];
$state['rounds'][$state['currentRound']] = $last;
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$next = $state['rounds'][$state['currentRound']];
$response['place'] = [
'panoId' => $next['panoId'],
'pov' => $next['pov']->toArray()
];
}
$session->set('state', $state);
$this->saveVisit($last['placeId']);
return new JsonContent($response);
}
// save the selected place for the round in UserPlayedPlace
private function saveVisit($placeId): void
{
$session = \Container::$request->session();
$userId = $session->get('userId');
if (isset($userId)) {
$userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
if (!$userPlayedPlace) {
$userPlayedPlace = new UserPlayedPlace();
$userPlayedPlace->setUserId($userId);
$userPlayedPlace->setPlaceId($placeId);
} else {
$userPlayedPlace->incrementOccurrences();
}
$userPlayedPlace->setLastTimeDate(new DateTime());
\Container::$persistentDataManager->saveToDb($userPlayedPlace);
}
}
public function multiGuess(): IContent
{
$roomId = \Container::$request->query('roomId');
$session = \Container::$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) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evaluateGuess($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 challengeGuess(): IContent
{
$session = \Container::$request->session();
$userId = $session->get('userId');
$challengeToken_str = \Container::$request->query('challengeToken');
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, ['challenge']);
if (!isset($userInChallenge)) {
return new JsonContent(['error' => 'game_not_found']);
}
$challenge = $userInChallenge->getChallenge();
$currentRound = $userInChallenge->getCurrentRound();
$currentPlaceInChallenge = $this->placeInChallengeRepository->getByRoundInChallenge($currentRound, $challenge, ['place', 'map']);
$currentPlace = $currentPlaceInChallenge->getPlace();
$map = $currentPlace->getMap();
// creating response
$nextRound = $currentRound + 1;
$response = $this->prepareChallengeResponse($userId, $challenge, $nextRound);
$response['position'] = $currentPlace->getPosition()->toArray();
if (\Container::$request->post('lat') && \Container::$request->post('lng')) {
$guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evaluateGuess($currentPlace->getPosition(), $guessPosition, $map->getArea());
// save guess
$guess = new Guess();
$guess->setUserId($userId);
$guess->setPlaceInChallenge($currentPlaceInChallenge);
$guess->setPosition($guessPosition);
$guess->setDistance($result['distance']);
$guess->setScore($result['score']);
\Container::$persistentDataManager->saveToDb($guess);
$response['result'] = $result;
} else {
// user didn't manage to guess in the round in the given timeframe
$response['result'] = ['distance' => null, 'score' => 0];
}
// save user relevant state of challenge
$userInChallenge->setCurrentRound($nextRound);
$timeLeft = \Container::$request->post('timeLeft');
if (isset($timeLeft)) {
$userInChallenge->setTimeLeft(intval($timeLeft));
}
\Container::$persistentDataManager->saveToDb($userInChallenge);
if ($challenge->getTimeLimitType() === 'game' && isset($timeLeft)) {
$timeLimit = max(10, intval($timeLeft));
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
}
if (isset($response['history'][$currentRound]['allResults'])) {
$response['allResults'] = $response['history'][$currentRound]['allResults'];
}
$this->saveVisit($currentPlace->getId());
return new JsonContent($response);
}
public function multiNextRound(): IContent
{
$roomId = \Container::$request->query('roomId');
$session = \Container::$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());
\Container::$persistentDataManager->saveToDb($room);
return new JsonContent(['ok' => true]);
}
private function evaluateGuess(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
{
$session = \Container::$request->session();
$userId = $session->get('userId');
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
$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

View File

@ -1,12 +1,21 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Response\Redirect;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Response\JsonContent;
use SokoWeb\Response\Redirect;
class HomeController
{
public function getIndex(): IRedirect
{
return new Redirect([\Container::$routeCollection->getRoute('maps'), []], IRedirect::TEMPORARY);
return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY);
}
public function startSession(): IContent
{
// session starts with the request, this method just sends valid data to the client
return new JsonContent(['antiCsrfToken' => \Container::$request->session()->get('anti_csrf_token')]);
}
}

View File

@ -1,78 +1,677 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Model\User;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
use DateInterval;
use DateTime;
use SokoWeb\Http\Request;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Mailing\Mail;
use SokoWeb\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Util\UsernameGenerator;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
use SokoWeb\Response\Redirect;
use SokoWeb\Util\CaptchaValidator;
use SokoWeb\Util\JwtParser;
class LoginController
{
private IRequest $request;
private UserRepository $userRepository;
public function __construct(IRequest $request)
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private string $redirectUrl;
public function __construct()
{
$this->request = $request;
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->redirectUrl = \Container::$request->session()->has('redirect_after_login') ?
\Container::$request->session()->get('redirect_after_login') :
\Container::$routeCollection->getRoute('index')->generateLink();
}
public function getLoginForm()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
$data = [];
return new HtmlContent('login', $data);
return new HtmlContent('login/login', ['redirectUrl' => $this->redirectUrl]);
}
public function getGoogleLoginRedirect(): IRedirect
{
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
\Container::$request->session()->set('oauth_state', $state);
\Container::$request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
\Container::$request->getBase() . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
$nonce
);
return new Redirect($url, IRedirect::TEMPORARY);
}
public function getSignupForm()
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
if (\Container::$request->session()->has('tmp_user_data')) {
$tmpUserData = \Container::$request->session()->get('tmp_user_data');
} else {
$tmpUserData = [];
}
return new HtmlContent('login/signup', $tmpUserData);
}
public function getSignupSuccess()
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
return new HtmlContent('login/signup_success');
}
public function getSignupWithGoogleForm()
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
if (!\Container::$request->session()->has('google_user_data')) {
return new Redirect(\Container::$routeCollection->getRoute('login-google')->generateLink(), IRedirect::TEMPORARY);
}
$userData = \Container::$request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']);
return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email'], 'redirectUrl' => $this->redirectUrl]);
}
public function getRequestPasswordResetForm()
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
return new HtmlContent('login/password_reset_request', ['email' => \Container::$request->query('email')]);
}
public function getRequestPasswordResetSuccess(): IContent
{
return new HtmlContent('login/password_reset_request_success');
}
public function getResetPasswordForm()
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
$token = \Container::$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(), 'redirectUrl' => $this->redirectUrl]);
}
public function login(): IContent
{
$session = $this->request->session();
if ($session->get('user')) {
$data = ['success' => true];
return new JsonContent($data);
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]);
}
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->where('email', '=', $this->request->post('email'));
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($userData === null) {
$data = ['error' => 'user_not_found'];
return new JsonContent($data);
if (
filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false &&
preg_match('/^[a-zA-Z0-9_\-\.]+$/', \Container::$request->post('email')) !== 1
) {
return new JsonContent(['error' => ['errorText' => 'This is not a valid email address or username.']]);
}
$user = new User($userData);
$user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
if ($user === null) {
if (strlen(\Container::$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!'
]
]);
}
$tmpUser = new User();
$tmpUser->setPlainPassword(\Container::$request->post('password'));
$tmpUserData = ['password_hashed' => $tmpUser->getPassword()];
if (filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
$tmpUserData['username'] = \Container::$request->post('email');
} else {
$tmpUserData['email'] = \Container::$request->post('email');
}
\Container::$request->session()->set('tmp_user_data', $tmpUserData);
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 / username, 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);
if (!$user->checkPassword(\Container::$request->post('password'))) {
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>!'
]
]);
}
$session->set('user', $user);
\Container::$request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]);
}
public function loginWithGoogle()
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
return new HtmlContent('login/google_login');
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
\Container::$request->query('code'),
\Container::$request->getBase() . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
);
if (!isset($tokenData['id_token'])) {
return new HtmlContent('login/google_login');
}
$jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
return new HtmlContent('login/google_login');
}
if (!$idToken['email_verified']) {
return new HtmlContent('login/google_login');
}
$user = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($user === null) {
\Container::$request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
}
\Container::$request->setUser($user);
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
public function logout(): IRedirect
{
$this->request->session()->delete('user');
\Container::$request->setUser(null);
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
public function signup(): IContent
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent(['redirect' => ['target' => $this->redirectUrl]]);
}
$newUser = new User();
$googleUserData = \Container::$request->session()->get('google_user_data');
if ($googleUserData !== null) {
$user = $this->userRepository->getByEmail($googleUserData['email']);
if ($user !== null) {
return new JsonContent([
'error' => [
'errorText' => 'There is a user already registered with the email address of this Google account, ' .
'but Google account is not linked to the user. Please <a href="/login?email=' .
urlencode($googleUserData['email']) . '" title="Login">login</a> first to link your Google account!'
]
]);
}
$newUser->setActive(true);
$newUser->setEmail($googleUserData['email']);
$newUser->setGoogleSub($googleUserData['sub']);
} else {
$user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
if ($user !== null) {
if ($user->getActive()) {
if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'There is a user already registered with the given email address / username, ' .
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
\Container::$request->setUser($user);
$this->deleteRedirectUrl();
$data = ['redirect' => ['target' => $this->redirectUrl]];
} else {
$data = [
'error' => [
'errorText' => 'There is a user already registered with the given email address / username. ' .
'Please check your email and click on the activation link!'
]
];
}
return new JsonContent($data);
}
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) {
if (!\Container::$request->post('g-recaptcha-response')) {
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
}
$captchaValidator = new CaptchaValidator();
$captchaResponse = $captchaValidator->validate(\Container::$request->post('g-recaptcha-response'));
if (!$captchaResponse['success']) {
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
}
}
if (filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if (\Container::$request->session()->has('tmp_user_data')) {
$tmpUserData = \Container::$request->session()->get('tmp_user_data');
$tmpUser = new User();
$tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword(\Container::$request->post('password'))) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
} else {
if (strlen(\Container::$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 (\Container::$request->post('password') !== \Container::$request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
}
$newUser->setActive(false);
$newUser->setEmail(\Container::$request->post('email'));
$newUser->setPlainPassword(\Container::$request->post('password'));
}
if (strlen(\Container::$request->post('username')) > 0) {
$username = \Container::$request->post('username');
if (preg_match('/^[a-zA-Z0-9_\-\.]+$/', $username) !== 1) {
return new JsonContent(['error' => ['errorText' => 'Username can contain only english letters, digits, - (hyphen), . (dot), _ (underscore).']]);
}
if ($this->userRepository->getByUsername($username) !== null) {
return new JsonContent(['error' => ['errorText' => 'The given username is already taken.']]);
}
} else {
$usernameGenerator = new UsernameGenerator();
do {
$username = $usernameGenerator->generate();
} while ($this->userRepository->getByUsername($username));
}
$newUser->setUsername($username);
$newUser->setCreatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($newUser);
if ($googleUserData !== null) {
$this->sendWelcomeEmail($newUser->getEmail());
\Container::$request->setUser($newUser);
} else {
$token = bin2hex(random_bytes(16));
$confirmation = new UserConfirmation();
$confirmation->setUser($newUser);
$confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime());
\Container::$persistentDataManager->saveToDb($confirmation);
$this->sendConfirmationEmail($newUser->getEmail(), $token, $newUser->getCreatedDate());
}
\Container::$request->session()->delete('tmp_user_data');
\Container::$request->session()->delete('google_user_data');
return new JsonContent(['success' => true]);
}
public function resetSignup(): IContent
{
\Container::$request->session()->delete('tmp_user_data');
return new JsonContent(['success' => true]);
}
public function resetGoogleSignup(): IContent
{
\Container::$request->session()->delete('google_user_data');
return new JsonContent(['success' => true]);
}
public function activate()
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken(substr(\Container::$request->query('token'), 0, 32));
if ($confirmation === null) {
return new HtmlContent('login/activate');
}
\Container::$persistentDataManager->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId());
$user->setActive(true);
\Container::$persistentDataManager->saveToDb($user);
\Container::$request->setUser($user);
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
public function cancel()
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken(substr(\Container::$request->query('token'), 0, 32));
if ($confirmation === null) {
return new HtmlContent('login/cancel', ['success' => false]);
}
\Container::$persistentDataManager->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId());
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
\Container::$persistentDataManager->deleteFromDb($user);
return new HtmlContent('login/cancel', ['success' => true]);
}
public function requestPasswordReset(): IContent
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent([
'redirect' => [
'target' => $this->redirectUrl
]
]);
}
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) {
if (!\Container::$request->post('g-recaptcha-response')) {
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
}
$captchaValidator = new CaptchaValidator();
$captchaResponse = $captchaValidator->validate(\Container::$request->post('g-recaptcha-response'));
if (!$captchaResponse['success']) {
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
}
}
if (
filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false &&
preg_match('/^[a-zA-Z0-9_\-\.]+$/', \Container::$request->post('email')) !== 1
) {
return new JsonContent(['error' => ['errorText' => 'This is not a valid email address or username.']]);
}
$user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
if ($user === null) {
return new JsonContent([
'error' => [
'errorText' => 'No user found with the given email address / username. 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 / username, 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);
if ($existingResetter !== null) {
\Container::$persistentDataManager->deleteFromDb($existingResetter);
}
\Container::$persistentDataManager->saveToDb($passwordResetter);
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
return new JsonContent(['success' => true]);
}
public function resetPassword(): IContent
{
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent([
'redirect' => [
'target' => $this->redirectUrl
]
]);
}
$token = \Container::$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(\Container::$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 (\Container::$request->post('password') !== \Container::$request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
\Container::$persistentDataManager->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword(\Container::$request->post('password'));
\Container::$persistentDataManager->saveToDb($user);
\Container::$request->setUser($user);
$this->deleteRedirectUrl();
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' => \Container::$request->getBase() .
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => \Container::$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());
\Container::$persistentDataManager->saveToDb($confirmation);
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
return true;
}
private function sendWelcomeEmail(string $email): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME']);
$mail->setBodyFromTemplate('signup-noconfirm', [
'EMAIL' => $email,
]);
$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' => \Container::$request->getBase() .
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i T')
]);
$mail->send();
}
private function deleteRedirectUrl(): void
{
\Container::$request->session()->delete('redirect_after_login');
}
}

View File

@ -1,83 +1,107 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Modify;
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 SokoWeb\Interfaces\Authentication\IUser;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov;
class MapAdminController implements ISecured
class MapAdminController implements IAuthenticationRequired, ISecured
{
private static string $unnamedMapName = '[unnamed map]';
private IRequest $request;
private MapRepository $mapRepository;
private PlaceRepository $placeRepository;
public function __construct(IRequest $request)
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private ChallengeRepository $challengeRepository;
private GuessRepository $guessRepository;
private PlaceInChallengeRepository $placeInChallengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
public function __construct()
{
$this->request = $request;
$this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->challengeRepository = new ChallengeRepository();
$this->guessRepository = new GuessRepository();
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function authorize(): bool
{
$user = $this->request->user();
return $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
return \Container::$request->user()->hasPermission(IUser::PERMISSION_ADMIN);
}
public function getMapEditor(): IContent
{
$mapId = (int) $this->request->query('mapId');
$mapId = (int) \Container::$request->query('mapId');
if ($mapId) {
$map = $this->mapRepository->getById($mapId);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$places = $this->getPlaces($mapId);
$places = $this->getPlaces($map);
} else {
$map = [
'name' => self::$unnamedMapName,
'description' => ''
];
$bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
$map = new Map();
$map->setName(self::$unnamedMapName);
$places = [];
}
$data = ['mapId' => $mapId, 'mapName' => $map['name'], 'mapDescription' => str_replace('<br>', "\n", $map['description']), 'bounds' => $bounds->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()),
'mapUnlisted' => $map->getUnlisted(),
'bounds' => $map->getBounds()->toArray(),
'places' => &$places
]);
}
public function getPlace(): IContent
{
$placeId = (int) $this->request->query('placeId');
$placeId = (int) \Container::$request->query('placeId');
$placeData = $this->placeRepository->getById($placeId);
$place = $this->placeRepository->getById($placeId);
$data = ['panoId' => $placeData['panoId']];
return new JsonContent($data);
return new JsonContent(['panoId' => $place->getFreshPanoId()]);
}
public function saveMap(): IContent
{
$mapId = (int) $this->request->query('mapId');
$mapId = (int) \Container::$request->query('mapId');
\Container::$dbConnection->startTransaction();
if (!$mapId) {
$mapId = $this->addNewMap();
if ($mapId) {
$map = $this->mapRepository->getById($mapId);
} else {
$map = new Map();
$map->setName(self::$unnamedMapName);
\Container::$persistentDataManager->saveToDb($map);
}
if (isset($_POST['added'])) {
@ -85,11 +109,23 @@ class MapAdminController implements ISecured
foreach ($_POST['added'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $this->placeRepository->addToMap($mapId, [
'lat' => (float) $placeRaw['lat'],
'lng' => (float) $placeRaw['lng'],
'pano_id_cached_timestamp' => $placeRaw['panoId'] === -1 ? (new DateTime('-1 day'))->format('Y-m-d H:i:s') : null
])];
$place = new Place();
$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'));
}
\Container::$persistentDataManager->saveToDb($place);
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $place->getId()];
}
} else {
$addedIds = [];
@ -99,10 +135,17 @@ class MapAdminController implements ISecured
foreach ($_POST['edited'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$this->placeRepository->modify((int) $placeRaw['id'], [
'lat' => (float) $placeRaw['lat'],
'lng' => (float) $placeRaw['lng']
]);
$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'));
\Container::$persistentDataManager->saveToDb($place);
}
}
@ -110,128 +153,108 @@ class MapAdminController implements ISecured
foreach ($_POST['deleted'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$this->placeRepository->delete($placeRaw['id']);
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$this->deletePlace($place);
}
}
$mapBounds = $this->calculateMapBounds($mapId);
$mapBounds = $this->calculateMapBounds($map);
$map = [
'bound_south_lat' => $mapBounds->getSouthLat(),
'bound_west_lng' => $mapBounds->getWestLng(),
'bound_north_lat' => $mapBounds->getNorthLat(),
'bound_east_lng' => $mapBounds->getEastLng(),
'area' => $mapBounds->calculateApproximateArea(),
];
$map->setBounds($mapBounds);
$map->setArea($mapBounds->calculateApproximateArea());
if (isset($_POST['name'])) {
$map['name'] = $_POST['name'] ? $_POST['name'] : self::$unnamedMapName;
$map->setName($_POST['name'] ? $_POST['name'] : self::$unnamedMapName);
}
if (isset($_POST['description'])) {
$map['description'] = str_replace(["\n", "\r\n"], '<br>', $_POST['description']);
$map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_POST['description']));
}
if (isset($_POST['unlisted'])) {
$map->setUnlisted((bool)$_POST['unlisted']);
}
$this->saveMapData($mapId, $map);
\Container::$persistentDataManager->saveToDb($map);
\Container::$dbConnection->commit();
$data = ['mapId' => $mapId, 'added' => $addedIds];
return new JsonContent($data);
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
}
public function deleteMap() {
$mapId = (int) $this->request->query('mapId');
\Container::$dbConnection->startTransaction();
$this->deletePlaces($mapId);
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($mapId);
$modify->delete();
\Container::$dbConnection->commit();
$data = ['success' => true];
return new JsonContent($data);
}
private function deletePlaces(int $mapId): void
public function deleteMap(): IContent
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id']);
$select->where('map_id', '=', $mapId);
$mapId = (int) \Container::$request->query('mapId');
$result = $select->execute();
$map = $this->mapRepository->getById($mapId);
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($place['id']);
$modify->delete();
$this->deletePlaces($map);
\Container::$persistentDataManager->deleteFromDb($map);
return new JsonContent(['success' => true]);
}
private function deletePlace(Place $place): void
{
foreach ($this->userPlayedPlaceRepository->getAllByPlace($place) as $userPlayedPlace) {
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
foreach ($this->challengeRepository->getAllByPlace($place) as $challenge) {
$this->deleteChallenge($challenge);
}
\Container::$persistentDataManager->deleteFromDb($place);
}
private function deletePlaces(Map $map): void
{
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$this->deletePlace($place);
}
}
private function calculateMapBounds(int $mapId): Bounds
private function deleteChallenge(Challenge $challenge): void
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['lat', 'lng']);
$select->where('map_id', '=', $mapId);
foreach ($this->userInChallengeRepository->getAllByChallenge($challenge) as $userInChallenge) {
\Container::$persistentDataManager->deleteFromDb($userInChallenge);
}
$result = $select->execute();
foreach ($this->guessRepository->getAllInChallenge($challenge, ['place_in_challange']) as $guess) {
\Container::$persistentDataManager->deleteFromDb($guess);
}
foreach ($this->placeInChallengeRepository->getAllByChallenge($challenge) as $placeInChallenge) {
\Container::$persistentDataManager->deleteFromDb($placeInChallenge);
}
\Container::$persistentDataManager->deleteFromDb($challenge);
}
private function calculateMapBounds(Map $map): Bounds
{
$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;
}
private function addNewMap(): int
private function &getPlaces(Map $map): array
{
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->fill([
'name' => self::$unnamedMapName,
'description' => '',
'bound_south_lat' => 0.0,
'bound_west_lng' => 0.0,
'bound_north_lat' => 0.0,
'bound_east_lng' => 0.0
]);
$modify->save();
return $modify->getId();
}
private function saveMapData(int $mapId, array $map): void
{
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($mapId);
$modify->fill($map);
$modify->save();
}
private function &getPlaces(int $mapId): array
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->where('map_id', '=', $mapId);
$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
];
}

View File

@ -1,24 +1,17 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select;
use MapGuesser\Database\RawExpression;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Response\HtmlContent;
use SokoWeb\Database\Query\Select;
use SokoWeb\Database\RawExpression;
use SokoWeb\Interfaces\Authentication\IUser;
use SokoWeb\Interfaces\Database\IResultSet;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\HtmlContent;
class MapsController
{
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getMaps(): IContent
{
//TODO: from repository - count should be added
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns([
['maps', 'id'],
@ -29,12 +22,19 @@ class MapsController
['maps', 'bound_north_lat'],
['maps', 'bound_east_lng'],
['maps', 'area'],
['maps', 'unlisted'],
new RawExpression('COUNT(places.id) AS num_places')
]);
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
$select->groupBy(['maps', 'id']);
$select->orderBy('name');
$user = \Container::$request->user();
$isAdmin = $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
if (!$isAdmin) {
$select->where(['maps', 'unlisted'], '=', false);
}
$result = $select->execute();
$maps = [];
@ -44,9 +44,11 @@ class MapsController
$maps[] = $map;
}
$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,
'isLoggedIn' => $user !== null,
'isAdmin' => $isAdmin
]);
}
private function formatMapAreaForHuman(float $area): array

View File

@ -1,192 +0,0 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Mailing\Mail;
use MapGuesser\Model\User;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
class SignupController
{
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getSignupForm()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$data = [];
return new HtmlContent('signup/signup', $data);
}
public function signup(): IContent
{
$session = $this->request->session();
if ($session->get('user')) {
//TODO: return with some error
$data = ['success' => true];
return new JsonContent($data);
}
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->where('email', '=', $this->request->post('email'));
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($userData !== null) {
$user = new User($userData);
if ($user->getActive()) {
$data = ['error' => 'user_found'];
} else {
$data = ['error' => 'not_active_user_found'];
}
return new JsonContent($data);
}
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => 'passwords_too_short'];
return new JsonContent($data);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
}
$user = new User([
'email' => $this->request->post('email'),
]);
$user->setPlainPassword($this->request->post('password'));
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$userId = $modify->getId();
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime());
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->set('user_id', $userId);
$modify->set('token', $token);
$modify->save();
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token);
$data = ['success' => true];
return new JsonContent($data);
}
public function activate()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$select = new Select(\Container::$dbConnection, 'user_confirmations');
$select->columns(['id', 'user_id']);
$select->where('token', '=', $this->request->query('token'));
$confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($confirmation === null) {
$data = [];
return new HtmlContent('signup/activate', $data);
}
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->setId($confirmation['id']);
$modify->delete();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->setId($confirmation['user_id']);
$modify->set('active', true);
$modify->save();
\Container::$dbConnection->commit();
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->whereId($confirmation['user_id']);
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
$user = new User($userData);
$session->set('user', $user);
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
public function cancel()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$select = new Select(\Container::$dbConnection, 'user_confirmations');
$select->columns(['id', 'user_id']);
$select->where('token', '=', $this->request->query('token'));
$confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($confirmation === null) {
$data = ['success' => false];
return new HtmlContent('signup/cancel', $data);
}
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->setId($confirmation['id']);
$modify->delete();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->setId($confirmation['user_id']);
$modify->delete();
\Container::$dbConnection->commit();
$data = ['success' => true];
return new HtmlContent('signup/cancel', $data);
}
private function sendConfirmationEmail($email, $token): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to MapGuesser - Activate your account');
$mail->setBodyFromTemplate('signup', [
'EMAIL' => $email,
'ACTIVATE_LINK' => $this->request->getBase() . '/signup/activate/' . $token,
'CANCEL_LINK' => $this->request->getBase() . '/signup/cancel/' . $token,
]);
$mail->send();
}
}

View File

@ -1,64 +1,399 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use DateTime;
use SokoWeb\Http\Request;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
use SokoWeb\Response\Redirect;
use SokoWeb\Util\JwtParser;
class UserController implements ISecured
class UserController implements IAuthenticationRequired
{
private IRequest $request;
private UserRepository $userRepository;
public function __construct(IRequest $request)
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private UserInChallengeRepository $userInChallengeRepository;
private GuessRepository $guessRepository;
public function __construct()
{
$this->request = $request;
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
$this->guessRepository = new GuessRepository();
}
public function authorize(): bool
public function isAuthenticationRequired(): bool
{
$user = $this->request->user();
return $user !== null;
return true;
}
public function getProfile(): IContent
public function getAccount(): IContent
{
$user = $this->request->user();
/**
* @var User $user
*/
$user = \Container::$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 getGoogleConnectRedirect(): IRedirect
{
$user = $this->request->user();
/**
* @var User $user
*/
$user = \Container::$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));
\Container::$request->session()->set('oauth_state', $state);
\Container::$request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleConnect-confirm')->generateLink(),
$nonce,
$user->getEmail()
);
return new Redirect($url, IRedirect::TEMPORARY);
}
public function getGoogleConnectConfirm(): IContent
{
$defaultError = 'Authentication with Google failed. Please <a href="' . \Container::$routeCollection->getRoute('account.googleConnect')->generateLink() . '" title="Connect with Google">try again</a>!';
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
}
if (strlen($this->request->post('password_new')) > 0) {
if (strlen($this->request->post('password_new')) < 6) {
$data = ['error' => 'passwords_too_short'];
return new JsonContent($data);
}
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
}
$user->setPlainPassword($this->request->post('password_new'));
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
\Container::$request->query('code'),
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleConnect-confirm')->generateLink()
);
if (!isset($tokenData['id_token'])) {
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
}
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
}
$data = ['success' => true];
return new JsonContent($data);
$anotherUser = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($anotherUser !== null) {
return new HtmlContent('account/google_connect', [
'success' => false,
'error' => 'This Google account is linked to another account.'
]);
}
\Container::$request->session()->set('google_user_data', $idToken);
/**
* @var User $user
*/
$user = \Container::$request->user();
return new HtmlContent('account/google_connect', [
'success' => true,
'googleAccount' => $idToken['email'],
'userEmail' => $user->getEmail()
]);
}
public function connectGoogle(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong.'
]
]);
}
$googleUserData = \Container::$request->session()->get('google_user_data');
$user->setGoogleSub($googleUserData['sub']);
\Container::$persistentDataManager->saveToDb($user);
return new JsonContent(['success' => true]);
}
public function getGoogleDisconnectConfirm(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
return new HtmlContent('account/google_disconnect', [
'success' => true,
'userEmail' => $user->getEmail()
]);
}
public function disconnectGoogle(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong.'
]
]);
}
$user->setGoogleSub(null);
\Container::$persistentDataManager->saveToDb($user);
return new JsonContent(['success' => true]);
}
public function getGoogleAuthenticateRedirect(): IRedirect
{
/**
* @var User $user
*/
$user = \Container::$request->user();
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
\Container::$request->session()->set('oauth_state', $state);
\Container::$request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
\Container::$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 = \Container::$request->user();
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
\Container::$request->query('code'),
\Container::$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'] !== \Container::$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');
\Container::$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 = \Container::$request->user();
return new HtmlContent('account/delete', ['user' => $user->toArray()]);
}
public function saveAccount(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
if (!$this->confirmUserIdentity(
$user,
\Container::$request->session()->get('authenticated_with_google_until'),
\Container::$request->post('password'),
$error
)) {
return new JsonContent(['error' => ['errorText' => $error]]);
}
$newEmail = \Container::$request->post('email');
if ($newEmail !== $user->getEmail()) {
if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) {
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if ($this->userRepository->getByEmail($newEmail) !== null) {
return new JsonContent(['error' => ['errorText' => 'The given email address belongs to another account.']]);
}
$user->setEmail($newEmail);
}
$newUsername = \Container::$request->post('username');
if ($newUsername !== $user->getUsername()) {
if (strlen($newUsername) == 0) {
return new JsonContent(['error' => ['errorText' => 'Username cannot be empty.']]);
}
if (preg_match('/^[a-zA-Z0-9_\-\.]+$/', $newUsername) !== 1) {
return new JsonContent(['error' => ['errorText' => 'Username can contain only english letters, digits, - (hyphen), . (dot), _ (underscore).']]);
}
if ($this->userRepository->getByUsername($newUsername) !== null) {
return new JsonContent(['error' => ['errorText' => 'The given username is already taken.']]);
}
$user->setUsername($newUsername);
}
if (strlen(\Container::$request->post('password_new')) > 0) {
if (strlen(\Container::$request->post('password_new')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if (\Container::$request->post('password_new') !== \Container::$request->post('password_new_confirm')) {
return new JsonContent([
'error' => [
'errorText' => 'The given new passwords do not match.'
]
]);
}
$user->setPlainPassword(\Container::$request->post('password_new'));
}
\Container::$persistentDataManager->saveToDb($user);
\Container::$request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]);
}
public function deleteAccount(): IContent
{
/**
* @var User $user
*/
$user = \Container::$request->user();
if (!$this->confirmUserIdentity(
$user,
\Container::$request->session()->get('authenticated_with_google_until'),
\Container::$request->post('password'),
$error
)) {
return new JsonContent(['error' => ['errorText' => $error]]);
}
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
\Container::$persistentDataManager->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
\Container::$persistentDataManager->deleteFromDb($userPasswordResetter);
}
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
foreach ($this->userInChallengeRepository->getAllByUser($user) as $userInChallenge) {
\Container::$persistentDataManager->deleteFromDb($userInChallenge);
}
foreach ($this->guessRepository->getAllByUser($user) as $guess) {
\Container::$persistentDataManager->deleteFromDb($guess);
}
\Container::$persistentDataManager->deleteFromDb($user);
\Container::$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;
}
}

View File

@ -1,114 +0,0 @@
<?php namespace MapGuesser\Database\Mysql;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Database\IStatement;
use mysqli;
class Connection implements IConnection
{
private mysqli $connection;
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');
}
if ($socket === null) {
$socket = ini_get('mysqli.default_socket');
}
$this->connection = new mysqli($host, $user, $password, $db, $port, $socket);
if ($this->connection->connect_error) {
throw new \Exception('Connection failed: ' . $this->connection->connect_error);
}
if (!$this->connection->set_charset('utf8mb4')) {
throw new \Exception($this->connection->error);
}
}
public function __destruct()
{
$this->connection->close();
}
public function startTransaction(): void
{
if (!$this->connection->autocommit(false)) {
throw new \Exception($this->connection->error);
}
}
public function commit(): void
{
if (!$this->connection->commit() || !$this->connection->autocommit(true)) {
throw new \Exception($this->connection->error);
}
}
public function rollback(): void
{
if (!$this->connection->rollback() || !$this->connection->autocommit(true)) {
throw new \Exception($this->connection->error);
}
}
public function query(string $query): ?IResultSet
{
if (!($result = $this->connection->query($query))) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
if ($result !== true) {
return new ResultSet($result);
}
return null;
}
public function multiQuery(string $query): array
{
if (!$this->connection->multi_query($query)) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
$ret = [];
do {
if ($result = $this->connection->store_result()) {
$ret[] = new ResultSet($result);
} else {
$ret[] = null;
}
$this->connection->more_results();
} while ($this->connection->next_result());
if ($this->connection->error) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
return $ret;
}
public function prepare(string $query): IStatement
{
if (!($stmt = $this->connection->prepare($query))) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
return new Statement($stmt);
}
public function lastId(): int
{
return $this->connection->insert_id;
}
public function getAffectedRows(): int
{
return $this->connection->affected_rows;
}
}

View File

@ -1,62 +0,0 @@
<?php namespace MapGuesser\Database\Mysql;
use MapGuesser\Interfaces\Database\IResultSet;
use mysqli_result;
class ResultSet implements IResultSet
{
private mysqli_result $result;
public function __construct(mysqli_result $result)
{
$this->result = $result;
}
public function fetch(int $type = IResultSet::FETCH_ASSOC): ?array
{
return $this->result->fetch_array($this->convertFetchType($type));
}
public function fetchAll(int $type = IResultSet::FETCH_ASSOC): array
{
return $this->result->fetch_all($this->convertFetchType($type));
}
public function fetchOneColumn(string $valueName, string $keyName = null): array
{
$array = [];
while ($r = $this->fetch(IResultSet::FETCH_ASSOC)) {
if (isset($keyName)) {
$array[$r[$keyName]] = $r[$valueName];
} else {
$array[] = $r[$valueName];
}
}
return $array;
}
private function convertFetchType(int $type): int
{
switch ($type) {
case IResultSet::FETCH_ASSOC:
$internal_type = MYSQLI_ASSOC;
break;
case IResultSet::FETCH_BOTH:
$internal_type = MYSQLI_BOTH;
break;
case IResultSet::FETCH_NUM:
$internal_type = MYSQLI_NUM;
break;
default:
$internal_type = MYSQLI_BOTH;
break;
}
return $internal_type;
}
}

View File

@ -1,79 +0,0 @@
<?php namespace MapGuesser\Database\Mysql;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Database\IStatement;
use mysqli_stmt;
class Statement implements IStatement
{
private mysqli_stmt $stmt;
public function __construct(mysqli_stmt $stmt)
{
$this->stmt = $stmt;
}
public function __destruct()
{
$this->stmt->close();
}
public function execute(array $params = []): ?IResultSet
{
if ($params) {
$ref_params = [''];
foreach ($params as &$param) {
$type = gettype($param);
switch ($type) {
case 'integer':
case 'double':
case 'string':
$t = $type[0];
break;
case 'NULL':
$t = 's';
break;
case 'boolean':
$param = (string) (int) $param;
$t = 's';
break;
case 'array':
$param = json_encode($param);
$t = 's';
break;
}
if (!isset($t)) {
throw new \Exception('Data type ' . $type . ' not supported!');
}
$ref_params[] = &$param;
$ref_params[0] .= $t;
}
if (!call_user_func_array([$this->stmt, 'bind_param'], $ref_params)) {
throw new \Exception($this->stmt->error);
}
}
if (!$this->stmt->execute()) {
throw new \Exception($this->stmt->error);
}
if ($result_set = $this->stmt->get_result()) {
return new ResultSet($result_set);
}
return null;
}
public function getAffectedRows(): int
{
return $this->stmt->affected_rows;
}
}

View File

@ -1,176 +0,0 @@
<?php namespace MapGuesser\Database\Query;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Database\Utils;
class Modify
{
private IConnection $connection;
private string $table;
private string $idName = 'id';
private array $attributes = [];
private array $original = [];
private ?string $externalId = null;
private bool $autoIncrement = true;
public function __construct(IConnection $connection, string $table)
{
$this->connection = $connection;
$this->table = $table;
}
public function setIdName(string $idName): Modify
{
$this->idName = $idName;
return $this;
}
public function setExternalId($id): Modify
{
$this->externalId = $id;
return $this;
}
public function setAutoIncrement(bool $autoIncrement = true): Modify
{
$this->autoIncrement = $autoIncrement;
return $this;
}
public function fill(array $attributes): Modify
{
$this->attributes = array_merge($this->attributes, $attributes);
return $this;
}
public function set(string $name, $value): Modify
{
$this->attributes[$name] = $value;
return $this;
}
public function setId($id): Modify
{
$this->attributes[$this->idName] = $id;
return $this;
}
public function getId()
{
return $this->attributes[$this->idName];
}
public function save(): void
{
if (isset($this->attributes[$this->idName])) {
$this->update();
} else {
$this->insert();
}
}
public function delete(): void
{
if (!isset($this->attributes[$this->idName])) {
throw new \Exception('No primary key specified!');
}
$query = 'DELETE FROM ' . Utils::backtick($this->table) . ' WHERE ' . Utils::backtick($this->idName) . '=?';
$stmt = $this->connection->prepare($query);
$stmt->execute([$this->idName => $this->attributes[$this->idName]]);
}
private function insert(): void
{
if ($this->externalId !== null) {
$this->attributes[$this->idName] = $this->externalId;
} elseif (!$this->autoIncrement) {
$this->attributes[$this->idName] = $this->generateKey();
}
$set = $this->generateColumnsWithBinding(array_keys($this->attributes));
$query = 'INSERT INTO ' . Utils::backtick($this->table) . ' SET ' . $set;
$stmt = $this->connection->prepare($query);
$stmt->execute($this->attributes);
if ($this->autoIncrement) {
$this->attributes[$this->idName] = $this->connection->lastId();
}
}
private function update(): void
{
/*$diff = $this->generateDiff();
if (count($diff) === 0) {
return;
}*/
$diff = $this->attributes;
unset($diff[$this->idName]);
$set = $this->generateColumnsWithBinding(array_keys($diff));
$query = 'UPDATE ' . Utils::backtick($this->table) . ' SET ' . $set . ' WHERE ' . Utils::backtick($this->idName) . '=?';
$stmt = $this->connection->prepare($query);
$stmt->execute(array_merge($diff, [$this->idName => $this->attributes[$this->idName]]));
}
private function readFromDB(array $columns): void
{
$select = (new Select($this->connection, $this->table))
->setIdName($this->idName)
->whereId($this->attributes[$this->idName])
->columns($columns);
$this->original = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
}
private function generateDiff(): array
{
$this->readFromDB(array_keys($this->attributes));
$diff = [];
foreach ($this->attributes as $name => $value) {
$original = $this->original[$name];
if ($original != $value) {
$diff[$name] = $value;
}
}
return $diff;
}
public static function generateColumnsWithBinding(array $columns): string
{
array_walk($columns, function(&$value, $key) {
$value = Utils::backtick($value) . '=?';
});
return implode(',', $columns);
}
private function generateKey(): string
{
return substr(hash('sha256', serialize($this->attributes) . random_bytes(10) . microtime()), 0, 7);
}
}

View File

@ -1,402 +0,0 @@
<?php namespace MapGuesser\Database\Query;
use Closure;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Database\RawExpression;
use MapGuesser\Database\Utils;
class Select
{
const CONDITION_WHERE = 0;
const CONDITION_HAVING = 1;
private IConnection $connection;
private string $table;
private string $idName = 'id';
private array $tableAliases = [];
private array $joins = [];
private array $columns = [];
private array $conditions = [self::CONDITION_WHERE => [], self::CONDITION_HAVING => []];
private array $groups = [];
private array $orders = [];
private array $limit;
public function __construct(IConnection $connection, string $table)
{
$this->connection = $connection;
$this->table = $table;
}
public function setIdName(string $idName): Select
{
$this->idName = $idName;
return $this;
}
public function setTableAliases(array $tableAliases): Select
{
$this->tableAliases = array_merge($this->tableAliases, $tableAliases);
return $this;
}
public function columns(array $columns): Select
{
$this->columns = array_merge($this->columns, $columns);
return $this;
}
public function innerJoin($table, $column1, string $relation, $column2): Select
{
$this->addJoin('INNER', $table, $column1, $relation, $column2);
return $this;
}
public function leftJoin($table, $column1, string $relation, $column2): Select
{
$this->addJoin('LEFT', $table, $column1, $relation, $column2);
return $this;
}
public function whereId($value): Select
{
$this->addWhereCondition('AND', $this->idName, '=', $value);
return $this;
}
public function where($column, string $relation = null, $value = null): Select
{
$this->addWhereCondition('AND', $column, $relation, $value);
return $this;
}
public function orWhere($column, string $relation = null, $value = null): Select
{
$this->addWhereCondition('OR', $column, $relation, $value);
return $this;
}
public function having($column, string $relation = null, $value = null): Select
{
$this->addHavingCondition('AND', $column, $relation, $value);
return $this;
}
public function orHaving($column, string $relation = null, $value = null): Select
{
$this->addHavingCondition('OR', $column, $relation, $value);
return $this;
}
public function groupBy($column): Select
{
$this->groups[] = $column;
return $this;
}
public function orderBy($column, string $type = 'ASC'): Select
{
$this->orders[] = [$column, $type];
return $this;
}
public function limit(int $limit, int $offset = 0): Select
{
$this->limit = [$limit, $offset];
return $this;
}
public function resetLimit(): void
{
$this->limit = null;
}
public function paginate(int $page, int $itemsPerPage)
{
$this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
return $this;
}
public function execute(): IResultSet
{
list($query, $params) = $this->generateQuery();
return $this->connection->prepare($query)->execute($params);
}
public function count(): int
{
if (count($this->groups) > 0 || count($this->conditions[self::CONDITION_HAVING]) > 0) {
$orders = $this->orders;
$this->orders = [];
list($query, $params) = $this->generateQuery();
$result = $this->connection->prepare('SELECT COUNT(*) num_rows FROM (' . $query . ') x')
->execute($params)
->fetch(IResultSet::FETCH_NUM);
$this->orders = $orders;
return $result[0];
} else {
$columns = $this->columns;
$orders = $this->orders;
$this->columns = [new RawExpression('COUNT(*) num_rows')];
$this->orders = [];
list($query, $params) = $this->generateQuery();
$result = $this->connection->prepare($query)
->execute($params)
->fetch(IResultSet::FETCH_NUM);
$this->columns = $columns;
$this->orders = $orders;
return $result[0];
}
}
private function addJoin(string $type, $table, $column1, string $relation, $column2): void
{
$this->joins[] = [$type, $table, $column1, $relation, $column2];
}
private function addWhereCondition(string $logic, $column, string $relation, $value): void
{
$this->conditions[self::CONDITION_WHERE][] = [$logic, $column, $relation, $value];
}
private function addHavingCondition(string $logic, $column, string $relation, $value): void
{
$this->conditions[self::CONDITION_HAVING][] = [$logic, $column, $relation, $value];
}
private function generateQuery(): array
{
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $this->generateTable($this->table, true);
if (count($this->joins) > 0) {
$queryString .= ' ' . $this->generateJoins();
}
if (count($this->conditions[self::CONDITION_WHERE]) > 0) {
list($wheres, $whereParams) = $this->generateConditions(self::CONDITION_WHERE);
$queryString .= ' WHERE ' . $wheres;
} else {
$whereParams = [];
}
if (count($this->groups) > 0) {
$queryString .= ' GROUP BY ' . $this->generateGroupBy();
}
if (count($this->conditions[self::CONDITION_HAVING]) > 0) {
list($havings, $havingParams) = $this->generateConditions(self::CONDITION_HAVING);
$queryString .= ' HAVING ' . $havings;
} else {
$havingParams = [];
}
if (count($this->orders) > 0) {
$queryString .= ' ORDER BY ' . $this->generateOrderBy();
}
if (isset($this->limit)) {
$queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0];
}
return [$queryString, array_merge($whereParams, $havingParams)];
}
private function generateTable($table, bool $defineAlias = false): string
{
if ($table instanceof RawExpression) {
return (string) $table;
}
if (isset($this->tableAliases[$table])) {
return ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
}
return Utils::backtick($table);
}
private function generateColumn($column): string
{
if ($column instanceof RawExpression) {
return (string) $column;
}
if (is_array($column)) {
$out = '';
if ($column[0]) {
$out .= $this->generateTable($column[0]) . '.';
}
$out .= Utils::backtick($column[1]);
if (!empty($column[2])) {
$out .= ' ' . Utils::backtick($column[2]);
}
return $out;
} else {
return Utils::backtick($column);
}
}
private function generateColumns(): string
{
$columns = $this->columns;
array_walk($columns, function (&$value, $key) {
$value = $this->generateColumn($value);
});
return implode(',', $columns);
}
private function generateJoins(): string
{
$joins = $this->joins;
array_walk($joins, function (&$value, $key) {
$value = $value[0] . ' JOIN ' . $this->generateTable($value[1], true) . ' ON ' . $this->generateColumn($value[2]) . ' ' . $value[3] . ' ' . $this->generateColumn($value[4]);
});
return implode(' ', $joins);
}
private function generateConditions(string $type): array
{
$conditions = '';
$params = [];
foreach ($this->conditions[$type] as $condition) {
list($logic, $column, $relation, $value) = $condition;
if ($column instanceof Closure) {
list($conditionsStringFragment, $paramsFragment) = $this->generateComplexConditionFragment($type, $column);
} else {
list($conditionsStringFragment, $paramsFragment) = $this->generateConditionFragment($condition);
}
if ($conditions !== '') {
$conditions .= ' ' . $logic . ' ';
}
$conditions .= $conditionsStringFragment;
$params = array_merge($params, $paramsFragment);
}
return [$conditions, $params];
}
private function generateConditionFragment(array $condition): array
{
list($logic, $column, $relation, $value) = $condition;
if ($column instanceof RawExpression) {
return [(string) $column, []];
}
$conditionsString = $this->generateColumn($column) . ' ';
if ($value === null) {
return [$conditionsString . ($relation == '=' ? 'IS NULL' : 'IS NOT NULL'), []];
}
$conditionsString .= strtoupper($relation) . ' ';;
switch ($relation = strtolower($relation)) {
case 'between':
$params = [$value[0], $value[1]];
$conditionsString .= '? AND ?';
break;
case 'in':
case 'not in':
$params = $value;
if (count($value) > 0) {
$conditionsString .= '(' . implode(', ', array_fill(0, count($value), '?')) . ')';
} else {
$conditionsString = $relation == 'in' ? '0' : '1';
}
break;
default:
$params = [$value];
$conditionsString .= '?';
}
return [$conditionsString, $params];
}
private function generateComplexConditionFragment(string $type, Closure $conditionCallback): array
{
$instance = new static($this->connection, $this->table);
$instance->tableAliases = $this->tableAliases;
$conditionCallback($instance);
list($conditions, $params) = $instance->generateConditions($type);
return ['(' . $conditions . ')', $params];
}
private function generateGroupBy(): string
{
$groups = $this->groups;
array_walk($groups, function (&$value, $key) {
$value = $this->generateColumn($value);
});
return implode(',', $groups);
}
private function generateOrderBy(): string
{
$orders = $this->orders;
array_walk($orders, function (&$value, $key) {
$value = $this->generateColumn($value[0]) . ' ' . strtoupper($value[1]);
});
return implode(',', $orders);
}
}

View File

@ -1,16 +0,0 @@
<?php namespace MapGuesser\Database;
class RawExpression
{
private string $expression;
public function __construct(string $expression)
{
$this->expression = $expression;
}
public function __toString(): string
{
return $this->expression;
}
}

View File

@ -1,7 +0,0 @@
<?php namespace MapGuesser\Database;
class Utils {
public static function backtick(string $name) {
return '`' . $name . '`';
}
}

View File

@ -1,93 +0,0 @@
<?php namespace MapGuesser\Http;
class Request
{
const HTTP_GET = 0;
const HTTP_POST = 1;
private string $url;
private int $method;
private string $query = '';
private array $headers = [];
public function __construct(string $url, int $method = self::HTTP_GET)
{
$this->url = $url;
$this->method = $method;
}
public function setQuery($query)
{
if (is_string($query)) {
$this->query = $query;
} else {
$this->query = http_build_query($query);
}
}
public function setHeaders(array $headers)
{
$this->headers = array_merge($this->headers, $headers);
}
public function send(): Response
{
$ch = curl_init();
if ($this->method === self::HTTP_GET) {
$url = $this->url . '?' . $this->query;
} elseif ($this->method === self::HTTP_POST) {
$url = $this->url;
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_USERAGENT, 'MapGuesser cURL/1.0');
if (count($this->headers) > 0) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
}
$responseHeaders = [];
curl_setopt(
$ch,
CURLOPT_HEADERFUNCTION,
function ($ch, $header) use (&$responseHeaders) {
$len = strlen($header);
$header = explode(':', $header, 2);
if (count($header) < 2) {
return $len;
}
$responseHeaders[strtolower(trim($header[0]))][] = trim($header[1]);
return $len;
}
);
$responseBody = curl_exec($ch);
if ($responseBody === false) {
$error = curl_error($ch);
curl_close($ch);
throw new \Exception($error);
}
curl_close($ch);
return new Response($responseBody, $responseHeaders);
}
}

View File

@ -1,24 +0,0 @@
<?php namespace MapGuesser\Http;
class Response
{
private string $body;
private array $headers;
public function __construct(string $body, array $headers)
{
$this->body = $body;
$this->headers = $headers;
}
public function getBody()
{
return $this->body;
}
public function getHeaders()
{
return $this->headers;
}
}

View File

@ -1,12 +0,0 @@
<?php namespace MapGuesser\Interfaces\Authentication;
interface IUser
{
const PERMISSION_NORMAL = 0;
const PERMISSION_ADMIN = 1;
public function hasPermission(int $permission): bool;
public function getDisplayName(): string;
}

Some files were not shown because too many files have changed in this diff Show More