Compare commits

..

204 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
140 changed files with 4572 additions and 5090 deletions

View File

@ -1,4 +1,5 @@
APP_NAME=MapGuesser APP_NAME=MapGuesser
APP_URL=mapguesser.dev
DEV=1 DEV=1
DB_HOST=mariadb DB_HOST=mariadb
DB_USER=mapguesser DB_USER=mapguesser
@ -7,6 +8,7 @@ DB_NAME=mapguesser
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url 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 LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles
STATIC_ROOT=/static STATIC_ROOT=/static
MAIL_FROM=mapguesser@mapguesser-dev.ch MAIL_FROM=mapguesser@mapguesser-dev.ch
@ -19,3 +21,6 @@ MULTI_INTERNAL_HOST=multi
MULTI_INTERNAL_PORT=5000 MULTI_INTERNAL_PORT=5000
MULTI_WS_URL=mapguesser-dev.ch:8090 MULTI_WS_URL=mapguesser-dev.ch:8090
MULTI_WS_PORT=8090 MULTI_WS_PORT=8090
ENABLE_GAME_FOR_GUESTS=0
RECAPTCHA_SITEKEY=your_recaptcha_sitekey
RECAPTCHA_SECRET=your_recaptcha_secret

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'
}
}
}
}
}
}

123
README.md
View File

@ -1,56 +1,16 @@
# MapGuesser # MapGuesser
[![Build Status](https://ci.esoko.eu/job/mapguesser/job/develop/badge/icon)](https://ci.esoko.eu/job/mapguesser/job/develop/)
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. 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 ## Installation
### Clone the Git repository
The first step is obviously cloning the repository to your machine:
```
git clone https://gitea.e5tv.hu/esoko/mapguesser.git
```
All the commands listed here should be executed from the repository root.
### Setup Docker stack (recommended)
The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`.
All you have to do is executing the following command:
```
docker-compose up -d
```
Attach shell to the container of `mapguesser_app`:
```
docker exec -it mapguesser_app_1 bash
```
All of the following commands should be executed there.
### Manual setup (alternative)
If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.
### Initialize project
This command installes all of the Composer requirements and creates a copy of the example `.env` file.
```
composer create-project
```
### Set environment variables ### 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. 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.
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production mode. **Important: `DEV` should NOT be set for production! See section Development if you want to use the application in development mode.**
**Hint:** If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value.
#### API keys #### API keys
@ -65,33 +25,80 @@ 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_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** * **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` 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. 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:
### (Production only) Create cron job
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommended to create a cron job that runs every hour:
``` ```
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1 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"
``` ```
### Finalize installation ### Docker Compose
After you followed the above steps, execute the following command: 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:
``` ```
scripts/install.sh
Execute the following command:
```bash
docker compose up -d
``` ```
**Warning: Because of a known issue the image `mapguesser_multi` fails to run without the installation steps. You have to relauch `docker-compose up -d` after you finished the installation process.**
**And you are done!** The application is ready to use and develop. In development mode an administrative user is also created by the installation script, email is **mapg@mapg.dev**, password is **123456**. In production mode you should create the first administrative user with the following command: **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 PASSWORD admin ./mapg user:add EMAIL USERNAME PASSWORD admin
``` ```
If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306. ## 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.
--- ---

View File

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

2030
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; ) 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`; DROP TABLE IF EXISTS `places`;
CREATE TABLE `places` ( CREATE TABLE `places` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,

View File

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

View File

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

View File

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

@ -2,22 +2,20 @@ version: '3'
services: services:
app: app:
build: build:
context: ./docker context: .
dockerfile: Dockerfile-app dockerfile: docker/Dockerfile
target: mapg_dev
depends_on:
mariadb:
condition: service_healthy
ports: ports:
- 80:80 - 80:80
volumes:
- .:/var/www/mapguesser
multi:
build:
context: ./docker
dockerfile: Dockerfile-multi
ports:
- 5000:5000 - 5000:5000
- 8090:8090 - 8090:8090
- 9229:9229 - 9229:9229
volumes: volumes:
- .:/var/www/mapguesser - .:/var/www/mapguesser
working_dir: /var/www/mapguesser
mariadb: mariadb:
image: mariadb:10.3 image: mariadb:10.3
ports: ports:
@ -29,6 +27,19 @@ services:
MYSQL_DATABASE: 'mapguesser' MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser' MYSQL_USER: 'mapguesser'
MYSQL_PASSWORD: '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: mail:
image: marcopas/docker-mailslurper:latest image: marcopas/docker-mailslurper:latest
ports: ports:

44
docker/Dockerfile Normal file
View File

@ -0,0 +1,44 @@
FROM ubuntu:22.04 AS mapg_base
ENV DEBIAN_FRONTEND noninteractive
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
RUN mkdir -p /run/php
COPY docker/configs/nginx.conf /etc/nginx/sites-available/default
COPY docker/scripts/install-composer.sh install-composer.sh
RUN ./install-composer.sh
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
EXPOSE 5000
EXPOSE 8090
EXPOSE 9229
ENTRYPOINT docker/scripts/entry-point-dev.sh
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,30 +0,0 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install Nginx, PHP and further necessary packages
RUN apt update --fix-missing
RUN apt install -y curl git unzip 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 php7.4-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
# Install Composer
COPY 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 yarn
EXPOSE 80
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
ENTRYPOINT /usr/sbin/php-fpm7.4 -F & /usr/sbin/nginx -g 'daemon off;'

View File

@ -1,16 +0,0 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install necessary packages
RUN apt update --fix-missing
RUN apt install -y curl build-essential
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
ENTRYPOINT /usr/bin/node --inspect=0.0.0.0:9229 multi

View File

@ -1,11 +1,15 @@
map $http_x_forwarded_proto $forwarded_scheme {
default $scheme;
http http;
https https;
}
server { server {
listen 80 default_server; listen 80 default_server;
listen [::]:80 default_server; listen [::]:80 default_server;
root /var/www/mapguesser/public; root /var/www/mapguesser/public;
index index.php index.html index.htm index.nginx-debian.html; index index.php index.html index.htm index.nginx-debian.html;
server_name mapguesser-dev.ch; server_name mapguesser-dev.ch;
location / { location / {
@ -14,7 +18,8 @@ server {
location ~ \.php$ { location ~ \.php$ {
include snippets/fastcgi-php.conf; 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 { 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

View File

@ -14,10 +14,12 @@ $dotenv->load();
class Container class Container
{ {
static MapGuesser\Interfaces\Database\IConnection $dbConnection; static SokoWeb\Interfaces\Database\IConnection $dbConnection;
static MapGuesser\Routing\RouteCollection $routeCollection; static SokoWeb\Interfaces\PersistentData\IPersistentDataManager $persistentDataManager;
static MapGuesser\Interfaces\Session\ISessionHandler $sessionHandler; static SokoWeb\Interfaces\Routing\IRouteCollection $routeCollection;
static MapGuesser\Interfaces\Request\IRequest $request; 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);

2
mapg
View File

@ -8,6 +8,6 @@ $app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\MigrateDatabaseCommand()); $app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
$app->add(new MapGuesser\Cli\AddUserCommand()); $app->add(new MapGuesser\Cli\AddUserCommand());
$app->add(new MapGuesser\Cli\LinkViewCommand()); $app->add(new MapGuesser\Cli\LinkViewCommand());
$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand()); $app->add(new MapGuesser\Cli\MaintainDatabaseCommand());
$app->run(); $app->run();

View File

@ -1,17 +1,43 @@
{ {
"name": "mapguesser-multi", "name": "mapguesser-multi",
"lockfileVersion": 3,
"requires": true, "requires": true,
"lockfileVersion": 1, "packages": {
"dependencies": { "": {
"dotenv": { "name": "mapguesser-multi",
"license": "GNU AGPL 3.0",
"dependencies": {
"dotenv": "^8.2.0",
"ws": "^7.4.4"
}
},
"node_modules/dotenv": {
"version": "8.2.0", "version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"engines": {
"node": ">=8"
}
}, },
"ws": { "node_modules/ws": {
"version": "7.4.4", "version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" "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
}
}
} }
} }
} }

View File

@ -1,53 +1,3 @@
<?php <?php
require '../web.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, $url == '' ? [] : 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')) {
$content = new MapGuesser\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
header('Content-Type: text/html; charset=UTF-8', true, 403);
$content->render();
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');
$response->render();
return;
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
return;
}
}
}
$content = new MapGuesser\Response\HtmlContent('error/404');
header('Content-Type: text/html; charset=UTF-8', true, 404);
$content->render();

View File

@ -15,6 +15,17 @@
right: 0; right: 0;
background-color: #000000; background-color: #000000;
opacity: 0.5; opacity: 0.5;
z-index: 4;
}
#panningBlockerCover {
display: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
opacity: 0;
z-index: 3; z-index: 3;
} }
@ -22,7 +33,7 @@
position: absolute; position: absolute;
bottom: 30px; bottom: 30px;
right: 20px; right: 20px;
z-index: 2; z-index: 3;
} }
#guess.result { #guess.result {
@ -153,6 +164,47 @@
z-index: 2; 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) { @media screen and (max-width: 599px) {
#mapName { #mapName {
display: none; display: none;

View File

@ -31,11 +31,11 @@ main {
color: #ffffff; 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; font-family: 'Roboto', sans-serif;
} }
h1, h2 { h1, h2, h3 {
font-weight: 500; font-weight: 500;
} }
@ -55,7 +55,11 @@ h2, header.small h1 {
font-size: 24px; font-size: 24px;
} }
p, h2 { h3 {
font-size: 18px;
}
p, h2, h3 {
line-height: 150%; line-height: 150%;
} }
@ -237,7 +241,7 @@ button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.
background-color: #1b7d31; background-color: #1b7d31;
} }
input, select, textarea { input.text, select, textarea {
background-color: #f9fafb; background-color: #f9fafb;
border: solid #c8d2e1 1px; border: solid #c8d2e1 1px;
border-radius: 2px; border-radius: 2px;
@ -246,22 +250,26 @@ input, select, textarea {
font-weight: 300; font-weight: 300;
} }
input, select { input.text, select {
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
padding: 0 5px; padding: 0 5px;
} }
input[type=checkbox], input[type=radio] {
margin-right: 0.5em;
}
textarea { textarea {
padding: 5px; padding: 5px;
resize: none; resize: none;
} }
input.big, select.big, textarea.big, div.inputWithButton>input { input.text.big, select.big, textarea.big, div.inputWithButton>input.text {
font-size: 18px; font-size: 18px;
} }
input.big, select.big, div.inputWithButton>input { input.text.big, select.big, div.inputWithButton>input.text {
height: 35px; height: 35px;
line-height: 35px; line-height: 35px;
padding: 0 6px; padding: 0 6px;
@ -276,19 +284,19 @@ input.fullWidth, select.fullWidth, textarea.fullWidth {
width: 100%; width: 100%;
} }
input:disabled, select:disabled, textarea:disabled { input.text:disabled, select:disabled, textarea:disabled {
background-color: #dfdfdf; background-color: #dfdfdf;
border: solid #dfdfdf 1px; border: solid #dfdfdf 1px;
color: #000000; color: #000000;
} }
input:focus, select:focus, textarea:focus { input.text:focus, select:focus, textarea:focus {
background-color: #ffffff; background-color: #ffffff;
border: solid #29457f 2px; border: solid #29457f 2px;
outline: none; outline: none;
} }
input:focus, select:focus { input.text:focus, select:focus {
padding: 0 4px; padding: 0 4px;
} }
@ -296,16 +304,16 @@ textarea:focus {
padding: 4px; padding: 4px;
} }
input.big:focus, select.big:focus { input.text.big:focus, select.big:focus {
padding: 0 5px; padding: 0 5px;
} }
div.inputWithButton>input { div.inputWithButton>input.text {
width: 100%; width: 100%;
padding: 0 83px 0 6px; padding: 0 83px 0 6px;
} }
div.inputWithButton>input:focus { div.inputWithButton>input.text:focus {
padding: 0 82px 0 5px; padding: 0 82px 0 5px;
} }
@ -368,7 +376,7 @@ header>p>span {
padding-left: 6px; padding-left: 6px;
} }
header>p>span>a:link, header>p>span>a:visited { header>p>span>a:link, header>p>span>a:visited, footer>p>a:link, footer>p>a:visited {
color: inherit; color: inherit;
} }
@ -451,6 +459,7 @@ div.box {
} }
.circleControl .controlItem { .circleControl .controlItem {
position: relative;
height: 60px; height: 60px;
margin-top: 10px; margin-top: 10px;
opacity: 70%; opacity: 70%;
@ -463,6 +472,8 @@ div.box {
.circleControl .controlItem div { .circleControl .controlItem div {
position: absolute; position: absolute;
width: 100%;
height: 100%;
} }
.circleControl .controlBackground { .circleControl .controlBackground {
@ -477,7 +488,6 @@ div.box {
margin: auto; margin: auto;
margin-top: 50%; margin-top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
-ms-transform: translateY(-50%);
} }
@media screen and (max-width: 599px) { @media screen and (max-width: 599px) {

View File

@ -13,6 +13,10 @@ div.mapItem.new {
align-items: center; align-items: center;
} }
div.mapItem.unlisted {
opacity: 0.6;
}
div.mapItem>div.title { div.mapItem>div.title {
background-color: #28a745; background-color: #28a745;
color: white; color: white;
@ -75,6 +79,10 @@ div.mapItem>div.buttonContainer {
grid-auto-flow: column; grid-auto-flow: column;
} }
#timeLimitType {
margin-left: 1.5em;
}
@media screen and (min-width: 1504px) { @media screen and (min-width: 1504px) {
#mapContainer { #mapContainer {
grid-template-columns: auto auto auto auto; grid-template-columns: auto auto auto auto;

View File

@ -1,10 +1,13 @@
'use strict'; 'use strict';
const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
(function () { (function () {
var Game = { var Game = {
NUMBER_OF_ROUNDS: 5, NUMBER_OF_ROUNDS: 5,
MAX_SCORE: 1000, MAX_SCORE: 1000,
type: GameType.SINGLE,
mapBounds: null, mapBounds: null,
multi: { token: null, owner: false }, multi: { token: null, owner: false },
rounds: [], rounds: [],
@ -16,6 +19,8 @@
guessMarker: null, guessMarker: null,
adaptGuess: false, adaptGuess: false,
googleLink: null, googleLink: null,
history: [],
restrictions: null,
readyToContinue: true, readyToContinue: true,
timeoutEnd: null, timeoutEnd: null,
@ -211,22 +216,24 @@
} }
}, },
getGameIdentifier: function () {
switch (Game.type) {
case GameType.SINGLE:
return '/game/' + mapId;
case GameType.MULTI:
return '/multiGame/' + roomId;
case GameType.CHALLENGE:
return '/challenge/' + challengeToken;
default:
return '/game/' + mapId;
}
},
prepare: function () { prepare: function () {
var data = new FormData(); var data = new FormData();
var userNames;
if (roomId) {
var userNames = localStorage.userNames ? JSON.parse(localStorage.userNames) : {};
if (!userNames.hasOwnProperty(roomId)) {
userNames[roomId] = prompt('Your name: ');
localStorage.userNames = JSON.stringify(userNames);
}
data.append('userName', userNames[roomId]);
}
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/prepare.json' : '/game/' + mapId + '/prepare.json'; var url = Game.getGameIdentifier() + '/prepare.json';
MapGuesser.httpRequest('POST', url, function () { MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
@ -269,7 +276,7 @@
} }
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/game/' + mapId + '/initialData.json', function () { MapGuesser.httpRequest('POST', Game.getGameIdentifier() + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('panoCover').style.visibility = 'hidden'; document.getElementById('panoCover').style.visibility = 'hidden';
@ -278,24 +285,199 @@
return; return;
} }
Game.panoId = this.response.place.panoId; Game.loadHistory(this.response);
Game.pov = this.response.place.pov;
for (var i = 0; i < this.response.history.length; ++i) { Game.restrictions = this.response.restrictions;
var round = this.response.history[i]; Game.displayRestrictions();
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.addPositionToResultMap(true); if (this.response.finished) {
Game.addGuessPositionToResultMap(round.result.guessPosition, null, true);
Game.scoreSum += round.result.score; Game.transitToResultMap();
Game.showSummary();
} else {
Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov;
Game.startNewRound();
} }
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS); document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE); document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
Game.startNewRound();
}); });
}, },
enableRestrictions: function () {
if (!Game.restrictions) {
return;
}
Game.panorama.setOptions({
clickToGo: !Game.restrictions.noMove,
linksControl: !(Game.restrictions.noMove || Game.restrictions.noPan),
scrollwheel: !Game.restrictions.noZoom
});
if (Game.restrictions.noPan) {
document.getElementById('panningBlockerCover').style.display = 'block';
}
if (Game.restrictions.timeLimit) {
Game.startCountdown(Game.restrictions.timeLimit, function () {
Game.guess();
});
}
},
displayRestrictions: function () {
if (!Game.restrictions) {
return;
}
var restrictionsForDisplay = [];
if (Game.restrictions.timeLimit) {
restrictionsForDisplay.push('time limit per ' + Game.restrictions.timeLimitType);
}
if (Game.restrictions.noPan) {
restrictionsForDisplay.push('no camera change');
}
else {
if (Game.restrictions.noMove) {
restrictionsForDisplay.push('no move');
}
if (Game.restrictions.noZoom) {
restrictionsForDisplay.push('no zoom');
}
}
if (restrictionsForDisplay.length == 0) {
return;
}
// create restrictions span for header
var restrictions = document.createElement('span');
restrictions.setAttribute('id', 'restrictions');
restrictions.setAttribute('class', 'hideOnNarrowScreen');
var restrictionsTitle = document.createElement('span');
restrictionsTitle.setAttribute('class', 'bold');
restrictionsTitle.innerText = 'Restrictions: ';
var restrictionsList = document.createElement('span');
restrictionsList.innerText = restrictionsForDisplay.join(', ');
restrictions.appendChild(restrictionsTitle);
restrictions.appendChild(restrictionsList);
var roundContainer = document.getElementById('roundContainer');
var header = roundContainer.parentNode;
header.insertBefore(restrictions, roundContainer);
},
disableRestrictions: function () {
Game.panorama.setOptions({
clickToGo: true,
linksControl: true,
scrollwheel: true
});
document.getElementById('panningBlockerCover').style.display = null;
Game.startCountdown(0);
Game.timeoutEnd = null;
},
hideRestrictions: function () {
var restrictions = document.getElementById('restrictions');
if (restrictions) {
var header = restrictions.parentNode;
header.removeChild(restrictions);
}
},
transitToResultMap: function () {
// TODO: refactor - it is necessary for mobile
if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') {
document.getElementById('showGuessButton').click();
}
if (Game.adaptGuess) {
document.getElementById('guess').classList.remove('adapt');
}
if (Game.guessMarker) {
Game.guessMarker.setMap(null);
Game.guessMarker = null;
}
document.getElementById('guess').classList.add('result');
Game.map.setOptions({
draggableCursor: 'grab'
});
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
document.getElementById('continueButton').style.display = 'none';
document.getElementById('showSummaryButton').style.display = 'block';
} else if (Game.type == GameType.MULTI) {
if (Game.multi.owner) {
if (!Game.readyToContinue) {
document.getElementById('continueButton').disabled = true;
}
} else {
document.getElementById('continueButton').style.display = 'none';
}
}
},
loadHistory: function (response) {
if (!response.history)
return;
Game.history = response.history;
for (var i = 0; i < Game.rounds.length; ++i) {
var round = Game.rounds[i];
if (round.realMarker) {
round.realMarker.setMap(null);
}
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setMap(null);
guessMarker.line.setMap(null);
if (guessMarker.info) {
guessMarker.info.close();
}
}
}
Game.rounds = [];
Game.scoreSum = 0;
for (var i = 0; i < Game.history.length; ++i) {
var round = Game.history[i];
if (round.result) {
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.addPositionToResultMap(true);
if (round.result.guessPosition) {
Game.addGuessPositionToResultMap(round.result.guessPosition, round.result, true);
}
Game.scoreSum += round.result.score;
if (round.allResults !== undefined) {
for (var j = 0; j < round.allResults.length; ++j) {
var result = round.allResults[j];
if (result.guessPosition) {
Game.addGuessPositionToResultMap(result.guessPosition, result, true);
}
}
}
}
}
},
reset: function () { reset: function () {
if (Game.guessMarker) { if (Game.guessMarker) {
Game.guessMarker.setMap(null); Game.guessMarker.setMap(null);
@ -325,6 +507,7 @@
distanceInfo.children[0].style.display = null; distanceInfo.children[0].style.display = null;
distanceInfo.children[1].style.display = null; distanceInfo.children[1].style.display = null;
distanceInfo.children[2].style.display = null; distanceInfo.children[2].style.display = null;
document.getElementById('summaryInfo').innerHTML = "Game finished."
var scoreInfo = document.getElementById('scoreInfo'); var scoreInfo = document.getElementById('scoreInfo');
scoreInfo.children[0].style.display = null; scoreInfo.children[0].style.display = null;
scoreInfo.children[1].style.display = null; scoreInfo.children[1].style.display = null;
@ -339,6 +522,13 @@
// needs to be set visible after the show guess map hid it in mobile view // needs to be set visible after the show guess map hid it in mobile view
document.getElementById("navigation").style.visibility = 'visible'; document.getElementById("navigation").style.visibility = 'visible';
Game.disableRestrictions();
Game.hideRestrictions();
document.getElementById('panningBlockerCover').style.display = null;
Game.history = [];
Game.initialize(); Game.initialize();
}, },
@ -391,6 +581,8 @@
// update the compass // update the compass
const heading = Game.panorama.getPov().heading; const heading = Game.panorama.getPov().heading;
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)"; document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
Game.enableRestrictions();
}, },
handleErrorResponse: function (error) { handleErrorResponse: function (error) {
@ -410,6 +602,14 @@
MapGuesser.showModalWithContent('Error', 'This game is already started, you cannot join.'); MapGuesser.showModalWithContent('Error', 'This game is already started, you cannot join.');
break; break;
case 'game_not_found':
MapGuesser.showModalWithContent('Error', 'The game was not found by this ID. Please check the link.');
break;
case 'anonymous_user':
MapGuesser.showModalWithContent('Error', 'You have to login to join this game!');
break;
default: default:
MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\''); MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\'');
break break
@ -437,7 +637,7 @@
resultBounds.extend(position); resultBounds.extend(position);
if (guessPosition) { if (guessPosition) {
Game.addGuessPositionToResultMap(guessPosition); Game.addGuessPositionToResultMap(guessPosition, result);
resultBounds.extend(guessPosition); resultBounds.extend(guessPosition);
} }
@ -473,25 +673,9 @@
}, },
showResultMap: function (result, resultBounds) { showResultMap: function (result, resultBounds) {
// TODO: refactor - it is necessary for mobile
if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') {
document.getElementById('showGuessButton').click();
}
if (Game.adaptGuess) { Game.transitToResultMap();
document.getElementById('guess').classList.remove('adapt');
}
if (Game.guessMarker) {
Game.guessMarker.setMap(null);
Game.guessMarker = null;
}
document.getElementById('guess').classList.add('result');
Game.map.setOptions({
draggableCursor: 'grab'
});
Game.map.fitBounds(resultBounds); Game.map.fitBounds(resultBounds);
var distanceInfo = document.getElementById('distanceInfo'); var distanceInfo = document.getElementById('distanceInfo');
@ -510,38 +694,32 @@
var scoreBar = document.getElementById('scoreBar'); var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width; scoreBar.style.width = scoreBarProperties.width;
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
document.getElementById('continueButton').style.display = 'none';
document.getElementById('showSummaryButton').style.display = 'block';
} else if (roomId) {
if (Game.multi.owner) {
if (!Game.readyToContinue) {
document.getElementById('continueButton').disabled = true;
}
} else {
document.getElementById('continueButton').style.display = 'none';
}
}
}, },
guess: function () { guess: function () {
if (!Game.guessMarker) {
return; var data = new FormData();
if (Game.timeoutEnd) {
var timeLeft = Math.ceil((Game.timeoutEnd - new Date()) / 1000);
data.append('timeLeft', timeLeft);
} }
var guessPosition = Game.guessMarker.getPosition().toJSON(); Game.disableRestrictions();
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
if (Game.guessMarker) {
var guessPosition = Game.guessMarker.getPosition().toJSON();
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
data.append('lat', String(guessPosition.lat));
data.append('lng', String(guessPosition.lng));
}
document.getElementById('guessButton').disabled = true; document.getElementById('guessButton').disabled = true;
document.getElementById('panoCover').style.visibility = 'visible'; document.getElementById('panoCover').style.visibility = 'visible';
var data = new FormData();
data.append('lat', String(guessPosition.lat));
data.append('lng', String(guessPosition.lng));
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/guess.json' : '/game/' + mapId + '/guess.json'; var url = Game.getGameIdentifier() + '/guess.json';
MapGuesser.httpRequest('POST', url, function () { MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
@ -550,12 +728,16 @@
return; return;
} }
Game.loadHistory(this.response);
Game.restrictions = this.response.restrictions;
Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults); Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults);
if (this.response.place) { if (this.response.place) {
Game.panoId = this.response.place.panoId; Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov; Game.pov = this.response.place.pov;
} }
}, data); }, data);
}, },
@ -589,8 +771,8 @@
var position = round.position; var position = round.position;
var guessMarker = { marker: null, line: null, info: null }; var guessMarker = { marker: null, line: null, info: null };
var markerSvg = result ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg'; var markerSvg = result && result.userName ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg';
var markerLabel = result ? result.userName.charAt(0).toUpperCase() : '?'; var markerLabel = result && result.userName ? result.userName.charAt(0).toUpperCase() : '?';
guessMarker.marker = new google.maps.Marker({ guessMarker.marker = new google.maps.Marker({
map: Game.map, map: Game.map,
@ -640,8 +822,9 @@
}); });
if (result) { if (result) {
const userName = result.userName ? result.userName : 'me';
guessMarker.info = new google.maps.InfoWindow({ guessMarker.info = new google.maps.InfoWindow({
content: '<p class="small bold">' + result.userName + '</p>' + content: '<p class="small bold">' + userName + '</p>' +
'<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>', '<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>',
}); });
@ -668,6 +851,36 @@
return { width: percent + '%', backgroundColor: color }; return { width: percent + '%', backgroundColor: color };
}, },
calculateHighScores: function () {
var highscores = new Map();
highscores.set('me', Game.scoreSum);
// collect the results of users who are through the last round
const round = Game.history[Game.history.length - 1];
if (round.allResults) {
for (const result of round.allResults) {
highscores.set(result.userName, result.score);
}
}
// add up scores only for the finishers
for (var i = Game.history.length - 2; i >= 0; --i) {
const round = Game.history[i];
if (round.allResults) {
for (const result of round.allResults) {
if (highscores.has(result.userName)) {
highscores.set(result.userName, highscores.get(result.userName) + result.score);
}
}
}
}
var sortedHighscores = Array.from(highscores, ([userName, score]) => ({ 'userName': userName, 'score': score }))
.sort(function (resultA, resultB) { return resultB.score - resultA.score });
return sortedHighscores;
},
showSummary: function () { showSummary: function () {
var distanceInfo = document.getElementById('distanceInfo'); var distanceInfo = document.getElementById('distanceInfo');
distanceInfo.children[0].style.display = 'none'; distanceInfo.children[0].style.display = 'none';
@ -678,11 +891,13 @@
scoreInfo.children[1].style.display = 'block'; scoreInfo.children[1].style.display = 'block';
document.getElementById('showSummaryButton').style.display = null; document.getElementById('showSummaryButton').style.display = null;
if (!roomId || Game.multi.owner) { if (Game.type == GameType.SINGLE || Game.multi.owner) {
document.getElementById('startNewGameButton').style.display = 'block'; document.getElementById('startNewGameButton').style.display = 'block';
if (!Game.readyToContinue) { if (!Game.readyToContinue) {
document.getElementById('startNewGameButton').disabled = true; document.getElementById('startNewGameButton').disabled = true;
} }
} else if (Game.type == GameType.CHALLENGE) {
document.getElementById('goToStart').style.display = 'block';
} }
var resultBounds = new google.maps.LatLngBounds(); var resultBounds = new google.maps.LatLngBounds();
@ -725,6 +940,48 @@
var scoreBar = document.getElementById('scoreBar'); var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width; scoreBar.style.width = scoreBarProperties.width;
Game.showHighscores();
},
showHighscores: function () {
if (Game.type == GameType.CHALLENGE) {
var highscores = this.calculateHighScores();
var summaryInfo = document.getElementById('summaryInfo');
if (highscores.length > 2) {
var table = document.getElementById('highscoresTable');
for (const result of highscores) {
var userName = document.createElement('td');
userName.innerHTML = result.userName;
var score = document.createElement('td');
score.innerHTML = result.score;
var line = document.createElement('tr');
line.appendChild(userName);
line.appendChild(score);
table.appendChild(line);
if (result.userName === 'me') {
line.setAttribute('class', 'ownPlayer');
}
}
MapGuesser.showModal('highscores');
} else if (highscores.length == 2) {
if (highscores[0].userName === 'me') {
summaryInfo.innerHTML = 'You won! <span class="hideOnNarrowScreen">' + highscores[1].userName + ' got only ' + highscores[1].score + ' points.</span>';
} else {
summaryInfo.innerHTML = 'You lost! <span class="hideOnNarrowScreen">' + highscores[0].userName + ' won with ' + highscores[0].score + ' points.</span>';
}
} else if (highscores.length == 1) {
summaryInfo.innerHTML = 'You are the first to finish. <span class="hideOnNarrowScreen">Invite your friends by sending them the link.</span>'
}
}
}, },
rewriteGoogleLink: function () { rewriteGoogleLink: function () {
@ -747,7 +1004,7 @@
}, 1); }, 1);
}, },
startCountdown: function (timeout) { startCountdown: function (timeout, timedOutHandler) {
if (Game.countdownHandler) { if (Game.countdownHandler) {
clearInterval(Game.countdownHandler); clearInterval(Game.countdownHandler);
} }
@ -769,7 +1026,12 @@
Game.setCountdownTime(timeLeft); Game.setCountdownTime(timeLeft);
if (timeLeft <= 0) { if (timeLeft <= 0) {
document.getElementById('panoCover').style.visibility = 'visible'; if (typeof timedOutHandler === 'function') {
timedOutHandler();
} else {
document.getElementById('panoCover').style.visibility = 'visible';
}
clearInterval(Game.countdownHandler); clearInterval(Game.countdownHandler);
} }
}, 1000); }, 1000);
@ -872,6 +1134,12 @@
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)"; document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
}); });
if (roomId !== null) {
Game.type = GameType.MULTI;
} else if (challengeToken !== null) {
Game.type = GameType.CHALLENGE;
}
if (COOKIES_CONSENT) { if (COOKIES_CONSENT) {
Game.prepare(); Game.prepare();
} }
@ -961,4 +1229,8 @@
document.getElementById('compassContainer').onclick = function () { document.getElementById('compassContainer').onclick = function () {
Game.panorama.setPov({ heading: 0, pitch: Game.panorama.getPov().pitch }); Game.panorama.setPov({ heading: 0, pitch: Game.panorama.getPov().pitch });
} }
document.getElementById('closeHighscoresButton').onclick = function () {
MapGuesser.hideModal();
};
})(); })();

View File

@ -18,6 +18,7 @@
MapEditor.metadata.name = form.elements.name.value; MapEditor.metadata.name = form.elements.name.value;
MapEditor.metadata.description = form.elements.description.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]'; document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
@ -80,16 +81,22 @@
MapEditor.panorama.setPano(panoLocationData.pano); MapEditor.panorama.setPano(panoLocationData.pano);
}, },
requestPanoData: function (location) { requestPanoData: function (location, canBeIndoor) {
var sv = new google.maps.StreetViewService(); var sv = new google.maps.StreetViewService();
sv.getPanorama({ sv.getPanorama({
location: location, location: location,
preference: google.maps.StreetViewPreference.NEAREST, preference: google.maps.StreetViewPreference.NEAREST,
radius: MapEditor.map.getSearchRadius(location), radius: MapEditor.map.getSearchRadius(location),
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
}, function (data, status) { }, function (data, status) {
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null; var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
if (panoLocationData === null && !canBeIndoor) {
MapEditor.requestPanoData(location, true);
return;
}
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
MapEditor.loadPanoForNewPlace(panoLocationData); MapEditor.loadPanoForNewPlace(panoLocationData);
@ -248,6 +255,9 @@
if (MapEditor.metadata.description !== null) { if (MapEditor.metadata.description !== null) {
data.append('description', MapEditor.metadata.description); data.append('description', MapEditor.metadata.description);
} }
if (MapEditor.metadata.unlisted !== null) {
data.append('unlisted', MapEditor.metadata.unlisted);
}
for (var placeId in MapEditor.added) { for (var placeId in MapEditor.added) {
if (!MapEditor.added.hasOwnProperty(placeId)) { if (!MapEditor.added.hasOwnProperty(placeId)) {
@ -382,7 +392,7 @@
L.tileLayer(tileUrl, { L.tileLayer(tileUrl, {
attribution: tileAttribution, attribution: tileAttribution,
subdomains: '1234', subdomains: tileSubdomains,
ppi: highResData.ppi, ppi: highResData.ppi,
tileSize: highResData.tileSize, tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset, zoomOffset: highResData.zoomOffset,

View File

@ -89,6 +89,9 @@ var MapGuesser = {
formError.style.display = 'block'; formError.style.display = 'block';
formError.innerHTML = this.response.error.errorText; formError.innerHTML = this.response.error.errorText;
if (typeof grecaptcha !== 'undefined') {
grecaptcha.reset();
}
return; return;
} }
@ -183,12 +186,23 @@ var MapGuesser = {
document.getElementById('cover').style.visibility = 'hidden'; document.getElementById('cover').style.visibility = 'hidden';
}, },
observeInput: function (input, buttonToToggle) { observeInput: function (form, observedInputs) {
if (input.defaultValue !== input.value) { var anyChanged = false;
buttonToToggle.disabled = false;
} else { for (var i = 0; i < observedInputs.length; i++) {
buttonToToggle.disabled = true; 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) { observeInputsInForm: function (form, observedInputs) {
@ -199,19 +213,19 @@ var MapGuesser = {
case 'INPUT': case 'INPUT':
case 'TEXTAREA': case 'TEXTAREA':
input.oninput = function () { input.oninput = function () {
MapGuesser.observeInput(this, form.elements.submit); MapGuesser.observeInput(form, observedInputs);
}; };
break; break;
case 'SELECT': case 'SELECT':
input.onchange = function () { input.onchange = function () {
MapGuesser.observeInput(this, form.elements.submit); MapGuesser.observeInput(form, observedInputs);
}; };
break; break;
} }
} }
form.onreset = function () { form.onreset = function () {
form.elements.submit.disabled = true; form.elements['submit_button'].disabled = true;
} }
} }
}; };

View File

@ -65,6 +65,32 @@
} }
}; };
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.addStaticMaps();
Maps.initializeDescriptionDivs(); Maps.initializeDescriptionDivs();
@ -85,10 +111,42 @@
window.location.href = '/multiGame/' + this.elements.roomId.value; window.location.href = '/multiGame/' + this.elements.roomId.value;
}; };
document.getElementById('multiButton').onclick = function () { document.getElementById('challengeForm').onsubmit = function (e) {
MapGuesser.showModal('multi'); e.preventDefault();
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
document.getElementById('multiForm').elements.roomId.select(); 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 () { document.getElementById('closePlayModeButton').onclick = function () {
@ -99,6 +157,10 @@
MapGuesser.hideModal(); MapGuesser.hideModal();
}; };
document.getElementById('closeChallengeButton').onclick = function () {
MapGuesser.hideModal();
}
var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton'); var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton');
for (var i = 0; i < buttons.length; i++) { for (var i = 0; i < buttons.length; i++) {
var button = buttons[i]; var button = buttons[i];
@ -107,6 +169,13 @@
MapGuesser.showModal('playMode'); MapGuesser.showModal('playMode');
document.getElementById('singleButton').href = '/game/' + this.dataset.mapId; document.getElementById('singleButton').href = '/game/' + this.dataset.mapId;
document.getElementById('multiButton').dataset.mapId = 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

@ -1,162 +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():
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
for line in process.stdout:
tag = line.decode().rstrip()
if isTagVerified(tag):
return tag
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
raise Exception("No verified 'Release*' tag found!")
def isTagVerified(tag):
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
return process.returncode == 0
def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], 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,35 +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 NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Installing MapGuesser DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/mapguesser.sql
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link)
else
echo "Creating the first user..."
(cd ${ROOT_DIR} && ./mapg user:add mapg@mapg.dev 123456 admin)
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 {} \;

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,29 +0,0 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
echo "Installing Composer packages..."
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
(cd ${ROOT_DIR} && composer install --no-dev)
else
(cd ${ROOT_DIR} && composer install --dev)
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link)
fi

View File

@ -1,7 +1,6 @@
<?php namespace MapGuesser\Cli; <?php namespace MapGuesser\Cli;
use DateTime; use DateTime;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -15,6 +14,7 @@ class AddUserCommand extends Command
$this->setName('user:add') $this->setName('user:add')
->setDescription('Adding of user.') ->setDescription('Adding of user.')
->addArgument('email', InputArgument::REQUIRED, 'Email of user') ->addArgument('email', InputArgument::REQUIRED, 'Email of user')
->addArgument('username', InputArgument::REQUIRED, 'Username of user')
->addArgument('password', InputArgument::REQUIRED, 'Password of user') ->addArgument('password', InputArgument::REQUIRED, 'Password of user')
->addArgument('type', InputArgument::OPTIONAL, 'Type of user');; ->addArgument('type', InputArgument::OPTIONAL, 'Type of user');;
} }
@ -23,17 +23,17 @@ class AddUserCommand extends Command
{ {
$user = new User(); $user = new User();
$user->setEmail($input->getArgument('email')); $user->setEmail($input->getArgument('email'));
$user->setUsername($input->getArgument('username'));
$user->setPlainPassword($input->getArgument('password')); $user->setPlainPassword($input->getArgument('password'));
$user->setActive(true); $user->setActive(true);
$user->setCreatedDate(new DateTime()); $user->setCreatedDate(new DateTime());
if ($input->hasArgument('type')) { if ($input->hasArgument('type') && $input->getArgument('type') !== null) {
$user->setType($input->getArgument('type')); $user->setType($input->getArgument('type'));
} }
try { try {
$pdm = new PersistentDataManager(); \Container::$persistentDataManager->saveToDb($user);
$pdm->saveToDb($user);
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Adding user failed!</error>'); $output->writeln('<error>Adding user failed!</error>');
$output->writeln(''); $output->writeln('');

View File

@ -1,7 +1,7 @@
<?php namespace MapGuesser\Cli; <?php namespace MapGuesser\Cli;
use FilesystemIterator; use FilesystemIterator;
use MapGuesser\View\Linker; use SokoWeb\View\Linker;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;

View File

@ -1,13 +1,13 @@
<?php namespace MapGuesser\Cli; <?php namespace MapGuesser\Cli;
use DateTime; use DateTime;
use MapGuesser\Database\Query\Modify; use SokoWeb\Database\Query\Modify;
use MapGuesser\Database\Query\Select; use SokoWeb\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet; use SokoWeb\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MultiRoomRepository; use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository; use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository; use MapGuesser\Repository\UserRepository;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -15,8 +15,6 @@ use Symfony\Component\Console\Output\OutputInterface;
class MaintainDatabaseCommand extends Command class MaintainDatabaseCommand extends Command
{ {
private PersistentDataManager $pdm;
private UserRepository $userRepository; private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository; private UserConfirmationRepository $userConfirmationRepository;
@ -25,15 +23,17 @@ class MaintainDatabaseCommand extends Command
private MultiRoomRepository $multiRoomRepository; private MultiRoomRepository $multiRoomRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->multiRoomRepository = new MultiRoomRepository(); $this->multiRoomRepository = new MultiRoomRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
} }
public function configure(): void public function configure(): void
@ -73,15 +73,19 @@ class MaintainDatabaseCommand extends Command
//TODO: these can be in some wrapper class //TODO: these can be in some wrapper class
$userConfirmation = $this->userConfirmationRepository->getByUser($user); $userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) { if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation); \Container::$persistentDataManager->deleteFromDb($userConfirmation);
} }
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user); $userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) { if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter); \Container::$persistentDataManager->deleteFromDb($userPasswordResetter);
} }
$this->pdm->deleteFromDb($user); foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
\Container::$persistentDataManager->deleteFromDb($user);
} }
\Container::$dbConnection->commit(); \Container::$dbConnection->commit();
@ -90,14 +94,14 @@ class MaintainDatabaseCommand extends Command
private function deleteExpiredPasswordResetters(): void private function deleteExpiredPasswordResetters(): void
{ {
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) { foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
$this->pdm->deleteFromDb($passwordResetter); \Container::$persistentDataManager->deleteFromDb($passwordResetter);
} }
} }
private function deleteExpiredRooms(): void private function deleteExpiredRooms(): void
{ {
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) { foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
$this->pdm->deleteFromDb($multiRoom); \Container::$persistentDataManager->deleteFromDb($multiRoom);
} }
} }

View File

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

View File

@ -1,24 +1,27 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use Faker\Factory; use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use MapGuesser\Interfaces\Request\IRequest; use SokoWeb\Response\HtmlContent;
use MapGuesser\Response\HtmlContent; use SokoWeb\Response\JsonContent;
use MapGuesser\Response\JsonContent; use SokoWeb\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IRedirect;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Multi\MultiConnector; use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\MultiRoom; use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\PersistentDataManager; 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\MapRepository;
use MapGuesser\Repository\MultiRoomRepository; use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Response\Redirect; 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 PersistentDataManager $pdm;
private MultiConnector $multiConnector; private MultiConnector $multiConnector;
@ -26,25 +29,37 @@ class GameController
private MapRepository $mapRepository; private MapRepository $mapRepository;
public function __construct(IRequest $request) private PlaceRepository $placeRepository;
private ChallengeRepository $challengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
public function __construct()
{ {
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector(); $this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository(); $this->multiRoomRepository = new MultiRoomRepository();
$this->mapRepository = new MapRepository(); $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 public function getGame(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) \Container::$request->query('mapId');
return new HtmlContent('game', ['mapId' => $mapId]); return new HtmlContent('game', ['mapId' => $mapId]);
} }
public function getNewMultiGame(): IRedirect public function getNewMultiGame(): IRedirect
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) \Container::$request->query('mapId');
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
$roomId = bin2hex(random_bytes(3)); $roomId = bin2hex(random_bytes(3));
$token = $this->getMultiToken($roomId); $token = $this->getMultiToken($roomId);
@ -60,7 +75,7 @@ class GameController
$room->setMembersArray(['owner' => $token, 'all' => []]); $room->setMembersArray(['owner' => $token, 'all' => []]);
$room->setUpdatedDate(new DateTime()); $room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room); \Container::$persistentDataManager->saveToDb($room);
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]); $this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
@ -74,16 +89,85 @@ class GameController
public function getMultiGame(): IContent public function getMultiGame(): IContent
{ {
$roomId = $this->request->query('roomId'); $roomId = \Container::$request->query('roomId');
return new HtmlContent('game', ['roomId' => $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 public function prepareGame(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) \Container::$request->query('mapId');
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
$session = $this->request->session(); $session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [ $session->set('state', [
@ -106,14 +190,23 @@ class GameController
public function prepareMultiGame(): IContent public function prepareMultiGame(): IContent
{ {
$roomId = $this->request->query('roomId'); /**
$userName = $this->request->post('userName'); * @var User|null $user
if (empty($userName)) { */
$faker = Factory::create(); $user = \Container::$request->user();
$userName = $faker->userName; if ($user === null)
{
return new JsonContent(['error' => 'anonymous_user']);
} }
$roomId = \Container::$request->query('roomId');
$room = $this->multiRoomRepository->getByRoomId($roomId); $room = $this->multiRoomRepository->getByRoomId($roomId);
if (!isset($room)) {
return new JsonContent(['error' => 'game_not_found']);
}
$state = $room->getStateArray(); $state = $room->getStateArray();
$map = $this->mapRepository->getById($state['mapId']); $map = $this->mapRepository->getById($state['mapId']);
$token = $this->getMultiToken($roomId); $token = $this->getMultiToken($roomId);
@ -131,12 +224,12 @@ class GameController
$room->setMembersArray($members); $room->setMembersArray($members);
$room->setUpdatedDate(new DateTime()); $room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room); \Container::$persistentDataManager->saveToDb($room);
$this->multiConnector->sendMessage('join_room', [ $this->multiConnector->sendMessage('join_room', [
'roomId' => $roomId, 'roomId' => $roomId,
'token' => $token, 'token' => $token,
'userName' => $userName 'userName' => $user->getDisplayName()
]); ]);
return new JsonContent([ return new JsonContent([
@ -149,9 +242,45 @@ class GameController
]); ]);
} }
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 private function getMultiToken(string $roomId): string
{ {
$session = $this->request->session(); $session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) { if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
$token = bin2hex(random_bytes(16)); $token = bin2hex(random_bytes(16));

View File

@ -1,26 +1,30 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use MapGuesser\Interfaces\Request\IRequest; use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent; use SokoWeb\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IContent;
use MapGuesser\Multi\MultiConnector; use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\PersistentDataManager; 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\PersistentData\Model\UserPlayedPlace;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MultiRoomRepository; use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository; use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository; use MapGuesser\Repository\UserPlayedPlaceRepository;
class GameFlowController class GameFlowController implements IAuthenticationRequired
{ {
const NUMBER_OF_ROUNDS = 5; const NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000; const MAX_SCORE = 1000;
private IRequest $request;
private PersistentDataManager $pdm;
private MultiConnector $multiConnector; private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository; private MultiRoomRepository $multiRoomRepository;
@ -29,29 +33,39 @@ class GameFlowController
private UserPlayedPlaceRepository $userPlayedPlaceRepository; private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct(IRequest $request) private UserInChallengeRepository $userInChallengeRepository;
private PlaceInChallengeRepository $placeInChallengeRepository;
private GuessRepository $guessRepository;
public function __construct()
{ {
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector(); $this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository(); $this->multiRoomRepository = new MultiRoomRepository();
$this->placeRepository = new PlaceRepository(); $this->placeRepository = new PlaceRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository(); $this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
$this->guessRepository = new GuessRepository();
}
public function isAuthenticationRequired(): bool
{
return empty($_ENV['ENABLE_GAME_FOR_GUESTS']);
} }
public function initialData(): IContent public function initialData(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) \Container::$request->query('mapId');
$session = $this->request->session(); $session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']); return new JsonContent(['error' => 'no_session_found']);
} }
$userId = $session->get('userId');
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) { if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
$this->startNewGame($state, $mapId, $userId); $this->startNewGame($state, $mapId);
$session->set('state', $state); $session->set('state', $state);
} }
@ -81,8 +95,8 @@ class GameFlowController
public function multiInitialData(): IContent public function multiInitialData(): IContent
{ {
$roomId = $this->request->query('roomId'); $roomId = \Container::$request->query('roomId');
$session = $this->request->session(); $session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) { if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']); return new JsonContent(['error' => 'no_session_found']);
@ -100,7 +114,7 @@ class GameFlowController
$this->startNewGame($state, $state['mapId']); $this->startNewGame($state, $state['mapId']);
$room->setStateArray($state); $room->setStateArray($state);
$room->setUpdatedDate(new DateTime()); $room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room); \Container::$persistentDataManager->saveToDb($room);
} }
$places = []; $places = [];
@ -117,18 +131,122 @@ class GameFlowController
return new JsonContent(['ok' => true]); 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()
];
}
}
// 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
];
$response['history'][$i]['position'] =
$this->placeRepository->getByRoundInChallenge($challenge, $i)->getPosition()->toArray();
}
}
$response['history']['length'] = $currentRound;
}
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 $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 public function guess(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) \Container::$request->query('mapId');
$session = $this->request->session(); $session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']); return new JsonContent(['error' => 'no_session_found']);
} }
$last = $state['rounds'][$state['currentRound']]; $last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng')); $guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']); $result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$last['guessPosition'] = $guessPosition; $last['guessPosition'] = $guessPosition;
$last['distance'] = $result['distance']; $last['distance'] = $result['distance'];
@ -152,12 +270,20 @@ class GameFlowController
$session->set('state', $state); $session->set('state', $state);
// save the selected place for the round in UserPlayedPlace $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'); $userId = $session->get('userId');
if(isset($userId)) {
$placeId = $last['placeId']; if (isset($userId)) {
$userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId); $userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
if(!$userPlayedPlace) { if (!$userPlayedPlace) {
$userPlayedPlace = new UserPlayedPlace(); $userPlayedPlace = new UserPlayedPlace();
$userPlayedPlace->setUserId($userId); $userPlayedPlace->setUserId($userId);
$userPlayedPlace->setPlaceId($placeId); $userPlayedPlace->setPlaceId($placeId);
@ -165,16 +291,14 @@ class GameFlowController
$userPlayedPlace->incrementOccurrences(); $userPlayedPlace->incrementOccurrences();
} }
$userPlayedPlace->setLastTimeDate(new DateTime()); $userPlayedPlace->setLastTimeDate(new DateTime());
$this->pdm->saveToDb($userPlayedPlace); \Container::$persistentDataManager->saveToDb($userPlayedPlace);
} }
return new JsonContent($response);
} }
public function multiGuess(): IContent public function multiGuess(): IContent
{ {
$roomId = $this->request->query('roomId'); $roomId = \Container::$request->query('roomId');
$session = $this->request->session(); $session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) { if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']); return new JsonContent(['error' => 'no_session_found']);
@ -184,8 +308,8 @@ class GameFlowController
$state = $room->getStateArray(); $state = $room->getStateArray();
$last = $state['rounds'][$state['currentRound']]; $last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng')); $guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']); $result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$responseFromMulti = $this->multiConnector->sendMessage('guess', [ $responseFromMulti = $this->multiConnector->sendMessage('guess', [
'roomId' => $roomId, 'roomId' => $roomId,
@ -208,10 +332,74 @@ class GameFlowController
return new JsonContent($response); 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 public function multiNextRound(): IContent
{ {
$roomId = $this->request->query('roomId'); $roomId = \Container::$request->query('roomId');
$session = $this->request->session(); $session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) { if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']); return new JsonContent(['error' => 'no_session_found']);
@ -232,12 +420,12 @@ class GameFlowController
$room->setStateArray($state); $room->setStateArray($state);
$room->setUpdatedDate(new DateTime()); $room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room); \Container::$persistentDataManager->saveToDb($room);
return new JsonContent(['ok' => true]); return new JsonContent(['ok' => true]);
} }
private function evalueteGuess(Position $realPosition, Position $guessPosition, float $area) private function evaluateGuess(Position $realPosition, Position $guessPosition, float $area)
{ {
$distance = $this->calculateDistance($realPosition, $guessPosition); $distance = $this->calculateDistance($realPosition, $guessPosition);
$score = $this->calculateScore($distance, $area); $score = $this->calculateScore($distance, $area);
@ -245,8 +433,11 @@ class GameFlowController
return ['distance' => $distance, 'score' => $score]; return ['distance' => $distance, 'score' => $score];
} }
private function startNewGame(array &$state, int $mapId, $userId = null): void 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); $places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
$state['rounds'] = []; $state['rounds'] = [];

View File

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

View File

@ -2,52 +2,56 @@
use DateInterval; use DateInterval;
use DateTime; use DateTime;
use MapGuesser\Http\Request; use SokoWeb\Http\Request;
use MapGuesser\Interfaces\Request\IRequest; use SokoWeb\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IRedirect;
use MapGuesser\Interfaces\Response\IRedirect; use SokoWeb\Mailing\Mail;
use MapGuesser\Mailing\Mail; use SokoWeb\OAuth\GoogleOAuth;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation; use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\Model\UserPasswordResetter; use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository; use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository; use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Util\UsernameGenerator;
use MapGuesser\Response\JsonContent; use SokoWeb\Response\HtmlContent;
use MapGuesser\Response\Redirect; use SokoWeb\Response\JsonContent;
use MapGuesser\Util\JwtParser; use SokoWeb\Response\Redirect;
use SokoWeb\Util\CaptchaValidator;
use SokoWeb\Util\JwtParser;
class LoginController class LoginController
{ {
private IRequest $request;
private PersistentDataManager $pdm;
private UserRepository $userRepository; private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository; private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request) private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private string $redirectUrl;
public function __construct()
{ {
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $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() public function getLoginForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
return new HtmlContent('login/login'); return new HtmlContent('login/login', ['redirectUrl' => $this->redirectUrl]);
} }
public function getGoogleLoginRedirect(): IRedirect public function getGoogleLoginRedirect(): IRedirect
@ -55,13 +59,13 @@ class LoginController
$state = bin2hex(random_bytes(16)); $state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16)); $nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state); \Container::$request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce); \Container::$request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl( $url = $oAuth->getDialogUrl(
$state, $state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(), \Container::$request->getBase() . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
$nonce $nonce
); );
@ -70,50 +74,56 @@ class LoginController
public function getSignupForm() public function getSignupForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
if ($this->request->session()->has('tmp_user_data')) { if (\Container::$request->session()->has('tmp_user_data')) {
$tmpUserData = $this->request->session()->get('tmp_user_data'); $tmpUserData = \Container::$request->session()->get('tmp_user_data');
$data = ['email' => $tmpUserData['email']];
} else { } else {
$data = []; $tmpUserData = [];
} }
return new HtmlContent('login/signup', $data); return new HtmlContent('login/signup', $tmpUserData);
} }
public function getSignupSuccess(): IContent public function getSignupSuccess()
{ {
if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
}
return new HtmlContent('login/signup_success'); return new HtmlContent('login/signup_success');
} }
public function getSignupWithGoogleForm() public function getSignupWithGoogleForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
if (!$this->request->session()->has('google_user_data')) { if (!\Container::$request->session()->has('google_user_data')) {
return new Redirect(\Container::$routeCollection->getRoute('login-google')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('login-google')->generateLink(), IRedirect::TEMPORARY);
} }
$userData = $this->request->session()->get('google_user_data'); $userData = \Container::$request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']); $user = $this->userRepository->getByEmail($userData['email']);
return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email']]); return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email'], 'redirectUrl' => $this->redirectUrl]);
} }
public function getRequestPasswordResetForm() public function getRequestPasswordResetForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]); return new HtmlContent('login/password_reset_request', ['email' => \Container::$request->query('email')]);
} }
public function getRequestPasswordResetSuccess(): IContent public function getRequestPasswordResetSuccess(): IContent
@ -123,11 +133,12 @@ class LoginController
public function getResetPasswordForm() public function getResetPasswordForm()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
$token = $this->request->query('token'); $token = \Container::$request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token); $resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) { if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
@ -136,19 +147,27 @@ class LoginController
$user = $this->userRepository->getById($resetter->getUserId()); $user = $this->userRepository->getById($resetter->getUserId());
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]); return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail(), 'redirectUrl' => $this->redirectUrl]);
} }
public function login(): IContent public function login(): IContent
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
$user = $this->userRepository->getByEmail($this->request->post('email')); 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) { if ($user === null) {
if (strlen($this->request->post('password')) < 6) { if (strlen(\Container::$request->post('password')) < 6) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!' 'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
@ -157,16 +176,20 @@ class LoginController
} }
$tmpUser = new User(); $tmpUser = new User();
$tmpUser->setPlainPassword($this->request->post('password')); $tmpUser->setPlainPassword(\Container::$request->post('password'));
$this->request->session()->set('tmp_user_data', [ $tmpUserData = ['password_hashed' => $tmpUser->getPassword()];
'email' => $this->request->post('email'), if (filter_var(\Container::$request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
'password_hashed' => $tmpUser->getPassword() $tmpUserData['username'] = \Container::$request->post('email');
]); } else {
$tmpUserData['email'] = \Container::$request->post('email');
}
\Container::$request->session()->set('tmp_user_data', $tmpUserData);
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink() 'target' => \Container::$routeCollection->getRoute('signup')->generateLink()
] ]
]); ]);
} }
@ -176,13 +199,13 @@ class LoginController
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' . '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!' 'Please check your email and click on the activation link!'
] ]
]); ]);
} }
if (!$user->checkPassword($this->request->post('password'))) { if (!$user->checkPassword(\Container::$request->post('password'))) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' . 'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' .
@ -191,25 +214,27 @@ class LoginController
]); ]);
} }
$this->request->setUser($user); \Container::$request->setUser($user);
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function loginWithGoogle() public function loginWithGoogle()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) { if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
return new HtmlContent('login/google_login'); return new HtmlContent('login/google_login');
} }
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken( $tokenData = $oAuth->getToken(
$this->request->query('code'), \Container::$request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink() \Container::$request->getBase() . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
); );
if (!isset($tokenData['id_token'])) { if (!isset($tokenData['id_token'])) {
@ -219,7 +244,7 @@ class LoginController
$jwtParser = new JwtParser($tokenData['id_token']); $jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload(); $idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) { if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
return new HtmlContent('login/google_login'); return new HtmlContent('login/google_login');
} }
@ -230,229 +255,271 @@ class LoginController
$user = $this->userRepository->getByGoogleSub($idToken['sub']); $user = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($user === null) { if ($user === null) {
$this->request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]); \Container::$request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
} }
$this->request->setUser($user); \Container::$request->setUser($user);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
public function logout(): IRedirect public function logout(): IRedirect
{ {
$this->request->setUser(null); \Container::$request->setUser(null);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
} }
public function signup(): IContent public function signup(): IContent
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new JsonContent(['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]); $this->deleteRedirectUrl();
return new JsonContent(['redirect' => ['target' => $this->redirectUrl]]);
} }
$user = $this->userRepository->getByEmail($this->request->post('email')); $newUser = new User();
if ($user !== null) { $googleUserData = \Container::$request->session()->get('google_user_data');
if ($user->getActive()) { if ($googleUserData !== null) {
if (!$user->checkPassword($this->request->post('password'))) { $user = $this->userRepository->getByEmail($googleUserData['email']);
return new JsonContent([
'error' => [
'errorText' => 'There is a user already registered with the given email address, ' .
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
$this->request->setUser($user); if ($user !== null) {
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]];
} else {
$data = [
'error' => [
'errorText' => 'There is a user already registered with the given email address. ' .
'Please check your email and click on the activation link!'
]
];
}
return new JsonContent($data);
}
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if ($this->request->session()->has('tmp_user_data')) {
$tmpUserData = $this->request->session()->get('tmp_user_data');
$tmpUser = new User();
$tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword($this->request->post('password'))) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
} else {
if (strlen($this->request->post('password')) < 6) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!' '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!'
] ]
]); ]);
} }
if ($this->request->post('password') !== $this->request->post('password_confirm')) { $newUser->setActive(true);
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]); $newUser->setEmail($googleUserData['email']);
} $newUser->setGoogleSub($googleUserData['sub']);
}
$user = new User();
$user->setEmail($this->request->post('email'));
$user->setPlainPassword($this->request->post('password'));
$user->setCreatedDate(new DateTime());
\Container::$dbConnection->startTransaction();
$this->pdm->saveToDb($user);
$token = bin2hex(random_bytes(16));
$confirmation = new UserConfirmation();
$confirmation->setUser($user);
$confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
$this->request->session()->delete('tmp_user_data');
return new JsonContent(['success' => true]);
}
public function signupWithGoogle(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent(['success' => true]);
}
$userData = $this->request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']);
if ($user === null) {
$sendWelcomeEmail = true;
$user = new User();
$user->setEmail($userData['email']);
$user->setCreatedDate(new DateTime());
} else { } else {
$sendWelcomeEmail = false; $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'));
} }
$user->setActive(true); if (strlen(\Container::$request->post('username')) > 0) {
$user->setGoogleSub($userData['sub']); $username = \Container::$request->post('username');
$this->pdm->saveToDb($user); 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 ($sendWelcomeEmail) { if ($this->userRepository->getByUsername($username) !== null) {
$this->sendWelcomeEmail($user->getEmail()); return new JsonContent(['error' => ['errorText' => 'The given username is already taken.']]);
}
} else {
$usernameGenerator = new UsernameGenerator();
do {
$username = $usernameGenerator->generate();
} while ($this->userRepository->getByUsername($username));
} }
$this->request->session()->delete('google_user_data'); $newUser->setUsername($username);
$this->request->setUser($user); $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]); return new JsonContent(['success' => true]);
} }
public function resetSignup(): IContent public function resetSignup(): IContent
{ {
$this->request->session()->delete('tmp_user_data'); \Container::$request->session()->delete('tmp_user_data');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function resetGoogleSignup(): IContent public function resetGoogleSignup(): IContent
{ {
$this->request->session()->delete('google_user_data'); \Container::$request->session()->delete('google_user_data');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function activate() public function activate()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32)); $confirmation = $this->userConfirmationRepository->getByToken(substr(\Container::$request->query('token'), 0, 32));
if ($confirmation === null) { if ($confirmation === null) {
return new HtmlContent('login/activate'); return new HtmlContent('login/activate');
} }
\Container::$dbConnection->startTransaction(); \Container::$persistentDataManager->deleteFromDb($confirmation);
$this->pdm->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId()); $user = $this->userRepository->getById($confirmation->getUserId());
$user->setActive(true); $user->setActive(true);
$this->pdm->saveToDb($user); \Container::$persistentDataManager->saveToDb($user);
\Container::$dbConnection->commit(); \Container::$request->setUser($user);
$this->request->setUser($user); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
} }
public function cancel() public function cancel()
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); $this->deleteRedirectUrl();
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
} }
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32)); $confirmation = $this->userConfirmationRepository->getByToken(substr(\Container::$request->query('token'), 0, 32));
if ($confirmation === null) { if ($confirmation === null) {
return new HtmlContent('login/cancel', ['success' => false]); return new HtmlContent('login/cancel', ['success' => false]);
} }
\Container::$dbConnection->startTransaction(); \Container::$persistentDataManager->deleteFromDb($confirmation);
$this->pdm->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId()); $user = $this->userRepository->getById($confirmation->getUserId());
$this->pdm->deleteFromDb($user); foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
\Container::$dbConnection->commit(); \Container::$persistentDataManager->deleteFromDb($user);
return new HtmlContent('login/cancel', ['success' => true]); return new HtmlContent('login/cancel', ['success' => true]);
} }
public function requestPasswordReset(): IContent public function requestPasswordReset(): IContent
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink() 'target' => $this->redirectUrl
] ]
]); ]);
} }
$user = $this->userRepository->getByEmail($this->request->post('email')); 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) { if ($user === null) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!' 'errorText' => 'No user found with the given email address / username. You can <a href="/signup" title="Sign up">sign up</a>!'
] ]
]); ]);
} }
@ -462,7 +529,7 @@ class LoginController
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' . '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!' 'Please check your email and click on the activation link!'
] ]
]); ]);
@ -486,15 +553,11 @@ class LoginController
$passwordResetter->setToken($token); $passwordResetter->setToken($token);
$passwordResetter->setExpiresDate($expires); $passwordResetter->setExpiresDate($expires);
\Container::$dbConnection->startTransaction();
if ($existingResetter !== null) { if ($existingResetter !== null) {
$this->pdm->deleteFromDb($existingResetter); \Container::$persistentDataManager->deleteFromDb($existingResetter);
} }
$this->pdm->saveToDb($passwordResetter); \Container::$persistentDataManager->saveToDb($passwordResetter);
\Container::$dbConnection->commit();
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires); $this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
@ -504,26 +567,27 @@ class LoginController
public function resetPassword(): IContent public function resetPassword(): IContent
{ {
if ($this->request->user() !== null) { if (\Container::$request->user() !== null) {
$this->deleteRedirectUrl();
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink() 'target' => $this->redirectUrl
] ]
]); ]);
} }
$token = $this->request->query('token'); $token = \Container::$request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token); $resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) { if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new JsonContent([ return new JsonContent([
'redirect' => [ 'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]) 'target' => \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
] ]
]); ]);
} }
if (strlen($this->request->post('password')) < 6) { if (strlen(\Container::$request->post('password')) < 6) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!' 'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
@ -531,23 +595,20 @@ class LoginController
]); ]);
} }
if ($this->request->post('password') !== $this->request->post('password_confirm')) { if (\Container::$request->post('password') !== \Container::$request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]); return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
} }
\Container::$dbConnection->startTransaction(); \Container::$persistentDataManager->deleteFromDb($resetter);
$this->pdm->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId()); $user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword($this->request->post('password')); $user->setPlainPassword(\Container::$request->post('password'));
$this->pdm->saveToDb($user); \Container::$persistentDataManager->saveToDb($user);
\Container::$dbConnection->commit(); \Container::$request->setUser($user);
$this->request->setUser($user);
$this->deleteRedirectUrl();
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
@ -558,9 +619,9 @@ class LoginController
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account'); $mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
$mail->setBodyFromTemplate('signup', [ $mail->setBodyFromTemplate('signup', [
'EMAIL' => $email, 'EMAIL' => $email,
'ACTIVATE_LINK' => $this->request->getBase() . '/' . 'ACTIVATE_LINK' => \Container::$request->getBase() .
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]), \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => $this->request->getBase() . '/' . 'CANCEL_LINK' => \Container::$request->getBase() .
\Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]), \Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T') 'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T')
]); ]);
@ -577,7 +638,7 @@ class LoginController
$confirmation->setLastSentDate(new DateTime()); $confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation); \Container::$persistentDataManager->saveToDb($confirmation);
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate()); $this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
@ -602,10 +663,15 @@ class LoginController
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset'); $mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
$mail->setBodyFromTemplate('password-reset', [ $mail->setBodyFromTemplate('password-reset', [
'EMAIL' => $email, 'EMAIL' => $email,
'RESET_LINK' => $this->request->getBase() . '/' . 'RESET_LINK' => \Container::$request->getBase() .
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]), \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i T') 'EXPIRES' => $expires->format('Y-m-d H:i T')
]); ]);
$mail->send(); $mail->send();
} }
private function deleteRedirectUrl(): void
{
\Container::$request->session()->delete('redirect_after_login');
}
} }

View File

@ -1,50 +1,68 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use MapGuesser\Interfaces\Authentication\IUser; use SokoWeb\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured; use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use MapGuesser\Interfaces\Request\IRequest; use SokoWeb\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MapRepository; use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository; use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Response\JsonContent; use MapGuesser\Repository\UserPlayedPlaceRepository;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;
use MapGuesser\Util\Geo\Bounds; use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Panorama\Pov; use MapGuesser\Util\Panorama\Pov;
class MapAdminController implements ISecured class MapAdminController implements IAuthenticationRequired, ISecured
{ {
private static string $unnamedMapName = '[unnamed map]'; private static string $unnamedMapName = '[unnamed map]';
private IRequest $request;
private PersistentDataManager $pdm;
private MapRepository $mapRepository; private MapRepository $mapRepository;
private PlaceRepository $placeRepository; 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->pdm = new PersistentDataManager();
$this->mapRepository = new MapRepository(); $this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository(); $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 public function authorize(): bool
{ {
$user = $this->request->user(); return \Container::$request->user()->hasPermission(IUser::PERMISSION_ADMIN);
return $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
} }
public function getMapEditor(): IContent public function getMapEditor(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) \Container::$request->query('mapId');
if ($mapId) { if ($mapId) {
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
@ -59,6 +77,7 @@ class MapAdminController implements ISecured
'mapId' => $mapId, 'mapId' => $mapId,
'mapName' => $map->getName(), 'mapName' => $map->getName(),
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()), 'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
'mapUnlisted' => $map->getUnlisted(),
'bounds' => $map->getBounds()->toArray(), 'bounds' => $map->getBounds()->toArray(),
'places' => &$places 'places' => &$places
]); ]);
@ -66,7 +85,7 @@ class MapAdminController implements ISecured
public function getPlace(): IContent public function getPlace(): IContent
{ {
$placeId = (int) $this->request->query('placeId'); $placeId = (int) \Container::$request->query('placeId');
$place = $this->placeRepository->getById($placeId); $place = $this->placeRepository->getById($placeId);
@ -75,16 +94,14 @@ class MapAdminController implements ISecured
public function saveMap(): IContent public function saveMap(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) \Container::$request->query('mapId');
\Container::$dbConnection->startTransaction();
if ($mapId) { if ($mapId) {
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
} else { } else {
$map = new Map(); $map = new Map();
$map->setName(self::$unnamedMapName); $map->setName(self::$unnamedMapName);
$this->pdm->saveToDb($map); \Container::$persistentDataManager->saveToDb($map);
} }
if (isset($_POST['added'])) { if (isset($_POST['added'])) {
@ -106,7 +123,7 @@ class MapAdminController implements ISecured
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day')); $place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
} }
$this->pdm->saveToDb($place); \Container::$persistentDataManager->saveToDb($place);
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $place->getId()]; $addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $place->getId()];
} }
@ -128,7 +145,7 @@ class MapAdminController implements ISecured
)); ));
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day')); $place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
$this->pdm->saveToDb($place); \Container::$persistentDataManager->saveToDb($place);
} }
} }
@ -138,7 +155,7 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById((int) $placeRaw['id']); $place = $this->placeRepository->getById((int) $placeRaw['id']);
$this->pdm->deleteFromDb($place); $this->deletePlace($place);
} }
} }
@ -153,38 +170,65 @@ class MapAdminController implements ISecured
if (isset($_POST['description'])) { if (isset($_POST['description'])) {
$map->setDescription(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->pdm->saveToDb($map); \Container::$persistentDataManager->saveToDb($map);
\Container::$dbConnection->commit();
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]); return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
} }
public function deleteMap(): IContent public function deleteMap(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) \Container::$request->query('mapId');
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
\Container::$dbConnection->startTransaction();
$this->deletePlaces($map); $this->deletePlaces($map);
$this->pdm->deleteFromDb($map); \Container::$persistentDataManager->deleteFromDb($map);
\Container::$dbConnection->commit();
return new JsonContent(['success' => true]); 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 private function deletePlaces(Map $map): void
{ {
foreach ($this->placeRepository->getAllForMap($map) as $place) { foreach ($this->placeRepository->getAllForMap($map) as $place) {
$this->pdm->deleteFromDb($place); $this->deletePlace($place);
} }
} }
private function deleteChallenge(Challenge $challenge): void
{
foreach ($this->userInChallengeRepository->getAllByChallenge($challenge) as $userInChallenge) {
\Container::$persistentDataManager->deleteFromDb($userInChallenge);
}
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 private function calculateMapBounds(Map $map): Bounds
{ {
$bounds = new Bounds(); $bounds = new Bounds();

View File

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

View File

@ -1,44 +1,50 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use MapGuesser\Http\Request; use SokoWeb\Http\Request;
use MapGuesser\Interfaces\Authorization\ISecured; use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use MapGuesser\Interfaces\Request\IRequest; use SokoWeb\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IRedirect;
use MapGuesser\Interfaces\Response\IRedirect; use SokoWeb\OAuth\GoogleOAuth;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPasswordResetterRepository; use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Response\JsonContent; use SokoWeb\Response\HtmlContent;
use MapGuesser\Response\Redirect; use SokoWeb\Response\JsonContent;
use MapGuesser\Util\JwtParser; use SokoWeb\Response\Redirect;
use SokoWeb\Util\JwtParser;
class UserController implements ISecured class UserController implements IAuthenticationRequired
{ {
private IRequest $request; private UserRepository $userRepository;
private PersistentDataManager $pdm;
private UserConfirmationRepository $userConfirmationRepository; private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request) private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private UserInChallengeRepository $userInChallengeRepository;
private GuessRepository $guessRepository;
public function __construct()
{ {
$this->request = $request; $this->userRepository = new UserRepository();
$this->pdm = new PersistentDataManager();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $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 true;
return $user !== null;
} }
public function getAccount(): IContent public function getAccount(): IContent
@ -46,29 +52,153 @@ class UserController implements ISecured
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
return new HtmlContent('account/account', ['user' => $user->toArray()]); return new HtmlContent('account/account', ['user' => $user->toArray()]);
} }
public function getGoogleConnectRedirect(): 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.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]);
}
$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]);
}
$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]);
}
$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 public function getGoogleAuthenticateRedirect(): IRedirect
{ {
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
$state = bin2hex(random_bytes(16)); $state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16)); $nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state); \Container::$request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce); \Container::$request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl( $url = $oAuth->getDialogUrl(
$state, $state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(), \Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
$nonce, $nonce,
$user->getEmail() $user->getEmail()
); );
@ -81,16 +211,16 @@ class UserController implements ISecured
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) { if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
return new HtmlContent('account/google_authenticate', ['success' => false]); return new HtmlContent('account/google_authenticate', ['success' => false]);
} }
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken( $tokenData = $oAuth->getToken(
$this->request->query('code'), \Container::$request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink() \Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
); );
if (!isset($tokenData['id_token'])) { if (!isset($tokenData['id_token'])) {
@ -100,7 +230,7 @@ class UserController implements ISecured
$jwtParser = new JwtParser($tokenData['id_token']); $jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload(); $idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) { if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_authenticate', ['success' => false]); return new HtmlContent('account/google_authenticate', ['success' => false]);
} }
@ -112,7 +242,7 @@ class UserController implements ISecured
} }
$authenticatedWithGoogleUntil = new DateTime('+45 seconds'); $authenticatedWithGoogleUntil = new DateTime('+45 seconds');
$this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil); \Container::$request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
return new HtmlContent('account/google_authenticate', [ return new HtmlContent('account/google_authenticate', [
'success' => true, 'success' => true,
@ -125,7 +255,7 @@ class UserController implements ISecured
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
return new HtmlContent('account/delete', ['user' => $user->toArray()]); return new HtmlContent('account/delete', ['user' => $user->toArray()]);
} }
@ -135,19 +265,49 @@ class UserController implements ISecured
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
if (!$this->confirmUserIdentity( if (!$this->confirmUserIdentity(
$user, $user,
$this->request->session()->get('authenticated_with_google_until'), \Container::$request->session()->get('authenticated_with_google_until'),
$this->request->post('password'), \Container::$request->post('password'),
$error $error
)) { )) {
return new JsonContent(['error' => ['errorText' => $error]]); return new JsonContent(['error' => ['errorText' => $error]]);
} }
if (strlen($this->request->post('password_new')) > 0) { $newEmail = \Container::$request->post('email');
if (strlen($this->request->post('password_new')) < 6) { 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([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!' 'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
@ -155,7 +315,7 @@ class UserController implements ISecured
]); ]);
} }
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) { if (\Container::$request->post('password_new') !== \Container::$request->post('password_new_confirm')) {
return new JsonContent([ return new JsonContent([
'error' => [ 'error' => [
'errorText' => 'The given new passwords do not match.' 'errorText' => 'The given new passwords do not match.'
@ -163,12 +323,12 @@ class UserController implements ISecured
]); ]);
} }
$user->setPlainPassword($this->request->post('password_new')); $user->setPlainPassword(\Container::$request->post('password_new'));
} }
$this->pdm->saveToDb($user); \Container::$persistentDataManager->saveToDb($user);
$this->request->session()->delete('authenticated_with_google_until'); \Container::$request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
@ -178,39 +338,47 @@ class UserController implements ISecured
/** /**
* @var User $user * @var User $user
*/ */
$user = $this->request->user(); $user = \Container::$request->user();
if (!$this->confirmUserIdentity( if (!$this->confirmUserIdentity(
$user, $user,
$this->request->session()->get('authenticated_with_google_until'), \Container::$request->session()->get('authenticated_with_google_until'),
$this->request->post('password'), \Container::$request->post('password'),
$error $error
)) { )) {
return new JsonContent(['error' => ['errorText' => $error]]); return new JsonContent(['error' => ['errorText' => $error]]);
} }
\Container::$dbConnection->startTransaction();
$userConfirmation = $this->userConfirmationRepository->getByUser($user); $userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) { if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation); \Container::$persistentDataManager->deleteFromDb($userConfirmation);
} }
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user); $userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) { if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter); \Container::$persistentDataManager->deleteFromDb($userPasswordResetter);
} }
$this->pdm->deleteFromDb($user); foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
\Container::$dbConnection->commit(); foreach ($this->userInChallengeRepository->getAllByUser($user) as $userInChallenge) {
\Container::$persistentDataManager->deleteFromDb($userInChallenge);
}
$this->request->session()->delete('authenticated_with_google_until'); 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]); return new JsonContent(['success' => true]);
} }
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, string &$error): bool private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, ?string &$error): bool
{ {
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) { if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
return true; return true;

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 = (int) ini_get('mysqli.default_port');
}
if ($socket === null) {
$socket = (string) 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,140 +0,0 @@
<?php namespace MapGuesser\Database\Query;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Database\Utils;
class Modify
{
private IConnection $connection;
private string $table;
private string $idName = 'id';
private array $attributes = [];
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
{
$attributes = $this->attributes;
unset($attributes[$this->idName]);
$set = $this->generateColumnsWithBinding(array_keys($attributes));
$query = 'UPDATE ' . Utils::backtick($this->table) . ' SET ' . $set . ' WHERE ' . Utils::backtick($this->idName) . '=?';
$stmt = $this->connection->prepare($query);
$stmt->execute(array_merge($attributes, [$this->idName => $this->attributes[$this->idName]]));
}
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(5) . microtime()), 0, 7);
}
}

View File

@ -1,446 +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;
const DERIVED_TABLE_KEY = 'DERIVED';
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 = null)
{
$this->connection = $connection;
if ($table !== null) {
$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 setDerivedTableAlias(string $tableAlias): Select
{
return $this->setTableAliases([Select::DERIVED_TABLE_KEY => $tableAlias]);
}
public function from(string $table): Select
{
$this->table = $table;
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): Select
{
$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 isDerivedTable(): bool
{
return array_key_exists(Select::DERIVED_TABLE_KEY, $this->tableAliases);
}
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
{
list($innerQuery, $innerParams) = $this->generateTable($this->table, true);
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $innerQuery;
if (count($this->joins) > 0) {
list($joinQuery, $joinParams) = $this->generateJoins();
$queryString .= ' ' . $joinQuery;
} else {
$joinParams = [];
}
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];
}
if($this->isDerivedTable()) {
$queryString = '(' . $queryString . ') AS ' . $this->tableAliases[Select::DERIVED_TABLE_KEY];
}
return [$queryString, array_merge($innerParams, $joinParams, $whereParams, $havingParams)];
}
private function generateTable($table, bool $defineAlias = false): array
{
$params = [];
if ($table instanceof RawExpression) {
return [(string) $table, $params];
}
if($table instanceof Select)
{
return $table->generateQuery();
}
if (isset($this->tableAliases[$table])) {
$queryString = ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
return [$queryString, $params];
}
return [Utils::backtick($table), $params];
}
private function generateColumn($column): string
{
if ($column instanceof RawExpression) {
return (string) $column;
}
if (is_array($column)) {
$out = '';
if ($column[0]) {
list($tableName, $params) = $this->generateTable($column[0]);
$out .= $tableName . '.';
}
$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(): array
{
$joins = $this->joins;
$joinQueries = [];
$params = [];
foreach($joins as $value) {
list($joinQueryFragment, $paramsFragment) = $this->generateTable($value[1], true);
array_push($joinQueries, $value[0] . ' JOIN ' . $joinQueryFragment . ' ON ' . $this->generateColumn($value[2]) . ' ' . $value[3] . ' ' . $this->generateColumn($value[4]));
$params = array_merge($params, $paramsFragment);
}
return [implode(' ', $joinQueries), $params];
}
private function generateConditions(int $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(int $type, Closure $conditionCallback): array
{
$instance = new self($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,8 +0,0 @@
<?php namespace MapGuesser\Database;
class Utils {
public static function backtick(string $name): string
{
return '`' . $name . '`';
}
}

View File

@ -1,102 +0,0 @@
<?php namespace MapGuesser\Http;
use MapGuesser\Interfaces\Http\IRequest;
use MapGuesser\Interfaces\Http\IResponse;
class Request implements IRequest
{
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 setUrl(string $url): void
{
$this->url = $url;
}
public function setMethod(int $method): void
{
$this->method = $method;
}
public function setQuery($query): void
{
if (is_string($query)) {
$this->query = $query;
} else {
$this->query = http_build_query($query);
}
}
public function setHeaders(array $headers): void
{
$this->headers = array_merge($this->headers, $headers);
}
public function send(): IResponse
{
$ch = curl_init();
if ($this->method === self::HTTP_POST) {
$url = $this->url;
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query);
} else {
$url = $this->url . '?' . $this->query;
}
curl_setopt($ch, CURLOPT_URL, $url);
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,26 +0,0 @@
<?php namespace MapGuesser\Http;
use MapGuesser\Interfaces\Http\IResponse;
class Response implements IResponse
{
private string $body;
private array $headers;
public function __construct(string $body, array $headers)
{
$this->body = $body;
$this->headers = $headers;
}
public function getBody(): string
{
return $this->body;
}
public function getHeaders(): array
{
return $this->headers;
}
}

View File

@ -1,16 +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 getUniqueId();
public function getDisplayName(): string;
public function checkPassword(string $password): bool;
}

View File

@ -1,6 +0,0 @@
<?php namespace MapGuesser\Interfaces\Authorization;
interface ISecured
{
public function authorize(): bool;
}

View File

@ -1,20 +0,0 @@
<?php namespace MapGuesser\Interfaces\Database;
interface IConnection
{
public function startTransaction(): void;
public function commit(): void;
public function rollback(): void;
public function query(string $query): ?IResultSet;
public function multiQuery(string $query): array;
public function prepare(string $query): IStatement;
public function lastId(): int;
public function getAffectedRows(): int;
}

View File

@ -1,16 +0,0 @@
<?php namespace MapGuesser\Interfaces\Database;
interface IResultSet
{
const FETCH_ASSOC = 0;
const FETCH_NUM = 1;
const FETCH_BOTH = 2;
public function fetch(int $type): ?array;
public function fetchAll(int $type): array;
public function fetchOneColumn(string $valueName, string $keyName): array;
}

View File

@ -1,8 +0,0 @@
<?php namespace MapGuesser\Interfaces\Database;
interface IStatement
{
public function execute(array $params): ?IResultSet;
public function getAffectedRows(): int;
}

View File

@ -1,18 +0,0 @@
<?php namespace MapGuesser\Interfaces\Http;
interface IRequest
{
const HTTP_GET = 0;
const HTTP_POST = 1;
public function setUrl(string $url): void;
public function setMethod(int $method): void;
public function setQuery($query): void;
public function setHeaders(array $headers): void;
public function send(): IResponse;
}

View File

@ -1,8 +0,0 @@
<?php namespace MapGuesser\Interfaces\Http;
interface IResponse
{
public function getBody(): string;
public function getHeaders(): array;
}

View File

@ -1,20 +0,0 @@
<?php namespace MapGuesser\Interfaces\Request;
use MapGuesser\Interfaces\Authentication\IUser;
interface IRequest
{
public function setParsedRouteParams(array &$routeParams): void;
public function getBase(): string;
public function query(string $key);
public function post(string $key);
public function session(): ISession;
public function setUser(?IUser $user): void;
public function user(): ?IUser;
}

View File

@ -1,12 +0,0 @@
<?php namespace MapGuesser\Interfaces\Request;
interface ISession
{
public function has(string $key): bool;
public function get(string $key);
public function set(string $key, $value): void;
public function delete(string $key): void;
}

View File

@ -1,12 +0,0 @@
<?php namespace MapGuesser\Interfaces\Response;
interface IContent
{
public function setData(array $data): void;
public function getData(): array;
public function render(): void;
public function getContentType(): string;
}

View File

@ -1,12 +0,0 @@
<?php namespace MapGuesser\Interfaces\Response;
interface IRedirect
{
const PERMANENT = 1;
const TEMPORARY = 2;
public function getUrl(): string;
public function getHttpCode(): int;
}

View File

@ -1,9 +0,0 @@
<?php namespace MapGuesser\Interfaces\Session;
use SessionHandlerInterface;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
interface ISessionHandler extends SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
{
}

View File

@ -1,87 +0,0 @@
<?php namespace MapGuesser\Mailing;
use PHPMailer\PHPMailer\PHPMailer;
class Mail
{
private array $recipients = [];
public string $subject = '';
public string $body = '';
public function addRecipient(string $mail, ?string $name = null): void
{
$this->recipients[] = [$mail, $name];
}
public function setSubject(string $subject): void
{
$this->subject = $subject;
}
public function setBody(string $body): void
{
$this->body = $body;
}
public function setBodyFromTemplate(string $template, array $params = []): void
{
$this->body = file_get_contents(ROOT . '/mail/' . $template . '.html');
$baseParameters = [
'APP_NAME' => $_ENV['APP_NAME'],
'BASE_URL' => \Container::$request->getBase(),
];
$params = array_merge($baseParameters, $params);
foreach ($params as $key => $param) {
$this->body = str_replace('{{' . $key . '}}', $param, $this->body);
}
}
public function send(): void
{
$mailer = new PHPMailer(true);
$mailer->CharSet = 'utf-8';
$mailer->Hostname = substr($_ENV['MAIL_FROM'], strpos($_ENV['MAIL_FROM'], '@') + 1);
if (!empty($_ENV['MAIL_HOST'])) {
$mailer->Mailer = 'smtp';
$mailer->Host = $_ENV['MAIL_HOST'];
$mailer->Port = !empty($_ENV['MAIL_PORT']) ? $_ENV['MAIL_PORT'] : 25;
$mailer->SMTPSecure = !empty($_ENV['MAIL_SECURE']) ? $_ENV['MAIL_SECURE'] : '';
if (!empty($_ENV['MAIL_USER'])) {
$mailer->SMTPAuth = true;
$mailer->Username = $_ENV['MAIL_USER'];
$mailer->Password = $_ENV['MAIL_PASSWORD'];
} else {
$mailer->SMTPAuth = false;
}
} else {
$mailer->Mailer = 'mail';
}
$mailer->setFrom($_ENV['MAIL_FROM'], $_ENV['APP_NAME']);
$mailer->addReplyTo($_ENV['MAIL_FROM'], $_ENV['APP_NAME']);
$mailer->Sender = !empty($_ENV['MAIL_BOUNCE']) ? $_ENV['MAIL_BOUNCE'] : $_ENV['MAIL_FROM'];
$mailer->Subject = $this->subject;
$mailer->msgHTML($this->body);
foreach ($this->recipients as $recipient) {
$this->sendMail($mailer, $recipient);
}
}
private function sendMail(PHPMailer $mailer, array $recipient): void
{
$mailer->clearAddresses();
$mailer->addAddress($recipient[0], $recipient[1]);
$mailer->send();
}
}

View File

@ -1,56 +0,0 @@
<?php namespace MapGuesser\OAuth;
use MapGuesser\Interfaces\Http\IRequest;
class GoogleOAuth
{
private static string $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
private static string $tokenUrlBase = 'https://oauth2.googleapis.com/token';
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getDialogUrl(string $state, string $redirectUrl, ?string $nonce = null, ?string $loginHint = null): string
{
$oauthParams = [
'response_type' => 'code',
'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'],
'scope' => 'openid email',
'redirect_uri' => $redirectUrl,
'state' => $state,
];
if ($nonce !== null) {
$oauthParams['nonce'] = $nonce;
}
if ($loginHint !== null) {
$oauthParams['login_hint'] = $loginHint;
}
return self::$dialogUrlBase . '?' . http_build_query($oauthParams);
}
public function getToken(string $code, string $redirectUrl): array
{
$tokenParams = [
'code' => $code,
'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'],
'client_secret' => $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'],
'redirect_uri' => $redirectUrl,
'grant_type' => 'authorization_code',
];
$this->request->setUrl(self::$tokenUrlBase);
$this->request->setMethod(IRequest::HTTP_POST);
$this->request->setQuery($tokenParams);
$response = $this->request->send();
return json_decode($response->getBody(), true);
}
}

View File

@ -0,0 +1,113 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class Challenge extends Model
{
protected static string $table = 'challenges';
protected static array $fields = ['token', 'time_limit', 'time_limit_type', 'no_move', 'no_pan', 'no_zoom', 'created'];
protected static array $relations = [];
private int $token;
private ?int $timeLimit = null;
private static array $timeLimitTypes = ['game', 'round'];
private string $timeLimitType = 'game';
private bool $noMove = false;
private bool $noPan = false;
private bool $noZoom = false;
private DateTime $created;
public function setToken(int $token): void
{
$this->token = $token;
}
public function setTimeLimit(?int $timeLimit): void
{
if (isset($timeLimit)) {
$this->timeLimit = $timeLimit;
}
}
public function setTimeLimitType(string $timeLimitType): void
{
if (in_array($timeLimitType, self::$timeLimitTypes)) {
$this->timeLimitType = $timeLimitType;
}
}
public function setNoMove(bool $noMove): void
{
$this->noMove = $noMove;
}
public function setNoPan(bool $noPan): void
{
$this->noPan = $noPan;
}
public function setNoZoom(bool $noZoom): void
{
$this->noZoom = $noZoom;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getToken(): int
{
return $this->token;
}
public function getTimeLimit(): ?int
{
return $this->timeLimit;
}
public function getTimeLimitType(): string
{
return $this->timeLimitType;
}
public function getNoMove(): bool
{
return $this->noMove;
}
public function getNoPan(): bool
{
return $this->noPan;
}
public function getNoZoom(): bool
{
return $this->noZoom;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
}

View File

@ -0,0 +1,134 @@
<?php namespace MapGuesser\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
use MapGuesser\Util\Geo\Position;
class Guess extends Model
{
protected static string $table = 'guesses';
protected static array $fields = ['user_id', 'place_in_challenge_id', 'lat', 'lng', 'score', 'distance', 'time_spent'];
protected static array $relations = ['user' => User::class, 'place_in_challenge' => PlaceInChallenge::class];
private ?User $user = null;
private ?int $userId = null;
private ?PlaceInChallenge $placeInChallenge = null;
private ?int $placeInChallengeId = null;
private Position $position;
private int $score = 0;
private int $distance = 0;
private int $timeSpent = 0;
public function __construct()
{
$this->position = new Position(0.0, 0.0);
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setPlaceInChallenge(PlaceInChallenge $placeInChallenge): void
{
$this->placeInChallenge = $placeInChallenge;
}
public function setPlaceInChallengeId(int $placeInChallengeId): void
{
$this->placeInChallengeId = $placeInChallengeId;
}
public function setPosition(Position $position): void
{
$this->position = $position;
}
public function setLat(float $lat): void
{
$this->position->setLat($lat);
}
public function setLng(float $lng): void
{
$this->position->setLng($lng);
}
public function setScore(int $score): void
{
$this->score = $score;
}
public function setDistance(int $distance): void
{
$this->distance = $distance;
}
public function setTimeSpent(int $timeSpent): void
{
$this->timeSpent = $timeSpent;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getPlaceInChallenge(): ?PlaceInChallenge
{
return $this->placeInChallenge;
}
public function getPlaceInChallengeId(): ?int
{
return $this->placeInChallengeId;
}
public function getPosition(): Position
{
return $this->position;
}
public function getLat(): float
{
return $this->position->getLat();
}
public function getLng(): float
{
return $this->position->getLng();
}
public function getScore(): int
{
return $this->score;
}
public function getDistance(): int
{
return $this->distance;
}
public function getTimeSpent(): ?int
{
return $this->timeSpent;
}
}

View File

@ -1,12 +1,13 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
use MapGuesser\Util\Geo\Bounds; use MapGuesser\Util\Geo\Bounds;
class Map extends Model class Map extends Model
{ {
protected static string $table = 'maps'; protected static string $table = 'maps';
protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area']; protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area', 'unlisted'];
private string $name = ''; private string $name = '';
@ -16,6 +17,8 @@ class Map extends Model
private float $area = 0.0; private float $area = 0.0;
private bool $unlisted = false;
public function __construct() public function __construct()
{ {
$this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0); $this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
@ -61,6 +64,11 @@ class Map extends Model
$this->area = $area; $this->area = $area;
} }
public function setUnlisted(bool $unlisted): void
{
$this->unlisted = $unlisted;
}
public function getName(): string public function getName(): string
{ {
return $this->name; return $this->name;
@ -100,4 +108,9 @@ class Map extends Model
{ {
return $this->area; return $this->area;
} }
public function getUnlisted(): bool
{
return $this->unlisted;
}
} }

View File

@ -1,69 +0,0 @@
<?php namespace MapGuesser\PersistentData\Model;
abstract class Model
{
protected static string $table;
protected static array $fields;
protected static array $relations = [];
protected $id = null;
private array $snapshot = [];
public static function getTable(): string
{
return static::$table;
}
public static function getFields(): array
{
return array_merge(['id'], static::$fields);
}
public static function getRelations(): array
{
return static::$relations;
}
public function setId($id): void
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
public function toArray(): array
{
$array = [];
foreach (self::getFields() as $key) {
$method = 'get' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($this, $method)) {
$array[$key] = $this->$method();
}
}
return $array;
}
public function saveSnapshot(): void
{
$this->snapshot = $this->toArray();
}
public function resetSnapshot(): void
{
$this->snapshot = [];
}
public function getSnapshot(): array
{
return $this->snapshot;
}
}

View File

@ -1,6 +1,7 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime; use DateTime;
use SokoWeb\PersistentData\Model\Model;
class MultiRoom extends Model class MultiRoom extends Model
{ {

View File

@ -2,8 +2,8 @@
use DateInterval; use DateInterval;
use DateTime; use DateTime;
use MapGuesser\Http\Request; use SokoWeb\PersistentData\Model\Model;
use MapGuesser\PersistentData\PersistentDataManager; use SokoWeb\Http\Request;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov; use MapGuesser\Util\Panorama\Pov;
@ -162,7 +162,7 @@ class Place extends Model
$this->panoIdCached = $panoId; $this->panoIdCached = $panoId;
$this->panoIdCachedTimestamp = new DateTime(); $this->panoIdCachedTimestamp = new DateTime();
(new PersistentDataManager())->saveToDb($this); \Container::$persistentDataManager->saveToDb($this);
return $panoId; return $panoId;
} }

View File

@ -0,0 +1,72 @@
<?php namespace MapGuesser\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class PlaceInChallenge extends Model
{
protected static string $table = 'place_in_challenge';
protected static array $fields = ['place_id', 'challenge_id', 'round'];
protected static array $relations = ['place' => Place::class, 'challenge' => Challenge::class];
private ?Place $place = null;
private ?int $placeId = null;
private ?Challenge $challenge = null;
private ?int $challengeId = null;
private int $round;
public function setPlace(Place $place): void
{
$this->place = $place;
}
public function setPlaceId(int $placeId): void
{
$this->placeId = $placeId;
}
public function setChallenge(Challenge $challenge): void
{
$this->challenge = $challenge;
}
public function setChallengeId(int $challengeId): void
{
$this->challengeId = $challengeId;
}
public function setRound(int $round): void
{
$this->round = $round;
}
public function getPlace(): ?Place
{
return $this->place;
}
public function getPlaceId(): ?int
{
return $this->placeId;
}
public function getChallenge(): ?Challenge
{
return $this->challenge;
}
public function getChallengeId(): ?int
{
return $this->challengeId;
}
public function getRound(): int
{
return $this->round;
}
}

View File

@ -1,18 +1,21 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime; use DateTime;
use MapGuesser\Interfaces\Authentication\IUser; use SokoWeb\PersistentData\Model\Model;
use SokoWeb\Interfaces\Authentication\IUser;
class User extends Model implements IUser class User extends Model implements IUser
{ {
protected static string $table = 'users'; protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub', 'created']; protected static array $fields = ['email', 'username', 'password', 'type', 'active', 'google_sub', 'created'];
private static array $types = ['user', 'admin']; private static array $types = ['user', 'admin'];
private string $email = ''; private string $email = '';
private string $username = '';
private ?string $password = null; private ?string $password = null;
private string $type = 'user'; private string $type = 'user';
@ -28,6 +31,11 @@ class User extends Model implements IUser
$this->email = $email; $this->email = $email;
} }
public function setUsername(string $username): void
{
$this->username = $username;
}
public function setPassword(?string $hashedPassword): void public function setPassword(?string $hashedPassword): void
{ {
$this->password = $hashedPassword; $this->password = $hashedPassword;
@ -70,6 +78,11 @@ class User extends Model implements IUser
return $this->email; return $this->email;
} }
public function getUsername(): string
{
return $this->username;
}
public function getPassword(): ?string public function getPassword(): ?string
{ {
return $this->password; return $this->password;
@ -119,7 +132,7 @@ class User extends Model implements IUser
public function getDisplayName(): string public function getDisplayName(): string
{ {
return $this->email; return $this->username;
} }
public function checkPassword(string $password): bool public function checkPassword(string $password): bool

View File

@ -1,6 +1,7 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime; use DateTime;
use SokoWeb\PersistentData\Model\Model;
class UserConfirmation extends Model class UserConfirmation extends Model
{ {

View File

@ -0,0 +1,98 @@
<?php namespace MapGuesser\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class UserInChallenge extends Model
{
protected static string $table = 'user_in_challenge';
protected static array $fields = ['user_id', 'challenge_id', 'current_round', 'time_left', 'is_owner'];
protected static array $relations = ['user' => User::class, 'challenge' => Challenge::class];
private ?User $user = null;
private ?int $userId = null;
private ?Challenge $challenge = null;
private ?int $challengeId = null;
private int $currentRound = 0;
private ?int $timeLeft = null;
private bool $isOwner = false;
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setChallenge(Challenge $challenge): void
{
$this->challenge = $challenge;
}
public function setChallengeId(int $challengeId): void
{
$this->challengeId = $challengeId;
}
public function setCurrentRound(int $currentRound): void
{
$this->currentRound = $currentRound;
}
public function setTimeLeft(?int $timeLeft): void
{
if (isset($timeLeft)) {
$this->timeLeft = max(0, $timeLeft);
}
}
public function setIsOwner(bool $isOwner): void
{
$this->isOwner = $isOwner;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getChallenge(): ?Challenge
{
return $this->challenge;
}
public function getChallengeId(): ?int
{
return $this->challengeId;
}
public function getCurrentRound(): int
{
return $this->currentRound;
}
public function getTimeLeft(): ?int
{
return $this->timeLeft;
}
public function getIsOwner(): bool
{
return $this->isOwner;
}
}

View File

@ -1,6 +1,7 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime; use DateTime;
use SokoWeb\PersistentData\Model\Model;
class UserPasswordResetter extends Model class UserPasswordResetter extends Model
{ {

View File

@ -1,6 +1,7 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime; use DateTime;
use SokoWeb\PersistentData\Model\Model;
class UserPlayedPlace extends Model class UserPlayedPlace extends Model
{ {

View File

@ -1,226 +0,0 @@
<?php namespace MapGuesser\PersistentData;
use Generator;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\Model;
class PersistentDataManager
{
public function selectFromDb(Select $select, string $type, bool $withRelations = false)
{
$select = $this->createSelect($select, $type, $withRelations);
$data = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($data === null) {
return null;
}
$model = new $type();
$this->fillWithData($data, $model);
return $model;
}
public function selectMultipleFromDb(Select $select, string $type, bool $withRelations = false): Generator
{
$select = $this->createSelect($select, $type, $withRelations);
$result = $select->execute();
while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) {
$model = new $type();
$this->fillWithData($data, $model);
yield $model;
}
}
public function selectFromDbById($id, string $type, bool $withRelations = false)
{
$select = new Select(\Container::$dbConnection);
$select->whereId($id);
return $this->selectFromDb($select, $type, $withRelations);
}
public function fillWithData(array $data, Model $model): void
{
$relations = $model::getRelations();
$relationData = [];
foreach ($data as $key => $value) {
if ($this->extractRelationData($key, $value, $relationData, $relations)) {
continue;
}
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($model, $method)) {
$model->$method($value);
}
}
$this->setRelations($model, $relationData);
$model->saveSnapshot();
}
public function loadRelationsFromDb(Model $model, bool $recursive): void
{
foreach ($model::getRelations() as $relation => $relationType) {
$camel = str_replace('_', '', ucwords($relation, '_'));
$methodGet = 'get' . $camel . 'Id';
$methodSet = 'set' . $camel;
$relationId = $model->$methodGet();
if ($relationId !== null) {
$relationModel = $this->selectFromDbById($relationId, $relationType, $recursive);
$model->$methodSet($relationModel);
}
}
}
public function saveToDb(Model $model): void
{
$this->syncRelations($model);
$modified = $model->toArray();
$id = $model->getId();
$modify = new Modify(\Container::$dbConnection, $model::getTable());
if ($id !== null) {
$original = $model->getSnapshot();
foreach ($original as $key => $value) {
if ($value === $modified[$key]) {
unset($modified[$key]);
}
}
if (count($modified) > 0) {
$modify->setId($id);
$modify->fill($modified);
$modify->save();
}
} else {
$modify->fill($modified);
$modify->save();
$model->setId($modify->getId());
}
$model->saveSnapshot();
}
public function deleteFromDb(Model $model): void
{
$modify = new Modify(\Container::$dbConnection, $model::getTable());
$modify->setId($model->getId());
$modify->delete();
$model->setId(null);
$model->resetSnapshot();
}
private function createSelect(Select $select, string $type, bool $withRelations = false): Select
{
$table = call_user_func([$type, 'getTable']);
$fields = call_user_func([$type, 'getFields']);
$select->from($table);
//TODO: only with some relations?
if ($withRelations) {
$relations = call_user_func([$type, 'getRelations']);
$columns = [];
foreach ($fields as $field) {
$columns[] = [$table, $field];
}
$columns = array_merge($columns, $this->getRelationColumns($relations));
$this->leftJoinRelations($select, $table, $relations);
$select->columns($columns);
} else {
$select->columns($fields);
}
return $select;
}
private function getRelationColumns(array $relations): array
{
$columns = [];
foreach ($relations as $relation => $relationType) {
$relationTable = call_user_func([$relationType, 'getTable']);
foreach (call_user_func([$relationType, 'getFields']) as $relationField) {
$columns[] = [$relationTable, $relationField, $relation . '__' . $relationField];
}
}
return $columns;
}
private function leftJoinRelations(Select $select, string $table, array $relations): void
{
foreach ($relations as $relation => $relationType) {
$relationTable = call_user_func([$relationType, 'getTable']);
$select->leftJoin($relationTable, [$relationTable, 'id'], '=', [$table, $relation . '_id']);
}
}
private function extractRelationData(string $key, $value, array &$relationData, array $relations): bool
{
$found = false;
foreach ($relations as $relation => $relationType) {
if (substr($key, 0, strlen($relation . '__')) === $relation . '__') {
$found = true;
$relationData[$relation][substr($key, strlen($relation . '__'))] = $value;
break;
}
}
return $found;
}
private function setRelations(Model $model, array &$relations): void
{
foreach ($model::getRelations() as $relation => $relationType) {
if (isset($relations[$relation])) {
$object = new $relationType();
$this->fillWithData($relations[$relation], $object);
$method = 'set' . str_replace('_', '', ucwords($relation, '_'));
$model->$method($object);
}
}
}
private function syncRelations(Model $model): void
{
foreach ($model::getRelations() as $relation => $relationType) {
$camel = str_replace('_', '', ucwords($relation, '_'));
$methodGet = 'get' . $camel;
$methodSet = 'set' . $camel . 'Id';
$relationModel = $model->$methodGet();
if ($relationModel !== null) {
$model->$methodSet($relationModel->getId());
}
}
}
}

View File

@ -0,0 +1,67 @@
<?php namespace MapGuesser\Repository;
use Generator;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\User;
class ChallengeRepository
{
public function getById(int $challengeId): ?Challenge
{
return \Container::$persistentDataManager->selectFromDbById($challengeId, Challenge::class);
}
public function getByToken(int $token): ?Challenge
{
$select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token);
return \Container::$persistentDataManager->selectFromDb($select, Challenge::class);
}
public function getByTokenStr(string $token_str): ?Challenge
{
// validate token string
foreach (str_split($token_str) as $char) {
if (!(('0' <= $char && $char <= '9') || ('a' <= $char && $char <= 'f'))) {
return null;
}
}
// convert token to int
$token = hexdec($token_str);
return $this->getByToken($token);
}
public function getAllByParticipant(User $user): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']);
$select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']);
$select->where(['user_in_challenge', 'user_id'], '=', $user->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Challenge::class);
}
public function getAllByOwner(User $user): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']);
$select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']);
$select->where(['user_in_challenge', 'user_id'], '=', $user->getId());
$select->where(['user_in_challenge', 'is_owner'], '=', true);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Challenge::class);
}
public function getAllByPlace(Place $place): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['challenges', 'id'], '=', ['place_in_challenge', 'challenge_id']);
$select->where(['place_in_challenge', 'place_id'], '=', $place->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Challenge::class);
}
}

View File

@ -0,0 +1,89 @@
<?php namespace MapGuesser\Repository;
use Generator;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Guess;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
class GuessRepository
{
public function getAllByUser(User $user): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
public function getAllByUserAndChallenge(User $user, Challenge $challenge): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('user_id', '=', $user->getId());
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
public function getByUserAndPlaceInChallenge(User $user, Challenge $challenge, Place $place): ?Guess
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('user_id', '=', $user->getId());
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->where(['place_in_challenge', 'place_id'], '=', $place->getId());
return \Container::$persistentDataManager->selectFromDb($select, Guess::class);
}
public function getAllInChallengeByUser(int $userId, Challenge $challenge): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where('user_id', '=', $userId);
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->orderBy(['place_in_challenge', 'round']);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
public function getAllInChallenge(Challenge $challenge, array $withRelations = []): Generator
{
if (count($withRelations)) {
$necessaryRelations = ['place_in_challenge'];
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
}
$select = new Select(\Container::$dbConnection);
$select->where(['guesses__place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->orderBy(['guesses__place_in_challenge', 'round']);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class, true, $withRelations);
}
public function getAllInChallengeByRound(int $round, Challenge $challenge, array $withRelations = []): Generator
{
if (count($withRelations)) {
$necessaryRelations = ['place_in_challenge'];
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
}
$select = new Select(\Container::$dbConnection);
$select->where(['guesses__place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->where(['guesses__place_in_challenge', 'round'], '=', $round);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class, true, $withRelations);
}
public function getAllByPlace(Place $place): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
$select->where(['place_in_challenge', 'place_id'], '=', $place->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Guess::class);
}
}

View File

@ -1,19 +1,30 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\Model\Place;
class MapRepository class MapRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $mapId): ?Map public function getById(int $mapId): ?Map
{ {
return $this->pdm->selectFromDbById($mapId, Map::class); return \Container::$persistentDataManager->selectFromDbById($mapId, Map::class);
}
public function getByPlace(Place $place): ?Map
{
return $this->getById($place->getMapId());
}
public function getByChallenge(Challenge $challenge): ?Map
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('places', ['maps', 'id'], '=', ['places', 'map_id']);
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->limit(1);
return \Container::$persistentDataManager->selectFromDb($select, Map::class);
} }
} }

View File

@ -2,22 +2,14 @@
use DateTime; use DateTime;
use Generator; use Generator;
use MapGuesser\Database\Query\Select; use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\MultiRoom; use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\PersistentDataManager;
class MultiRoomRepository class MultiRoomRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $id): ?MultiRoom public function getById(int $id): ?MultiRoom
{ {
return $this->pdm->selectFromDbById($id, MultiRoom::class); return \Container::$persistentDataManager->selectFromDbById($id, MultiRoom::class);
} }
public function getByRoomId(string $roomId): ?MultiRoom public function getByRoomId(string $roomId): ?MultiRoom
@ -25,7 +17,7 @@ class MultiRoomRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('room_id', '=', $roomId); $select->where('room_id', '=', $roomId);
return $this->pdm->selectFromDb($select, MultiRoom::class); return \Container::$persistentDataManager->selectFromDb($select, MultiRoom::class);
} }
public function getAllExpired(): Generator public function getAllExpired(): Generator
@ -33,6 +25,6 @@ class MultiRoomRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('updated', '<', (new DateTime('-7 day'))->format('Y-m-d H:i:s')); $select->where('updated', '<', (new DateTime('-7 day'))->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, MultiRoom::class); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, MultiRoom::class);
} }
} }

View File

@ -0,0 +1,45 @@
<?php namespace MapGuesser\Repository;
use Generator;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
class PlaceInChallengeRepository
{
public function getAllByPlace(Place $place, array $withRelations = []) : Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('place_id', '=', $place->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, PlaceInChallenge::class, true, $withRelations);
}
public function getAllByChallenge(Challenge $challenge) : Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, PlaceInChallenge::class);
}
public function getByPlaceAndChallenge(Place $place, Challenge $challenge) : ?PlaceInChallenge
{
$select = new Select(\Container::$dbConnection);
$select->where('place_id', '=', $place->getId());
$select->where('challenge_id', '=', $challenge->getId());
return \Container::$persistentDataManager->selectFromDb($select, PlaceInChallenge::class);
}
public function getByRoundInChallenge(int $round, Challenge $challenge, array $withRelations = []): ?PlaceInChallenge
{
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
$select->limit(1, $round);
return \Container::$persistentDataManager->selectFromDb($select, PlaceInChallenge::class, true, $withRelations);
}
}

View File

@ -1,23 +1,16 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use Generator; use Generator;
use MapGuesser\Database\Query\Select; use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
class PlaceRepository class PlaceRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $placeId): ?Place public function getById(int $placeId): ?Place
{ {
return $this->pdm->selectFromDbById($placeId, Place::class); return \Container::$persistentDataManager->selectFromDbById($placeId, Place::class);
} }
public function getAllForMap(Map $map): Generator public function getAllForMap(Map $map): Generator
@ -25,31 +18,36 @@ class PlaceRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('map_id', '=', $map->getId()); $select->where('map_id', '=', $map->getId());
yield from $this->pdm->selectMultipleFromDb($select, Place::class); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Place::class);
} }
//TODO: use Map and User instead of id //TODO: use Map and User instead of id
public function getRandomNPlaces(int $mapId, int $n, int $userId = null): array public function getRandomNPlaces(int $mapId, int $n, ?int $userId): array
{ {
if(!isset($userId)) { if (!isset($userId)) { // anonymous single player
return $this->getRandomNForMapWithValidPano($mapId, $n); return $this->getRandomNForMapWithValidPano($mapId, $n);
} else { } else { // authorized user or multiplayer game with selection based on what the host played before
$unvisitedPlaces = $this->getRandomUnvisitedNForMapWithValidPano($mapId, $n, $userId); $unvisitedPlaces = $this->getRandomUnvisitedNForMapWithValidPano($mapId, $n, $userId);
if (count($unvisitedPlaces) == $n) {
return $unvisitedPlaces;
}
$oldPlaces = $this->getRandomOldNForMapWithValidPano($mapId, $n - count($unvisitedPlaces), $userId); $oldPlaces = $this->getRandomOldNForMapWithValidPano($mapId, $n - count($unvisitedPlaces), $userId);
return array_merge($unvisitedPlaces, $oldPlaces); return array_merge($unvisitedPlaces, $oldPlaces);
} }
} }
//TODO: use Map instead of id //TODO: use Map instead of id
public function getRandomNForMapWithValidPano(int $mapId, int $n, array $exclude = []): array private function getRandomNForMapWithValidPano(int $mapId, int $n): array
{ {
$places = []; $places = [];
$select = new Select(\Container::$dbConnection, 'places'); $select = new Select(\Container::$dbConnection, 'places');
$select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $mapId); $select->where('map_id', '=', $mapId);
$numberOfPlaces = $select->count(); $numberOfPlaces = $select->count();
$exclude = [];
for ($i = 1; $i <= $n; ++$i) { for ($i = 1; $i <= $n; ++$i) {
$place = $this->getRandomForMapWithValidPano($numberOfPlaces, $select, $exclude); $place = $this->getRandomForMapWithValidPano($numberOfPlaces, $select, $exclude);
@ -60,41 +58,40 @@ class PlaceRepository
return $places; return $places;
} }
private function getRandomForMapWithValidPano($numberOfPlaces, $select, array &$exclude, $randomSelection = null): ?Place private function getRandomForMapWithValidPano(int $numberOfPlaces, Select $select, array &$exclude, ?callable $pickRandomInt = null): ?Place
{ {
do { do {
$numberOfPlacesLeft = $numberOfPlaces - count($exclude); $numberOfPlacesLeft = $numberOfPlaces - count($exclude);
$place = $this->selectRandomFromDbForMap($numberOfPlacesLeft, $select, $exclude, $randomSelection); $place = $this->selectRandomFromDbForMap($numberOfPlacesLeft, $select, $exclude, $pickRandomInt);
if($place === null) { if ($place === null) {
// there is no more never visited place left // there is no more never visited place left
return null; return null;
} }
$panoId = $place->getFreshPanoId(); $panoId = $place->getFreshPanoId();
if($panoId === null) { if ($panoId === null) {
$exclude[] = $place->getId(); $exclude[] = $place->getId();
} }
} while($panoId === null); } while ($panoId === null);
return $place; return $place;
} }
private function selectRandomFromDbForMap($numberOfPlacesLeft, $select, array $exclude, $randomSelection): ?Place private function selectRandomFromDbForMap(int $numberOfPlacesLeft, Select $select, array $exclude, ?callable $pickRandomInt): ?Place
{ {
if($numberOfPlacesLeft <= 0) if ($numberOfPlacesLeft <= 0)
return null; return null;
if(!isset($randomSelection)) { if (!isset($pickRandomInt)) {
$randomOffset = random_int(0, $numberOfPlacesLeft - 1); $randomOffset = random_int(0, $numberOfPlacesLeft - 1);
} else { } else {
$randomOffset = $randomSelection($numberOfPlacesLeft); $randomOffset = $pickRandomInt($numberOfPlacesLeft);
} }
// $select_unvisited->orderBy('last_time');
$select->where('id', 'NOT IN', $exclude); $select->where('id', 'NOT IN', $exclude);
$select->limit(1, $randomOffset); $select->limit(1, $randomOffset);
return $this->pdm->selectFromDb($select, Place::class); return \Container::$persistentDataManager->selectFromDb($select, Place::class);
} }
// Never visited places // Never visited places
@ -112,18 +109,18 @@ class PlaceRepository
// count the places never visited // count the places never visited
$selectUnvisited = new Select(\Container::$dbConnection, 'places'); $selectUnvisited = new Select(\Container::$dbConnection, 'places');
$selectUnvisited->leftJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']); $selectUnvisited->leftJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
$selectUnvisited->where('map_id', '=', $mapId); $selectUnvisited->where(['places', 'map_id'], '=', $mapId);
$selectUnvisited->where('last_time', '=', null); $selectUnvisited->where(['places_by_current_user', 'last_time'], '=', null);
$numberOfUnvisitedPlaces = $selectUnvisited->count(); $numberOfUnvisitedPlaces = $selectUnvisited->count();
// look for as many new places as possible but maximum $n // look for as many new places as possible but maximum $n
do { do {
$place = $this->getRandomForMapWithValidPano($numberOfUnvisitedPlaces, $selectUnvisited, $exclude); $place = $this->getRandomForMapWithValidPano($numberOfUnvisitedPlaces, $selectUnvisited, $exclude);
if(isset($place)) { if (isset($place)) {
$places[] = $place; $places[] = $place;
$exclude[] = $place->getId(); $exclude[] = $place->getId();
} }
} while(count($places) < $n && isset($place)); } while (count($places) < $n && isset($place));
return $places; return $places;
} }
@ -143,14 +140,14 @@ class PlaceRepository
// count places that were visited at least once // count places that were visited at least once
$selectOldPlaces = new Select(\Container::$dbConnection, 'places'); $selectOldPlaces = new Select(\Container::$dbConnection, 'places');
$selectOldPlaces->innerJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']); $selectOldPlaces->innerJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
$selectOldPlaces->where('map_id', '=', $mapId); $selectOldPlaces->where(['places', 'map_id'], '=', $mapId);
$numberOfOldPlaces = $selectOldPlaces->count(); $numberOfOldPlaces = $selectOldPlaces->count();
// set order by datetime, oldest first // set order by datetime, oldest first
$selectOldPlaces->orderBy('last_time'); $selectOldPlaces->orderBy(['places_by_current_user', 'last_time']);
// selection algorithm with preference (weighting) for older places // selection algorithm with preference (weighting) for older places using Box-Muller transform
$gaussianRandomSelection = function($numberOfPlaces) { $pickGaussianRandomInt = function($numberOfPlaces) {
$stdev = 0.2; $stdev = 0.2;
$avg = 0.0; $avg = 0.0;
$x = mt_rand() / mt_getrandmax(); $x = mt_rand() / mt_getrandmax();
@ -160,10 +157,10 @@ class PlaceRepository
}; };
// look for n - numberOfUnvisitedPlaces places // look for n - numberOfUnvisitedPlaces places
while(count($places) < $n) while (count($places) < $n)
{ {
$place = $this->getRandomForMapWithValidPano($numberOfOldPlaces, $selectOldPlaces, $exclude, $gaussianRandomSelection); $place = $this->getRandomForMapWithValidPano($numberOfOldPlaces, $selectOldPlaces, $exclude, $pickGaussianRandomInt);
if(isset($place)) if (isset($place))
{ {
$places[] = $place; $places[] = $place;
$exclude[] = $place->getId(); $exclude[] = $place->getId();
@ -173,5 +170,25 @@ class PlaceRepository
return $places; return $places;
} }
public function getByRoundInChallenge(Challenge $challenge, int $round): ?Place
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->orderBy(['place_in_challenge', 'round']);
$select->limit(1, $round);
return \Container::$persistentDataManager->selectFromDb($select, Place::class);
}
public function getAllInChallenge(Challenge $challenge): Generator
{
$select = new Select(\Container::$dbConnection);
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
$select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId());
$select->orderBy(['place_in_challenge', 'round']);
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Place::class);
}
} }

View File

@ -1,22 +1,14 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use MapGuesser\Database\Query\Select; use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation; use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\PersistentDataManager;
class UserConfirmationRepository class UserConfirmationRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userConfirmationId): ?UserConfirmation public function getById(int $userConfirmationId): ?UserConfirmation
{ {
return $this->pdm->selectFromDbById($userConfirmationId, UserConfirmation::class); return \Container::$persistentDataManager->selectFromDbById($userConfirmationId, UserConfirmation::class);
} }
public function getByToken(string $token): ?UserConfirmation public function getByToken(string $token): ?UserConfirmation
@ -24,7 +16,7 @@ class UserConfirmationRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token); $select->where('token', '=', $token);
return $this->pdm->selectFromDb($select, UserConfirmation::class); return \Container::$persistentDataManager->selectFromDb($select, UserConfirmation::class);
} }
public function getByUser(User $user): ?UserConfirmation public function getByUser(User $user): ?UserConfirmation
@ -32,6 +24,6 @@ class UserConfirmationRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId()); $select->where('user_id', '=', $user->getId());
return $this->pdm->selectFromDb($select, UserConfirmation::class); return \Container::$persistentDataManager->selectFromDb($select, UserConfirmation::class);
} }
} }

View File

@ -0,0 +1,73 @@
<?php namespace MapGuesser\Repository;
use Generator;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserInChallenge;
class UserInChallengeRepository
{
public function getAllByUser(User $user) : Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, UserInChallenge::class);
}
public function getAllByChallenge(Challenge $challenge) : Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, UserInChallenge::class);
}
public function getAllByChallengeWithUsers(Challenge $challenge) : Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, UserInChallenge::class, true, ['user']);
}
public function getByUserIdAndChallenge(int $userId, Challenge $challenge): ?UserInChallenge
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $userId);
$select->where('challenge_id', '=', $challenge->getId());
return \Container::$persistentDataManager->selectFromDb($select, UserInChallenge::class);
}
public function getByUserIdAndToken(int $userId, string $token_str, array $withRelations = []): ?UserInChallenge
{
if (count($withRelations)) {
$necessaryRelations = ['challange'];
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
}
// validate token string
if (!ctype_xdigit($token_str)) {
return null;
}
// convert token to int
$token = hexdec($token_str);
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $userId);
$select->where(['user_in_challenge__challenge', 'token'], '=', $token);
return \Container::$persistentDataManager->selectFromDb($select, UserInChallenge::class, true, $withRelations);
}
public function isUserParticipatingInChallenge(int $userId, Challenge $challenge): bool
{
$select = new Select(\Container::$dbConnection, 'user_in_challenge');
$select->where('user_id', '=', $userId);
$select->where('challenge_id', '=', $challenge->getId());
return $select->count() != 0;
}
}

View File

@ -2,23 +2,15 @@
use DateTime; use DateTime;
use Generator; use Generator;
use MapGuesser\Database\Query\Select; use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserPasswordResetter; use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager;
class UserPasswordResetterRepository class UserPasswordResetterRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userConfirmationId): ?UserPasswordResetter public function getById(int $userConfirmationId): ?UserPasswordResetter
{ {
return $this->pdm->selectFromDbById($userConfirmationId, UserPasswordResetter::class); return \Container::$persistentDataManager->selectFromDbById($userConfirmationId, UserPasswordResetter::class);
} }
public function getByToken(string $token): ?UserPasswordResetter public function getByToken(string $token): ?UserPasswordResetter
@ -26,7 +18,7 @@ class UserPasswordResetterRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token); $select->where('token', '=', $token);
return $this->pdm->selectFromDb($select, UserPasswordResetter::class); return \Container::$persistentDataManager->selectFromDb($select, UserPasswordResetter::class);
} }
public function getByUser(User $user): ?UserPasswordResetter public function getByUser(User $user): ?UserPasswordResetter
@ -34,7 +26,7 @@ class UserPasswordResetterRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId()); $select->where('user_id', '=', $user->getId());
return $this->pdm->selectFromDb($select, UserPasswordResetter::class); return \Container::$persistentDataManager->selectFromDb($select, UserPasswordResetter::class);
} }
public function getAllExpired(): Generator public function getAllExpired(): Generator
@ -42,6 +34,6 @@ class UserPasswordResetterRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s')); $select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, UserPasswordResetter::class); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, UserPasswordResetter::class);
} }
} }

View File

@ -1,36 +1,27 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use DateTime;
use Generator; use Generator;
use MapGuesser\Database\Query\Select; use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\UserPlayedPlace; use MapGuesser\PersistentData\Model\UserPlayedPlace;
use MapGuesser\PersistentData\PersistentDataManager;
class UserPlayedPlaceRepository class UserPlayedPlaceRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getByUser(User $user): Generator public function getByUser(User $user): Generator
{ {
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId()); $select->where('user_id', '=', $user->getId());
yield from $this->pdm->selectMultipleFromDb($select, UserPlayedPlace::class); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, UserPlayedPlace::class);
} }
public function getByPlace(Place $place): Generator public function getAllByPlace(Place $place): Generator
{ {
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('place_id', '=', $place->getId()); $select->where('place_id', '=', $place->getId());
yield from $this->pdm->selectMultipleFromDb($select, UserPlayedPlace::class); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, UserPlayedPlace::class);
} }
public function getAllByUser(User $user) : Generator public function getAllByUser(User $user) : Generator
@ -38,7 +29,7 @@ class UserPlayedPlaceRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId()); $select->where('user_id', '=', $user->getId());
yield from $this->pdm->selectMultipleFromDb($select, UserPlayedPlace::class, true); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, UserPlayedPlace::class);
} }
public function getByUserIdAndPlaceId(int $userId, int $placeId) : ?UserPlayedPlace public function getByUserIdAndPlaceId(int $userId, int $placeId) : ?UserPlayedPlace
@ -47,6 +38,6 @@ class UserPlayedPlaceRepository
$select->where('user_id', '=', $userId); $select->where('user_id', '=', $userId);
$select->where('place_id', '=', $placeId); $select->where('place_id', '=', $placeId);
return $this->pdm->selectFromDb($select, UserPlayedPlace::class); return \Container::$persistentDataManager->selectFromDb($select, UserPlayedPlace::class);
} }
} }

View File

@ -2,22 +2,16 @@
use DateTime; use DateTime;
use Generator; use Generator;
use MapGuesser\Database\Query\Select; use SokoWeb\Interfaces\Repository\IUserRepository;
use SokoWeb\Database\Query\Select;
use MapGuesser\PersistentData\Model\Guess;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
class UserRepository class UserRepository implements IUserRepository
{ {
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userId): ?User public function getById(int $userId): ?User
{ {
return $this->pdm->selectFromDbById($userId, User::class); return \Container::$persistentDataManager->selectFromDbById($userId, User::class);
} }
public function getByEmail(string $email): ?User public function getByEmail(string $email): ?User
@ -25,7 +19,24 @@ class UserRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('email', '=', $email); $select->where('email', '=', $email);
return $this->pdm->selectFromDb($select, User::class); return \Container::$persistentDataManager->selectFromDb($select, User::class);
}
public function getByUsername(string $username): ?User
{
$select = new Select(\Container::$dbConnection);
$select->where('username', '=', $username);
return \Container::$persistentDataManager->selectFromDb($select, User::class);
}
public function getByEmailOrUsername(string $emailOrUsername): ?User
{
if (filter_var($emailOrUsername, FILTER_VALIDATE_EMAIL)) {
return $this->getByEmail($emailOrUsername);
}
return $this->getByUsername($emailOrUsername);
} }
public function getByGoogleSub(string $sub): ?User public function getByGoogleSub(string $sub): ?User
@ -33,7 +44,7 @@ class UserRepository
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('google_sub', '=', $sub); $select->where('google_sub', '=', $sub);
return $this->pdm->selectFromDb($select, User::class); return \Container::$persistentDataManager->selectFromDb($select, User::class);
} }
public function getAllInactiveExpired(): Generator public function getAllInactiveExpired(): Generator
@ -42,6 +53,11 @@ class UserRepository
$select->where('active', '=', false); $select->where('active', '=', false);
$select->where('created', '<', (new DateTime('-1 day'))->format('Y-m-d H:i:s')); $select->where('created', '<', (new DateTime('-1 day'))->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, User::class); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, User::class);
}
public function getByGuess(Guess $guess): ?User
{
return $this->getById($guess->getUserId());
} }
} }

View File

@ -1,93 +0,0 @@
<?php namespace MapGuesser\Request;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Request\ISession;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserRepository;
class Request implements IRequest
{
private string $base;
private array $get;
private array $routeParams = [];
private array $post;
private Session $session;
private UserRepository $userRepository;
private ?User $user = null;
public function __construct(string $base, array &$get, array &$post, array &$session)
{
$this->base = $base;
$this->get = &$get;
$this->post = &$post;
$this->session = new Session($session);
$this->userRepository = new UserRepository();
$userId = $this->session->get('userId');
if ($userId !== null) {
$this->user = $this->userRepository->getById($userId);
}
}
public function setParsedRouteParams(array &$routeParams): void
{
$this->routeParams = &$routeParams;
}
public function getBase(): string
{
return $this->base;
}
public function query($key)
{
if (isset($this->get[$key])) {
return $this->get[$key];
}
if (isset($this->routeParams[$key])) {
return $this->routeParams[$key];
}
return null;
}
public function post($key)
{
if (isset($this->post[$key])) {
return $this->post[$key];
}
return null;
}
public function session(): ISession
{
return $this->session;
}
public function setUser(?IUser $user): void
{
if ($user === null) {
$this->session->delete('userId');
return;
}
$this->session->set('userId', $user->getUniqueId());
}
public function user(): ?IUser
{
return $this->user;
}
}

View File

@ -1,37 +0,0 @@
<?php namespace MapGuesser\Request;
use MapGuesser\Interfaces\Request\ISession;
class Session implements ISession
{
private array $data;
public function __construct(array &$data)
{
$this->data = &$data;
}
public function has(string $key): bool
{
return isset($this->data[$key]);
}
public function get(string $key)
{
if (isset($this->data[$key])) {
return $this->data[$key];
}
return null;
}
public function set(string $key, $value): void
{
$this->data[$key] = $value;
}
public function delete(string $key): void
{
unset($this->data[$key]);
}
}

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