Compare commits

...
Author SHA1 Message Date
balazs 784037de6f Merge pull request 'feature/MAPG-235-basic-challenge-mode' (#48) from feature/MAPG-235-basic-challenge-mode into develop
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
balazs 6f27450423 MAPG-235 replaced explicit margin-top and margin-bottom with marginTop and marginBottom classes 2021-05-28 08:36:45 +02:00
balazs 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
balazs 28165d76d3 MAPG-235 refactored challenge token generation and check 2021-05-28 08:07:02 +02:00
balazs 1c1e5f051d MAPG-235 simplified highscore calculation 2021-05-26 08:16:32 +02:00
balazs 283c214c50 MAPG-235 noZoom label fix 2021-05-26 07:49:05 +02:00
balazs 7970927654 MAPG-235 refactored whitespaces 2021-05-22 21:17:49 +02:00
balazs 567602c749 MAPG-235 hide restrictions information if there are no restrictions for the challenge 2021-05-22 21:00:54 +02:00
balazs a5238234d2 MAPG-235 information of the current restrictions displayed on the ribbon in the top 2021-05-22 20:54:58 +02:00
balazs 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
balazs b00fe4ebe3 MAPG-235 highscores added to the end of challenge 2021-05-22 11:34:55 +02:00
balazs 599d8bf153 MAPG-235 fix unwanted time limit after page reload 2021-05-21 15:41:17 +02:00
balazs 636c47366a MAPG-235 fix loading other players' history and calculating final score 2021-05-21 15:40:43 +02:00
balazs 4520d11559 MAPG-235 fix calculating timelimit when 0 sec time was left from the last round 2021-05-21 14:45:40 +02:00
balazs 5f0b639fe7 MAPG-235 distance and score info added also to the player's own markers 2021-05-21 14:44:28 +02:00
balazs d4a279d2f4 MAPG-235 refactored human readable time format 2021-05-21 11:51:47 +02:00
balazs aadeda05af MAPG-235 timeLimit restored after reload of the page. Time limit selector usability improved 2021-05-21 11:30:30 +02:00
balazs b78d564f6f MAPG-235 time limit restriction for the whole game feature added 2021-05-20 21:32:01 +02:00
balazs c2df9b7713 MAPG-235 timelimit restriction per round functionality implemented for challenges 2021-05-20 20:55:44 +02:00
balazs bbb66ca979 MAPG-235 challenge token related error handling 2021-05-20 08:31:52 +02:00
balazs 3b98570f6d MAPG-235 noMove, noZoom, noPan restrictions implemented 2021-05-19 21:16:59 +02:00
balazs 77ff175794 MAPG-235 added radio button for time limit type and fixed saving restrictions for challenge 2021-05-19 17:55:04 +02:00
balazs ae7fd98256 MAPG-235 allow only logged in users to start a challenge 2021-05-19 17:11:49 +02:00
balazs 517d758e8e MAPG-235 fixed place/map deletion 2021-05-19 16:33:18 +02:00
balazs 2a1b51b014 MAPG-235 added time limit type field to the challenge 2021-05-19 14:52:49 +02:00
balazs c085a5a1ca MAPG-235 fix user deletion bug 2021-05-19 13:52:19 +02:00
balazs a7cfc3c19f MAPG-235 queries with selected relations enabled; reduced size of some of the queries 2021-05-19 13:06:46 +02:00
balazs 7898c52328 MAPG-235 go back to start button added to the end of the challenge 2021-05-18 20:22:43 +02:00
balazs 62251b1062 MAPG-235 created error handling for challenge access without login 2021-05-18 19:55:05 +02:00
balazs 069c6b37c8 MAPG-235 fixed bugs with displaying score and round 2021-05-18 18:06:02 +02:00
balazs 30f4b7ad19 MAPG-235 fixed sending history to initializer and fixed pov data format 2021-05-17 21:30:07 +02:00
balazs 45ddb7f56a MAPG-235 reduced number of queries for init and guess in challenge 2021-05-16 20:46:53 +02:00
balazs 7792f1c3ff MAPG-235 refactor: renamed fields in challenge related tables 2021-05-16 13:14:00 +02:00
balazs 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
balazs 44df94eb98 MAPG-235 queries with relations on any recursion level implemented 2021-05-15 11:43:00 +02:00
balazs 00afd65d75 MAP-235 queries with relations on any recursion level implemented 2021-05-15 11:32:41 +02:00
balazs 21e41b7c36 MAP-235 fixed overlapping markers of history and user's guesses 2021-05-13 19:55:32 +02:00
balazs 69964acfb2 MAPG-235 results of other players shown 2021-05-13 19:48:25 +02:00
balazs e7869d67f7 MAPG-235 guesses of other players in the round are sent after guess 2021-05-13 13:06:24 +02:00
balazs 93f8fc3f34 MAPG-235 basic gameflow works 2021-05-12 21:01:41 +02:00
balazs 5daed10036 MAPG-235 challengeInitialData implemented and first round is loaded 2021-05-12 15:34:15 +02:00
balazs d7147b30d6 MAPG-235 new challenge can be created and is prepared 2021-05-12 13:51:06 +02:00
bence 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
balazs 99244e93d2 MAPG-235 updated structure and created models 2021-05-11 11:16:39 +02:00
balazs 313a3568aa MAPG-235 extended database structure for challenges 2021-05-10 21:22:07 +02:00
bence 54bc9c31db MAPG-143 make it possible to disable game for guests (default is disable) 2021-05-10 19:10:09 +02:00
bence 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
default-pipeline default-pipeline #204
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/45
2021-05-09 17:22:13 +02:00
bence 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
default-pipeline default-pipeline #203
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/44
2021-05-09 17:21:46 +02:00
bence 6cb9e17ddf Merge pull request 'give example for LEAFLET_TILESERVER_ATTRIBUTION' (#46) from feature/extend-leaflet-env-examples into develop
default-pipeline default-pipeline #202
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/46
2021-05-09 17:17:18 +02:00
bence 88ad6e9d61 give example for LEAFLET_TILESERVER_ATTRIBUTION
default-pipeline default-pipeline #197
2021-05-09 17:11:51 +02:00
bence 2fb807c479 MAPG-229 make .controlItem relative so it's children can be sized more properly
default-pipeline default-pipeline #196
2021-05-09 16:15:29 +02:00
bence 8e9e5b08f9 MAPG-230 renew session cookie if it already exists
default-pipeline default-pipeline #194
2021-05-09 15:49:27 +02:00
bence 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
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
balazs 71c728d56c reverted removal of preference for outdoor panorama in the front end
default-pipeline default-pipeline #190
2021-05-09 11:46:03 +02:00
balazs 216d30329f Merge pull request 'feature/avoid-repeating-places-in-game' (#38) from feature/avoid-repeating-places-in-game into develop
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
balazs 3f8311d708 implemented review findings
default-pipeline default-pipeline #188
2021-05-08 23:37:36 +02:00
balazs 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
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
balazs 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
balazs 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
balazs 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
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
balazs 7232b322ab extended readme
default-pipeline default-pipeline #183
2021-05-08 18:30:37 +02:00
balazs e6819b6dd3 added leaflet subdomains to the environment variables, which are used now for initializing a leaflet map
default-pipeline default-pipeline #182
2021-05-08 18:20:54 +02:00
balazs 01a4fdf5e5 added error handling when requesting to join non existing room
default-pipeline default-pipeline #181
2021-05-08 18:03:17 +02:00
balazs 1cc3294ee7 check the user type if it has valid value
default-pipeline default-pipeline #180
2021-05-08 16:19:49 +02:00
balazs ad0b8d078f allow error string to be null in case the password was correct
default-pipeline default-pipeline #178
2021-05-08 09:34:34 +02:00
balazs 899817a853 removed unnecessary comment
default-pipeline default-pipeline #177
2021-05-06 20:41:23 +02:00
balazs c626e36bbb specified explicit type in function parameter
default-pipeline default-pipeline #176
2021-05-06 20:39:00 +02:00
balazs 8d8074977b fixing comments
default-pipeline default-pipeline #175
2021-05-06 20:25:48 +02:00
balazs 8b3c95bdc7 handling of deleting places or user updated
default-pipeline default-pipeline #174
2021-05-06 20:09:05 +02:00
balazs b2535ad78a refactored function and variable names, and replaced variables in inner scope
default-pipeline default-pipeline #173
2021-05-06 17:12:18 +02:00
balazs 886bd02f88 removed mistakenly added parameter
default-pipeline default-pipeline #172
2021-05-06 10:09:28 +02:00
balazs 7143c7ec63 moved saving to UserPlayedPlace to seperate function
default-pipeline default-pipeline #171
2021-05-06 10:05:23 +02:00
balazs fb7c0e7a5c renamed the random int generator function
default-pipeline default-pipeline #170
2021-05-06 09:36:49 +02:00
balazs 8136273b33 replaced left join with inner join for older places search
default-pipeline default-pipeline #169
2021-05-05 21:58:19 +02:00
balazs 3c6a7a3c5f selection with preference for older places; refactored PlaceRepository 2021-05-05 21:50:23 +02:00
balazs 01f3c7c8cf added preference for places not seen by user in the selection of places for the game 2021-05-05 19:19:38 +02:00
balazs 66c871fbc2 Extended Select functionality with handling of derived tables 2021-05-05 19:18:07 +02:00
balazs 807b4d024f extended database with UserPlayedPlace table for tracking when and what place a User played in game 2021-05-05 19:16:54 +02:00
bence 430b32d0c6 Merge pull request 'do not exclude indoor panoramas' (#37) from feature/pano-could-be-indoor into develop
default-pipeline default-pipeline #179
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/37
2021-05-02 18:44:40 +02:00
bence 11eee64895 do not exclude indoor panoramas
default-pipeline default-pipeline #166
2021-05-02 18:38:29 +02:00
balazs cc24f96395 Merge pull request 'radius calculation for new panorama search based on latitude and mercator projection for google maps' (#36) from feature/more-accurate-new-selection-in-mapeditor-on-google-maps into develop
default-pipeline default-pipeline #165
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/36
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-02 18:02:28 +02:00
balazs c3e5a6db1c radius calculation for new panorama search based on latitude and mercator projection for google maps
default-pipeline default-pipeline #164
2021-05-02 16:57:47 +02:00
bence 398204c5d5 Merge pull request 'feature/place-streetview-cover-to-the-map' (#35) from feature/place-streetview-cover-to-the-map into develop
default-pipeline default-pipeline #162
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/35
2021-05-02 16:27:41 +02:00
bence aaa5a1daba Merge pull request 'bugfix/streetviewcontrol-is-still-visible' (#34) from bugfix/streetviewcontrol-is-still-visible into develop
default-pipeline default-pipeline #161
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/34
2021-05-02 16:27:34 +02:00
bence c112a25409 move street view cover button to the map as circleControl
default-pipeline default-pipeline #160
2021-05-02 15:04:14 +02:00
bence 35cab17c21 make circle control items more general 2021-05-02 15:04:14 +02:00
bence f9c1829a00 enable zoomControl for Leaflet map to be consistent with Google map
default-pipeline default-pipeline #157
2021-05-02 13:45:34 +02:00
bence e03850ce05 switch off streetViewControl explicitly 2021-05-02 13:42:46 +02:00
bence 55ec226407 Merge pull request 'feature/google-maps-in-mapeditor-added' (#33) from feature/google-maps-in-mapeditor-added into develop
default-pipeline default-pipeline #155
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/33
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-02 13:38:17 +02:00
bence 6ef04fcd4e fit both google and leaflet map to bounds if map already exists
default-pipeline default-pipeline #154
2021-05-02 13:29:48 +02:00
bence 3045d8acbb remove builtin streetViewControl from google map 2021-05-02 13:13:11 +02:00
balazs 19a1b79b0a toggling of street view cover added
default-pipeline default-pipeline #153
2021-05-02 12:56:25 +02:00
balazs d432a5b584 cleaned up the comment 2021-05-02 12:34:57 +02:00
balazs 8b9345eb36 removed unused css declarations; renamed invalidateSize to resize 2021-05-02 12:34:57 +02:00
balazs e1eb0077b1 hide the map selection and jump to location elements on mobile view 2021-05-02 12:34:57 +02:00
balazs 745bda11c0 refactoring and bug fixing related to the map switching 2021-05-02 12:34:57 +02:00
balazs 0bb31cbe14 added switching functionality between maps in mapeditor 2021-05-02 12:34:57 +02:00
balazs ef8ad69506 organised Leaflet map and Google Maps into wrappers 2021-05-02 12:23:37 +02:00
bence f10ad37a74 Merge pull request 'update readme' (#32) from feature/update-readme into develop
default-pipeline default-pipeline #152
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/32
2021-04-29 20:03:19 +02:00
bence 70b60d2fb8 create the first user during installation in development mode 2021-04-29 20:02:00 +02:00
bence 7e68580db6 update readme 2021-04-29 20:01:42 +02:00
balazs 8218883faf Merge pull request 'Feature In-Game Compass' (#27) from feature/in-game-compass into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/27
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 19:15:44 +02:00
balazs 145cb5f637 Merge branch 'develop' into feature/in-game-compass
default-pipeline default-pipeline #151
2021-04-29 19:00:11 +02:00
balazs 6d3f74f911 increased the left margin for mobile view
default-pipeline default-pipeline #150
2021-04-29 18:59:24 +02:00
balazs 3d663fb5d6 Merge pull request 'Quickfix for single player game cannot be restarted' (#30) from bugfix/quickfix-for-single-play-cannot-be-restarted into develop
default-pipeline default-pipeline #149
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/30
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:58 +02:00
balazs 759f654a0d Merge pull request 'feature/return-to-starting-point-button' (#26) from feature/return-to-starting-point-button into develop
default-pipeline default-pipeline #148
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/26
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:22 +02:00
bence c09cce3c97 Merge pull request 'MAPG-167 set background color' (#31) from feature/MAPG-167-set-background-color into develop
default-pipeline default-pipeline #147
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/31
2021-04-29 18:46:26 +02:00
bence 276e331a47 MAPG-167 set background color
default-pipeline default-pipeline #146
2021-04-29 18:43:30 +02:00
balazs 62f4fd7b84 readyToContinue is true by default and set to false only in the multigame initializing
default-pipeline default-pipeline #145
2021-04-29 13:57:32 +02:00
balazs b22f13887f clicking on compass sets the heading direction to north
default-pipeline default-pipeline #144
2021-04-29 12:39:44 +02:00
balazs 2a3f2212a7 replaced span with div for the compass item
default-pipeline default-pipeline #143
2021-04-29 12:12:17 +02:00
balazs 98df08e5fa Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 12:10:02 +02:00
balazs 8f0cfd4489 replaced spans with divs again and fixed the css issue
default-pipeline default-pipeline #142
2021-04-29 12:08:04 +02:00
balazs d42280eb10 only the image is rotated, not the whole div
default-pipeline default-pipeline #141
2021-04-29 09:18:24 +02:00
balazs 0739c65961 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 09:11:32 +02:00
balazs 3122696364 added the visibility reset to resetRound and reverted the span div replacement in the view
default-pipeline default-pipeline #140
2021-04-29 09:07:44 +02:00
balazs 462f6cde80 Merge branch 'feature/return-to-starting-point-button' of gitea.e5tv.hu:esoko/mapguesser into feature/return-to-starting-point-button
default-pipeline default-pipeline #139
2021-04-29 09:05:32 +02:00
balazs 3f4fa83d87 reverted replacing spans with divs 2021-04-29 09:04:50 +02:00
balazs ea599809b8 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 08:34:39 +02:00
balazs f0b0de405c Merge branch 'develop' into feature/return-to-starting-point-button
default-pipeline default-pipeline #138
2021-04-29 08:25:52 +02:00
balazs ffc4aaf110 fixed findings
default-pipeline default-pipeline #137
2021-04-29 08:24:20 +02:00
bence 58cdc1be8d Merge pull request 'fix typo' (#29) from feature/modify-author into develop
default-pipeline default-pipeline #134
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/29
2021-04-29 00:58:57 +02:00
bence 4039202268 fix typo
default-pipeline default-pipeline #133
2021-04-29 00:58:20 +02:00
bence 4677cc5229 Merge pull request 'generalize author attribution' (#28) from feature/modify-author into develop
default-pipeline default-pipeline #131
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/28
2021-04-29 00:53:26 +02:00
bence 8800724aa9 generalize author attribution
default-pipeline default-pipeline #130
2021-04-29 00:52:46 +02:00
balazs 08c38c6374 reverted style tailoring for guess
default-pipeline default-pipeline #129
2021-04-28 20:46:52 +02:00
balazs 854775011c tailored the style for the navigation buttons
default-pipeline default-pipeline #128
2021-04-28 20:42:42 +02:00
balazs e33d8c02e9 added compass button and coupled its transform:rotate style attribute with the POV heading value 2021-04-28 20:33:17 +02:00
balazs 3d52c967b6 reformatted: added new line breaks to the game.css
default-pipeline default-pipeline #127
2021-04-28 18:30:58 +02:00
balazs 5c8f3d6fb9 returns to start when clicked on home symbol
default-pipeline default-pipeline #126
2021-04-28 18:26:46 +02:00
balazs b9927f79fc added a new button to the view for the return to starting point feature 2021-04-28 17:58:48 +02:00
balazs 56f3a9b380 Merge pull request 'Stuck scoring at zero fixed by regular map area update in session' (#24) from bugfix/scoring-stuck-at-zero into develop
default-pipeline default-pipeline #124
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/24
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 20:36:41 +02:00
balazs a0d41c388b Merge branch 'develop' into bugfix/scoring-stuck-at-zero
default-pipeline default-pipeline #123
2021-04-27 17:56:52 +02:00
balazs 092cc63148 Merge pull request 'jump to coordinate feature added to the header stripe' (#25) from feature/jump-to-specific-coordinates-in-mapeditor into develop
default-pipeline default-pipeline #122
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/25
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 17:56:35 +02:00
balazs 95553296bd removed extra line break
default-pipeline default-pipeline #121
2021-04-27 17:54:47 +02:00
balazs 9330227879 Merge branch 'feature/jump-to-specific-coordinates-in-mapeditor' of gitea.e5tv.hu:esoko/mapguesser into feature/jump-to-specific-coordinates-in-mapeditor
default-pipeline default-pipeline #120
2021-04-27 17:50:10 +02:00
balazs faaff33841 removed #jumpForm and reformatted the code 2021-04-27 17:48:45 +02:00
balazs 508a6899d8 Merge branch 'develop' into bugfix/scoring-stuck-at-zero
default-pipeline default-pipeline #119
2021-04-27 17:36:18 +02:00
balazs be0f7b64b8 refactored comment
default-pipeline default-pipeline #118
2021-04-27 17:35:06 +02:00
balazs 15e3248ee1 Merge branch 'develop' into feature/jump-to-specific-coordinates-in-mapeditor
default-pipeline default-pipeline #117
2021-04-27 17:26:44 +02:00
balazs 04a897c958 Merge pull request 'folder name db changed to database in the path' (#23) from bugfix/change-path-to-renamed-folder into develop
default-pipeline default-pipeline #116
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/23
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 17:25:54 +02:00
balazs 845d77ebda area of map is updated in the session at each game preparation to avoid outdated map area and wrong score calculation
default-pipeline default-pipeline #115
2021-04-27 16:46:40 +02:00
balazs 8bc3fda49b jump to coordinate feature added to the header stripe
default-pipeline default-pipeline #114
2021-04-27 10:18:28 +02:00
balazs d78817180b folder name db changed to database in the path
default-pipeline default-pipeline #113
2021-04-26 20:21:21 +02:00
bence 0f810bd2ba Merge pull request 'feature/MAPG-228-deploy-signed-and-verified-tags-only' (#22) from feature/MAPG-228-deploy-signed-and-verified-tags-only into develop
default-pipeline default-pipeline #111
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/22
2021-04-25 15:16:46 +02:00
bence 9d1fc8b60c MAPG-228 prune local tags when fetching
default-pipeline default-pipeline #107
2021-04-25 15:11:49 +02:00
bence dd46fb3d32 MAPG-228 check if the Release* tag is signed and verified 2021-04-25 15:11:20 +02:00
bence 1a40e3a18d Merge pull request 'MAPG-226 change remaining time calculation' (#21) from feature/MAPG-226-change-timeout-algorithm into develop
default-pipeline default-pipeline #106
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/21
2021-04-10 19:00:20 +02:00
bence 1aa5d3b43e MAPG-226 change remaining time calculation 2021-04-10 18:59:27 +02:00
bence a418d5cf66 Merge pull request 'MAPG-213 move visibility workaround to Game.showResultMap' (#20) from bugfix/MAPG-213-result-map-not-show-on-mobile-after-reload into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/20
2021-04-04 23:41:05 +02:00
bence 5cae286a5b MAPG-213 move visibility workaround to Game.showResultMap 2021-04-04 23:40:27 +02:00
bence c5d2591371 Merge pull request 'MAPG-223 send error if member already guessed' (#19) from feature/MAPG-223-user-should-not-be-able-to-guess-after-round-is-over into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/19
2021-04-04 23:29:04 +02:00
bence cf59f937c9 MAPG-223 send error if member already guessed
default-pipeline default-pipeline #104
2021-04-04 23:28:44 +02:00
bence 257f59c96e Merge pull request 'MAPG-222 fixes: skip invalid guessPosition, hide showSummaryButton, handle countdown time if timeout is 0' (#18) from bugfix/MAPG-222-show-summary-can-be-clicked-when-game-is-restarted into develop
default-pipeline default-pipeline #98
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/18
2021-04-04 23:05:47 +02:00
bence c2f5806394 MAPG-222 fixes: skip invalid guessPosition, hide showSummaryButton, handle countdown time if timeout is 0
default-pipeline default-pipeline #96
2021-04-04 23:04:07 +02:00
bence ac0b71fe09 Merge pull request 'feature/MAPG-213-user-shouldn-t-be-able-to-re-guess-when-page-is-reloaded' (#17) from feature/MAPG-213-user-shouldn-t-be-able-to-re-guess-when-page-is-reloaded into develop
default-pipeline default-pipeline #94
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/17
2021-04-04 22:31:29 +02:00
bence 19f7b50002 MAPG-213 show results immediately if user already guessed
default-pipeline default-pipeline #91
2021-04-04 22:24:40 +02:00
bence 6342bb1e79 MAPG-213 send round data at initialization if user guessed 2021-04-04 22:18:00 +02:00
bence 3abdb6910e MAPG-213 clean up init
default-pipeline default-pipeline #90
2021-04-04 20:52:28 +02:00
bence 7d2588f7cd Merge pull request 'feature/MAPG-219-disable-continue-if-timeout-is-not-reached' (#16) from feature/MAPG-219-disable-continue-if-timeout-is-not-reached into develop
default-pipeline default-pipeline #88
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/16
2021-04-04 19:23:08 +02:00
bence be40acdb2d MAPG-219 disable continue button until round is over
default-pipeline default-pipeline #84
2021-04-04 18:48:31 +02:00
bence a2f2080959 MAPG-219 send 'end_round' when all members are finished
default-pipeline default-pipeline #83
2021-04-04 17:43:37 +02:00
bence cc49c56075 Merge pull request 'feature/MAPG-205-set-timeout-in-multiplayer-rooms' (#15) from feature/MAPG-205-set-timeout-in-multiplayer-rooms into develop
default-pipeline default-pipeline #82
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/15
2021-04-04 14:22:59 +02:00
bence 69329b9f81 MAPG-205 show countdown time
default-pipeline default-pipeline #80
2021-04-04 14:22:10 +02:00
bence 20914b466e MAPG-205 add elements for showing countdown 2021-04-04 14:20:12 +02:00
bence 3630637ec1 MAPG-205 handle round timeout on frontend 2021-04-04 14:20:12 +02:00
bence 42224b074e MAPG-205 set timeout for rounds 2021-04-04 14:20:12 +02:00
bence 0e8ed14305 MAPG-205 delete unnecessary empty lines 2021-04-03 23:39:20 +02:00
bence d3a12fb6f1 Merge pull request 'feature/MAPG-204-show-guesses-from-others' (#14) from feature/MAPG-204-show-guesses-from-others into develop
default-pipeline default-pipeline #76
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/14
2021-04-03 22:13:52 +02:00
bence 3f9d104adc MAPG-204 show guesses from others
default-pipeline default-pipeline #72
2021-04-03 22:10:21 +02:00
bence 9e5d20796c MAPG-204 clean up result sending 2021-04-03 18:24:43 +02:00
bence 7433813337 Merge pull request 'MAPG-208 error handling adaptations' (#13) from bugfix/MAPG-208-error-messages into develop
default-pipeline default-pipeline #75
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/13
2021-03-21 16:39:43 +01:00
bence 42854fe975 MAPG-208 error handling adaptations
default-pipeline default-pipeline #53
2021-03-21 14:37:08 +01:00
bence 71681f1e55 Merge pull request 'MAPG-207 don't use MapGuesser.showmodal for modal 'multi'' (#12) from bugfix/MAPG-207-prevent-closing-multi-modal into develop
default-pipeline default-pipeline #50
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/12
2021-03-21 12:47:35 +01:00
bence 1cf0ab44fd MAPG-207 don't use MapGuesser.showmodal for modal 'multi'
default-pipeline default-pipeline #49
2021-03-21 12:45:57 +01:00
bence fc9eb183f1 Merge pull request 'MAPG-203 fix wrong variable name' (#11) from bugfix/MAPG-203-fix-condition-in-js into develop
default-pipeline default-pipeline #46
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/11
2021-03-20 22:26:33 +01:00
bence d431a693fc MAPG-203 fix wrong variable name
default-pipeline default-pipeline #45
2021-03-20 22:24:47 +01:00
bence ace490b7e9 Merge pull request 'MAPG-203 use MULTI_WS_URL environment variable' (#10) from feature/MAPG-203-fine-tune-env-variables into develop
default-pipeline default-pipeline #43
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/10
2021-03-20 21:24:20 +01:00
bence e3771d36ce MAPG-203 use MULTI_WS_URL environment variable
default-pipeline default-pipeline #42
2021-03-20 21:21:40 +01:00
bence 6e03476173 Merge pull request 'bugfix/MAPG-203-fix-singleplayer' (#9) from bugfix/MAPG-203-fix-singleplayer into develop
default-pipeline default-pipeline #39
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/9
2021-03-20 21:00:14 +01:00
bence f3ec71d6dd MAPG-203 fix condition in game.js
default-pipeline default-pipeline #38
2021-03-20 20:59:10 +01:00
bence c2f0a1dd51 MAPG-203 fix signatures in GameController 2021-03-20 20:58:57 +01:00
bence a417fbd760 Merge pull request 'feature/MAPG-203-initial-multiplayer-implementation' (#8) from feature/MAPG-203-initial-multiplayer-implementation into develop
default-pipeline default-pipeline #37
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/8
2021-03-20 20:46:36 +01:00
bence a9cda56586 MAPG-203 install NPM packages on install and update
default-pipeline default-pipeline #36
2021-03-20 19:45:21 +01:00
bence 02fcbd2f9c MAPG-203 prepare GameFlowController for multiplayer 2021-03-20 19:45:21 +01:00
bence 2f665381c3 MAPG-203 prepare GameController for multiplayer 2021-03-20 19:45:21 +01:00
bence 573440868e MAPG-203 add new routes for multiplayer 2021-03-20 19:45:21 +01:00
bence 79490a0616 MAPG-203 add class for multiplayer internal connection (PHP-NodeJS TCP) 2021-03-20 19:45:21 +01:00
bence ed343f2359 MAPG-203 add new environment variables 2021-03-20 19:45:20 +01:00
bence 563f900423 MAPG-203 implement game mode selection UI 2021-03-20 19:45:20 +01:00
bence 24a10c534e MAPG-203 frontend adaptations for multiplayer 2021-03-20 19:44:48 +01:00
bence 0cbb7ba145 MAPG-203 add debugger config for NodeJS 2021-03-20 19:44:48 +01:00
bence 5dd2ce0d5a MAPG-203 add cleanup for multi_rooms to db:maintain 2021-03-20 19:44:48 +01:00
bence e5fb725c69 MAPG-203 add new table and model for multi_room 2021-03-20 19:44:48 +01:00
bence fc40c18679 MAPG-203 multiplayer handler server (NodeJS) 2021-03-20 19:44:48 +01:00
bence 4fabc39d44 MAPG-203 restructure Docker stack 2021-03-20 19:44:48 +01:00
bence 2120fbebbc MAPG-203 add Dockerfile for multiplayer (NodeJS) 2021-03-20 19:44:47 +01:00
bence b9f0529dce MAPG-203 add new Composer packages 2021-03-20 19:44:47 +01:00
bence a3ca7638c7 MAPG-203 add NPM packages for multiplayer 2021-03-20 15:21:07 +01:00
bence 8538d0a119 MAPG-203 add node_modules to gitignore 2021-03-17 23:09:07 +01:00
bence 0cc63ef936 Merge pull request 'MAPG-203 this.response.newPlace -> this.response.place' (#7) from bugfix/MAPG-203-fix-wrong-object-key into develop
default-pipeline default-pipeline #28
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/7
2021-03-17 22:54:45 +01:00
bence 10d239bfb4 MAPG-203 this.response.newPlace -> this.response.place
default-pipeline default-pipeline #25
2021-03-17 22:54:06 +01:00
bence e2493e1b7e Merge pull request 'MAPG-203 select places at the beginning of the game' (#6) from feature/MAPG-203-initial-multiplayer-implementation into develop
default-pipeline default-pipeline #24
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/6
2021-03-17 22:48:43 +01:00
bence edd8c0f8a4 MAPG-203 select places at the beginning of the game
default-pipeline default-pipeline #21
2021-03-17 22:47:11 +01:00
bence 01f0f7d4bc Merge pull request 'feature/MAPG-203-initial-multiplayer-implementation' (#5) from feature/MAPG-203-initial-multiplayer-implementation into develop
default-pipeline default-pipeline #17
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/5
2021-03-15 12:32:54 +01:00
bence c317a0abf4 MAPG-203 adapt game.js to the new endpoints
default-pipeline default-pipeline #15
2021-03-15 12:28:25 +01:00
bence 9c4b7448a9 MAPG-203 refactor GameFlowController 2021-03-15 12:28:06 +01:00
bence 60e20bd92b MAPG-203 rename game/newPlace endpoint 2021-03-15 12:26:15 +01:00
bence 2c4d809d49 MAPG-203 remove placesWithoutPano logic from PlaceRepository 2021-03-15 12:25:49 +01:00
bence bf27f15709 Merge pull request 'MAPG-2020 run PHPStan with 1G memory limit' (#4) from bugfix/MAPG-2020-phpstan-needs-bigger-memory-limit into develop
default-pipeline default-pipeline #12
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/4
2021-01-01 22:36:56 +01:00
bence db3094eb62 MAPG-2020 run PHPStan with 1G memory limit
default-pipeline default-pipeline #11
2021-01-01 22:34:32 +01:00
bence 77c71ab2da Merge pull request 'MAPG-201 update Git link to gitea.e5tv.hu' (#2) from feature/MAPG-201-migrate-to-own-git-server into develop
Check 2 Check 2 was successful
Check 3 Check 3 was not successful
Check 4 Check 4 was successful
Check 5 Check 5 was successful
Check Check was successful
Pipeline Pipeline #1
test_pipeline test_pipeline #3
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/2
2020-12-27 22:12:33 +00:00
bence 8327cd975a MAPG-201 update Git link to gitea.e5tv.hu 2020-12-27 23:08:30 +01:00
bence cc8148fcf9 Merged in bugfix/MAPG-196-fix-remote-static-root-detection (pull request #181)
MAPG-196 fix regex that recognizes remote static root
2020-07-06 20:00:49 +00:00
bence 2a7b5fb992 MAPG-196 fix regex that recognizes remote static root 2020-07-06 21:59:25 +02:00
bence a4c77c3d32 Merged in bugfix/MAPG-196-fix-compatibility-issues (pull request #180)
MAPG-196 revert link preload to plain stylesheet because it is not supported everywhere
2020-07-06 19:15:32 +00:00
bence 317329ae84 MAPG-196 revert link preload to plain stylesheet because it is not supported everywhere 2020-07-06 21:12:56 +02:00
bence 234cb7f086 Merged in feature/MAPG-196-fix-issues-reported-by-lighthouse (pull request #179)
Feature/MAPG-196 fix issues reported by lighthouse
2020-07-06 18:05:51 +00:00
bence 66c709f00d MAPG-196 reposition of cookie notice 2020-07-06 20:04:39 +02:00
bence 2737c7d6d6 MAPG-196 speed up asset loading, add missing description, add missing image alts 2020-07-06 20:04:39 +02:00
bence 2ccee26424 Merged in feature/MAPG-199-static-analyser-save-artifact (pull request #178)
MAPG-199 save static code analysis results as artifact
2020-07-06 16:52:21 +00:00
bence 8cbf611189 MAPG-199 save static code analysis results as artifact 2020-07-06 18:50:17 +02:00
bence 2ac9794e3f Merged in feature/MAPG-199-introduce-static-php-analyser (pull request #177)
Feature/MAPG-199 introduce static php analyser
2020-07-05 21:23:24 +00:00
bence 9e5f7405d1 MAPG-199 move test views to another folder to prevent code analysis errors 2020-07-05 23:18:57 +02:00
bence af98d71924 MAPG-199 fix issues found by PHPStan 2020-07-05 23:18:56 +02:00
bence 39d691fd7f MAPG-199 add static code analysis step to pipeline 2020-07-05 22:51:36 +02:00
bence 405488cf4e MAPG-199 add PHPStan to require-dev 2020-07-05 21:35:49 +02:00
bence f2736d4d6e Merged in hotfix/MAPG-198-fix-not-working-game (pull request #176)
MAPG-198 re-add table name to Select
2020-07-05 19:19:53 +00:00
bence 88b9821c61 MAPG-198 re-add table name to Select
(table name is used when counting)
2020-07-05 21:17:29 +02:00
bence da2c8d96a4 Merged in bugfix/MAPG-142-fix-timestamp-onupdate (pull request #175)
MAPG-142 fix timestamp onupdate issue
2020-07-05 16:24:17 +00:00
bence cd581c6fcd MAPG-142 fix timestamp onupdate issue 2020-07-05 18:23:02 +02:00
bence f7b268e10f Merged in feature/MAPG-185-create-cron-script (pull request #174)
Feature/MAPG-185 create cron script
2020-07-05 15:56:12 +00:00
bence a8b1ab3057 MAPG-185 adapt README 2020-07-05 17:52:13 +02:00
bence 8403c21ec0 MAPG-185 consolidation of email templates 2020-07-05 17:37:02 +02:00
bence 0ba1cda884 MAPG-185 fix getter of User 2020-07-05 17:36:27 +02:00
bence b2e09c394b MAPG-185 switch off builtin session gc 2020-07-05 16:48:37 +02:00
bence 0bcb0187c1 MAPG-185 rename migrate database command 2020-07-05 16:45:38 +02:00
bence f3d1608164 MAPG-185 add maintain database command 2020-07-05 16:43:36 +02:00
bence b254603fb1 MAPG-185 add getter for expired users and password resetters 2020-07-05 16:37:37 +02:00
bence 776561a41c Merged in feature/MAPG-193-replace-direct-selects-to-repository-calls (pull request #173)
Feature/MAPG-193 replace direct selects to repository calls
2020-07-05 13:20:08 +00:00
bence 27fc687043 MAPG-193 replace Select usage to repository calls where it is easily possible 2020-07-05 15:17:28 +02:00
bence 45869321ac MAPG-193 fix PersistentDataManager::selectMultipleFromDb 2020-07-05 15:15:29 +02:00
bence b58137acc9 Merged in feature/MAPG-142-resend-activation-link-functionality (pull request #172)
Feature/MAPG-142 resend activation link functionality
2020-07-05 12:32:32 +00:00
bence dd6bb5ef9a MAPG-142 limit password reset query if the existing is not expired 2020-07-05 14:24:06 +02:00
bence 37094e552b MAPG-142 modify UserPasswordResetterRepository to return only one resetter for user
(it is not possible for an user to have multiple)
2020-07-05 14:24:06 +02:00
bence 32392590bd MAPG-142 save user creation date 2020-07-05 14:24:06 +02:00
bence 38885849de MAPG-142 remove unnecessary 'now' from DateTime constructor 2020-07-05 14:24:05 +02:00
bence ef013b8b9e MAPG-142 rename app to MapGuesser in .env.example 2020-07-05 14:24:05 +02:00
bence ab23b37b97 MAPG-142 redefine tokens and increase OAuth security with nonce 2020-07-05 14:24:05 +02:00
bence 7e3315fc88 MAPG-142 implemenet confirmation mail resend 2020-07-05 13:39:56 +02:00
bence 6cafac1b65 MAPG-142 modify UserConfirmationRepository to return only one confirmation for user
(it is not possible for an user to have multiple)
2020-07-05 13:33:02 +02:00
bence 631582a912 Merged in feature/MAPG-191-get-rid-of-unnecessary-passing-by-reference (pull request #171)
Feature/MAPG-191 get rid of unnecessary passing by reference
2020-07-04 23:02:57 +00:00
bence 6db6208896 MAPG-191 don't create separate variable for data where not necessary 2020-07-05 00:58:03 +02:00
bence e17cf68007 MAPG-191 don't pass data by reference to IContent 2020-07-05 00:25:34 +02:00
bence 091afb0aab Merged in feature/MAPG-141-password-forgotten-functionality (pull request #170)
Feature/MAPG-141 password forgotten functionality
2020-07-04 22:15:50 +00:00
bence 954f111254 MAPG-141 delete password resetters when deleting account 2020-07-05 00:09:45 +02:00
bence de1d7338a4 MAPG-141 add reset password functionality 2020-07-05 00:09:11 +02:00
bence 7539f637b0 MAPG-141 add and fix some links to login/signup 2020-07-05 00:04:35 +02:00
bence 67534a22f5 MAPG-141 add password resetter table, model and repository 2020-07-05 00:03:56 +02:00
bence 43aef22c4e Merged in feature/MAPG-44-possibility-to-add-pov-data-to-locations (pull request #169)
Feature/MAPG-44 possibility to add pov data to locations
2020-07-03 23:12:56 +00:00
bence 3123643bb7 MAPG-44 modify game to use pov of place 2020-07-04 01:11:04 +02:00
bence b736ce8970 MAPG-44 modify MapAdminController to get/save place pov data 2020-07-04 01:10:03 +02:00
bence 4e48e6ae73 MAPG-44 add Pov class and adjust Place model to store pov 2020-07-04 01:09:00 +02:00
bence 028d8a22a2 MAPG-44 modify DB structure to store pov data 2020-07-04 01:06:37 +02:00
bence a73e60ae5a Merged in bugfix/MAPG-189-fix-inputwithbuttons-input-padding (pull request #168)
MAPG-189 fix div.inputWithButton>input's padding when it is focused
2020-07-03 21:51:38 +00:00
bence 5383da81b3 MAPG-189 fix div.inputWithButton>input's padding when it is focused 2020-07-03 23:50:22 +02:00
bence e8adc9f730 Merged in feature/MAPG-177-create-unit-tests-for-all-units (pull request #167)
MAPG-177 add tests for View\Parser
2020-06-28 21:13:32 +00:00
bence 149003250c MAPG-177 add tests for View\Parser 2020-06-28 22:55:15 +02:00
bence 171b4278a6 Merged in feature/MAPG-175-unify-styles (pull request #166)
MAPG-175 make map title one liner
2020-06-28 18:16:23 +00:00
bence 091018d22f MAPG-175 make map title one liner 2020-06-28 20:15:11 +02:00
bence 34f0c92eb5 Merged in bugfix/MAPG-187-dont-evaluate-asset-paths (pull request #165)
Bugfix/MAPG-187 dont evaluate asset paths
2020-06-28 17:38:03 +00:00
bence 8ac5d5c6e2 MAPG-187 adapt views to the new parser 2020-06-28 19:35:14 +02:00
bence e883d184b7 MAPG-187 refactor view parsing and linking 2020-06-28 19:35:14 +02:00
bence 71b1e28aa9 Merged in feature/MAPG-188-pagespeed-improvements (pull request #164)
MAPG-188 get rid of inline js
2020-06-28 16:13:16 +00:00
bence 3555be9b63 MAPG-188 get rid of inline js 2020-06-28 17:56:47 +02:00
bence d03a3934c6 Merged in feature/MAPG-181-handle-the-case-when-user-has-no-password (pull request #163)
Feature/MAPG-181 handle the case when user has no password
2020-06-28 08:42:10 +00:00
bence bd5b49b09b MAPG-181 set user active when added from CLI 2020-06-28 03:19:07 +02:00
bence 4333240acc MAPG-181 add stuff to authenticate without password 2020-06-28 03:18:51 +02:00
bence 4099f1d962 MAPG-181 add new layout type (minimal) 2020-06-28 03:16:48 +02:00
bence 2fac828098 MAPG-181 text fixes 2020-06-28 03:16:28 +02:00
bence f3225ae275 MAPG-181 ability to add login_hint to dialog url 2020-06-28 03:15:20 +02:00
bence 48eba75cf4 MAPG-181 fix and unify styles for buttons and inputs 2020-06-28 03:12:16 +02:00
bence a97a6f8977 Merged in bugfix/MAPG-180-dont-echo-void (pull request #162)
MAPG-180 don't echo void
2020-06-27 12:26:57 +00:00
bence 2a3f891aa0 MAPG-180 don't echo void 2020-06-27 14:26:19 +02:00
bence 38a427416c Merged in bugfix/MAPG-180-unify-inline-js (pull request #161)
MAPG-180 don't parse REVISION in view linking time
2020-06-27 11:21:02 +00:00
bence 2f14e1f4c3 MAPG-180 don't parse REVISION in view linking time 2020-06-27 13:20:11 +02:00
bence 6098b7bb8d Merged in feature/MAPG-175-unify-styles (pull request #160)
MAPG-175 add theme color
2020-06-27 11:11:49 +00:00
bence 596b29f841 MAPG-175 add theme color 2020-06-27 13:09:24 +02:00
bence 34a4026ddc Merged in feature/MAPG-180-inline-js-css-automatically (pull request #159)
Feature/MAPG-180 inline js css automatically
2020-06-27 11:02:21 +00:00
bence a55ece7dac MAPG-180 don't buffer content output to variable 2020-06-27 12:56:47 +02:00
bence efdd5c54be MAPG-180 ability to inline CSS and JS assets 2020-06-27 12:51:00 +02:00
bence 825e0744ec MAPG-180 rename pagescript to pageScript 2020-06-27 12:47:13 +02:00
bence 18bf9aaade MAPG-180 declare CSS and JS files 2020-06-27 02:25:54 +02:00
bence 5e645a1313 Merged in hotfix/MAPG-184-show-runtime (pull request #158)
MAPG-184 measure runtime and show in web output
2020-06-26 23:21:02 +00:00
bence 9c9edcbbd1 MAPG-184 measure runtime and show in web output 2020-06-27 01:19:17 +02:00
bence de009637de Merged in feature/MAPG-89-create-a-better-templating-engine (pull request #156)
Feature/MAPG-89 create a better templating engine
2020-06-26 22:50:14 +00:00
bence 6fce002458 MAPG-89 unify styles 2020-06-27 00:47:14 +02:00
bence 1287fa0bc9 MAPG-89 rewrite views and view templates to the new concept 2020-06-27 00:47:14 +02:00
bence bf8e5b5f4d MAPG-89 adaptations in HtmlContent and index.php 2020-06-27 00:44:38 +02:00
bence c8a49396c2 MAPG-89 install and update sh scripts to link views in non-DEV mode 2020-06-27 00:44:38 +02:00
bence c2323c4c89 MAPG-89 add command that can link views from command line 2020-06-27 00:44:38 +02:00
bence 60cbe64718 MAPG-89 add cache folder that can hold cache files 2020-06-27 00:44:38 +02:00
bence c48bbdc3c2 MAPG-89 add classes that can link views into one PHP 2020-06-27 00:44:38 +02:00
bence ee6aa59253 Merged in bugfix/MAPG-183-tooling-fixes (pull request #157)
MAPG-183 tooling fixes
2020-06-26 22:40:14 +00:00
bence e1df28123f MAPG-183 tooling fixes 2020-06-27 00:38:19 +02:00
bence b4769b62dd Merged in feature/MAPG-177-create-unit-tests-for-all-units (pull request #155)
Feature/MAPG-177 create unit tests for all units
2020-06-25 22:33:56 +00:00
bence bee991e300 MAPG-177 add test for Model 2020-06-26 00:28:37 +02:00
bence ddc56426c2 MAPG-177 add test for GoogleOAuth 2020-06-26 00:28:37 +02:00
bence c06dd1e1d2 MAPG-177 refactor GoogleOAuth, Http\Request, Http\Response to be more testable 2020-06-26 00:28:37 +02:00
bence fd657b4244 MAPG-177 add test for JwtParser 2020-06-26 00:28:37 +02:00
bence 532f099ec9 Merged in feature/MAPG-180-unify-inline-js (pull request #154)
Feature/MAPG-180 unify inline js
2020-06-25 20:41:47 +00:00
bence ec969167e4 MAPG-180 standardize form submit with AJAX 2020-06-25 22:35:04 +02:00
bence e4a51c139f MAPG-180 improve form functionality 2020-06-25 22:34:32 +02:00
bence 27115e44b6 MAPG-180 extract all inline JS into separate JS file 2020-06-25 20:42:12 +02:00
bence 45904a98c2 Merged in feature/MAPG-156-delete-account-functionality (pull request #153)
Feature/MAPG-156 delete account functionality
2020-06-25 18:30:42 +00:00
bence b1ae7391e7 MAPG-156 implement user deletion 2020-06-25 20:26:51 +02:00
bence 8987b563dd MAPG-156 add getByUser to UserConfirmationRepository 2020-06-25 20:26:51 +02:00
bence 70d8807f38 MAPG-156 extend PDM to be able to return with Generator 2020-06-25 20:26:51 +02:00
bence 910fdddf34 MAPG-156 adapt account's JS to the new helper functions 2020-06-25 20:26:51 +02:00
bence 87c4c06aa6 MAPG-156 add some form helper functions 2020-06-25 20:26:51 +02:00
bence ad24f8ac28 MAPG-156 rename profile to account 2020-06-25 16:44:34 +02:00
bence e2067627c4 Merged in feature/MAPG-179-extend-the-readme (pull request #152)
MAPG-179 add basic stuff to the README
2020-06-25 13:33:27 +00:00
bence a8419535e1 MAPG-179 export MariaDB server port 2020-06-25 15:30:38 +02:00
bence 725d14400b MAPG-179 add basic stuff to the README 2020-06-25 15:30:21 +02:00
bence b75eb16b3d Merged in feature/MAPG-178-add-google-analytics (pull request #151)
Feature/MAPG-178 add google analytics
2020-06-25 12:06:43 +00:00
bence f8a570d0d5 MAPG-178 add Google Analytics
style adaptations
cookie notice improvements
2020-06-25 14:02:39 +02:00
bence e7ee7bbe8e MAPG-178 add endpoint that can send valid data based on started session 2020-06-25 14:01:17 +02:00
bence 2f4f66cc94 MAPG-178 make style compatible with cookie notice on the top of the page 2020-06-25 14:00:38 +02:00
bence 4afbc046ff MAPG-178 add GOOGLE_ANALITICS_ID to .env.example 2020-06-25 13:57:05 +02:00
bence 79bd5b98d5 Merged in feature/MAPG-94-unittest-output (pull request #150)
MAPG-94 save test results and save only the result xml as artifact
2020-06-25 08:17:45 +00:00
bence c1cdeba1d3 MAPG-94 save test results and save only the result xml as artifact 2020-06-25 10:16:28 +02:00
bence 255168ffb6 Merged in bugfix/MAPG-94-introduce-automatic-unit-testing (pull request #147)
MAPG-94 fix possible syntax error in yml
2020-06-24 22:45:01 +00:00
bence ca0b8da733 MAPG-94 fix possible syntax error in yml 2020-06-25 00:44:24 +02:00
bence 34206cac12 Merged in feature/MAPG-94-introduce-automatic-unit-testing (pull request #146)
Feature/MAPG-94 introduce automatic unit testing
2020-06-24 22:38:36 +00:00
bence 47a2f15fc5 MAPG-94 don't install Composer dev packages during update 2020-06-25 00:37:48 +02:00
bence 17658f70ba MAPG-94 create Bitbucket pipeline for unit testing 2020-06-25 00:35:36 +02:00
bence 500ec958cb MAPG-94 add tests for Util\Geo 2020-06-25 00:35:36 +02:00
bence d2fa2050ac MAPG-94 install php-xml to Docker because it is a dependency of PHPUnit
install unzip as well because it is sometimes needed
2020-06-25 00:35:36 +02:00
bence e8be433ee6 MAPG-94 add PHPUnit to composer.json 2020-06-25 00:27:01 +02:00
bence 506072017c Merged in hotfix/MAPG-172-create-cookie-notice (pull request #145)
MAPG-172 make variable local
2020-06-24 21:22:05 +00:00
bence 4561c825a4 MAPG-172 make variable local 2020-06-24 23:21:14 +02:00
bence 693bddbcf0 Merged in hotfix/MAPG-172-create-cookie-notice (pull request #144)
MAPG-172 don't close the cookie DIV if not the agree button was clicked
2020-06-24 21:01:34 +00:00
bence eae7cefae1 MAPG-172 don't close the cookie DIV if not the agree button was clicked 2020-06-24 23:00:26 +02:00
bence 212728e138 Merged in bugfix/MAPG-172-create-cookie-notice (pull request #143)
MAPG-172 quote I agree
2020-06-24 20:55:36 +00:00
bence 0d627f297e MAPG-172 quote I agree 2020-06-24 22:55:13 +02:00
bence f07ae979a2 Merged in feature/MAPG-175-unify-styles (pull request #142)
MAPG-175 align noPano middle and center as other elements
2020-06-24 20:37:59 +00:00
bence 8bdbbafcff MAPG-175 align noPano middle and center as other elements 2020-06-24 22:37:33 +02:00
bence 3856ccb576 Merged in feature/MAPG-174-adjust-map-description-style (pull request #141)
MAPG-174 create separate div for description and align description vertically middle
2020-06-24 20:34:00 +00:00
bence 9f839d3f5d MAPG-174 create separate div for description and align description vertically middle 2020-06-24 22:32:54 +02:00
bence 721b611cdb Merged in feature/MAPG-174-adjust-map-description-style (pull request #140)
MAPG-174 align description center
2020-06-24 20:11:08 +00:00
bence 70fb30f673 MAPG-174 align description center 2020-06-24 22:10:24 +02:00
bence e200dab2a4 Merged in feature/MAPG-172-create-cookie-notice (pull request #139)
Feature/MAPG-172 create cookie notice
2020-06-24 20:03:29 +00:00
bence e8c246f002 MAPG-172 add cookie consent DIV and JS handler 2020-06-24 21:57:41 +02:00
bence 251bf1be3d MAPG-172 add new styles 2020-06-24 21:57:40 +02:00
bence 0be20a6097 MAPG-172 don't send session if user didn't consent the cookies 2020-06-24 21:57:40 +02:00
bence d5d3563ddc Merged in feature/MAPG-169-make-map-boxes-height-identical (pull request #138)
Feature/MAPG-169 make map boxes height identical
2020-06-23 21:44:58 +00:00
bence 19aa4098e5 MAPG-169 set mapContainer's inners' height dynamically to be identical in a row 2020-06-23 23:36:19 +02:00
bence 394c8b8830 MAPG-169 make static map background instead of img 2020-06-23 23:36:19 +02:00
bence dc4ba4b5a4 Merged in feature/MAPG-168-prevent-home-redirection (pull request #137)
Feature/MAPG-168 prevent home redirection
2020-06-23 18:48:24 +00:00
bence eda4e00378 MAPG-168 remove headers until a new home page is created 2020-06-23 20:44:26 +02:00
bence dfc74375d7 MAPG-168 make maps as home temporarily to prevent redirections 2020-06-23 20:41:28 +02:00
bence dc1aedd28f Merged in hotfix/MAPG-166-response-to-head-requests (pull request #136)
MAPG-166 response to HEAD request with the GET content
2020-06-23 13:27:48 +00:00
bence 787427b211 MAPG-166 response to HEAD request with the GET content 2020-06-23 15:26:53 +02:00
bence 05b1a84168 Merged in bugfix/MAPG-164-fix-mail-sender (pull request #134)
MAPG-164 use env variable for mail sender instead of hardcoded value
2020-06-22 21:37:40 +00:00
bence e2a0da3dd2 MAPG-164 use env variable for mail sender instead of hardcoded value 2020-06-22 21:37:17 +00:00
161 changed files with 9542 additions and 1171 deletions

No files matched your search

+8 -1
View File
@@ -1,4 +1,4 @@
APP_NAME=MapQuiz
APP_NAME=MapGuesser
DEV=1
DB_HOST=mariadb
DB_USER=mapguesser
@@ -7,6 +7,7 @@ DB_NAME=mapguesser
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url
LEAFLET_TILESERVER_SUBDOMAINS=list_of_subdomains_for_the_tileserver_without_separators
LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles
STATIC_ROOT=/static
MAIL_FROM=mapguesser@mapguesser-dev.ch
@@ -14,3 +15,9 @@ MAIL_HOST=mail
MAIL_PORT=2500
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
GOOGLE_ANALITICS_ID=your_google_analytics_id
MULTI_INTERNAL_HOST=multi
MULTI_INTERNAL_PORT=5000
MULTI_WS_URL=mapguesser-dev.ch:8090
MULTI_WS_PORT=8090
ENABLE_GAME_FOR_GUESTS=0
+1
View File
@@ -1,3 +1,4 @@
.env
installed
vendor
node_modules
+8
View File
@@ -9,6 +9,14 @@
"pathMappings": {
"/var/www/mapguesser": "${workspaceRoot}",
}
},
{
"name": "Listen for NodeJS Inspector in Docker",
"type": "node",
"request": "attach",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/var/www/mapguesser"
}
]
}
+101 -2
View File
@@ -1,5 +1,104 @@
# MapGuesser
This is the MapGuesser Application project.
This is the MapGuesser Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications.
License: GNU AGPL 3.0
## Installation
### Clone the Git repository
The first step is obviously cloning the repository to your machine:
```
git clone https://gitea.e5tv.hu/esoko/mapguesser.git
```
All the commands listed here should be executed from the repository root.
### 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
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production 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
**You should set the API keys that enable playing the game. Without these API keys the application cannot work well. To get Google API keys visit this page: https://console.developers.google.com/**
Required Google APIs:
* **Maps JavaScript API**: for the interactive maps and street views
* **Maps Static API**: for the static map images
* **Street View Static API**: for the backend metadata requests
Required API keys:
* **GOOGLE_MAPS_SERVER_API_KEY**: this it used by the backend and should have access to **Street View Static API**
* **GOOGLE_MAPS_JS_API_KEY**: this is used by the frontend and should have access to **Maps JavaScript API** and **Maps Static API**
Additionally, a tile provider is also needed for map editor. This should be configured by `LEAFLET_TILESERVER_URL`, `LEAFLET_TILESERVER_SUBDOMAINS` and `LEAFLET_TILESERVER_ATTRIBUTION`. You can find some providers here: https://wiki.openstreetmap.org/wiki/Tile_servers. OpenStreetMap's tile server is fine for testing.
Example:
```
LEAFLET_TILESERVER_URL=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
LEAFLET_TILESERVER_SUBDOMAINS=abc
LEAFLET_TILESERVER_ATTRIBUTION="&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
```
### (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
```
### Finalize installation
After you followed the above steps, execute the following command:
```
scripts/install.sh
```
**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:
```
./mapg user:add EMAIL 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.
---
*License: **GNU AGPL 3.0**. Full license text can be found in file `LICENSE`.*
+26
View File
@@ -0,0 +1,26 @@
image: php:7.4.7-cli-buster
pipelines:
default:
- step:
name: Unit Testing
caches:
- composer
artifacts:
- unit_test_results.xml
script:
- apt-get update && apt-get install -y unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install
- vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests
- step:
name: Static Code Analysis
caches:
- composer
artifacts:
- static_code_analysis_results.json
script:
- apt-get update && apt-get install -y unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install
- php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+6 -2
View File
@@ -6,9 +6,13 @@
"require": {
"vlucas/phpdotenv": "^4.1",
"symfony/console": "^5.1",
"phpmailer/phpmailer": "^6.1"
"phpmailer/phpmailer": "^6.1",
"fzaninotto/faker": "^1.9"
},
"require-dev": {
"phpunit/phpunit": "^9",
"phpstan/phpstan": "^0.12.32"
},
"require-dev": {},
"autoload": {
"psr-4": {
"MapGuesser\\": "src"
Generated
+1813 -3
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,8 @@
ALTER TABLE
`places`
ADD
`pov_heading` decimal(6, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_pitch` decimal(5, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_zoom` decimal(5, 4) NOT NULL DEFAULT 0.0;
@@ -0,0 +1,10 @@
CREATE TABLE `user_password_resetters` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`token` varchar(32) CHARACTER SET ascii NOT NULL,
`expires` timestamp NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `token` (`token`),
CONSTRAINT `user_password_resetters_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -0,0 +1,5 @@
UPDATE `user_confirmations` SET token=SUBSTRING(token, 1, 32);
ALTER TABLE `user_confirmations`
ADD `last_sent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `token` varchar(32) CHARACTER SET ascii NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE `sessions`
MODIFY `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,4 @@
UPDATE `sessions` SET id=SUBSTRING(id, 1, 32);
ALTER TABLE `sessions`
MODIFY `id` varchar(32) CHARACTER SET ascii NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE `users`
ADD `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,9 @@
CREATE TABLE `multi_rooms` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`room_id` varchar(6) NOT NULL,
`state` text NOT NULL,
`members` text NOT NULL,
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `room_id` (`room_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -0,0 +1,12 @@
CREATE TABLE `user_played_place` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`place_id` int(10) unsigned NOT NULL,
`last_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`occurrences` int(10) NOT NULL DEFAULT 1,
PRIMARY KEY(`id`),
KEY `user_id` (`user_id`),
KEY `place_id` (`place_id`),
CONSTRAINT `user_played_place_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `user_played_place_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -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;
+15 -4
View File
@@ -1,16 +1,27 @@
version: '3'
services:
app:
build: ./docker
build:
context: ./docker
dockerfile: Dockerfile-app
ports:
- 80:80
volumes:
- .:/var/www/mapguesser
links:
- 'mariadb'
- 'mail'
multi:
build:
context: ./docker
dockerfile: Dockerfile-multi
ports:
- 5000:5000
- 8090:8090
- 9229:9229
volumes:
- .:/var/www/mapguesser
mariadb:
image: mariadb:10.3
ports:
- 3306:3306
volumes:
- mysql:/var/lib/mysql
environment:
+3 -3
View File
@@ -4,8 +4,8 @@ ENV DEBIAN_FRONTEND noninteractive
# Install Nginx, PHP and further necessary packages
RUN apt update --fix-missing
RUN apt install -y curl git mariadb-client nginx \
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip
RUN apt 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
@@ -21,7 +21,7 @@ RUN ./install-composer.sh
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
RUN npm install -g uglify-js clean-css-cli svgo
RUN npm install -g uglify-js clean-css-cli svgo yarn
EXPOSE 80
VOLUME /var/www/mapguesser
+16
View File
@@ -0,0 +1,16 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install necessary packages
RUN apt update --fix-missing
RUN apt install -y curl build-essential
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
ENTRYPOINT /usr/bin/node --inspect=0.0.0.0:9229 multi
+13
View File
@@ -0,0 +1,13 @@
Hi,
<br><br>
You recently requested password reset on {{APP_NAME}} with this email address ({{EMAIL}}).
To reset the password to your account, please click on the following link:<br>
<a href="{{RESET_LINK}}" title="Reset password">{{RESET_LINK}}</a>
<br><br>
You can reset your password with this link util {{EXPIRES}}.
<br><br>
If you did not requested password reset, no further action is required, your account is not touched.
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
+7 -3
View File
@@ -1,10 +1,14 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}). To activate your account, please click on the following link:<br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
To activate your account, please click on the following link:<br>
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required, your email address will be deleted soon.<br>
However if you want to immediately delete it, please click on the following link:<br>
You can activate your account until {{ACTIVATABLE_UNTIL}}.
If you don't activate your account, your email address will be permanently deleted after this point of time.
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
However if you want to immediately delete your email address, please click on the following link:<br>
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
<br><br>
Have fun on {{APP_NAME}}!
+3 -1
View File
@@ -1,5 +1,7 @@
<?php
define('SCRIPT_STARTED', hrtime(true));
require 'vendor/autoload.php';
const ROOT = __DIR__;
@@ -14,7 +16,7 @@ class Container
{
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
static MapGuesser\Routing\RouteCollection $routeCollection;
static \SessionHandlerInterface $sessionHandler;
static MapGuesser\Interfaces\Session\ISessionHandler $sessionHandler;
static MapGuesser\Interfaces\Request\IRequest $request;
}
+3 -1
View File
@@ -5,7 +5,9 @@ require 'main.php';
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\DatabaseMigration());
$app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
$app->add(new MapGuesser\Cli\AddUserCommand());
$app->add(new MapGuesser\Cli\LinkViewCommand());
$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand());
$app->run();
+369
View File
@@ -0,0 +1,369 @@
'use strict';
process.title = 'mapguesser-multi';
class MultiGame {
static ROUND_TIMEOUT_DEFAULT = 120000;
static ROUND_TIMEOUT_MINIMUM = 15000;
static ROUND_TIMEOUT_DIVIDER = 1.5;
static ROUND_TIMEOUT_OFFSET = 500;
constructor() {
this.rooms = new Map();
}
cleanupRooms() {
this.rooms.forEach(function (room, roomId) {
var lastValidDate = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000);
if (room.updated < lastValidDate) {
this.rooms.delete(roomId);
}
});
}
connectToRoom(roomId, token, connection) {
if (!this.rooms.has(roomId) || !this.rooms.get(roomId).members.has(token)) {
return;
}
var room = this.rooms.get(roomId)
var member = room.members.get(token);
member.connection = connection;
this._sendInitialData(room, member, token);
}
createRoom(roomId) {
this.rooms.set(roomId, { members: new Map(), rounds: [], currentRound: -1, updated: new Date() });
return { ok: true };
}
joinRoom(roomId, token, userName) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
if (room.members.has(token)) {
return { error: 'member_already_joined' };
}
var data = { userName: userName };
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'member_joined', data);
});
room.members.set(token, { userName: userName, connection: null });
return { ok: true };
}
startGame(roomId, places) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var rounds = [];
places.forEach(function (place) {
rounds.push({
place: place,
results: new Map(),
timeout: MultiGame.ROUND_TIMEOUT_DEFAULT,
timeoutStarted: null,
timeoutHandler: null
})
});
room.rounds = rounds;
this.nextRound(roomId, 0);
return { ok: true };
}
guess(roomId, token, guessPosition, distance, score) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var round = room.rounds[room.currentRound];
if (round.results.has(token)) {
return { error: 'already_guessed' };
}
var member = room.members.get(token);
var allResults = this._collectResultsInRound(room, round);
this._broadcastGuess(room, member.userName, guessPosition, distance, score);
round.results.set(token, { guessPosition: guessPosition, distance: distance, score: score });
this._setNewTimeout(room, round);
return { allResults: allResults };
}
nextRound(roomId, currentRound) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
room.currentRound = currentRound;
var round = room.rounds[room.currentRound];
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
var data = {};
data.place = { panoId: round.place.panoId, pov: round.place.pov };
data.timeout = round.timeout;
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'new_round', data);
});
return { ok: true };
}
_setNewTimeout(room, round) {
clearTimeout(round.timeoutHandler);
if (room.members.size === round.results.size) {
round.timeout = 0;
round.timeoutStarted = new Date();
this._endRound(room, round);
} else {
round.timeout = round.timeout - (new Date() - round.timeoutStarted);
if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = Math.round(round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER);
} else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM;
}
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
this._broadcastTimeout(room, round);
}
}
_endRound(room, round) {
var allResults = this._collectResultsInRound(room, round);
var self = this;
room.members.forEach(function (member, token) {
var result = { guessPosition: null, distance: null, score: 0 };
if (round.results.has(token)) {
result = round.results.get(token);
} else {
round.results.set(token, result);
}
var data = { position: round.place.position, result: result, allResults: allResults };
self._sendToMember(member, 'end_round', data);
});
}
_sendInitialData(room, member, token) {
var data = {};
if (room.currentRound >= 0) {
var round = room.rounds[room.currentRound];
data.place = round.place;
data.timeout = round.timeout - (new Date() - round.timeoutStarted);
}
data.history = [];
for (var i = 0; i <= room.currentRound; ++i) {
var round = room.rounds[i];
if (i === room.currentRound && !round.results.has(token)) {
continue;
}
var result = { guessPosition: null, distance: null, score: 0 };
var allResults = [];
round.results.forEach(function (currentResult, currentToken) {
if (token === currentToken) {
result = currentResult;
return;
}
allResults.push({ userName: room.members.get(currentToken).userName, guessPosition: currentResult.guessPosition, distance: currentResult.distance, score: currentResult.score });
});
data.history.push({
position: round.place.position,
result: result,
allResults: allResults
});
}
data.members = [];
room.members.forEach(function (currentMember) {
data.members.push({ userName: currentMember.userName, me: member === currentMember });
});
data.readyToContinue = room.currentRound >= 0 && room.members.size === room.rounds[room.currentRound].results.size
this._sendToMember(member, 'initialize', data);
}
_collectResultsInRound(room, round) {
var results = [];
round.results.forEach(function (result, token) {
results.push({
userName: room.members.get(token).userName,
guessPosition: result.guessPosition,
distance: result.distance,
score: result.score
});
});
return results;
}
_broadcastTimeout(room, round) {
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'timeout_changed', { timeout: round.timeout });
});
}
_broadcastGuess(room, userName, guessPosition, distance, score) {
var data = { userName: userName, guessPosition: guessPosition, distance: distance, score: score };
var round = room.rounds[room.currentRound];
var self = this;
room.members.forEach(function (member, token) {
if (!round.results.has(token)) {
return;
}
self._sendToMember(member, 'guess', data);
});
}
_sendToMember(member, type, data) {
if (!member.connection) {
return;
}
if (member.connection.readyState !== ws.OPEN) {
member.connection = null;
return;
}
member.connection.send(JSON.stringify({ type: type, data: data }));
}
}
require('dotenv').config();
var
net = require('net'),
ws = require('ws');
var multiGame = new MultiGame();
//TODO: following should be in a separate class/function
var tcpServer = net.createServer(function (socket) {
socket.on('data', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
var response;
switch (data.func) {
case 'create_room':
response = multiGame.createRoom(data.args.roomId);
break;
case 'join_room':
response = multiGame.joinRoom(data.args.roomId, data.args.token, data.args.userName);
break;
case 'start_game':
response = multiGame.startGame(data.args.roomId, data.args.places);
break
case 'guess':
response = multiGame.guess(data.args.roomId, data.args.token, data.args.guessPosition, data.args.distance, data.args.score);
break;
case 'next_round':
response = multiGame.nextRound(data.args.roomId, data.args.currentRound);
break;
}
socket.write(JSON.stringify(response));
socket.end();
});
});
tcpServer.on('listening', function () {
console.log('[INFO] TCP server started');
});
tcpServer.listen(process.env.MULTI_INTERNAL_PORT);
var wsServer = new ws.Server({ port: process.env.MULTI_WS_PORT });
wsServer.on('connection', function (connection, request) {
console.log('[INFO] New WS connection: ' + request.connection.remoteAddress);
connection.on('message', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
switch (data.func) {
case 'connect_to_room':
multiGame.connectToRoom(data.args.roomId, data.args.token, connection);
break;
}
});
connection.on('close', function () {
console.log('[INFO] WS connection ended: ' + request.connection.remoteAddress);
});
});
wsServer.on('listening', function () {
console.log('[INFO] WS server started');
});
setInterval(function () {
multiGame.cleanupRooms();
}, 24 * 60 * 60 * 1000);
+17
View File
@@ -0,0 +1,17 @@
{
"name": "mapguesser-multi",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
},
"ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "mapguesser-multi",
"version": "",
"description": "MapGuesser Application - Multiplayer",
"main": "index.js",
"dependencies": {
"dotenv": "^8.2.0",
"ws": "^7.4.4"
},
"scripts": {},
"author": "Pőcze Bence and The MapGuesser Contributors <bence@pocze.ch>",
"license": "GNU AGPL 3.0"
}
+9
View File
@@ -0,0 +1,9 @@
parameters:
level: 5
checkMissingIterableValueType: false
paths:
- main.php
- mapg
- web.php
- src
- tests
+5 -3
View File
@@ -26,8 +26,9 @@ if ($match !== null) {
}
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
$content = new MapGuesser\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
header('Content-Type: text/html; charset=UTF-8', true, 403);
echo json_encode(['error' => 'no_valid_anti_csrf_token']);
$content->render();
return;
}
@@ -36,7 +37,7 @@ if ($match !== null) {
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
echo $response->render();
$response->render();
return;
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
@@ -47,5 +48,6 @@ if ($match !== null) {
}
}
$content = new MapGuesser\Response\HtmlContent('error/404');
header('Content-Type: text/html; charset=UTF-8', true, 404);
require ROOT . '/views/error/404.php';
$content->render();
+129 -11
View File
@@ -1,17 +1,31 @@
#panorama {
width: 100%;
height: calc(100% - 40px);
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
#guessCover {
#panoCover {
position: absolute;
top: 40px;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: #000000;
opacity: 0.5;
z-index: 4;
}
#panningBlockerCover {
display: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
opacity: 0;
z-index: 3;
}
@@ -19,7 +33,7 @@
position: absolute;
bottom: 30px;
right: 20px;
z-index: 2;
z-index: 3;
}
#guess.result {
@@ -76,7 +90,7 @@
line-height: 1;
}
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
#distanceInfo>p:nth-child(2), #distanceInfo>p:nth-child(3), #scoreInfo>p:nth-child(2) {
display: none;
}
@@ -99,19 +113,111 @@
display: none;
}
#startMultiGameButton {
display: none;
}
#players > p {
font-size: 14px;
font-weight: bold;
}
#countdown {
position: absolute;
top: 5px;
right: 5px;
height: 28px;
line-height: 28px;
padding: 0 8px;
background-color: #eeeeee;
border: solid 1px #555555;
border-radius: 3px;
opacity: 0.95;
z-index: 5;
visibility: hidden;
}
#countdown.yellow {
background-color: #f7c789;
border: solid 1px #e8a349;
}
#countdown.red {
background-color: #f7a5a5;
border: solid 1px #aa5e5e;
}
#countdown p {
font-size: 16px;
line-height: inherit;
}
#countdown.yellow p {
color: #9c4308;
}
#countdown.red p {
color: #701919;
}
#navigation {
z-index: 2;
}
#goToStart {
display: none;
}
#highscoresTable {
margin: 1em;
border-collapse: collapse;
width: 90%;
}
#highscoresTable td, #highscoresTable th {
border: 1px solid #ddd;
padding: 8px;
}
#highscoresTable tr:nth-child(even) {
background-color: #f2f2f2;
}
#highscoresTable tr:hover {
background-color: #ddd;
}
#highscoresTable th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #e8a349;
color: white;
}
#highscoresTable tr.ownPlayer {
font-weight: 500;
}
@media screen and (max-width: 899px) {
.hideOnNarrowScreen {
display: none;
}
}
@media screen and (max-width: 599px) {
#mapName {
display: none;
}
#showGuessButtonContainer {
position: absolute;
left: 20px;
left: 65px;
bottom: 30px;
right: 20px;
z-index: 2;
}
#guess {
top: 50px;
top: 10px;
left: 20px;
opacity: 0.95;
visibility: hidden;
@@ -122,6 +228,10 @@
#scoreBarBase {
width: 100%;
}
#navigation {
bottom: 25px;
left: 10px;
}
}
@media screen and (min-width: 600px) {
@@ -157,7 +267,7 @@
#guess.result {
width: initial;
height: initial;
top: 50px;
top: 10px;
left: 50px;
right: 50px;
bottom: 50px;
@@ -165,13 +275,17 @@
#scoreBarBase {
width: 60%;
}
#navigation {
bottom: 50px;
left: 20px;
}
@media screen and (max-height: 424px) {
#guess {
top: 50px;
top: 10px;
height: initial;
}
#guess.adapt:hover {
top: 50px;
top: 10px;
height: initial;
}
#guess.result {
@@ -179,5 +293,9 @@
right: 20px;
bottom: 30px;
}
#navigation {
bottom: 30px;
left: 10px;
}
}
}
+35 -10
View File
@@ -1,9 +1,20 @@
#map {
width: 100%;
height: calc(100% - 40px);
.map {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
#mapSelection img {
display: inline;
width: 1em;
height: 1em;
vertical-align: -0.15em;
}
/* modify the cursor for the Leaflet map */
.leaflet-container {
cursor: crosshair;
}
@@ -16,6 +27,8 @@
#noPano {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 2;
visibility: hidden;
@@ -23,12 +36,12 @@
}
#noPano>p {
margin: auto;
text-align: center;
}
#control {
position: absolute;
top: 50px;
top: 10px;
right: 10px;
width: 125px;
z-index: 3;
@@ -42,37 +55,49 @@
visibility: hidden;
}
#overlayControl {
bottom: 20px;
right: 10px;
z-index: 3;
}
#deleteButton {
display: none;
}
@media screen and (max-width: 999px) and (min-height: 600px) {
#map.selected {
height: calc(50% - 20px);
height: 50%;
}
#panorama, #noPano {
left: 0;
bottom: 0;
right: 0;
height: calc(50% - 20px);
height: 50%;
}
#placeControl {
top: calc(50% + 30px);
top: calc(50% + 10px);
}
.hideOnMobile {
display: none;
}
}
@media screen and (min-width: 1000px), (max-height: 599px) {
#map.selected {
top: 0;
bottom: 0;
left: 0;
width: 50%;
}
#panorama, #noPano {
top: 40px;
top: 0;
bottom: 0;
right: 0;
width: 50%;
}
#placeControl {
top: 50px;
top: 10px;
}
#modified.selected {
right: calc(50% + 10px);
+155 -14
View File
@@ -12,21 +12,30 @@ html, body {
padding: 0;
}
body {
background-color: #cccccc;
}
button::-moz-focus-inner, input::-moz-focus-inner {
padding: 0;
border: 0;
}
/* to be compatible with browsers that don't know <main> */
main {
display: block;
}
::selection {
background-color: #28a745;
color: #ffffff;
}
p, h1, h2, input, textarea, select, button, a {
p, h1, h2, h3, input, textarea, select, button, a, table, label {
font-family: 'Roboto', sans-serif;
}
h1, h2 {
h1, h2, h3 {
font-weight: 500;
}
@@ -46,7 +55,11 @@ h2, header.small h1 {
font-size: 24px;
}
p, h2 {
h3 {
font-size: 18px;
}
p, h2, h3 {
line-height: 150%;
}
@@ -84,7 +97,7 @@ hr {
}
p.small, span.small {
font-size: 12px;
font-size: 14px;
}
.justify {
@@ -148,6 +161,27 @@ button, a.button {
line-height: 35px;
}
button.small, div.inputWithButton>button {
font-size: 14px;
padding: 0 12px;
height: 32px;
line-height: 32px;
}
button.small {
height: 32px;
line-height: 32px;
}
div.inputWithButton>button {
border-radius: 2px;
height: 27px;
line-height: 27px;
width: 75px;
margin-left: -79px;
vertical-align: 2px;
}
button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus {
background-color: #29457f;
outline: none;
@@ -179,7 +213,7 @@ button.gray, a.button.gray {
background-color: #808080;
}
button.gray:hover, button.gray:focus, a.button.gray:hover, a.button.gray:focus {
button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus {
background-color: #555555;
}
@@ -187,7 +221,7 @@ button.red, a.button.red {
background-color: #aa5e5e;
}
button.red:hover, button.red:focus, a.button.red:hover, a.button.red:focus {
button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus {
background-color: #7f2929;
}
@@ -195,7 +229,7 @@ button.yellow, a.button.yellow {
background-color: #e8a349;
}
button.yellow:hover, button.yellow:focus, a.button.yellow:hover, a.button.yellow:focus {
button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus {
background-color: #c37713;
}
@@ -203,7 +237,7 @@ button.green, a.button.green {
background-color: #28a745;
}
button.green:hover, button.green:focus, a.button.green:hover, a.button.green:focus {
button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.button.green:focus {
background-color: #1b7d31;
}
@@ -211,22 +245,36 @@ input, select, textarea {
background-color: #f9fafb;
border: solid #c8d2e1 1px;
border-radius: 2px;
padding: 4px;
box-sizing: border-box;
font-size: 15px;
font-weight: 300;
}
input, select {
height: 30px;
line-height: 30px;
padding: 0 5px;
}
textarea {
font-size: 13px;
padding: 5px;
resize: none;
}
input.big, select.big, textarea.big {
padding: 5px;
input.big, select.big, textarea.big, div.inputWithButton>input {
font-size: 18px;
}
input.big, select.big, div.inputWithButton>input {
height: 35px;
line-height: 35px;
padding: 0 6px;
}
textarea.big {
padding: 6px;
}
input.fullWidth, select.fullWidth, textarea.fullWidth {
display: block;
width: 100%;
@@ -241,14 +289,34 @@ input:disabled, select:disabled, textarea:disabled {
input:focus, select:focus, textarea:focus {
background-color: #ffffff;
border: solid #29457f 2px;
padding: 3px;
outline: none;
}
input.big:focus, select.big:focus, textarea.big:focus {
input:focus, select:focus {
padding: 0 4px;
}
textarea:focus {
padding: 4px;
}
input.big:focus, select.big:focus {
padding: 0 5px;
}
div.inputWithButton>input {
width: 100%;
padding: 0 83px 0 6px;
}
div.inputWithButton>input:focus {
padding: 0 82px 0 5px;
}
textarea.big:focus {
padding: 5px;
}
div.modal {
position: fixed;
background-color: #ffffff;
@@ -314,9 +382,17 @@ header>p>span:not(:last-child) {
}
main {
background-color: #ffffff;
padding: 6px 12px;
}
main.full {
position: relative;
width: 100%;
height: calc(100% - 40px);
padding: 0;
}
footer {
background-color: #444444;
padding: 6px 12px;
@@ -336,6 +412,20 @@ div.buttonContainer>button {
margin: 0 auto;
}
#cookiesNotice {
position: fixed;
left: 0;
bottom: 0;
right: 0;
margin: 20px;
background-color: #eeeeee;
border: solid #888888 1px;
border-radius: 3px;
padding: 10px;
text-align: center;
z-index: 10;
}
#loading {
position: fixed;
width: 64px;
@@ -357,6 +447,45 @@ div.box {
box-sizing: border-box;
}
.circleControl {
position: absolute;
width: 60px;
bottom: 20px;
right: 10px;
}
.circleControl .controlItem {
position: relative;
height: 60px;
margin-top: 10px;
opacity: 70%;
cursor: pointer;
}
.circleControl .controlItem:hover {
opacity: 100%;
}
.circleControl .controlItem div {
position: absolute;
width: 100%;
height: 100%;
}
.circleControl .controlBackground {
width: 100%;
height: 100%;
opacity: 50%;
}
.circleControl .controlIcon {
width: 75%;
height: 75%;
margin: auto;
margin-top: 50%;
transform: translateY(-50%);
}
@media screen and (max-width: 599px) {
header h1 span {
display: none;
@@ -383,6 +512,12 @@ div.box {
div.box {
width: initial;
}
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
}
@media screen and (min-width: 600px) {
@@ -412,6 +547,12 @@ div.box {
padding-top: 10px;
padding-bottom: 10px;
}
.circleControl {
width: 45px;
}
.circleControl .controlItem {
height: 45px;
}
}
@media screen and (min-height: 400px) and (max-height: 499px) {
+36 -7
View File
@@ -18,7 +18,16 @@ div.mapItem>div.title {
color: white;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 4px 8px;
padding: 0 8px;
height: 35px;
line-height: 35px;
}
div.mapItem>div.title>p {
line-height: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
div.mapItem>div.title>p.title {
@@ -29,17 +38,16 @@ div.mapItem>div.title>p.title {
div.mapItem>div.imgContainer {
width: 100%;
padding-top: 50%;
background: #cccccc;
}
div.mapItem>div.imgContainer>img {
width: 100%;
margin-top: -50%
background-color: #cccccc;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
div.mapItem>div.inner {
background-color: #eeeeee;
padding: 10px 8px;
box-sizing: border-box;
}
div.mapItem>div.inner>div.info {
@@ -47,6 +55,12 @@ div.mapItem>div.inner>div.info {
grid-template-columns: auto auto;
}
div.mapItem>div.inner>div.description {
display: flex;
justify-content: center;
align-items: center;
}
div.mapItem>div.inner>div.info>p:nth-child(1) {
text-align: left;
}
@@ -61,6 +75,21 @@ div.mapItem>div.buttonContainer {
grid-auto-flow: column;
}
#restrictions input {
height: auto;
margin: 0.5em;
}
#restrictions input[type=range] {
height: 1.5em;
margin-left: 2em;
width: 70%;
}
#timeLimitType {
margin-left: 2em;
}
@media screen and (min-width: 1504px) {
#mapContainer {
grid-template-columns: auto auto auto auto;
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 16 16">
<circle cx="50%" cy="50%" r="50%" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 146 B

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

After

Width:  |  Height:  |  Size: 328 B

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

After

Width:  |  Height:  |  Size: 556 B

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

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#3183ce"
fill-rule="evenodd"
stroke="#19456d"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
</svg>

After

Width:  |  Height:  |  Size: 529 B

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

After

Width:  |  Height:  |  Size: 718 B

+69
View File
@@ -0,0 +1,69 @@
var Account = {
original: null,
countdown: null,
openGoogleAuthenticate: function () {
window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600')
},
authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) {
var password = document.getElementsByTagName('form')[0].elements.password;
var button = document.getElementById('authenticateWithGoogleButton');
Account.original = {
type: password.type,
placeholder: password.placeholder,
required: password.required,
disabled: password.disabled
};
password.type = 'text';
password.placeholder = 'Authenticated!'
password.value = '';
password.required = false;
password.disabled = true;
button.disabled = true;
Account.countdown = setInterval(function () {
var timeLeft = Math.ceil((authenticatedWithGoogleUntil.getTime() - new Date().getTime()) / 1000);
if (timeLeft > 30) {
return;
}
if (timeLeft <= 0) {
Account.resetGoogleAuthentication();
return;
}
password.placeholder = 'Authenticated! ' + timeLeft + ' seconds left...';
}, 1000);
},
resetGoogleAuthentication: function () {
if (Account.countdown !== null) {
clearInterval(Account.countdown);
}
var password = document.getElementsByTagName('form')[0].elements.password;
var button = document.getElementById('authenticateWithGoogleButton');
password.type = Account.original.type;
password.placeholder = Account.original.placeholder
password.required = Account.original.required;
password.disabled = Account.original.disabled;
button.disabled = false;
}
};
(function () {
document.getElementById('authenticateWithGoogleButton').onclick = function () {
Account.openGoogleAuthenticate();
};
document.getElementsByTagName('form')[0].onreset = function () {
Account.resetGoogleAuthentication();
};
})();
@@ -0,0 +1,10 @@
(function () {
if (success) {
window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil);
window.close();
} else {
document.getElementById('closeWindowButton').onclick = function () {
window.close();
}
}
})();
+888 -100
View File
File diff suppressed because it is too large. Load diff
-43
View File
@@ -1,43 +0,0 @@
(function () {
var form = document.getElementById('loginForm');
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, function () {
if (this.response.error) {
if (this.response.error === 'user_not_found') {
window.location.replace('/signup');
return;
}
var errorText;
switch (this.response.error) {
case 'password_too_short':
errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!'
break;
case 'user_not_active':
errorText = 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!';
break;
case 'password_not_match':
errorText = 'The given password is wrong.'
break;
}
document.getElementById('loading').style.visibility = 'hidden';
var loginFormError = document.getElementById('loginFormError');
loginFormError.style.display = 'block';
loginFormError.innerHTML = errorText;
return;
}
window.location.replace('/');
}, formData);
};
})();
+9
View File
@@ -0,0 +1,9 @@
(function () {
document.getElementById('cancelGoogleSignupButton').onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/signup/google/reset', function () {
window.location.replace('/signup');
});
};
})();
+12
View File
@@ -0,0 +1,12 @@
(function () {
var resetSignupButton = document.getElementById('resetSignupButton');
if (resetSignupButton) {
resetSignupButton.onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/signup/reset', function () {
window.location.reload();
});
};
}
})();
+371 -75
View File
@@ -7,7 +7,6 @@
description: null
},
map: null,
markers: null,
panorama: null,
selectedMarker: null,
added: {},
@@ -87,14 +86,14 @@
sv.getPanorama({
location: location,
preference: google.maps.StreetViewPreference.NEAREST,
radius: 100,
radius: MapEditor.map.getSearchRadius(location),
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
}, function (data, status) {
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
if (panoLocationData === null && !canBeIndoor) {
MapEditor.requestPanoData(location, true);
return;
if (panoLocationData === null && !canBeIndoor) {
MapEditor.requestPanoData(location, true);
return;
}
document.getElementById('loading').style.visibility = 'hidden';
@@ -118,16 +117,13 @@
MapEditor.resetSelected();
MapEditor.selectedMarker = marker;
MapEditor.map.invalidateSize(true);
MapEditor.map.resize();
MapEditor.map.panTo(marker.getLatLng());
MapEditor.panorama.setVisible(false);
if (marker.placeId) {
MapEditor.markers.removeLayer(MapEditor.selectedMarker);
MapEditor.map.addLayer(MapEditor.selectedMarker);
marker.setIcon(IconCollection.iconBlue);
marker.setZIndexOffset(2000);
MapEditor.map.changeMarkerIcon(marker, MapEditor.map.iconCollection.iconBlue);
document.getElementById('deleteButton').style.display = 'block';
@@ -165,13 +161,13 @@
var placeId = MapEditor.selectedMarker.placeId
if (places[placeId].id && !del) {
MapEditor.map.removeLayer(MapEditor.selectedMarker);
MapEditor.markers.addLayer(MapEditor.selectedMarker);
MapEditor.selectedMarker.setIcon(places[placeId].noPano ? IconCollection.iconRed : IconCollection.iconGreen);
MapEditor.selectedMarker.setZIndexOffset(1000);
MapEditor.map.changeMarkerIcon(
MapEditor.selectedMarker,
places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen
);
} else {
delete places[placeId];
MapEditor.map.removeLayer(MapEditor.selectedMarker);
MapEditor.map.removeMarker(MapEditor.selectedMarker);
}
document.getElementById('deleteButton').style.display = 'none';
@@ -223,7 +219,7 @@
MapEditor.resetSelected(del);
MapEditor.selectedMarker = null;
MapEditor.map.invalidateSize(true);
MapEditor.map.resize();
},
deletePlace: function () {
@@ -239,6 +235,7 @@
delete MapEditor.added[placeId];
delete MapEditor.edited[placeId];
delete places[placeId];
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
@@ -313,24 +310,6 @@
}
};
var IconCollection = {
iconGreen: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconRed: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconBlue: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
};
var Util = {
getHighResData: function () {
if (window.devicePixelRatio >= 4) {
@@ -340,64 +319,337 @@
} else {
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
}
},
extractCoordinates: function (coordinatesStr) {
var coordinates = { valid: false, latlng: { lat: 0., lng: 0. } };
var delimiters = [',', ' ', ';'];
coordinatesStr = coordinatesStr.trim();
if (coordinatesStr.length == 0) {
return coordinates;
}
for (var delimiter of delimiters) {
if (coordinatesStr.indexOf(delimiter) != -1) {
var coordinatesArr = coordinatesStr.split(delimiter);
coordinates.latlng.lat = parseFloat(coordinatesArr[0]);
coordinates.latlng.lng = parseFloat(coordinatesArr[1]);
if (!isNaN(coordinates.latlng.lat) && !isNaN(coordinates.latlng.lng)) {
coordinates.valid = true;
return coordinates;
}
}
}
return coordinates;
}
};
MapEditor.map = L.map('map', {
zoomControl: false
});
var LMapWrapper = {
map: null,
markers: null,
divId: null,
iconCollection: {
iconGreen: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconRed: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconBlue: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
})
},
MapEditor.map.on('click', function (e) {
var marker = L.marker(e.latlng, {
icon: IconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(MapEditor.map)
.on('click', function () {
MapEditor.select(this);
});
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
MapEditor.select(marker);
});
if (!LMapWrapper.map) {
LMapWrapper.divId = divId;
LMapWrapper.map = L.map(LMapWrapper.divId, {
center: { lat: 0., lng: 0. },
zoom: 2
});
var highResData = Util.getHighResData();
LMapWrapper.map.on('click', function (e) {
LMapWrapper.placeMarker(e.latlng);
});
L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: '1234',
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(MapEditor.map);
var highResData = Util.getHighResData();
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: tileSubdomains,
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(LMapWrapper.map);
MapEditor.markers = L.markerClusterGroup({
maxClusterRadius: 50
});
if (mapId) {
LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
}
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
LMapWrapper.loadMarkers(places);
document.getElementById('streetViewCoverSelector').style.display = 'none';
},
hide: function () {
document.getElementById(LMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!LMapWrapper.markers) {
LMapWrapper.markers = L.markerClusterGroup({
maxClusterRadius: 50
});
} else {
LMapWrapper.markers.clearLayers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? LMapWrapper.iconCollection.iconRed : LMapWrapper.iconCollection.iconGreen,
zIndexOffset: 1000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
}
LMapWrapper.map.addLayer(LMapWrapper.markers);
},
// TODO: check whether marker is already existing on the map for the coordinates
// or alternatively block saving for matching coordinates
placeMarker: function (latLng) {
var marker = L.marker(latLng, {
icon: LMapWrapper.iconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
},
panTo: function (latLng) {
LMapWrapper.map.panTo(latLng);
},
resize: function () {
LMapWrapper.map.invalidateSize(true);
},
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
marker.setZIndexOffset(2000);
},
removeMarker: function (marker) {
LMapWrapper.markers.removeLayer(marker);
},
toggleStreetViewCover: function () { },
getSearchRadius: function (location) {
return 100;
}
};
var place = places[placeId];
var GMapWrapper = {
map: null,
markers: null,
divId: null,
streetViewCover: null,
streetViewCoverOn: false,
iconCollection: {
iconGreen: {
url: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconRed: {
url: STATIC_ROOT + '/img/markers/marker-red.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconBlue: {
url: STATIC_ROOT + '/img/markers/marker-blue.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
}
},
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? IconCollection.iconRed : IconCollection.iconGreen,
zIndexOffset: 1000
})
.addTo(MapEditor.markers)
.on('click', function () {
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
if (!GMapWrapper.map) {
GMapWrapper.divId = divId;
GMapWrapper.map = new google.maps.Map(document.getElementById(GMapWrapper.divId), {
center: { lat: 0., lng: 0. },
zoom: 2,
fullscreenControl: false,
zoomControl: true,
zoomControlOptions: {
position: google.maps.ControlPosition.LEFT_BOTTOM
},
streetViewControl: false,
draggableCursor: 'crosshair'
});
GMapWrapper.streetViewCover = new google.maps.StreetViewCoverageLayer();
GMapWrapper.map.addListener('click', function (mapsMouseEvent) {
GMapWrapper.placeMarker({
lat: mapsMouseEvent.latLng.lat(),
lng: mapsMouseEvent.latLng.lng()
});
});
if (mapId) {
GMapWrapper.map.fitBounds({ south: mapBounds.south, west: mapBounds.west, north: mapBounds.north, east: mapBounds.east });
}
}
GMapWrapper.loadMarkers(places);
document.getElementById('streetViewCoverSelector').style.display = 'block'
},
hide: function () {
document.getElementById(GMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!GMapWrapper.markers) {
GMapWrapper.markers = new MarkerClusterer(GMapWrapper.map, [], {
imagePath: STATIC_ROOT + '/img/markers/m',
imageExtension: 'png?rev' + REVISION
});
} else {
GMapWrapper.markers.clearMarkers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = new google.maps.Marker({
position: {
lat: place.lat,
lng: place.lng
},
icon: place.noPano ? GMapWrapper.iconCollection.iconRed : GMapWrapper.iconCollection.iconGreen
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
GMapWrapper.markers.addMarker(marker);
}
},
// TODO: check whether marker is already existing on the map for the coordinates
// or alternatively block saving for matching coordinates
placeMarker: function (latLng) {
var marker = new google.maps.Marker({
map: GMapWrapper.map,
position: latLng,
icon: GMapWrapper.iconCollection.iconBlue,
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
marker.placeId = place.id;
}
GMapWrapper.markers.addMarker(marker);
MapEditor.map.addLayer(MapEditor.markers);
MapEditor.select(marker);
},
panTo: function (latLng) {
GMapWrapper.map.panTo(latLng);
},
resize: function () {
google.maps.event.trigger(GMapWrapper.map, 'resize');
},
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
},
removeMarker: function (marker) {
GMapWrapper.markers.removeMarker(marker);
},
toggleStreetViewCover: function () {
if (GMapWrapper.streetViewCoverOn) {
GMapWrapper.streetViewCover.setMap(null);
GMapWrapper.streetViewCoverOn = false;
} else {
GMapWrapper.streetViewCover.setMap(GMapWrapper.map);
GMapWrapper.streetViewCoverOn = true;
}
},
getSearchRadius: function (location) {
// source: https://www.yorku.ca/mack/CHI01.htm
var movementOffset = 4;
// source: https://groups.google.com/g/google-maps-js-api-v3/c/hDRO4oHVSeM/m/osOYQYXg2oUJ?pli=1
var metersPerPixel = 156543.03392 * Math.cos(location.lat * Math.PI / 180) / Math.pow(2, GMapWrapper.map.getZoom());
var minSearchRadius = 5;
var searchRadius = Math.max(minSearchRadius, Math.round(movementOffset * metersPerPixel));
return searchRadius;
}
};
// initialize content of #map with google maps
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
// switch off fullscreenControl because positioning doesn't work
@@ -440,4 +692,48 @@
document.getElementById('deleteButton').onclick = function () {
MapEditor.deletePlace();
};
document.getElementById('jumpButton').onclick = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
if (coordinates.valid) {
MapEditor.map.placeMarker(coordinates.latlng);
}
};
document.getElementById('jumpCoordinates').onkeyup = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
var jumpButton = document.getElementById("jumpButton");
if (coordinates.valid) {
jumpButton.disabled = false;
if (e.key == 'Enter') {
MapEditor.map.placeMarker(coordinates.latlng);
}
}
else {
jumpButton.disabled = true;
}
};
document.getElementById('mapSelector').onclick = function () {
MapEditor.closePlace();
MapEditor.map.hide();
if (MapEditor.map === GMapWrapper) {
MapEditor.map = LMapWrapper;
MapEditor.map.init('lmap', places);
} else {
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
}
}
document.getElementById('streetViewCoverSelector').onclick = function () {
MapEditor.map.toggleStreetViewCover();
}
})();
+153 -3
View File
@@ -1,4 +1,50 @@
var MapGuesser = {
isSecure: window.location.protocol === 'https:',
cookiesAgreed: false,
sessionAvailableHooks: {},
initGoogleAnalitics: function () {
if (typeof GOOGLE_ANALITICS_ID === 'undefined') {
return;
}
// Global site tag (gtag.js) - Google Analytics
var script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + GOOGLE_ANALITICS_ID;
script.async = true;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', GOOGLE_ANALITICS_ID);
},
agreeCookies: function () {
if (MapGuesser.cookiesAgreed) {
return;
}
var expirationDate = new Date(new Date().getTime() + 20 * 365 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = 'COOKIES_CONSENT=1; expires=' + expirationDate + '; path=/';
MapGuesser.initGoogleAnalitics();
MapGuesser.httpRequest('GET', '/startSession.json', function () {
ANTI_CSRF_TOKEN = this.response.antiCsrfToken;
for (var hookId in MapGuesser.sessionAvailableHooks) {
if (!MapGuesser.sessionAvailableHooks.hasOwnProperty(hookId)) {
continue;
}
MapGuesser.sessionAvailableHooks[hookId]();
}
});
MapGuesser.cookiesAgreed = true;
},
httpRequest: function (method, url, callback, data) {
var xhr = new XMLHttpRequest();
@@ -21,6 +67,52 @@ var MapGuesser = {
}
},
setOnsubmitForForm: function (form) {
form.onsubmit = function (e) {
e.preventDefault();
document.getElementById('loading').style.visibility = 'visible';
var formData = new FormData(form);
var formError = form.getElementsByClassName('formError')[0];
var pageLeaveOnSuccess = form.dataset.redirectOnSuccess || form.dataset.reloadOnSuccess;
MapGuesser.httpRequest('POST', form.action, function () {
if (!pageLeaveOnSuccess) {
document.getElementById('loading').style.visibility = 'hidden';
}
if (this.response.error) {
if (pageLeaveOnSuccess) {
document.getElementById('loading').style.visibility = 'hidden';
}
formError.style.display = 'block';
formError.innerHTML = this.response.error.errorText;
return;
}
if (this.response.redirect) {
window.location.replace(this.response.redirect.target);
return;
}
if (!pageLeaveOnSuccess) {
formError.style.display = 'none';
form.reset();
} else {
if (form.dataset.redirectOnSuccess) {
window.location.replace(form.dataset.redirectOnSuccess);
} else if (form.dataset.reloadOnSuccess) {
window.location.reload();
}
}
}, formData);
}
},
showModal: function (id) {
document.getElementById(id).style.visibility = 'visible';
document.getElementById('cover').style.visibility = 'visible';
@@ -91,11 +183,35 @@ var MapGuesser = {
document.getElementById('cover').style.visibility = 'hidden';
},
toggleDisableOnChange: function (input, button) {
observeInput: function (input, buttonToToggle) {
if (input.defaultValue !== input.value) {
button.disabled = false;
buttonToToggle.disabled = false;
} else {
button.disabled = true;
buttonToToggle.disabled = true;
}
},
observeInputsInForm: function (form, observedInputs) {
for (var i = 0; i < observedInputs.length; i++) {
var input = form.elements[observedInputs[i]];
switch (input.tagName) {
case 'INPUT':
case 'TEXTAREA':
input.oninput = function () {
MapGuesser.observeInput(this, form.elements.submit);
};
break;
case 'SELECT':
input.onchange = function () {
MapGuesser.observeInput(this, form.elements.submit);
};
break;
}
}
form.onreset = function () {
form.elements.submit.disabled = true;
}
}
};
@@ -111,7 +227,41 @@ var MapGuesser = {
}
}
var forms = document.getElementsByTagName('form');
for (var i = 0; i < forms.length; i++) {
var form = forms[i];
if (form.dataset.noSubmit) {
continue;
}
MapGuesser.setOnsubmitForForm(form);
if (form.dataset.observeInputs) {
MapGuesser.observeInputsInForm(form, form.dataset.observeInputs.split(','));
}
}
document.getElementById('cover').onclick = function () {
MapGuesser.hideModal();
};
if (COOKIES_CONSENT) {
MapGuesser.initGoogleAnalitics();
} else {
// we don't want user to agree cookies when clicking on the notice itself
document.getElementById('cookiesNotice').onclick = function (e) {
e.stopPropagation();
};
document.getElementById('agreeCookiesButton').onclick = function () {
MapGuesser.agreeCookies();
document.getElementById('cookiesNotice').style.display = 'none';
};
window.onclick = function () {
MapGuesser.agreeCookies();
};
}
})();
+181
View File
@@ -0,0 +1,181 @@
(function () {
var Maps = {
descriptionDivs: null,
addStaticMaps: function () {
var imgContainers = document.getElementById('mapContainer').getElementsByClassName('imgContainer');
for (var i = 0; i < imgContainers.length; i++) {
var imgContainer = imgContainers[i];
var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' +
'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&' +
'visible=' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng +
'&key=' + GOOGLE_MAPS_JS_API_KEY;
imgContainer.style.backgroundImage = 'url("' + imgSrc + '")';
}
},
initializeDescriptionDivs: function () {
Maps.descriptionDivs = document.getElementById('mapContainer').getElementsByClassName('description');
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
var description = Maps.descriptionDivs[i];
var boundingClientRect = description.getBoundingClientRect();
description.defaultHeight = boundingClientRect.height;
}
},
calculateDescriptionDivHeights: function () {
var currentY;
var rows = [];
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
var description = Maps.descriptionDivs[i];
var boundingClientRect = description.getBoundingClientRect();
if (currentY !== boundingClientRect.y) {
rows.push([]);
}
rows[rows.length - 1].push(description);
currentY = boundingClientRect.y;
}
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var maxHeight = 0;
for (var j = 0; j < row.length; j++) {
var description = row[j];
if (description.defaultHeight > maxHeight) {
maxHeight = description.defaultHeight;
}
}
for (var j = 0; j < row.length; j++) {
var description = row[j];
description.style.height = maxHeight + 'px';
}
}
}
};
var Util = {
printTimeForHuman: function (time) {
const minutes = Math.floor(time / 60);
const seconds = time % 60;
var time_str = '';
if (minutes == 1) {
time_str += '1 minute';
} else if (minutes > 1) {
time_str += minutes + ' minutes';
}
if (minutes > 0 && seconds > 0) {
time_str += ' and ';
}
if (seconds == 1) {
time_str += '1 second';
} else if (seconds > 1) {
time_str += seconds + ' seconds';
}
return time_str;
}
};
Maps.addStaticMaps();
Maps.initializeDescriptionDivs();
Maps.calculateDescriptionDivHeights();
window.onresize = function () {
Maps.calculateDescriptionDivHeights();
};
document.getElementById('multiForm').onsubmit = function (e) {
e.preventDefault();
var roomId = this.elements.roomId.value;
if (roomId.length !== 6) {
return;
}
window.location.href = '/multiGame/' + this.elements.roomId.value;
};
document.getElementById('challengeForm').onsubmit = function (e) {
e.preventDefault();
var url = '/challenge/create.json';
var formData = new FormData(this);
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', url, function() {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
window.location.href = '/challenge/' + this.response.challengeToken;
}, formData);
};
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('createNewChallengeButton').href = '/challenge/new/' + this.dataset.mapId;
document.getElementById('playMode').style.visibility = 'hidden';
var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
};
}
document.getElementById('closePlayModeButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeMultiButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeChallengeButton').onclick = function () {
MapGuesser.hideModal();
}
var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton');
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
button.onclick = function () {
MapGuesser.showModal('playMode');
document.getElementById('singleButton').href = '/game/' + this.dataset.mapId;
document.getElementById('multiButton').dataset.mapId = this.dataset.mapId;
document.getElementById('challengeMapId').value = this.dataset.mapId;
};
}
document.getElementById('timeLimit').oninput = function () {
var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
document.getElementById('timerEnabled').checked = true;
}
})();
+34
View File
@@ -0,0 +1,34 @@
(function () {
Maps = {
deleteMap: function (mapId, mapName) {
MapGuesser.showModalWithContent('Delete map', 'Are you sure you want to delete map \'' + mapName + '\'?', [{
type: 'button',
classNames: ['red'],
text: 'Delete',
onclick: function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/admin/deleteMap/' + mapId, function () {
if (this.response.error) {
document.getElementById('loading').style.visibility = 'hidden';
//TODO: handle this error
return;
}
window.location.reload();
});
}
}]);
}
};
var buttons = document.getElementById('mapContainer').getElementsByClassName('deleteButton');
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
button.onclick = function () {
Maps.deleteMap(this.dataset.mapId, this.dataset.mapName);
};
}
})();
-48
View File
@@ -1,48 +0,0 @@
(function () {
var form = document.getElementById('profileForm');
form.elements.password_new.onkeyup = function () {
MapGuesser.toggleDisableOnChange(this, form.elements.save);
};
form.elements.password_new_confirm.onkeyup = function () {
MapGuesser.toggleDisableOnChange(this, form.elements.save);
};
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
var errorText;
switch (this.response.error) {
case 'password_not_match':
errorText = 'The given current password is wrong.'
break;
case 'password_too_short':
errorText = 'The given new password is too short. Please choose a password that is at least 6 characters long!'
break;
case 'passwords_not_match':
errorText = 'The given new passwords do not match.'
break;
}
var profileFormError = document.getElementById('profileFormError');
profileFormError.style.display = 'block';
profileFormError.innerHTML = errorText;
return;
}
document.getElementById('profileFormError').style.display = 'none';
form.reset();
form.elements.save.disabled = true;
}, formData);
};
})();
-60
View File
@@ -1,60 +0,0 @@
(function () {
var form = document.getElementById('signupForm');
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, function () {
if (this.response.error) {
if (this.response.error === 'user_found') {
window.location.replace('/');
return;
}
var errorText;
switch (this.response.error) {
case 'email_not_valid':
errorText = 'The given email address is not valid.'
break;
case 'password_too_short':
errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!'
break;
case 'passwords_not_match':
errorText = 'The given passwords do not match.'
break;
case 'user_found_user_not_active':
errorText = 'There is a user already registered with the given email address. Please check your email and click on the activation link!';
break;
case 'user_found_password_not_match':
errorText = 'There is a user already registered with the given email address, but the given password is wrong.'
break;
}
document.getElementById('loading').style.visibility = 'hidden';
var signupFormError = document.getElementById('signupFormError');
signupFormError.style.display = 'block';
signupFormError.innerHTML = errorText;
return;
}
window.location.replace('/signup/success');
}, formData);
};
var resetSignupButton = document.getElementById('resetSignupButton');
if (resetSignupButton) {
resetSignupButton.onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/signup/reset', function () {
window.location.reload();
});
};
}
})();
+17 -2
View File
@@ -59,10 +59,25 @@ def getRevisionForRef(ref):
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
def getLatestReleaseTag():
return subprocess.check_output(["git", "for-each-ref", "refs/tags/Release*", "--count=1", "--sort=-creatordate", "--format=%(refname:short)"], cwd=REPO).decode().strip()
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
for line in process.stdout:
tag = line.decode().rstrip()
if isTagVerified(tag):
return tag
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
raise Exception("No verified 'Release*' tag found!")
def isTagVerified(tag):
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
return process.returncode == 0
def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune"], cwd=REPO)
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
def checkoutWorktree(worktreePath, ref):
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
+11 -2
View File
@@ -9,18 +9,27 @@ if [ -f ${ROOT_DIR}/installed ]; then
exit 1
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Installing MapGuesser DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/mapguesser.sql
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate)
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link)
else
echo "Creating the first user..."
(cd ${ROOT_DIR} && ./mapg user:add mapg@mapg.dev 123456 admin)
fi
touch ${ROOT_DIR}/installed
+12 -2
View File
@@ -5,15 +5,25 @@ ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
echo "Installing Composer packages..."
(cd ${ROOT_DIR} && composer install)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
(cd ${ROOT_DIR} && composer install --no-dev)
else
(cd ${ROOT_DIR} && composer install --dev)
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate)
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link)
fi
+5 -2
View File
@@ -1,5 +1,6 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command;
@@ -9,7 +10,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class AddUserCommand extends Command
{
public function configure()
public function configure(): void
{
$this->setName('user:add')
->setDescription('Adding of user.')
@@ -23,8 +24,10 @@ class AddUserCommand extends Command
$user = new User();
$user->setEmail($input->getArgument('email'));
$user->setPlainPassword($input->getArgument('password'));
$user->setActive(true);
$user->setCreatedDate(new DateTime());
if ($input->hasArgument('type')) {
if ($input->hasArgument('type') && $input->getArgument('type') !== null) {
$user->setType($input->getArgument('type'));
}
+69
View File
@@ -0,0 +1,69 @@
<?php namespace MapGuesser\Cli;
use FilesystemIterator;
use MapGuesser\View\Linker;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LinkViewCommand extends Command
{
public function configure(): void
{
$this->setName('view:link')
->setDescription('Linking of views.')
->addArgument('view', InputArgument::OPTIONAL, 'View file to be linked.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$views = [];
$view = $input->getArgument('view');
if ($view !== null) {
$views[] = $view;
} else {
$folder = ROOT . '/views';
$folderLength = strlen($folder) + 1;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
if ($file->isDir() || $file->getExtension() !== 'php') {
continue;
}
$view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php');
if (strpos($view, 'templates') === 0 || strpos($view, 'tests') === 0) {
continue;
}
$views[] = $view;
}
}
try {
foreach ($views as $view) {
$generator = new Linker($view);
$generator->generate();
}
} catch (\Exception $e) {
$output->writeln('<error>Linking view(s) failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>View(s) successfully linked!</info>');
return 0;
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MaintainDatabaseCommand extends Command
{
private PersistentDataManager $pdm;
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private MultiRoomRepository $multiRoomRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct()
{
parent::__construct();
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->multiRoomRepository = new MultiRoomRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
}
public function configure(): void
{
$this->setName('db:maintain')
->setDescription('Maintain database.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
try {
$this->deleteInactiveExpiredUsers();
$this->deleteExpiredPasswordResetters();
$this->deleteExpiredRooms();
$this->deleteExpiredSessions();
} catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>Maintenance was successful!</info>');
$output->writeln('');
return 0;
}
private function deleteInactiveExpiredUsers(): void
{
\Container::$dbConnection->startTransaction();
foreach ($this->userRepository->getAllInactiveExpired() as $user) {
//TODO: these can be in some wrapper class
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter);
}
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
$this->pdm->deleteFromDb($userPlayedPlace);
}
$this->pdm->deleteFromDb($user);
}
\Container::$dbConnection->commit();
}
private function deleteExpiredPasswordResetters(): void
{
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
$this->pdm->deleteFromDb($passwordResetter);
}
}
private function deleteExpiredRooms(): void
{
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
$this->pdm->deleteFromDb($multiRoom);
}
}
private function deleteExpiredSessions(): void
{
//TODO: model may be used for sessions too
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']);
$select->where('updated', '<', (new DateTime('-7 days'))->format('Y-m-d H:i:s'));
$result = $select->execute();
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($session['id']);
$modify->delete();
}
}
}
@@ -7,11 +7,11 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DatabaseMigration extends Command
class MigrateDatabaseCommand extends Command
{
public function configure()
public function configure(): void
{
$this->setName('migrate')
$this->setName('db:migrate')
->setDescription('Migration of database changes.');
}
@@ -88,6 +88,10 @@ class DatabaseMigration extends Command
$path = ROOT . '/database/migrations/' . $type;
$dir = opendir($path);
if ($dir === false) {
throw new \Exception('Cannot open dir: ' . $path);
}
$files = [];
while ($file = readdir($dir)) {
$filePath = $path . '/' . $file;
+261 -10
View File
@@ -1,51 +1,302 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use Faker\Factory;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Response\Redirect;
class GameController
class GameController implements ISecured
{
const NUMBER_OF_ROUNDS = 5;
private IRequest $request;
private PersistentDataManager $pdm;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private MapRepository $mapRepository;
private PlaceRepository $placeRepository;
private ChallengeRepository $challengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository();
$this->challengeRepository = new ChallengeRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
}
public function authorize(): bool
{
return !empty($_ENV['ENABLE_GAME_FOR_GUESTS']) || $this->request->user() !== null;
}
public function getGame(): IContent
{
$mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new HtmlContent('game', $data);
return new HtmlContent('game', ['mapId' => $mapId]);
}
public function getGameJson(): IContent
public function getNewMultiGame(): IRedirect
{
$mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new JsonContent($data);
$map = $this->mapRepository->getById($mapId);
$roomId = bin2hex(random_bytes(3));
$token = $this->getMultiToken($roomId);
$room = new MultiRoom();
$room->setRoomId($roomId);
$room->setStateArray([
'mapId' => $mapId,
'area' => $map->getArea(),
'rounds' => [],
'currentRound' => -1
]);
$room->setMembersArray(['owner' => $token, 'all' => []]);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
return new Redirect(
\Container::$routeCollection
->getRoute('multiGame')
->generateLink(['roomId' => $roomId]),
IRedirect::TEMPORARY
);
}
private function prepareGame(int $mapId)
public function getMultiGame(): IContent
{
$map = $this->mapRepository->getById($mapId);
$roomId = $this->request->query('roomId');
return new HtmlContent('game', ['roomId' => $roomId]);
}
public function getChallenge(): IContent
{
$challengeToken = $this->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 ($this->request->post('timerEnabled') !== null && $this->request->post('timeLimit') !== null) {
$challenge->setTimeLimit($this->request->post('timeLimit'));
}
if ($this->request->post('timeLimitType') !== null) {
$challenge->setTimeLimitType($this->request->post('timeLimitType'));
}
if ($this->request->post('noMove') !== null) {
$challenge->setNoMove(true);
}
if ($this->request->post('noPan') !== null) {
$challenge->setNoPan(true);
}
if ($this->request->post('noZoom') !== null) {
$challenge->setNoZoom(true);
}
$this->pdm->saveToDb($challenge);
// save owner/creator
$session = $this->request->session();
$userId = $session->get('userId');
$userInChallenge = new UserInChallenge();
$userInChallenge->setUserId($userId);
$userInChallenge->setChallenge($challenge);
$userInChallenge->setTimeLeft($challenge->getTimeLimit());
$userInChallenge->setIsOwner(true);
$this->pdm->saveToDb($userInChallenge);
// select places
$mapId = (int) $this->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++);
$this->pdm->saveToDb($placeInChallenge);
}
return new JsonContent(['challengeToken' => dechex($challengeToken)]);
}
public function prepareGame(): IContent
{
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [
'mapId' => $mapId,
'area' => $map->getArea(),
'rounds' => []
'rounds' => [],
'currentRound' => -1
]);
} else { // update the area of the map in the session in any case
$state['area'] = $map->getArea();
$session->set('state', $state);
}
return ['mapId' => $mapId, 'mapName' => $map->getName(), 'bounds' => $map->getBounds()->toArray()];
return new JsonContent([
'mapId' => $mapId,
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
public function prepareMultiGame(): IContent
{
$roomId = $this->request->query('roomId');
$userName = $this->request->post('userName');
if (empty($userName)) {
$faker = Factory::create();
$userName = $faker->userName;
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
if (!isset($room)) {
return new JsonContent(['error' => 'game_not_found']);
}
$state = $room->getStateArray();
$map = $this->mapRepository->getById($state['mapId']);
$token = $this->getMultiToken($roomId);
$members = $room->getMembersArray();
if (!in_array($token, $members['all'])) {
if ($state['currentRound'] >= 0) {
return new JsonContent(['error' => 'game_already_started']);
}
$members['all'][] = $token;
}
$room->setMembersArray($members);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
$this->multiConnector->sendMessage('join_room', [
'roomId' => $roomId,
'token' => $token,
'userName' => $userName
]);
return new JsonContent([
'roomId' => $roomId,
'token' => $token,
'owner' => $members['owner'] == $token,
'mapId' => $state['mapId'],
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
public function prepareChallenge(): IContent
{
$challengeToken_str = $this->request->query('challengeToken');
$session = $this->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());
$this->pdm->saveToDb($userInChallenge);
}
$map = $this->mapRepository->getByChallenge($challenge);
return new JsonContent([
'mapId' => $map->getId(),
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
private function getMultiToken(string $roomId): string
{
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
$token = bin2hex(random_bytes(16));
$session->set('multiState', [
'roomId' => $roomId,
'token' => $token
]);
} else {
$token = $multiState['token'];
}
return $token;
}
}
+411 -70
View File
@@ -1,135 +1,476 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
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\Repository\ChallengeRepository;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository;
class GameFlowController
class GameFlowController implements ISecured
{
const NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000;
private IRequest $request;
private PersistentDataManager $pdm;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private PlaceRepository $placeRepository;
private MapRepository $mapRepository;
private UserRepository $userRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private ChallengeRepository $challengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
private PlaceInChallengeRepository $placeInChallengeRepository;
private GuessRepository $guessRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->placeRepository = new PlaceRepository();
$this->mapRepository = new MapRepository();
$this->userRepository = new UserRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->challengeRepository = new ChallengeRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
$this->guessRepository = new GuessRepository();
}
public function getNewPlace(): IContent
public function authorize(): bool
{
return !empty($_ENV['ENABLE_GAME_FOR_GUESTS']) || $this->request->user() !== null;
}
public function initialData(): IContent
{
$mapId = (int) $this->request->query('mapId');
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
return new JsonContent(['error' => 'no_session_found']);
}
if (count($state['rounds']) === 0) {
$placesWithoutPano = [];
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, [], $placesWithoutPano);
$this->addNewRoundToState($state, $place, $placesWithoutPano);
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
$this->startNewGame($state, $mapId);
$session->set('state', $state);
}
$data = ['panoId' => $place->getPanoIdCached()];
} else {
$rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1];
$response = [];
$history = [];
for ($i = 0; $i < $rounds - 1; ++$i) {
$round = $state['rounds'][$i];
$history[] = [
'position' => $round['position']->toArray(),
$last = $state['rounds'][$state['currentRound']];
$response['place'] = [
'panoId' => $last['panoId'],
'pov' => $last['pov']->toArray()
];
$response['history'] = [];
for ($i = 0; $i < $state['currentRound']; ++$i) {
$round = $state['rounds'][$i];
$response['history'][] = [
'position' => $round['position']->toArray(),
'result' => [
'guessPosition' => $round['guessPosition']->toArray(),
'distance' => $round['distance'],
'score' => $round['score']
];
}
$data = [
'history' => $history,
'panoId' => $last['panoId']
]
];
}
return new JsonContent($data);
return new JsonContent($response);
}
public function evaluateGuess(): IContent
public function multiInitialData(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
$this->startNewGame($state, $state['mapId']);
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
}
$places = [];
foreach ($state['rounds'] as $round) {
$places[] = [
'position' => $round['position']->toArray(),
'panoId' => $round['panoId'],
'pov' => $round['pov']->toArray()
];
}
$this->multiConnector->sendMessage('start_game', ['roomId' => $roomId, 'places' => $places]);
return new JsonContent(['ok' => true]);
}
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::class, PlaceInChallenge::class, Place::class];
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::class]) 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 = $this->request->session();
$userId = $session->get('userId');
$challengeToken_str = $this->request->query('challengeToken');
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, [Challenge::class]);
if (!isset($userInChallenge)) {
return new JsonContent(['error' => 'game_not_found']);
}
$challenge = $userInChallenge->getChallenge();
$currentRound = $userInChallenge->getCurrentRound();
$response = $this->prepareChallengeResponse($userId, $challenge, $currentRound, true);
if ($challenge->getTimeLimitType() === 'game' && $challenge->getTimeLimit() !== null && $userInChallenge->getCurrentRound() > 0) {
$timeLimit = max(10, $userInChallenge->getTimeLeft());
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
}
return new JsonContent($response);
}
public function guess(): IContent
{
$mapId = (int) $this->request->query('mapId');
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
return new JsonContent(['error' => 'no_session_found']);
}
$last = $state['rounds'][count($state['rounds']) - 1];
$position = $last['position'];
$last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$distance = $this->calculateDistance($position, $guessPosition);
$score = $this->calculateScore($distance, $state['area']);
$result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$last['guessPosition'] = $guessPosition;
$last['distance'] = $distance;
$last['score'] = $score;
$state['rounds'][count($state['rounds']) - 1] = $last;
$last['distance'] = $result['distance'];
$last['score'] = $result['score'];
if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
$exclude = [];
$response = [
'position' => $last['position']->toArray(),
'result' => $result
];
foreach ($state['rounds'] as $round) {
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
}
$state['rounds'][$state['currentRound']] = $last;
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$next = $state['rounds'][$state['currentRound']];
$placesWithoutPano = [];
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, $exclude, $placesWithoutPano);
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$panoId = $place->getPanoIdCached();
} else {
$state['rounds'] = [];
$panoId = null;
$response['place'] = [
'panoId' => $next['panoId'],
'pov' => $next['pov']->toArray()
];
}
$session->set('state', $state);
$data = [
'result' => [
'position' => $position->toArray(),
'distance' => $distance,
'score' => $score
],
'panoId' => $panoId
];
return new JsonContent($data);
$this->saveVisit($last['placeId']);
return new JsonContent($response);
}
private function addNewRoundToState(&$state, Place $place, array $placesWithoutPano): void
// save the selected place for the round in UserPlayedPlace
private function saveVisit($placeId): void
{
$state['rounds'][] = [
'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached()
$session = $this->request->session();
$userId = $session->get('userId');
if (isset($userId)) {
$userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
if (!$userPlayedPlace) {
$userPlayedPlace = new UserPlayedPlace();
$userPlayedPlace->setUserId($userId);
$userPlayedPlace->setPlaceId($placeId);
} else {
$userPlayedPlace->incrementOccurrences();
}
$userPlayedPlace->setLastTimeDate(new DateTime());
$this->pdm->saveToDb($userPlayedPlace);
}
}
public function multiGuess(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$responseFromMulti = $this->multiConnector->sendMessage('guess', [
'roomId' => $roomId,
'token' => $multiState['token'],
'guessPosition' => $guessPosition->toArray(),
'distance' => $result['distance'],
'score' => $result['score']
]);
if (isset($responseFromMulti['error'])) {
return new JsonContent(['error' => $responseFromMulti['error']]);
}
$response = [
'position' => $last['position']->toArray(),
'result' => $result,
'allResults' => $responseFromMulti['allResults']
];
return new JsonContent($response);
}
public function challengeGuess(): IContent
{
$session = $this->request->session();
$userId = $session->get('userId');
$challengeToken_str = $this->request->query('challengeToken');
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, [Challenge::class]);
if (!isset($userInChallenge)) {
return new JsonContent(['error' => 'game_not_found']);
}
$challenge = $userInChallenge->getChallenge();
$currentRound = $userInChallenge->getCurrentRound();
$currentPlaceInChallenge = $this->placeInChallengeRepository->getByRoundInChallenge($currentRound, $challenge, [Place::class, Map::class]);
$currentPlace = $currentPlaceInChallenge->getPlace();
$map = $currentPlace->getMap();
// creating response
$nextRound = $currentRound + 1;
$response = $this->prepareChallengeResponse($userId, $challenge, $nextRound);
$response['position'] = $currentPlace->getPosition()->toArray();
if ($this->request->post('lat') && $this->request->post('lng')) {
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->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']);
$this->pdm->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 = $this->request->post('timeLeft');
if (isset($timeLeft)) {
$userInChallenge->setTimeLeft(intval($timeLeft));
}
$this->pdm->saveToDb($userInChallenge);
if ($challenge->getTimeLimitType() === 'game' && isset($timeLeft)) {
$timeLimit = max(10, intval($timeLeft));
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
}
if (isset($response['history'][$currentRound]['allResults'])) {
$response['allResults'] = $response['history'][$currentRound]['allResults'];
}
$this->saveVisit($currentPlace->getId());
return new JsonContent($response);
}
public function multiNextRound(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$this->multiConnector->sendMessage('next_round', ['roomId' => $roomId, 'currentRound' => $state['currentRound']]);
}
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
return new JsonContent(['ok' => true]);
}
private function evaluateGuess(Position $realPosition, Position $guessPosition, float $area)
{
$distance = $this->calculateDistance($realPosition, $guessPosition);
$score = $this->calculateScore($distance, $area);
return ['distance' => $distance, 'score' => $score];
}
private function startNewGame(array &$state, int $mapId): void
{
$session = $this->request->session();
$userId = $session->get('userId');
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
$state['rounds'] = [];
$state['currentRound'] = 0;
foreach ($places as $place) {
$state['rounds'][] = [
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()
];
}
}
private function calculateDistance(Position $realPosition, Position $guessPosition): float
+17
View File
@@ -1,12 +1,29 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
class HomeController
{
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getIndex(): IRedirect
{
return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY);
}
public function startSession(): IContent
{
// session starts with the request, this method just sends valid data to the client
return new JsonContent(['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')]);
}
}
+294 -70
View File
@@ -1,5 +1,8 @@
<?php namespace MapGuesser\Controller;
use DateInterval;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
@@ -7,8 +10,11 @@ use MapGuesser\Mailing\Mail;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
@@ -25,12 +31,18 @@ class LoginController
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
}
public function getLoginForm()
@@ -39,18 +51,23 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$data = [];
return new HtmlContent('login/login', $data);
return new HtmlContent('login/login');
}
public function getGoogleLoginRedirect(): IRedirect
{
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth();
$url = $oAuth->getDialogUrl($state, $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink());
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
$nonce
);
return new Redirect($url, IRedirect::TEMPORARY);
}
@@ -74,8 +91,7 @@ class LoginController
public function getSignupSuccess(): IContent
{
$data = [];
return new HtmlContent('login/signup_success', $data);
return new HtmlContent('login/signup_success');
}
public function getSignupWithGoogleForm()
@@ -92,48 +108,96 @@ class LoginController
$user = $this->userRepository->getByEmail($userData['email']);
$data = ['found' => $user !== null, 'email' => $userData['email']];
return new HtmlContent('login/google_signup', $data);
return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email']]);
}
public function getRequestPasswordResetForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
}
public function getRequestPasswordResetSuccess(): IContent
{
return new HtmlContent('login/password_reset_request_success');
}
public function getResetPasswordForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new HtmlContent('login/reset_password', ['success' => false]);
}
$user = $this->userRepository->getById($resetter->getUserId());
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]);
}
public function login(): IContent
{
if ($this->request->user() !== null) {
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) {
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => 'password_too_short'];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
$tmpUser = new User();
$tmpUser->setPlainPassword($this->request->post('password'));
$this->request->session()->set('tmp_user_data', ['email' => $this->request->post('email'), 'password_hashed' => $tmpUser->getPassword()]);
$this->request->session()->set('tmp_user_data', [
'email' => $this->request->post('email'),
'password_hashed' => $tmpUser->getPassword()
]);
$data = ['error' => 'user_not_found'];
return new JsonContent($data);
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()
]
]);
}
if (!$user->getActive()) {
$data = ['error' => 'user_not_active'];
return new JsonContent($data);
$this->resendConfirmationEmail($user);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
}
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => 'password_not_match'];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function loginWithGoogle()
@@ -143,30 +207,34 @@ class LoginController
}
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
$data = [];
return new HtmlContent('login/google_login', $data);
return new HtmlContent('login/google_login');
}
$oAuth = new GoogleOAuth();
$tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink());
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
$this->request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
);
if (!isset($tokenData['id_token'])) {
$data = [];
return new HtmlContent('login/google_login', $data);
return new HtmlContent('login/google_login');
}
$jwtParser = new JwtParser($tokenData['id_token']);
$userData = $jwtParser->getPayload();
$idToken = $jwtParser->getPayload();
if (!$userData['email_verified']) {
$data = [];
return new HtmlContent('login/google_login', $data);
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('login/google_login');
}
$user = $this->userRepository->getByGoogleSub($userData['sub']);
if (!$idToken['email_verified']) {
return new HtmlContent('login/google_login');
}
$user = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($user === null) {
$this->request->session()->set('google_user_data', $userData);
$this->request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
}
@@ -186,8 +254,7 @@ class LoginController
public function signup(): IContent
{
if ($this->request->user() !== null) {
$data = ['error' => 'logged_in'];
return new JsonContent($data);
return new JsonContent(['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
@@ -195,22 +262,31 @@ class LoginController
if ($user !== null) {
if ($user->getActive()) {
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => 'user_found_password_not_match'];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'There is a user already registered with the given email address, ' .
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
$this->request->setUser($user);
$data = ['error' => 'user_found'];
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]];
} else {
$data = ['error' => 'user_found_user_not_active'];
$data = [
'error' => [
'errorText' => 'There is a user already registered with the given email address. ' .
'Please check your email and click on the activation link!'
]
];
}
return new JsonContent($data);
}
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
$data = ['error' => 'email_not_valid'];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if ($this->request->session()->has('tmp_user_data')) {
@@ -220,52 +296,53 @@ class LoginController
$tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword($this->request->post('password'))) {
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
} else {
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => 'password_too_short'];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
}
$user = new User();
$user->setEmail($this->request->post('email'));
$user->setPlainPassword($this->request->post('password'));
$user->setCreatedDate(new DateTime());
\Container::$dbConnection->startTransaction();
$this->pdm->saveToDb($user);
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime());
$token = bin2hex(random_bytes(16));
$confirmation = new UserConfirmation();
$confirmation->setUser($user);
$confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token);
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
$this->request->session()->delete('tmp_user_data');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function signupWithGoogle(): IContent
{
if ($this->request->user() !== null) {
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
$userData = $this->request->session()->get('google_user_data');
@@ -277,6 +354,7 @@ class LoginController
$user = new User();
$user->setEmail($userData['email']);
$user->setCreatedDate(new DateTime());
} else {
$sendWelcomeEmail = false;
}
@@ -293,24 +371,21 @@ class LoginController
$this->request->session()->delete('google_user_data');
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function resetSignup(): IContent
{
$this->request->session()->delete('tmp_user_data');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function resetGoogleSignup(): IContent
{
$this->request->session()->delete('google_user_data');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function activate()
@@ -319,11 +394,10 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) {
$data = [];
return new HtmlContent('login/activate', $data);
return new HtmlContent('login/activate');
}
\Container::$dbConnection->startTransaction();
@@ -348,11 +422,10 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) {
$data = ['success' => false];
return new HtmlContent('login/cancel', $data);
return new HtmlContent('login/cancel', ['success' => false]);
}
\Container::$dbConnection->startTransaction();
@@ -361,27 +434,164 @@ class LoginController
$user = $this->userRepository->getById($confirmation->getUserId());
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
$this->pdm->deleteFromDb($userPlayedPlace);
}
$this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit();
$data = ['success' => true];
return new HtmlContent('login/cancel', $data);
return new HtmlContent('login/cancel', ['success' => true]);
}
private function sendConfirmationEmail(string $email, string $token): void
public function requestPasswordReset(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) {
return new JsonContent([
'error' => [
'errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!'
]
]);
}
if (!$user->getActive()) {
$this->resendConfirmationEmail($user);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
}
$existingResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($existingResetter !== null && $existingResetter->getExpiresDate() > new DateTime()) {
return new JsonContent([
'error' => [
'errorText' => 'Password reset was recently requested for this account. Please check your email, or try again later!'
]
]);
}
$token = bin2hex(random_bytes(16));
$expires = new DateTime('+1 hour');
$passwordResetter = new UserPasswordResetter();
$passwordResetter->setUser($user);
$passwordResetter->setToken($token);
$passwordResetter->setExpiresDate($expires);
\Container::$dbConnection->startTransaction();
if ($existingResetter !== null) {
$this->pdm->deleteFromDb($existingResetter);
}
$this->pdm->saveToDb($passwordResetter);
\Container::$dbConnection->commit();
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
return new JsonContent(['success' => true]);
}
public function resetPassword(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
]
]);
}
if (strlen($this->request->post('password')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword($this->request->post('password'));
$this->pdm->saveToDb($user);
\Container::$dbConnection->commit();
$this->request->setUser($user);
return new JsonContent(['success' => true]);
}
private function sendConfirmationEmail(string $email, string $token, DateTime $created): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
$mail->setBodyFromTemplate('signup', [
'EMAIL' => $email,
'ACTIVATE_LINK' => $this->request->getBase() . '/'. \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
'ACTIVATE_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T')
]);
$mail->send();
}
private function resendConfirmationEmail(User $user): bool
{
$confirmation = $this->userConfirmationRepository->getByUser($user);
if ($confirmation === null || (clone $confirmation->getLastSentDate())->add(new DateInterval('PT1H')) > new DateTime()) {
return false;
}
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
return true;
}
private function sendWelcomeEmail(string $email): void
{
$mail = new Mail();
@@ -392,4 +602,18 @@ class LoginController
]);
$mail->send();
}
private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
$mail->setBodyFromTemplate('password-reset', [
'EMAIL' => $email,
'RESET_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i T')
]);
$mail->send();
}
}
+89 -51
View File
@@ -1,21 +1,25 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov;
class MapAdminController implements ISecured
{
@@ -29,12 +33,27 @@ class MapAdminController implements ISecured
private PlaceRepository $placeRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private ChallengeRepository $challengeRepository;
private GuessRepository $guessRepository;
private PlaceInChallengeRepository $placeInChallengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->challengeRepository = new ChallengeRepository();
$this->guessRepository = new GuessRepository();
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
}
public function authorize(): bool
@@ -57,8 +76,13 @@ class MapAdminController implements ISecured
$places = [];
}
$data = ['mapId' => $mapId, 'mapName' => $map->getName(), 'mapDescription' => str_replace('<br>', "\n", $map->getDescription()), 'bounds' => $map->getBounds()->toArray(), 'places' => &$places];
return new HtmlContent('admin/map_editor', $data);
return new HtmlContent('admin/map_editor', [
'mapId' => $mapId,
'mapName' => $map->getName(),
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
'bounds' => $map->getBounds()->toArray(),
'places' => &$places
]);
}
public function getPlace(): IContent
@@ -67,8 +91,7 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById($placeId);
$data = ['panoId' => $place->getFreshPanoId()];
return new JsonContent($data);
return new JsonContent(['panoId' => $place->getFreshPanoId()]);
}
public function saveMap(): IContent
@@ -94,6 +117,11 @@ class MapAdminController implements ISecured
$place->setMap($map);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
if ($placeRaw['panoId'] === -1) {
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
@@ -114,6 +142,11 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
$this->pdm->saveToDb($place);
@@ -126,7 +159,7 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$this->pdm->deleteFromDb($place);
$this->deletePlace($place);
}
}
@@ -146,11 +179,11 @@ class MapAdminController implements ISecured
\Container::$dbConnection->commit();
$data = ['mapId' => $map->getId(), 'added' => $addedIds];
return new JsonContent($data);
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
}
public function deleteMap() {
public function deleteMap(): IContent
{
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
@@ -163,40 +196,52 @@ class MapAdminController implements ISecured
\Container::$dbConnection->commit();
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
private function deletePlace(Place $place): void
{
foreach ($this->userPlayedPlaceRepository->getAllByPlace($place) as $userPlayedPlace) {
$this->pdm->deleteFromDb($userPlayedPlace);
}
foreach ($this->challengeRepository->getAllByPlace($place) as $challenge) {
$this->deleteChallenge($challenge);
}
$this->pdm->deleteFromDb($place);
}
private function deletePlaces(Map $map): void
{
//TODO: relations?
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(Place::getFields());
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
while ($placeData = $result->fetch(IResultSet::FETCH_ASSOC)) {
$place = new Place();
$this->pdm->fillWithData($placeData, $place);
$this->pdm->deleteFromDb($place);
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$this->deletePlace($place);
}
}
private function deleteChallenge(Challenge $challenge): void
{
foreach ($this->userInChallengeRepository->getAllByChallenge($challenge) as $userInChallenge) {
$this->pdm->deleteFromDb($userInChallenge);
}
foreach ($this->guessRepository->getAllInChallenge($challenge, [PlaceInChallenge::class]) as $guess) {
$this->pdm->deleteFromDb($guess);
}
foreach ($this->placeInChallengeRepository->getAllByChallenge($challenge) as $placeInChallenge) {
$this->pdm->deleteFromDb($placeInChallenge);
}
$this->pdm->deleteFromDb($challenge);
}
private function calculateMapBounds(Map $map): Bounds
{
//TODO: from repository or relations
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['lat', 'lng']);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
$bounds = new Bounds();
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$bounds->extend(new Position($place['lat'], $place['lng']));
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$bounds->extend($place->getPosition());
}
return $bounds;
@@ -204,26 +249,19 @@ class MapAdminController implements ISecured
private function &getPlaces(Map $map): array
{
//TODO: from repository or relations
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
$places = [];
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
//$panoId = ???
//$pov = ???
$noPano = $place['pano_id_cached_timestamp'] && $place['pano_id_cached'] === null;
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$noPano = $place->getPanoIdCachedTimestampDate() !== null && $place->getPanoIdCached() === null;
$places[$place['id']] = [
'id' => $place['id'],
'lat' => $place['lat'],
'lng' => $place['lng'],
$placeId = $place->getId();
$places[$placeId] = [
'id' => $placeId,
'lat' => $place->getLat(),
'lng' => $place->getLng(),
'panoId' => null,
'pov' => ['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
'pov' => $place->getPov()->toArray(),
'noPano' => $noPano
];
}
+6 -3
View File
@@ -19,7 +19,7 @@ class MapsController
public function getMaps(): IContent
{
//TODO: from repository
//TODO: from repository - count should be added
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns([
['maps', 'id'],
@@ -46,8 +46,11 @@ class MapsController
}
$user = $this->request->user();
$data = ['maps' => $maps, 'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)];
return new HtmlContent('maps', $data);
return new HtmlContent('maps', [
'maps' => $maps,
'isLoggedIn' => $user !== null,
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
]);
}
private function formatMapAreaForHuman(float $area): array
+197 -13
View File
@@ -1,12 +1,24 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
use MapGuesser\Util\JwtParser;
class UserController implements ISecured
{
@@ -14,10 +26,25 @@ class UserController implements ISecured
private PersistentDataManager $pdm;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private UserInChallengeRepository $userInChallengeRepository;
private GuessRepository $guessRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
$this->guessRepository = new GuessRepository();
}
public function authorize(): bool
@@ -27,38 +54,126 @@ class UserController implements ISecured
return $user !== null;
}
public function getProfile(): IContent
public function getAccount(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
$data = ['user' => $user->toArray()];
return new HtmlContent('profile', $data);
return new HtmlContent('account/account', ['user' => $user->toArray()]);
}
public function saveProfile(): IContent
public function getGoogleAuthenticateRedirect(): IRedirect
{
/**
* @var User $user
*/
$user = $this->request->user();
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => 'password_not_match'];
return new JsonContent($data);
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
$nonce,
$user->getEmail()
);
return new Redirect($url, IRedirect::TEMPORARY);
}
public function authenticateWithGoogle(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
$this->request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
);
if (!isset($tokenData['id_token'])) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
$jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
if ($idToken['sub'] !== $user->getGoogleSub()) {
return new HtmlContent('account/google_authenticate', [
'success' => false,
'errorText' => 'This Google account is not linked to your account.'
]);
}
$authenticatedWithGoogleUntil = new DateTime('+45 seconds');
$this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
return new HtmlContent('account/google_authenticate', [
'success' => true,
'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil
]);
}
public function getDeleteAccount(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
return new HtmlContent('account/delete', ['user' => $user->toArray()]);
}
public function saveAccount(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if (!$this->confirmUserIdentity(
$user,
$this->request->session()->get('authenticated_with_google_until'),
$this->request->post('password'),
$error
)) {
return new JsonContent(['error' => ['errorText' => $error]]);
}
if (strlen($this->request->post('password_new')) > 0) {
if (strlen($this->request->post('password_new')) < 6) {
$data = ['error' => 'password_too_short'];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given new passwords do not match.'
]
]);
}
$user->setPlainPassword($this->request->post('password_new'));
@@ -66,7 +181,76 @@ class UserController implements ISecured
$this->pdm->saveToDb($user);
$data = ['success' => true];
return new JsonContent($data);
$this->request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]);
}
public function deleteAccount(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if (!$this->confirmUserIdentity(
$user,
$this->request->session()->get('authenticated_with_google_until'),
$this->request->post('password'),
$error
)) {
return new JsonContent(['error' => ['errorText' => $error]]);
}
\Container::$dbConnection->startTransaction();
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter);
}
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
$this->pdm->deleteFromDb($userPlayedPlace);
}
foreach ($this->userInChallengeRepository->getAllByUser($user) as $userInChallenge) {
$this->pdm->deleteFromDb($userInChallenge);
}
foreach ($this->guessRepository->getAllByUser($user) as $guess) {
$this->pdm->deleteFromDb($guess);
}
$this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit();
$this->request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]);
}
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, ?string &$error): bool
{
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
return true;
}
if ($password !== null) {
if ($user->checkPassword($password)) {
return true;
}
$error = 'The given current password is wrong.';
return false;
}
$error = 'Could not confirm your identity. Please try again!';
return false;
}
}
+2 -2
View File
@@ -12,11 +12,11 @@ class Connection implements IConnection
public function __construct(string $host, string $user, string $password, string $db, int $port = -1, string $socket = null)
{
if ($port < 0) {
$port = ini_get('mysqli.default_port');
$port = (int) ini_get('mysqli.default_port');
}
if ($socket === null) {
$socket = ini_get('mysqli.default_socket');
$socket = (string) ini_get('mysqli.default_socket');
}
$this->connection = new mysqli($host, $user, $password, $db, $port, $socket);
+1 -1
View File
@@ -135,6 +135,6 @@ class Modify
private function generateKey(): string
{
return substr(hash('sha256', serialize($this->attributes) . random_bytes(10) . microtime()), 0, 7);
return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7);
}
}
+52 -19
View File
@@ -12,6 +12,8 @@ class Select
const CONDITION_HAVING = 1;
const DERIVED_TABLE_KEY = 'DERIVED';
private IConnection $connection;
private string $table;
@@ -30,7 +32,7 @@ class Select
private array $orders = [];
private array $limit;
private ?array $limit;
public function __construct(IConnection $connection, ?string $table = null)
{
@@ -55,6 +57,11 @@ class Select
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;
@@ -144,7 +151,7 @@ class Select
$this->limit = null;
}
public function paginate(int $page, int $itemsPerPage)
public function paginate(int $page, int $itemsPerPage): Select
{
$this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
@@ -194,6 +201,11 @@ class Select
}
}
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];
@@ -211,10 +223,14 @@ class Select
private function generateQuery(): array
{
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $this->generateTable($this->table, true);
list($tableQuery, $tableParams) = $this->generateTable($this->table, true);
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $tableQuery;
if (count($this->joins) > 0) {
$queryString .= ' ' . $this->generateJoins();
list($joinQuery, $joinParams) = $this->generateJoins();
$queryString .= ' ' . $joinQuery;
} else {
$joinParams = [];
}
if (count($this->conditions[self::CONDITION_WHERE]) > 0) {
@@ -245,20 +261,32 @@ class Select
$queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0];
}
return [$queryString, array_merge($whereParams, $havingParams)];
if ($this->isDerivedTable()) {
$queryString = '(' . $queryString . ') AS ' . $this->tableAliases[Select::DERIVED_TABLE_KEY];
}
return [$queryString, array_merge($tableParams, $joinParams, $whereParams, $havingParams)];
}
private function generateTable($table, bool $defineAlias = false): string
private function generateTable($table, bool $defineAlias = false): array
{
$params = [];
if ($table instanceof RawExpression) {
return (string) $table;
return [(string) $table, $params];
}
if ($table instanceof Select)
{
return $table->generateQuery();
}
if (isset($this->tableAliases[$table])) {
return ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
$queryString = ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
return [$queryString, $params];
}
return Utils::backtick($table);
return [Utils::backtick($table), $params];
}
private function generateColumn($column): string
@@ -271,7 +299,8 @@ class Select
$out = '';
if ($column[0]) {
$out .= $this->generateTable($column[0]) . '.';
list($tableName, $params) = $this->generateTable($column[0]);
$out .= $tableName . '.';
}
$out .= Utils::backtick($column[1]);
@@ -297,18 +326,22 @@ class Select
return implode(',', $columns);
}
private function generateJoins(): string
private function generateJoins(): array
{
$joins = $this->joins;
array_walk($joins, function (&$value, $key) {
$value = $value[0] . ' JOIN ' . $this->generateTable($value[1], true) . ' ON ' . $this->generateColumn($value[2]) . ' ' . $value[3] . ' ' . $this->generateColumn($value[4]);
});
$joinQueries = [];
$params = [];
return implode(' ', $joins);
foreach ($this->joins as $join) {
list($joinQueryFragment, $paramsFragment) = $this->generateTable($join[1], true);
$joinQueries[] = $join[0] . ' JOIN ' . $joinQueryFragment . ' ON ' . $this->generateColumn($join[2]) . ' ' . $join[3] . ' ' . $this->generateColumn($join[4]);
$params = array_merge($params, $paramsFragment);
}
return [implode(' ', $joinQueries), $params];
}
private function generateConditions(string $type): array
private function generateConditions(int $type): array
{
$conditions = '';
$params = [];
@@ -376,9 +409,9 @@ class Select
return [$conditionsString, $params];
}
private function generateComplexConditionFragment(string $type, Closure $conditionCallback): array
private function generateComplexConditionFragment(int $type, Closure $conditionCallback): array
{
$instance = new static($this->connection, $this->table);
$instance = new self($this->connection, $this->table);
$instance->tableAliases = $this->tableAliases;
$conditionCallback($instance);
+2 -1
View File
@@ -1,7 +1,8 @@
<?php namespace MapGuesser\Database;
class Utils {
public static function backtick(string $name) {
public static function backtick(string $name): string
{
return '`' . $name . '`';
}
}
+22 -13
View File
@@ -1,11 +1,10 @@
<?php namespace MapGuesser\Http;
class Request
use MapGuesser\Interfaces\Http\IRequest;
use MapGuesser\Interfaces\Http\IResponse;
class Request implements IRequest
{
const HTTP_GET = 0;
const HTTP_POST = 1;
private string $url;
private int $method;
@@ -14,13 +13,23 @@ class Request
private array $headers = [];
public function __construct(string $url, int $method = self::HTTP_GET)
public function __construct(string $url = '', int $method = self::HTTP_GET)
{
$this->url = $url;
$this->method = $method;
}
public function setQuery($query)
public function setUrl(string $url): void
{
$this->url = $url;
}
public function setMethod(int $method): void
{
$this->method = $method;
}
public function setQuery($query): void
{
if (is_string($query)) {
$this->query = $query;
@@ -29,22 +38,22 @@ class Request
}
}
public function setHeaders(array $headers)
public function setHeaders(array $headers): void
{
$this->headers = array_merge($this->headers, $headers);
}
public function send(): Response
public function send(): IResponse
{
$ch = curl_init();
if ($this->method === self::HTTP_GET) {
$url = $this->url . '?' . $this->query;
} elseif ($this->method === self::HTTP_POST) {
$url = $this->url;
if ($this->method === self::HTTP_POST) {
$url = $this->url;
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query);
} else {
$url = $this->url . '?' . $this->query;
}
curl_setopt($ch, CURLOPT_URL, $url);
+5 -3
View File
@@ -1,6 +1,8 @@
<?php namespace MapGuesser\Http;
class Response
use MapGuesser\Interfaces\Http\IResponse;
class Response implements IResponse
{
private string $body;
@@ -12,12 +14,12 @@ class Response
$this->headers = $headers;
}
public function getBody()
public function getBody(): string
{
return $this->body;
}
public function getHeaders()
public function getHeaders(): array
{
return $this->headers;
}
+18
View File
@@ -0,0 +1,18 @@
<?php namespace MapGuesser\Interfaces\Http;
interface IRequest
{
const HTTP_GET = 0;
const HTTP_POST = 1;
public function setUrl(string $url): void;
public function setMethod(int $method): void;
public function setQuery($query): void;
public function setHeaders(array $headers): void;
public function send(): IResponse;
}
+8
View File
@@ -0,0 +1,8 @@
<?php namespace MapGuesser\Interfaces\Http;
interface IResponse
{
public function getBody(): string;
public function getHeaders(): array;
}
+1 -1
View File
@@ -4,7 +4,7 @@ use MapGuesser\Interfaces\Authentication\IUser;
interface IRequest
{
public function setParsedRouteParams(array &$routeParams);
public function setParsedRouteParams(array &$routeParams): void;
public function getBase(): string;
+4 -2
View File
@@ -2,9 +2,11 @@
interface IContent
{
public function &getData(): array;
public function setData(array $data): void;
public function &render(): string;
public function getData(): array;
public function render(): void;
public function getContentType(): string;
}
@@ -0,0 +1,9 @@
<?php namespace MapGuesser\Interfaces\Session;
use SessionHandlerInterface;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
interface ISessionHandler extends SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
{
}
+3 -3
View File
@@ -65,8 +65,8 @@ class Mail
$mailer->Mailer = 'mail';
}
$mailer->setFrom($_ENV['MAIL_FROM'], 'MapGuesser');
$mailer->addReplyTo($_ENV['MAIL_FROM'], 'MapGuesser');
$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;
@@ -77,7 +77,7 @@ class Mail
}
}
private function sendMail(PHPMailer $mailer, array $recipient)
private function sendMail(PHPMailer $mailer, array $recipient): void
{
$mailer->clearAddresses();
$mailer->addAddress($recipient[0], $recipient[1]);
+22
View File
@@ -0,0 +1,22 @@
<?php namespace MapGuesser\Multi;
class MultiConnector
{
public function sendMessage(string $func, array $args = [])
{
$message = json_encode([
'func' => $func,
'args' => $args
]);
$connection = fsockopen($_ENV['MULTI_INTERNAL_HOST'], $_ENV['MULTI_INTERNAL_PORT']);
fwrite($connection, $message);
$response = '';
while (!feof($connection)) {
$response .= fgets($connection);
}
fclose($connection);
return json_decode($response, true);
}
}
+24 -9
View File
@@ -1,14 +1,21 @@
<?php namespace MapGuesser\OAuth;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Http\IRequest;
class GoogleOAuth
{
private static $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
private static string $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
private static $tokenUrlBase = 'https://oauth2.googleapis.com/token';
private static string $tokenUrlBase = 'https://oauth2.googleapis.com/token';
public function getDialogUrl(string $state, string $redirectUrl): string
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getDialogUrl(string $state, string $redirectUrl, ?string $nonce = null, ?string $loginHint = null): string
{
$oauthParams = [
'response_type' => 'code',
@@ -16,13 +23,20 @@ class GoogleOAuth
'scope' => 'openid email',
'redirect_uri' => $redirectUrl,
'state' => $state,
'nonce' => hash('sha256', random_bytes(10) . microtime()),
];
if ($nonce !== null) {
$oauthParams['nonce'] = $nonce;
}
if ($loginHint !== null) {
$oauthParams['login_hint'] = $loginHint;
}
return self::$dialogUrlBase . '?' . http_build_query($oauthParams);
}
public function getToken(string $code, string $redirectUrl)
public function getToken(string $code, string $redirectUrl): array
{
$tokenParams = [
'code' => $code,
@@ -32,9 +46,10 @@ class GoogleOAuth
'grant_type' => 'authorization_code',
];
$request = new Request(self::$tokenUrlBase, Request::HTTP_POST);
$request->setQuery($tokenParams);
$response = $request->send();
$this->request->setUrl(self::$tokenUrlBase);
$this->request->setMethod(IRequest::HTTP_POST);
$this->request->setQuery($tokenParams);
$response = $this->request->send();
return json_decode($response->getBody(), true);
}
+112
View File
@@ -0,0 +1,112 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
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');
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php namespace MapGuesser\PersistentData\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;
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class MultiRoom extends Model
{
protected static string $table = 'multi_rooms';
protected static array $fields = ['room_id', 'state', 'members', 'updated'];
private string $roomId = '';
private array $state = [];
private array $members = [];
private DateTime $updated;
public function setRoomId(string $roomId): void
{
$this->roomId = $roomId;
}
public function setStateArray(array $state): void
{
$this->state = $state;
}
public function setMembersArray(array $members): void
{
$this->members = $members;
}
public function setState(string $state): void
{
$this->state = unserialize($state);
}
public function setMembers(string $members): void
{
$this->members = unserialize($members);
}
public function setUpdatedDate(DateTime $updated): void
{
$this->updated = $updated;
}
public function setUpdated(string $updated): void
{
$this->updated = new DateTime($updated);
}
public function getRoomId(): string
{
return $this->roomId;
}
public function getStateArray(): array
{
return $this->state;
}
public function getState(): string
{
return serialize($this->state);
}
public function getMembersArray(): array
{
return $this->members;
}
public function getMembers(): string
{
return serialize($this->members);
}
public function getUpdatedDate(): DateTime
{
return $this->updated;
}
public function getUpdated(): string
{
return $this->updated->format('Y-m-d H:i:s');
}
}
+48 -10
View File
@@ -5,12 +5,13 @@ use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov;
class Place extends Model
{
protected static string $table = 'places';
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp'];
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp', 'pov_heading', 'pov_pitch', 'pov_zoom'];
protected static array $relations = ['map' => Map::class];
@@ -24,9 +25,12 @@ class Place extends Model
private ?DateTime $panoIdCachedTimestamp = null;
private Pov $pov;
public function __construct()
{
$this->position = new Position(0.0, 0.0);
$this->pov = new Pov(0.0, 0.0, 0.0);
}
public function setMap(Map $map): void
@@ -54,6 +58,26 @@ class Place extends Model
$this->position->setLng($lng);
}
public function setPov(Pov $pov): void
{
$this->pov = $pov;
}
public function setPovHeading(float $heading): void
{
$this->pov->setHeading($heading);
}
public function setPovPitch(float $pitch): void
{
$this->pov->setPitch($pitch);
}
public function setPovZoom(float $zoom): void
{
$this->pov->setZoom($zoom);
}
public function setPanoIdCached(?string $panoIdCached): void
{
$this->panoIdCached = $panoIdCached;
@@ -96,7 +120,27 @@ class Place extends Model
return $this->position->getLng();
}
public function getFreshPanoId(bool $canBeIndoor = false): ?string
public function getPov(): Pov
{
return $this->pov;
}
public function getPovHeading(): float
{
return $this->pov->getHeading();
}
public function getPovPitch(): float
{
return $this->pov->getPitch();
}
public function getPovZoom(): float
{
return $this->pov->getZoom();
}
public function getFreshPanoId(): ?string
{
if (
$this->panoIdCachedTimestamp !== null &&
@@ -108,21 +152,15 @@ class Place extends Model
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
$request->setQuery([
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
'location' => $this->position->getLat() . ',' . $this->position->getLng(),
'source' => $canBeIndoor ? 'default' : 'outdoor'
'location' => $this->position->getLat() . ',' . $this->position->getLng()
]);
$response = $request->send();
$panoData = json_decode($response->getBody(), true);
$panoId = $panoData['status'] === 'OK' ? $panoData['pano_id'] : null;
// enable indoor panos if no outdoor found
if ($panoId === null && !$canBeIndoor) {
return $this->getFreshPanoId(true);
}
$this->panoIdCached = $panoId;
$this->panoIdCachedTimestamp = new DateTime('now');
$this->panoIdCachedTimestamp = new DateTime();
(new PersistentDataManager())->saveToDb($this);
@@ -0,0 +1,70 @@
<?php namespace MapGuesser\PersistentData\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;
}
}
+32 -5
View File
@@ -1,12 +1,13 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
use MapGuesser\Interfaces\Authentication\IUser;
class User extends Model implements IUser
{
protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub'];
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub', 'created'];
private static array $types = ['user', 'admin'];
@@ -20,6 +21,8 @@ class User extends Model implements IUser
private ?string $googleSub = null;
private DateTime $created;
public function setEmail(string $email): void
{
$this->email = $email;
@@ -42,9 +45,9 @@ class User extends Model implements IUser
}
}
public function setActive($active): void
public function setActive(bool $active): void
{
$this->active = (bool) $active;
$this->active = $active;
}
public function setGoogleSub(?string $googleSub): void
@@ -52,6 +55,16 @@ class User extends Model implements IUser
$this->googleSub = $googleSub;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getEmail(): string
{
return $this->email;
@@ -77,15 +90,25 @@ class User extends Model implements IUser
return $this->googleSub;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
public function hasPermission(int $permission): bool
{
switch ($permission) {
case IUser::PERMISSION_NORMAL:
return true;
break;
case IUser::PERMISSION_ADMIN:
return $this->type === 'admin';
break;
default:
throw new \Exception('Permission does not exist: ' . $permission);
}
}
@@ -101,6 +124,10 @@ class User extends Model implements IUser
public function checkPassword(string $password): bool
{
if ($this->password === null) {
return false;
}
return password_verify($password, $this->password);
}
}
+25 -1
View File
@@ -1,10 +1,12 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserConfirmation extends Model
{
protected static string $table = 'user_confirmations';
protected static array $fields = ['user_id', 'token'];
protected static array $fields = ['user_id', 'token', 'last_sent'];
protected static array $relations = ['user' => User::class];
@@ -14,6 +16,8 @@ class UserConfirmation extends Model
private string $token = '';
private DateTime $lastSent;
public function setUser(User $user): void
{
$this->user = $user;
@@ -29,6 +33,16 @@ class UserConfirmation extends Model
$this->token = $token;
}
public function setLastSentDate(DateTime $lastSent): void
{
$this->lastSent = $lastSent;
}
public function setLastSent(string $lastSent): void
{
$this->lastSent = new DateTime($lastSent);
}
public function getUser(): ?User
{
return $this->user;
@@ -43,4 +57,14 @@ class UserConfirmation extends Model
{
return $this->token;
}
public function getLastSentDate(): DateTime
{
return $this->lastSent;
}
public function getLastSent(): string
{
return $this->lastSent->format('Y-m-d H:i:s');
}
}
@@ -0,0 +1,96 @@
<?php namespace MapGuesser\PersistentData\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;
}
}
@@ -0,0 +1,70 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserPasswordResetter extends Model
{
protected static string $table = 'user_password_resetters';
protected static array $fields = ['user_id', 'token', 'expires'];
protected static array $relations = ['user' => User::class];
private ?User $user = null;
private ?int $userId = null;
private string $token = '';
private DateTime $expires;
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setToken(string $token): void
{
$this->token = $token;
}
public function setExpiresDate(DateTime $expires): void
{
$this->expires = $expires;
}
public function setExpires(string $expires): void
{
$this->expires = new DateTime($expires);
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getToken(): string
{
return $this->token;
}
public function getExpiresDate(): DateTime
{
return $this->expires;
}
public function getExpires(): string
{
return $this->expires->format('Y-m-d H:i:s');
}
}
@@ -0,0 +1,99 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserPlayedPlace extends Model
{
protected static string $table = 'user_played_place';
protected static array $fields = ['user_id', 'place_id', 'last_time', 'occurrences'];
protected static array $relations = ['user' => User::class, 'place' => Place::class];
private ?User $user = null;
private ?int $userId = null;
private ?Place $place = null;
private ?int $placeId = null;
private DateTime $lastTime;
private int $occurrences = 1;
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setPlace(Place $place): void
{
$this->place = $place;
}
public function setPlaceId(int $placeId): void
{
$this->placeId = $placeId;
}
public function setLastTimeDate(DateTime $lastTime): void
{
$this->lastTime = $lastTime;
}
public function setLastTime(string $lastTime): void
{
$this->lastTime = new DateTime($lastTime);
}
public function setOccurrences(int $occurrences): void
{
$this->occurrences = $occurrences;
}
public function incrementOccurrences(): void
{
$this->occurrences++;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getPlace(): ?Place
{
return $this->place;
}
public function getPlaceId(): ?int
{
return $this->placeId;
}
public function getLastTimeDate(): DateTime
{
return $this->lastTime;
}
public function getLastTime(): string
{
return $this->lastTime->format('Y-m-d H:i:s');
}
public function getOccurrences(): int
{
return $this->occurrences;
}
}
+100 -71
View File
@@ -1,5 +1,6 @@
<?php namespace MapGuesser\PersistentData;
use Generator;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
@@ -7,32 +8,10 @@ use MapGuesser\PersistentData\Model\Model;
class PersistentDataManager
{
public function selectFromDb(Select $select, string $type, bool $withRelations = false): ?Model
public function selectFromDb(Select $select, string $type, bool $useRelations = false, array $withRelations = [])
{
$table = call_user_func([$type, 'getTable']);
$fields = call_user_func([$type, 'getFields']);
$select = $this->createSelect($select, $type, $useRelations, $withRelations);
$select->from($table);
//TODO: only with some relations?
if ($withRelations) {
$relations = call_user_func([$type, 'getRelations']);
$columns = [];
foreach ($fields as $field) {
$columns[] = [$table, $field];
}
$columns = array_merge($columns, $this->getRelationColumns($relations));
$this->leftJoinRelations($select, $table, $relations);
$select->columns($columns);
} else {
$select->columns($fields);
}
//TODO: return with array?
$data = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($data === null) {
@@ -40,37 +19,75 @@ class PersistentDataManager
}
$model = new $type();
$this->fillWithData($data, $model);
$this->fillWithData($data, $model, $withRelations);
return $model;
}
public function selectFromDbById($id, string $type, bool $withRelations = false): ?Model
public function selectMultipleFromDb(Select $select, string $type, bool $useRelations = false, array $withRelations = []): Generator
{
$select = $this->createSelect($select, $type, $useRelations, $withRelations);
$result = $select->execute();
while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) {
$model = new $type();
$this->fillWithData($data, $model, $withRelations);
yield $model;
}
}
public function selectFromDbById($id, string $type, bool $useRelations = false)
{
$select = new Select(\Container::$dbConnection);
$select->whereId($id);
return $this->selectFromDb($select, $type, $withRelations);
return $this->selectFromDb($select, $type, $useRelations);
}
public function fillWithData(array $data, Model $model): void
public function fillWithData(array &$data, Model $model, array $withRelations = [], ?string $modelKey = null): 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);
}
if (count($withRelations)) {
$relations = array_intersect($relations, $withRelations);
}
$this->setRelations($model, $relationData);
while (key($data)) {
$key = key($data);
$value = current($data);
$relation = key($relations);
if (strpos($key, '__') == false) {
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($model, $method) && isset($value)) {
$model->$method($value);
}
next($data);
} else if (isset($modelKey) && substr($key, 0, strlen($modelKey . '__')) === $modelKey . '__') {
$key = substr($key, strlen($modelKey) + 2);
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($model, $method) && isset($value)) {
$model->$method($value);
}
next($data);
} else if (substr($key, 0, strlen($relation . '__')) === $relation . '__') {
$relationType = current($relations);
$relationModel = new $relationType();
$this->fillWithData($data, $relationModel, $withRelations, $relation);
$method = 'set' . str_replace('_', '', ucwords($relation, '_'));
$model->$method($relationModel);
next($relations);
} else {
return;
}
}
$model->saveSnapshot();
}
@@ -136,7 +153,37 @@ class PersistentDataManager
$model->resetSnapshot();
}
private function getRelationColumns(array $relations): array
private function createSelect(Select $select, string $type, bool $useRelations = false, array $withRelations = []): Select
{
$table = call_user_func([$type, 'getTable']);
$fields = call_user_func([$type, 'getFields']);
$columns = [];
foreach ($fields as $field) {
$columns[] = [$table, $field];
}
$select->from($table);
if ($useRelations) {
$relations = call_user_func([$type, 'getRelations']);
if (count($withRelations)) {
$relations = array_intersect($relations, $withRelations);
}
$columns = array_merge($columns, $this->getRelationColumns($relations, $withRelations));
$this->leftJoinRelations($select, $table, $relations, $withRelations);
$select->columns($columns);
} else {
$select->columns($columns);
}
return $select;
}
private function getRelationColumns(array $relations, array $withRelations): array
{
$columns = [];
@@ -145,46 +192,28 @@ class PersistentDataManager
foreach (call_user_func([$relationType, 'getFields']) as $relationField) {
$columns[] = [$relationTable, $relationField, $relation . '__' . $relationField];
}
$nextOrderRelations = call_user_func([$relationType, 'getRelations']);
if (count($withRelations)) {
$nextOrderRelations = array_intersect($nextOrderRelations, $withRelations);
}
$columns = array_merge($columns, $this->getRelationColumns($nextOrderRelations, $withRelations));
}
return $columns;
}
private function leftJoinRelations(Select $select, string $table, array $relations): void
private function leftJoinRelations(Select $select, string $table, array $relations, array $withRelations): 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);
$nextOrderRelations = call_user_func([$relationType, 'getRelations']);
if (count($withRelations)) {
$nextOrderRelations = array_intersect($nextOrderRelations, $withRelations);
}
$this->leftJoinRelations($select, $relationTable, $nextOrderRelations, $withRelations);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
class ChallengeRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $challengeId): ?Challenge
{
return $this->pdm->selectFromDbById($challengeId, Challenge::class);
}
public function getByToken(int $token): ?Challenge
{
$select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token);
return $this->pdm->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_id', '=', $user->getId());
yield from $this->pdm->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_id', '=', $user->getId());
$select->where('is_owner', '=', true);
yield from $this->pdm->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_id', '=', $place->getId());
yield from $this->pdm->selectMultipleFromDb($select, Challenge::class);
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Guess;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\PersistentDataManager;
class GuessRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getAllByUser(User $user): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
yield from $this->pdm->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('challenge_id', '=', $challenge->getId());
yield from $this->pdm->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('challenge_id', '=', $challenge->getId());
$select->where('place_id', '=', $place->getId());
return $this->pdm->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('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
yield from $this->pdm->selectMultipleFromDb($select, Guess::class);
}
public function getAllInChallenge(Challenge $challenge, array $withRelations = []): Generator
{
if (count($withRelations)) {
$necessaryRelations = [PlaceInChallenge::class];
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
}
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
$select->orderBy('round');
yield from $this->pdm->selectMultipleFromDb($select, Guess::class, true, $withRelations);
}
public function getAllInChallengeByRound(int $round, Challenge $challenge, array $withRelations = []): Generator
{
if (count($withRelations)) {
$necessaryRelations = [PlaceInChallenge::class];
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
}
$select = new Select(\Container::$dbConnection);
$select->where('challenge_id', '=', $challenge->getId());
$select->where('round', '=', $round);
yield from $this->pdm->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_id', '=', $place->getId());
yield from $this->pdm->selectMultipleFromDb($select, Guess::class);
}
}
+19
View File
@@ -1,6 +1,9 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
class MapRepository
@@ -16,4 +19,20 @@ class MapRepository
{
return $this->pdm->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('challenge_id', '=', $challenge->getId());
$select->limit(1);
return $this->pdm->selectFromDb($select, Map::class);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php namespace MapGuesser\Repository;
use DateTime;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\PersistentDataManager;
class MultiRoomRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $id): ?MultiRoom
{
return $this->pdm->selectFromDbById($id, MultiRoom::class);
}
public function getByRoomId(string $roomId): ?MultiRoom
{
$select = new Select(\Container::$dbConnection);
$select->where('room_id', '=', $roomId);
return $this->pdm->selectFromDb($select, MultiRoom::class);
}
public function getAllExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('updated', '<', (new DateTime('-7 day'))->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, MultiRoom::class);
}
}
@@ -0,0 +1,54 @@
<?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\PersistentDataManager;
class PlaceInChallengeRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getAllByPlace(Place $place, array $withRelations = []) : Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('place_id', '=', $place->getId());
yield from $this->pdm->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 $this->pdm->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 $this->pdm->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 $this->pdm->selectFromDb($select, PlaceInChallenge::class, true, $withRelations);
}
}
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff. Show more