Compare commits

...
Author SHA1 Message Date
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
bence e751fdaa3e Merged in feature/MAPG-165-make-version-number-more-nice (pull request #133)
MAPG-165 don't write 'Release_' into footer
2020-06-22 19:17:38 +00:00
bence 675d7e60d0 MAPG-165 don't write 'Release_' into footer 2020-06-22 21:17:02 +02:00
bence 0d097aacb5 Merged in feature/MAPG-164-make-possible-to-rename-application (pull request #132)
MAPG-164 show application's name from env variable
2020-06-22 19:07:39 +00:00
bence 98cd15e91f MAPG-164 show application's name from env variable 2020-06-22 21:06:15 +02:00
bence 4f680db8f3 Merged in bugfix/MAPG-163-fix-modal-when-height-is-low (pull request #131)
MAPG-163 optimize modal height
2020-06-21 21:09:08 +00:00
bence ea98af0681 MAPG-163 optimize modal sizes 2020-06-21 23:07:47 +02:00
bence 05934d6245 Merged in feature/MAPG-87-create-footer-with-author-copyright (pull request #130)
Feature/MAPG-87 create footer with author copyright
2020-06-21 20:27:40 +00:00
bence 3ab8ac5fe6 MAPG-87 use HTML5 semantic elements, new styles, new footer 2020-06-21 22:25:29 +02:00
bence 94e60e4ac5 MAPG-87 unify some styles 2020-06-21 22:20:04 +02:00
bence 9df5028c20 MAPG-87 introduce footer 2020-06-21 22:18:35 +02:00
bence 49bd03f303 Merged in feature/MAPG-162-add-script-that-can-run-sql-on-db (pull request #129)
MAPG-162 add script that can run external SQLs on the DB
2020-06-21 17:45:23 +00:00
bence 7210e12656 MAPG-162 add script that can run external SQLs on the DB 2020-06-21 19:44:02 +02:00
bence 8320b94210 Merged in feature/MAPG-161-make-static-maps-look-good-on-mobile (pull request #128)
MAPG-161 load static maps with JS and set scale based on devicePixelRatio
2020-06-21 17:37:35 +00:00
bence 28aa762153 MAPG-161 load static maps with JS and set scale based on devicePixelRatio 2020-06-21 19:32:30 +02:00
bence 33eb154112 Merged in feature/MAPG-160-replace-space-with-margin (pull request #127)
MAPG-160 replace space with margin-right next to inline images
2020-06-21 15:37:14 +00:00
bence a2ca7888f4 MAPG-160 replace space with margin-right next to inline images 2020-06-21 17:36:08 +02:00
bence e0395132d6 Merged in feature/MAPG-159-update-docker-composer-to-mariadb-103 (pull request #126)
MAPG-159 update MariaDB to 10.3 in docker-compose.yml
2020-06-21 14:07:49 +00:00
bence aeaf0fe8d3 MAPG-159 update MariaDB to 10.3 in docker-compose.yml 2020-06-21 16:05:14 +02:00
bence d96ac938f8 Merged in bugfix/MAPG-69-dont-resend-welcome-email (pull request #125)
MAPG-69 welcome email shouldn't be sent to existing user
2020-06-21 14:03:28 +00:00
bence cb5229a5e8 MAPG-69 welcome email shouldn't be sent to existing user 2020-06-21 16:01:46 +02:00
bence 5a4fe48a9e Merged in bugfix/MAPG-69-add-env-example (pull request #124)
MAPG-69 add Google OAuth environment variables to .env.example
2020-06-21 14:01:12 +00:00
bence 02d6298d95 MAPG-69 add Google OAuth environment variables to .env.example 2020-06-21 15:51:52 +02:00
bence a56ce849c0 Merged in feature/MAPG-69-implement-google-registration-login (pull request #123)
Feature/MAPG-69 implement google registration login
2020-06-21 13:48:28 +00:00
bence 87d476065d MAPG-69 fix routing to handle emptry string properly 2020-06-21 15:44:40 +02:00
bence df60efbb8c MAPG-69 group routes to better overview 2020-06-21 15:43:49 +02:00
bence 6338e35cfb MAPG-69 ability to cancel Google signup 2020-06-21 15:29:02 +02:00
bence 1cfbef418e MAPG-69 add missing return types in LoginController 2020-06-21 15:19:36 +02:00
bence a8777b897b MAPG-69 ability to reset signup filled after login attempt 2020-06-21 15:12:45 +02:00
bence 285f2dd0ac MAPG-69 login user when trying to sign up 2020-06-21 15:12:45 +02:00
bence 10b7766458 MAPG-69 redirect to sign up when user not found during login 2020-06-21 15:12:45 +02:00
bence 9697163457 MAPG-69 merge SignupController to LoginController and introduce Google login/sign up 2020-06-21 15:12:45 +02:00
bence d1c9e221f7 MAPG-69 add Google OAuth handler 2020-06-21 15:12:44 +02:00
bence 2d2e218002 MAPG-69 add simple JWT parser 2020-06-21 15:12:44 +02:00
bence 7ba11f34cc MAPG-69 add new field to User 2020-06-21 15:12:44 +02:00
bence 99c72d99be MAPG-69 make Redirect able to redirect to external sources 2020-06-21 15:12:44 +02:00
bence 295570a28d MAPG-69 fix missing relations in UserConfirmation 2020-06-21 01:21:42 +02:00
bence e612d0d93f Merged in feature/MAPG-155-adjust-minzoom-and-maxzoom-to-z (pull request #122)
MAPG-155 minZoom and maxZoom should depend on zoomOffset
2020-06-20 00:15:18 +00:00
bence a2d2b94cc6 MAPG-155 minZoom and maxZoom should depend on zoomOffset 2020-06-20 02:14:17 +02:00
bence 80b850fdd7 Merged in feature/MAPG-140-solve-persistent-model-anomalies (pull request #121)
Feature/MAPG-140 solve persistent model anomalies
2020-06-20 00:10:48 +00:00
bence 0bf6e900fe MAPG-140 add UserConfirmation model and repository and adapt SignupController 2020-06-20 02:03:11 +02:00
bence bd46809d3a MAPG-140 adapt PlaceRepository and classes that use it to use persistent model 2020-06-20 02:03:11 +02:00
bence 821a9d80c0 MAPG-140 adapt MapRepository and classes that use it to use persistent model 2020-06-20 02:03:11 +02:00
bence eafa98571e MAPG-140 add Map persistent model 2020-06-20 02:03:11 +02:00
bence 316e39f2f2 MAPG-140 add Place persistent model 2020-06-20 02:03:11 +02:00
bence 5ff720e3c6 MAPG-140 MAPG-154 store user in session based on id 2020-06-20 02:03:11 +02:00
bence a9eec05288 MAPG-140 refactor user handling to use the PersistentDataManager 2020-06-20 02:03:11 +02:00
bence d6750777c2 MAPG-140 add PersistentDataManager and refactor of models 2020-06-20 01:48:20 +02:00
bence 1a4d928143 Merged in bugfix/MAPG-149-fix-cover (pull request #120)
MAPG-149 fix z-indexes
2020-06-18 14:38:03 +00:00
bence b35668c2fb MAPG-149 fix z-indexes 2020-06-18 16:37:32 +02:00
bence 5814620b4f Merged in bugfix/MAPG-149-fix-new-client-side-bugs (pull request #119)
Bugfix/MAPG-149 fix new client side bugs
2020-06-18 14:28:44 +00:00
bence 64b6197bd4 MAPG-149 omit focusing/selecting input elements after processing 2020-06-18 16:28:01 +02:00
bence 1be51b3e07 MAPG-149 make current password the first input element 2020-06-18 16:23:30 +02:00
bence 43775c8f4f Merged in bugfix/MAPG-148-fix-xmlhttp-responsetype-error (pull request #118)
MAPG-148 set xhr responseType after xhr open() because it is accepted by all browsers
2020-06-18 14:21:30 +00:00
bence 95eb956403 Merged in bugfix/MAPG-152-save-user-to-session-after-modi (pull request #117)
MAPG-152 save modified user to session
2020-06-18 14:21:23 +00:00
bence ce96e15911 MAPG-148 set xhr responseType after xhr open() because it is accepted by all browsers 2020-06-18 16:20:45 +02:00
bence 3726573f4c MAPG-152 save modified user to session 2020-06-18 16:18:41 +02:00
bence a8913b0d44 Merged in bugfix/MAPG-151-check-email-address-validity-on (pull request #116)
Bugfix/MAPG-151 check email address validity on
2020-06-18 14:16:32 +00:00
bence e014bf9dc1 MAPG-151 add some client side input checks 2020-06-18 16:15:03 +02:00
bence 50c7e3972c MAPG-151 check email address validity on server side 2020-06-18 16:12:40 +02:00
75 changed files with 3833 additions and 762 deletions

No files matched your search

+4
View File
@@ -1,3 +1,4 @@
APP_NAME=MapQuiz
DEV=1
DB_HOST=mariadb
DB_USER=mapguesser
@@ -11,3 +12,6 @@ STATIC_ROOT=/static
MAIL_FROM=mapguesser@mapguesser-dev.ch
MAIL_HOST=mail
MAIL_PORT=2500
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
GOOGLE_ANALITICS_ID=your_google_analytics_id
+1
View File
@@ -1,3 +1,4 @@
.env
installed
unit_test_results.xml
vendor
+15
View File
@@ -0,0 +1,15 @@
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
+3 -1
View File
@@ -8,7 +8,9 @@
"symfony/console": "^5.1",
"phpmailer/phpmailer": "^6.1"
},
"require-dev": {},
"require-dev": {
"phpunit/phpunit": "^9"
},
"autoload": {
"psr-4": {
"MapGuesser\\": "src"
Generated
+1701 -2
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,8 @@
ALTER TABLE
`users`
ADD
`google_sub` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NULL DEFAULT NULL,
ADD
UNIQUE `google_sub` (`google_sub`),
MODIFY
`password` varchar(60) NULL DEFAULT NULL;
+1 -1
View File
@@ -10,7 +10,7 @@ services:
- 'mariadb'
- 'mail'
mariadb:
image: mariadb:10.1
image: mariadb:10.3
volumes:
- mysql:/var/lib/mysql
environment:
+2 -2
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
+9
View File
@@ -0,0 +1,9 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this Google account ({{EMAIL}}).
<br><br>
Have fun on {{APP_NAME}}!
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
+5 -4
View File
@@ -1,13 +1,14 @@
Hi,
<br><br>
You recently signed up on MapGuesser with this email address ({{EMAIL}}). To activate your account, please click on the following link:<br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}). To activate your account, please click on the following link:<br>
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
<br><br>
If you did not sign up on MapGuesser or changed your mind, no further action is required, your email address will be deleted soon.<br>
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>
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
<br><br>
Have fun on MapGuesser!
Have fun on {{APP_NAME}}!
<br><br>
Regards,<br>
MapGuesser
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
+2 -2
View File
@@ -9,7 +9,7 @@ if (($pos = strpos($url, '?')) !== false) {
}
$url = rawurldecode($url);
$match = Container::$routeCollection->match($method, explode('/', $url));
$match = Container::$routeCollection->match($method, $url == '' ? [] : explode('/', $url));
if ($match !== null) {
list($route, $params) = $match;
@@ -40,7 +40,7 @@ if ($match !== null) {
return;
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
header('Location: ' . Container::$request->getBase() . '/' . $response->getUrl(), true, $response->getHttpCode());
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
return;
}
+11 -8
View File
@@ -1,12 +1,15 @@
#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;
@@ -111,7 +114,7 @@
z-index: 2;
}
#guess {
top: 50px;
top: 10px;
left: 20px;
opacity: 0.95;
visibility: hidden;
@@ -157,7 +160,7 @@
#guess.result {
width: initial;
height: initial;
top: 50px;
top: 10px;
left: 50px;
right: 50px;
bottom: 50px;
@@ -167,11 +170,11 @@
}
@media screen and (max-height: 424px) {
#guess {
top: 50px;
top: 10px;
height: initial;
}
#guess.adapt:hover {
top: 50px;
top: 10px;
height: initial;
}
#guess.result {
+18 -9
View File
@@ -1,9 +1,13 @@
#map {
width: 100%;
height: calc(100% - 40px);
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
/* modify the cursor for the Leaflet map */
.leaflet-container {
cursor: crosshair;
}
@@ -16,6 +20,8 @@
#noPano {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 2;
visibility: hidden;
@@ -23,12 +29,12 @@
}
#noPano>p {
margin: auto;
text-align: center;
}
#control {
position: absolute;
top: 50px;
top: 10px;
right: 10px;
width: 125px;
z-index: 3;
@@ -48,31 +54,34 @@
@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);
}
}
@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);
+100 -16
View File
@@ -42,7 +42,7 @@ h1>a:hover, h1>a:focus {
text-decoration: none;
}
h2, div.header.small h1 {
h2, header.small h1 {
font-size: 24px;
}
@@ -83,8 +83,8 @@ hr {
font-weight: 500;
}
.small {
font-size: 12px;
p.small, span.small {
font-size: 14px;
}
.justify {
@@ -107,6 +107,10 @@ hr {
margin-right: 10px;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
@@ -115,6 +119,7 @@ svg.inline, img.inline {
display: inline;
width: 1em;
height: 1em;
margin-right: 0.3em;
vertical-align: -0.15em;
}
@@ -143,6 +148,13 @@ button, a.button {
line-height: 35px;
}
button.small {
font-size: 14px;
padding: 0 12px;
height: 32px;
line-height: 32px;
}
button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus {
background-color: #29457f;
outline: none;
@@ -246,12 +258,11 @@ input.big:focus, select.big:focus, textarea.big:focus {
div.modal {
position: fixed;
top: 100px;
background-color: #ffffff;
border-radius: 3px;
padding: 20px;
box-sizing: border-box;
z-index: 3;
overflow-y: auto;
z-index: 6;
visibility: hidden;
}
@@ -263,7 +274,7 @@ div.modal {
right: 0;
background-color: #000000;
opacity: 0.5;
z-index: 2;
z-index: 5;
visibility: hidden;
}
@@ -276,7 +287,7 @@ p.formError {
display: none;
}
div.header {
header {
display: grid;
grid-template-columns: auto auto;
background-color: #333333;
@@ -286,33 +297,50 @@ div.header {
color: white;
}
div.header.small {
header.small {
height: 40px;
line-height: 40px;
}
div.header>p.header {
header>p {
line-height: inherit;
text-align: right;
}
div.header>p.header>span {
header>p>span {
padding-left: 6px;
}
div.header>p.header>span>a:link, div.header>p.header>span>a:visited {
header>p>span>a:link, header>p>span>a:visited {
color: inherit;
}
div.header>p.header>span:not(:last-child) {
header>p>span:not(:last-child) {
border-right: solid white 1px;
padding-right: 6px;
}
div.main {
main {
padding: 6px 12px;
}
div.full {
position: relative;
width: 100%;
height: calc(100% - 40px);
}
footer {
background-color: #444444;
padding: 6px 12px;
color: white;
text-align: center;
}
footer>p {
font-size: 13px;
}
div.buttonContainer {
height: 35px;
}
@@ -321,6 +349,12 @@ div.buttonContainer>button {
margin: 0 auto;
}
#cookiesNotice {
background-color: #eeeeee;
padding: 10px;
text-align: center;
}
#loading {
position: fixed;
width: 64px;
@@ -329,7 +363,7 @@ div.buttonContainer>button {
left: 50%;
margin-top: -32px;
margin-left: -32px;
z-index: 5;
z-index: 7;
visibility: hidden;
}
@@ -343,16 +377,27 @@ div.box {
}
@media screen and (max-width: 599px) {
div.header h1 span {
header h1 span {
display: none;
}
footer>p:not(:first-child) {
margin-top: 4px;
}
button, a.button {
padding: 0;
width: 100%;
}
button.marginLeft, a.button.marginLeft {
margin-left: 0;
}
button.marginRight, a.button.marginRight {
margin-right: 0;
}
div.modal {
left: 20px;
right: 20px;
padding-left: 15px;
padding-right: 15px;
}
div.box {
width: initial;
@@ -360,9 +405,48 @@ div.box {
}
@media screen and (min-width: 600px) {
footer>p {
display: inline;
}
footer>p:not(:first-child) {
padding-left: 6px;
}
footer>p:not(:last-child) {
border-right: solid white 1px;
padding-right: 6px;
}
div.modal {
width: 540px;
left: 50%;
margin-left: -270px;
padding-left: 20px;
padding-right: 20px;
}
}
@media screen and (max-height: 399px) {
div.modal {
top: 20px;
max-height: calc(100% - 40px);
padding-top: 10px;
padding-bottom: 10px;
}
}
@media screen and (min-height: 400px) and (max-height: 499px) {
div.modal {
top: 50px;
max-height: calc(100% - 100px);
padding-top: 15px;
padding-bottom: 15px;
}
}
@media screen and (min-height: 500px) {
div.modal {
top: 75px;
max-height: calc(100% - 150px);
padding-top: 15px;
padding-bottom: 15px;
}
}
+11 -6
View File
@@ -29,17 +29,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 +46,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;
}
+12 -4
View File
@@ -16,7 +16,7 @@
initialize: function () {
document.getElementById('loading').style.visibility = 'visible';
document.getElementById('guessCover').style.visibility = 'visible';
document.getElementById('panoCover').style.visibility = 'visible';
document.getElementById('currentRound').innerHTML = '1/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = '0/0';
@@ -27,7 +27,7 @@
MapGuesser.httpRequest('GET', '/game/' + mapId + '/newPlace.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('guessCover').style.visibility = 'hidden';
document.getElementById('panoCover').style.visibility = 'hidden';
if (this.response.error) {
//TODO: handle this error
@@ -98,7 +98,7 @@
lastRound.line.setVisible(false);
}
document.getElementById('guessCover').style.visibility = 'hidden';
document.getElementById('panoCover').style.visibility = 'hidden';
document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null;
document.getElementById('guess').classList.remove('result')
@@ -152,7 +152,7 @@
document.getElementById('guess').classList.remove('adapt');
}
document.getElementById('loading').style.visibility = 'visible';
document.getElementById('guessCover').style.visibility = 'visible';
document.getElementById('panoCover').style.visibility = 'visible';
var data = new FormData();
data.append('lat', String(guessPosition.lat));
@@ -370,6 +370,14 @@
}
};
MapGuesser.sessionAvailableHooks.reinitializeGame = function () {
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
mapBounds = this.response.bounds;
Game.initialize();
});
};
if (!('ontouchstart' in document.documentElement)) {
Game.adaptGuess = true;
}
+7 -4
View File
@@ -10,10 +10,15 @@
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 'user_not_found':
errorText = 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up here</a>!';
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!';
@@ -29,8 +34,6 @@
loginFormError.style.display = 'block';
loginFormError.innerHTML = errorText;
form.elements.email.select();
return;
}
+5 -5
View File
@@ -334,11 +334,11 @@
var Util = {
getHighResData: function () {
if (window.devicePixelRatio >= 4) {
return { ppi: 320, tileSize: 128, zoomOffset: 1 };
return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 };
} else if (window.devicePixelRatio >= 2) {
return { ppi: 250, tileSize: 256, zoomOffset: 0 };
return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 };
} else {
return { ppi: 72, tileSize: 512, zoomOffset: -1 };
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
}
}
};
@@ -368,8 +368,8 @@
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: 2,
maxZoom: 20
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(MapEditor.map);
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
+47 -1
View File
@@ -1,12 +1,58 @@
var MapGuesser = {
cookiesAgreed: false,
sessionAvailableHooks: {},
initGoogleAnalitics: function () {
if (typeof GOOGLE_ANALITICS_ID === 'undefined') {
return;
}
// Global site tag (gtag.js) - Google Analytics
var script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + GOOGLE_ANALITICS_ID;
script.async = true;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', GOOGLE_ANALITICS_ID);
},
agreeCookies: function () {
if (MapGuesser.cookiesAgreed) {
return;
}
var expirationDate = new Date(new Date().getTime() + 20 * 365 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = 'COOKIES_CONSENT=1; expires=' + expirationDate + '; path=/';
MapGuesser.initGoogleAnalitics();
MapGuesser.httpRequest('GET', '/startSession.json', function () {
ANTI_CSRF_TOKEN = this.response.antiCsrfToken;
for (var hookId in MapGuesser.sessionAvailableHooks) {
if (!MapGuesser.sessionAvailableHooks.hasOwnProperty(hookId)) {
continue;
}
MapGuesser.sessionAvailableHooks[hookId]();
}
});
MapGuesser.cookiesAgreed = true;
},
httpRequest: function (method, url, callback, data) {
var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onload = callback;
xhr.open(method, url, true);
xhr.responseType = 'json';
if (method === 'POST') {
if (typeof data === 'undefined') {
data = new FormData();
+1 -4
View File
@@ -25,7 +25,7 @@
case 'password_not_match':
errorText = 'The given current password is wrong.'
break;
case 'passwords_too_short':
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':
@@ -37,15 +37,12 @@
profileFormError.style.display = 'block';
profileFormError.innerHTML = errorText;
form.elements.password_new.select();
return;
}
document.getElementById('profileFormError').style.display = 'none';
form.reset();
form.elements.save.disabled = true;
form.elements.password_new.focus();
}, formData);
};
})();
+27 -14
View File
@@ -9,39 +9,52 @@
var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
if (this.response.error === 'user_found') {
window.location.replace('/');
return;
}
var errorText;
switch (this.response.error) {
case 'passwords_too_short':
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':
errorText = 'There is a user already registered with the given email address. Please <a href="/login" title="Login">login here</a>!';
break;
case 'not_active_user_found':
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;
form.elements.email.select();
return;
}
document.getElementById('signupFormError').style.display = 'none';
form.reset();
form.elements.email.focus();
MapGuesser.showModalWithContent('Sign up successful', 'Sign up was successful. Please check your email and click on the activation link to activate your account!');
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();
});
};
}
})();
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <sql_file>"
exit 1
fi
SQL_FILE=$1
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "This script can only be used in DEV mode!"
exit 1
fi
echo "Running SQL on DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${SQL_FILE}
+1 -1
View File
@@ -5,7 +5,7 @@ ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
echo "Installing Composer packages..."
(cd ${ROOT_DIR} && composer install)
(cd ${ROOT_DIR} && composer install --no-dev)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
+6 -9
View File
@@ -1,7 +1,7 @@
<?php namespace MapGuesser\Cli;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -20,10 +20,8 @@ class AddUserCommand extends Command
public function execute(InputInterface $input, OutputInterface $output): int
{
$user = new User([
'email' => $input->getArgument('email'),
]);
$user = new User();
$user->setEmail($input->getArgument('email'));
$user->setPlainPassword($input->getArgument('password'));
if ($input->hasArgument('type')) {
@@ -31,9 +29,8 @@ class AddUserCommand extends Command
}
try {
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$pdm = new PersistentDataManager();
$pdm->saveToDb($user);
} catch (\Exception $e) {
$output->writeln('<error>Adding user failed!</error>');
$output->writeln('');
+2 -5
View File
@@ -1,7 +1,6 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
@@ -37,18 +36,16 @@ class GameController
{
$map = $this->mapRepository->getById($mapId);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [
'mapId' => $mapId,
'area' => $map['area'],
'area' => $map->getArea(),
'rounds' => []
]);
}
return ['mapId' => $mapId, 'mapName' => $map['name'], 'bounds' => $bounds->toArray()];
return ['mapId' => $mapId, 'mapName' => $map->getName(), 'bounds' => $map->getBounds()->toArray()];
}
}
+24 -8
View File
@@ -4,6 +4,7 @@ use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\Repository\PlaceRepository;
class GameFlowController
@@ -33,11 +34,14 @@ class GameFlowController
}
if (count($state['rounds']) === 0) {
$place = $this->placeRepository->getForMapWithValidPano($mapId);
$state['rounds'][] = $place;
$placesWithoutPano = [];
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, [], $placesWithoutPano);
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$session->set('state', $state);
$data = ['panoId' => $place['panoId']];
$data = ['panoId' => $place->getPanoIdCached()];
} else {
$rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1];
@@ -93,18 +97,20 @@ class GameFlowController
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
}
$place = $this->placeRepository->getForMapWithValidPano($mapId, $exclude);
$state['rounds'][] = $place;
$session->set('state', $state);
$placesWithoutPano = [];
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, $exclude, $placesWithoutPano);
$panoId = $place['panoId'];
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$panoId = $place->getPanoIdCached();
} else {
$state['rounds'] = [];
$session->set('state', $state);
$panoId = null;
}
$session->set('state', $state);
$data = [
'result' => [
'position' => $position->toArray(),
@@ -116,6 +122,16 @@ class GameFlowController
return new JsonContent($data);
}
private function addNewRoundToState(&$state, Place $place, array $placesWithoutPano): void
{
$state['rounds'][] = [
'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached()
];
}
private function calculateDistance(Position $realPosition, Position $guessPosition): float
{
return $realPosition->calculateDistanceTo($guessPosition);
+19 -1
View File
@@ -1,12 +1,30 @@
<?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'), []], IRedirect::TEMPORARY);
return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY);
}
public function startSession(): IContent
{
// session starts with the request, this method just sends valid data to the client
$data = ['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')];
return new JsonContent($data);
}
}
+338 -21
View File
@@ -1,58 +1,125 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Model\User;
use MapGuesser\Mailing\Mail;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
use MapGuesser\Util\JwtParser;
class LoginController
{
private IRequest $request;
private PersistentDataManager $pdm;
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
}
public function getLoginForm()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$data = [];
return new HtmlContent('login', $data);
return new HtmlContent('login/login', $data);
}
public function getGoogleLoginRedirect(): IRedirect
{
$state = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state);
$oAuth = new GoogleOAuth();
$url = $oAuth->getDialogUrl($state, $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink());
return new Redirect($url, IRedirect::TEMPORARY);
}
public function getSignupForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
if ($this->request->session()->has('tmp_user_data')) {
$tmpUserData = $this->request->session()->get('tmp_user_data');
$data = ['email' => $tmpUserData['email']];
} else {
$data = [];
}
return new HtmlContent('login/signup', $data);
}
public function getSignupSuccess(): IContent
{
$data = [];
return new HtmlContent('login/signup_success', $data);
}
public function getSignupWithGoogleForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
if (!$this->request->session()->has('google_user_data')) {
return new Redirect(\Container::$routeCollection->getRoute('login-google')->generateLink(), IRedirect::TEMPORARY);
}
$userData = $this->request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']);
$data = ['found' => $user !== null, 'email' => $userData['email']];
return new HtmlContent('login/google_signup', $data);
}
public function login(): IContent
{
$session = $this->request->session();
if ($session->get('user')) {
if ($this->request->user() !== null) {
$data = ['success' => true];
return new JsonContent($data);
}
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->where('email', '=', $this->request->post('email'));
$user = $this->userRepository->getByEmail($this->request->post('email'));
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($user === null) {
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => 'password_too_short'];
return new JsonContent($data);
}
$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()]);
if ($userData === null) {
$data = ['error' => 'user_not_found'];
return new JsonContent($data);
}
$user = new User($userData);
if (!$user->getActive()) {
$data = ['error' => 'user_not_active'];
return new JsonContent($data);
@@ -63,16 +130,266 @@ class LoginController
return new JsonContent($data);
}
$session->set('user', $user);
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
}
public function loginWithGoogle()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
$data = [];
return new HtmlContent('login/google_login', $data);
}
$oAuth = new GoogleOAuth();
$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);
}
$jwtParser = new JwtParser($tokenData['id_token']);
$userData = $jwtParser->getPayload();
if (!$userData['email_verified']) {
$data = [];
return new HtmlContent('login/google_login', $data);
}
$user = $this->userRepository->getByGoogleSub($userData['sub']);
if ($user === null) {
$this->request->session()->set('google_user_data', $userData);
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
}
$this->request->setUser($user);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
public function logout(): IRedirect
{
$this->request->session()->delete('user');
$this->request->setUser(null);
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
public function signup(): IContent
{
if ($this->request->user() !== null) {
$data = ['error' => 'logged_in'];
return new JsonContent($data);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user !== null) {
if ($user->getActive()) {
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => 'user_found_password_not_match'];
return new JsonContent($data);
}
$this->request->setUser($user);
$data = ['error' => 'user_found'];
} else {
$data = ['error' => 'user_found_user_not_active'];
}
return new JsonContent($data);
}
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
$data = ['error' => 'email_not_valid'];
return new JsonContent($data);
}
if ($this->request->session()->has('tmp_user_data')) {
$tmpUserData = $this->request->session()->get('tmp_user_data');
$tmpUser = new User();
$tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword($this->request->post('password'))) {
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
}
} else {
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => 'password_too_short'];
return new JsonContent($data);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
}
}
$user = new User();
$user->setEmail($this->request->post('email'));
$user->setPlainPassword($this->request->post('password'));
\Container::$dbConnection->startTransaction();
$this->pdm->saveToDb($user);
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime());
$confirmation = new UserConfirmation();
$confirmation->setUser($user);
$confirmation->setToken($token);
$this->pdm->saveToDb($confirmation);
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token);
$this->request->session()->delete('tmp_user_data');
$data = ['success' => true];
return new JsonContent($data);
}
public function signupWithGoogle(): IContent
{
if ($this->request->user() !== null) {
$data = ['success' => true];
return new JsonContent($data);
}
$userData = $this->request->session()->get('google_user_data');
$user = $this->userRepository->getByEmail($userData['email']);
if ($user === null) {
$sendWelcomeEmail = true;
$user = new User();
$user->setEmail($userData['email']);
} else {
$sendWelcomeEmail = false;
}
$user->setActive(true);
$user->setGoogleSub($userData['sub']);
$this->pdm->saveToDb($user);
if ($sendWelcomeEmail) {
$this->sendWelcomeEmail($user->getEmail());
}
$this->request->session()->delete('google_user_data');
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
}
public function resetSignup(): IContent
{
$this->request->session()->delete('tmp_user_data');
$data = ['success' => true];
return new JsonContent($data);
}
public function resetGoogleSignup(): IContent
{
$this->request->session()->delete('google_user_data');
$data = ['success' => true];
return new JsonContent($data);
}
public function activate()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
if ($confirmation === null) {
$data = [];
return new HtmlContent('login/activate', $data);
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId());
$user->setActive(true);
$this->pdm->saveToDb($user);
\Container::$dbConnection->commit();
$this->request->setUser($user);
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
public function cancel()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
if ($confirmation === null) {
$data = ['success' => false];
return new HtmlContent('login/cancel', $data);
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($confirmation);
$user = $this->userRepository->getById($confirmation->getUserId());
$this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit();
$data = ['success' => true];
return new HtmlContent('login/cancel', $data);
}
private function sendConfirmationEmail(string $email, string $token): 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]),
]);
$mail->send();
}
private function sendWelcomeEmail(string $email): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME']);
$mail->setBodyFromTemplate('signup-noconfirm', [
'EMAIL' => $email,
]);
$mail->send();
}
}
+66 -74
View File
@@ -1,13 +1,15 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Response\HtmlContent;
@@ -21,6 +23,8 @@ class MapAdminController implements ISecured
private IRequest $request;
private PersistentDataManager $pdm;
private MapRepository $mapRepository;
private PlaceRepository $placeRepository;
@@ -28,6 +32,7 @@ class MapAdminController implements ISecured
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository();
}
@@ -45,18 +50,14 @@ class MapAdminController implements ISecured
if ($mapId) {
$map = $this->mapRepository->getById($mapId);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$places = $this->getPlaces($mapId);
$places = $this->getPlaces($map);
} else {
$map = [
'name' => self::$unnamedMapName,
'description' => ''
];
$bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
$map = new Map();
$map->setName(self::$unnamedMapName);
$places = [];
}
$data = ['mapId' => $mapId, 'mapName' => $map['name'], 'mapDescription' => str_replace('<br>', "\n", $map['description']), 'bounds' => $bounds->toArray(), 'places' => &$places];
$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);
}
@@ -64,9 +65,9 @@ class MapAdminController implements ISecured
{
$placeId = (int) $this->request->query('placeId');
$placeData = $this->placeRepository->getById($placeId);
$place = $this->placeRepository->getById($placeId);
$data = ['panoId' => $placeData['panoId']];
$data = ['panoId' => $place->getFreshPanoId()];
return new JsonContent($data);
}
@@ -76,8 +77,12 @@ class MapAdminController implements ISecured
\Container::$dbConnection->startTransaction();
if (!$mapId) {
$mapId = $this->addNewMap();
if ($mapId) {
$map = $this->mapRepository->getById($mapId);
} else {
$map = new Map();
$map->setName(self::$unnamedMapName);
$this->pdm->saveToDb($map);
}
if (isset($_POST['added'])) {
@@ -85,11 +90,18 @@ class MapAdminController implements ISecured
foreach ($_POST['added'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $this->placeRepository->addToMap($mapId, [
'lat' => (float) $placeRaw['lat'],
'lng' => (float) $placeRaw['lng'],
'pano_id_cached_timestamp' => $placeRaw['panoId'] === -1 ? (new DateTime('-1 day'))->format('Y-m-d H:i:s') : null
])];
$place = new Place();
$place->setMap($map);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
if ($placeRaw['panoId'] === -1) {
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
}
$this->pdm->saveToDb($place);
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $place->getId()];
}
} else {
$addedIds = [];
@@ -99,10 +111,12 @@ class MapAdminController implements ISecured
foreach ($_POST['edited'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$this->placeRepository->modify((int) $placeRaw['id'], [
'lat' => (float) $placeRaw['lat'],
'lng' => (float) $placeRaw['lng']
]);
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
$this->pdm->saveToDb($place);
}
}
@@ -110,45 +124,42 @@ class MapAdminController implements ISecured
foreach ($_POST['deleted'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$this->placeRepository->delete($placeRaw['id']);
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$this->pdm->deleteFromDb($place);
}
}
$mapBounds = $this->calculateMapBounds($mapId);
$mapBounds = $this->calculateMapBounds($map);
$map = [
'bound_south_lat' => $mapBounds->getSouthLat(),
'bound_west_lng' => $mapBounds->getWestLng(),
'bound_north_lat' => $mapBounds->getNorthLat(),
'bound_east_lng' => $mapBounds->getEastLng(),
'area' => $mapBounds->calculateApproximateArea(),
];
$map->setBounds($mapBounds);
$map->setArea($mapBounds->calculateApproximateArea());
if (isset($_POST['name'])) {
$map['name'] = $_POST['name'] ? $_POST['name'] : self::$unnamedMapName;
$map->setName($_POST['name'] ? $_POST['name'] : self::$unnamedMapName);
}
if (isset($_POST['description'])) {
$map['description'] = str_replace(["\n", "\r\n"], '<br>', $_POST['description']);
$map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_POST['description']));
}
$this->saveMapData($mapId, $map);
$this->pdm->saveToDb($map);
\Container::$dbConnection->commit();
$data = ['mapId' => $mapId, 'added' => $addedIds];
$data = ['mapId' => $map->getId(), 'added' => $addedIds];
return new JsonContent($data);
}
public function deleteMap() {
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
\Container::$dbConnection->startTransaction();
$this->deletePlaces($mapId);
$this->deletePlaces($map);
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($mapId);
$modify->delete();
$this->pdm->deleteFromDb($map);
\Container::$dbConnection->commit();
@@ -156,26 +167,30 @@ class MapAdminController implements ISecured
return new JsonContent($data);
}
private function deletePlaces(int $mapId): void
private function deletePlaces(Map $map): void
{
//TODO: relations?
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id']);
$select->where('map_id', '=', $mapId);
$select->columns(Place::getFields());
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($place['id']);
$modify->delete();
while ($placeData = $result->fetch(IResultSet::FETCH_ASSOC)) {
$place = new Place();
$this->pdm->fillWithData($placeData, $place);
$this->pdm->deleteFromDb($place);
}
}
private function calculateMapBounds(int $mapId): Bounds
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', '=', $mapId);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
@@ -187,35 +202,12 @@ class MapAdminController implements ISecured
return $bounds;
}
private function addNewMap(): int
{
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->fill([
'name' => self::$unnamedMapName,
'description' => '',
'bound_south_lat' => 0.0,
'bound_west_lng' => 0.0,
'bound_north_lat' => 0.0,
'bound_east_lng' => 0.0
]);
$modify->save();
return $modify->getId();
}
private function saveMapData(int $mapId, array $map): void
{
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($mapId);
$modify->fill($map);
$modify->save();
}
private function &getPlaces(int $mapId): array
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', '=', $mapId);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
+1
View File
@@ -19,6 +19,7 @@ class MapsController
public function getMaps(): IContent
{
//TODO: from repository
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns([
['maps', 'id'],
-192
View File
@@ -1,192 +0,0 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Mailing\Mail;
use MapGuesser\Model\User;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
class SignupController
{
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getSignupForm()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$data = [];
return new HtmlContent('signup/signup', $data);
}
public function signup(): IContent
{
$session = $this->request->session();
if ($session->get('user')) {
//TODO: return with some error
$data = ['success' => true];
return new JsonContent($data);
}
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->where('email', '=', $this->request->post('email'));
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($userData !== null) {
$user = new User($userData);
if ($user->getActive()) {
$data = ['error' => 'user_found'];
} else {
$data = ['error' => 'not_active_user_found'];
}
return new JsonContent($data);
}
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => 'passwords_too_short'];
return new JsonContent($data);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
$data = ['error' => 'passwords_not_match'];
return new JsonContent($data);
}
$user = new User([
'email' => $this->request->post('email'),
]);
$user->setPlainPassword($this->request->post('password'));
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$userId = $modify->getId();
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime());
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->set('user_id', $userId);
$modify->set('token', $token);
$modify->save();
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token);
$data = ['success' => true];
return new JsonContent($data);
}
public function activate()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$select = new Select(\Container::$dbConnection, 'user_confirmations');
$select->columns(['id', 'user_id']);
$select->where('token', '=', $this->request->query('token'));
$confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($confirmation === null) {
$data = [];
return new HtmlContent('signup/activate', $data);
}
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->setId($confirmation['id']);
$modify->delete();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->setId($confirmation['user_id']);
$modify->set('active', true);
$modify->save();
\Container::$dbConnection->commit();
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->whereId($confirmation['user_id']);
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
$user = new User($userData);
$session->set('user', $user);
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
public function cancel()
{
$session = $this->request->session();
if ($session->get('user')) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$select = new Select(\Container::$dbConnection, 'user_confirmations');
$select->columns(['id', 'user_id']);
$select->where('token', '=', $this->request->query('token'));
$confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($confirmation === null) {
$data = ['success' => false];
return new HtmlContent('signup/cancel', $data);
}
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->setId($confirmation['id']);
$modify->delete();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->setId($confirmation['user_id']);
$modify->delete();
\Container::$dbConnection->commit();
$data = ['success' => true];
return new HtmlContent('signup/cancel', $data);
}
private function sendConfirmationEmail($email, $token): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to MapGuesser - Activate your account');
$mail->setBodyFromTemplate('signup', [
'EMAIL' => $email,
'ACTIVATE_LINK' => $this->request->getBase() . '/signup/activate/' . $token,
'CANCEL_LINK' => $this->request->getBase() . '/signup/cancel/' . $token,
]);
$mail->send();
}
}
+13 -5
View File
@@ -1,9 +1,10 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
@@ -11,9 +12,12 @@ class UserController implements ISecured
{
private IRequest $request;
private PersistentDataManager $pdm;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
}
public function authorize(): bool
@@ -25,6 +29,9 @@ class UserController implements ISecured
public function getProfile(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
$data = ['user' => $user->toArray()];
@@ -33,6 +40,9 @@ class UserController implements ISecured
public function saveProfile(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if (!$user->checkPassword($this->request->post('password'))) {
@@ -42,7 +52,7 @@ class UserController implements ISecured
if (strlen($this->request->post('password_new')) > 0) {
if (strlen($this->request->post('password_new')) < 6) {
$data = ['error' => 'passwords_too_short'];
$data = ['error' => 'password_too_short'];
return new JsonContent($data);
}
@@ -54,9 +64,7 @@ class UserController implements ISecured
$user->setPlainPassword($this->request->post('password_new'));
}
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$this->pdm->saveToDb($user);
$data = ['success' => true];
return new JsonContent($data);
+4 -40
View File
@@ -1,7 +1,6 @@
<?php namespace MapGuesser\Database\Query;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Database\Utils;
class Modify
@@ -14,8 +13,6 @@ class Modify
private array $attributes = [];
private array $original = [];
private ?string $externalId = null;
private bool $autoIncrement = true;
@@ -116,48 +113,15 @@ class Modify
private function update(): void
{
/*$diff = $this->generateDiff();
$attributes = $this->attributes;
unset($attributes[$this->idName]);
if (count($diff) === 0) {
return;
}*/
$diff = $this->attributes;
unset($diff[$this->idName]);
$set = $this->generateColumnsWithBinding(array_keys($diff));
$set = $this->generateColumnsWithBinding(array_keys($attributes));
$query = 'UPDATE ' . Utils::backtick($this->table) . ' SET ' . $set . ' WHERE ' . Utils::backtick($this->idName) . '=?';
$stmt = $this->connection->prepare($query);
$stmt->execute(array_merge($diff, [$this->idName => $this->attributes[$this->idName]]));
}
private function readFromDB(array $columns): void
{
$select = (new Select($this->connection, $this->table))
->setIdName($this->idName)
->whereId($this->attributes[$this->idName])
->columns($columns);
$this->original = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
}
private function generateDiff(): array
{
$this->readFromDB(array_keys($this->attributes));
$diff = [];
foreach ($this->attributes as $name => $value) {
$original = $this->original[$name];
if ($original != $value) {
$diff[$name] = $value;
}
}
return $diff;
$stmt->execute(array_merge($attributes, [$this->idName => $this->attributes[$this->idName]]));
}
public static function generateColumnsWithBinding(array $columns): string
+12 -2
View File
@@ -32,10 +32,13 @@ class Select
private array $limit;
public function __construct(IConnection $connection, string $table)
public function __construct(IConnection $connection, ?string $table = null)
{
$this->connection = $connection;
$this->table = $table;
if ($table !== null) {
$this->table = $table;
}
}
public function setIdName(string $idName): Select
@@ -52,6 +55,13 @@ class Select
return $this;
}
public function from(string $table): Select
{
$this->table = $table;
return $this;
}
public function columns(array $columns): Select
{
$this->columns = array_merge($this->columns, $columns);
+4
View File
@@ -8,5 +8,9 @@ interface IUser
public function hasPermission(int $permission): bool;
public function getUniqueId();
public function getDisplayName(): string;
public function checkPassword(string $password): bool;
}
+2
View File
@@ -14,5 +14,7 @@ interface IRequest
public function session(): ISession;
public function setUser(?IUser $user): void;
public function user(): ?IUser;
}
+9 -2
View File
@@ -29,6 +29,13 @@ class Mail
{
$this->body = file_get_contents(ROOT . '/mail/' . $template . '.html');
$baseParameters = [
'APP_NAME' => $_ENV['APP_NAME'],
'BASE_URL' => \Container::$request->getBase(),
];
$params = array_merge($baseParameters, $params);
foreach ($params as $key => $param) {
$this->body = str_replace('{{' . $key . '}}', $param, $this->body);
}
@@ -58,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;
-49
View File
@@ -1,49 +0,0 @@
<?php namespace MapGuesser\Model;
abstract class BaseModel
{
protected static array $fields;
protected $id = null;
public static function getFields(): array
{
return array_merge(['id'], static::$fields);
}
public function __construct(array $data)
{
foreach ($data as $key => $value) {
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($this, $method)) {
$this->$method($value);
}
}
}
public function setId($id): void
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
function toArray(): array
{
$array = [];
foreach (self::getFields() as $key) {
$method = 'get' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($this, $method)) {
$array[$key] = $this->$method();
}
}
return $array;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php namespace MapGuesser\OAuth;
use MapGuesser\Http\Request;
class GoogleOAuth
{
private static $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
private static $tokenUrlBase = 'https://oauth2.googleapis.com/token';
public function getDialogUrl(string $state, string $redirectUrl): string
{
$oauthParams = [
'response_type' => 'code',
'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'],
'scope' => 'openid email',
'redirect_uri' => $redirectUrl,
'state' => $state,
'nonce' => hash('sha256', random_bytes(10) . microtime()),
];
return self::$dialogUrlBase . '?' . http_build_query($oauthParams);
}
public function getToken(string $code, string $redirectUrl)
{
$tokenParams = [
'code' => $code,
'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'],
'client_secret' => $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'],
'redirect_uri' => $redirectUrl,
'grant_type' => 'authorization_code',
];
$request = new Request(self::$tokenUrlBase, Request::HTTP_POST);
$request->setQuery($tokenParams);
$response = $request->send();
return json_decode($response->getBody(), true);
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php namespace MapGuesser\PersistentData\Model;
use MapGuesser\Util\Geo\Bounds;
class Map extends Model
{
protected static string $table = 'maps';
protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area'];
private string $name = '';
private string $description = '';
private Bounds $bounds;
private float $area = 0.0;
public function __construct()
{
$this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
}
public function setName(string $name): void
{
$this->name = $name;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function setBounds(Bounds $bounds): void
{
$this->bounds = $bounds;
}
public function setBoundSouthLat(float $bound_south_lat): void
{
$this->bounds->setSouthLat($bound_south_lat);
}
public function setBoundWestLng(float $bound_west_lng): void
{
$this->bounds->setWestLng($bound_west_lng);
}
public function setBoundNorthLat(float $bound_north_lat): void
{
$this->bounds->setNorthLat($bound_north_lat);
}
public function setBoundEastLng(float $bound_east_lng): void
{
$this->bounds->setEastLng($bound_east_lng);
}
public function setArea(float $area): void
{
$this->area = $area;
}
public function getName(): string
{
return $this->name;
}
public function getDescription(): string
{
return $this->description;
}
public function getBounds(): Bounds
{
return $this->bounds;
}
public function getBoundSouthLat(): float
{
return $this->bounds->getSouthLat();
}
public function getBoundWestLng(): float
{
return $this->bounds->getWestLng();
}
public function getBoundNorthLat(): float
{
return $this->bounds->getNorthLat();
}
public function getBoundEastLng(): float
{
return $this->bounds->getEastLng();
}
public function getArea(): float
{
return $this->area;
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php namespace MapGuesser\PersistentData\Model;
abstract class Model
{
protected static string $table;
protected static array $fields;
protected static array $relations = [];
protected $id = null;
private array $snapshot = [];
public static function getTable(): string
{
return static::$table;
}
public static function getFields(): array
{
return array_merge(['id'], static::$fields);
}
public static function getRelations(): array
{
return static::$relations;
}
public function setId($id): void
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
public function toArray(): array
{
$array = [];
foreach (self::getFields() as $key) {
$method = 'get' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($this, $method)) {
$array[$key] = $this->$method();
}
}
return $array;
}
public function saveSnapshot(): void
{
$this->snapshot = $this->toArray();
}
public function resetSnapshot(): void
{
$this->snapshot = [];
}
public function getSnapshot(): array
{
return $this->snapshot;
}
}
+150
View File
@@ -0,0 +1,150 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateInterval;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Util\Geo\Position;
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 $relations = ['map' => Map::class];
private ?Map $map = null;
private ?int $mapId = null;
private Position $position;
private ?string $panoIdCached = null;
private ?DateTime $panoIdCachedTimestamp = null;
public function __construct()
{
$this->position = new Position(0.0, 0.0);
}
public function setMap(Map $map): void
{
$this->map = $map;
}
public function setMapId(int $mapId): void
{
$this->mapId = $mapId;
}
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 setPanoIdCached(?string $panoIdCached): void
{
$this->panoIdCached = $panoIdCached;
}
public function setPanoIdCachedTimestampDate(?DateTime $panoIdCachedTimestamp): void
{
$this->panoIdCachedTimestamp = $panoIdCachedTimestamp;
}
public function setPanoIdCachedTimestamp(?string $panoIdCachedTimestamp): void
{
if ($panoIdCachedTimestamp !== null) {
$this->panoIdCachedTimestamp = new DateTime($panoIdCachedTimestamp);
}
}
public function getMap(): ?Map
{
return $this->map;
}
public function getMapId(): ?int
{
return $this->mapId;
}
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 getFreshPanoId(bool $canBeIndoor = false): ?string
{
if (
$this->panoIdCachedTimestamp !== null &&
(clone $this->panoIdCachedTimestamp)->add(new DateInterval('P1D')) > new DateTime()
) {
return $this->panoIdCached;
}
$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'
]);
$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');
(new PersistentDataManager())->saveToDb($this);
return $panoId;
}
public function getPanoIdCached(): ?string
{
return $this->panoIdCached;
}
public function getPanoIdCachedTimestampDate(): ?DateTime
{
return $this->panoIdCachedTimestamp;
}
public function getPanoIdCachedTimestamp(): ?string
{
if ($this->panoIdCachedTimestamp === null) {
return null;
}
return $this->panoIdCachedTimestamp->format('Y-m-d H:i:s');
}
}
@@ -1,27 +1,31 @@
<?php namespace MapGuesser\Model;
<?php namespace MapGuesser\PersistentData\Model;
use MapGuesser\Interfaces\Authentication\IUser;
class User extends BaseModel implements IUser
class User extends Model implements IUser
{
protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub'];
private static array $types = ['user', 'admin'];
protected static array $fields = ['email', 'password', 'type', 'active'];
private string $email = '';
private string $email;
private string $password;
private ?string $password = null;
private string $type = 'user';
private bool $active = false;
private ?string $googleSub = null;
public function setEmail(string $email): void
{
$this->email = $email;
}
public function setPassword(string $hashedPassword): void
public function setPassword(?string $hashedPassword): void
{
$this->password = $hashedPassword;
}
@@ -43,12 +47,17 @@ class User extends BaseModel implements IUser
$this->active = (bool) $active;
}
public function setGoogleSub(?string $googleSub): void
{
$this->googleSub = $googleSub;
}
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): string
public function getPassword(): ?string
{
return $this->password;
}
@@ -63,6 +72,11 @@ class User extends BaseModel implements IUser
return $this->active;
}
public function getGoogleSub(): ?string
{
return $this->googleSub;
}
public function hasPermission(int $permission): bool
{
switch ($permission) {
@@ -75,6 +89,11 @@ class User extends BaseModel implements IUser
}
}
public function getUniqueId()
{
return $this->id;
}
public function getDisplayName(): string
{
return $this->email;
@@ -0,0 +1,46 @@
<?php namespace MapGuesser\PersistentData\Model;
class UserConfirmation extends Model
{
protected static string $table = 'user_confirmations';
protected static array $fields = ['user_id', 'token'];
protected static array $relations = ['user' => User::class];
private ?User $user = null;
private ?int $userId = null;
private string $token = '';
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 getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getToken(): string
{
return $this->token;
}
}
@@ -0,0 +1,206 @@
<?php namespace MapGuesser\PersistentData;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\Model;
class PersistentDataManager
{
public function selectFromDb(Select $select, string $type, bool $withRelations = false): ?Model
{
$table = call_user_func([$type, 'getTable']);
$fields = call_user_func([$type, 'getFields']);
$select->from($table);
//TODO: only with some relations?
if ($withRelations) {
$relations = call_user_func([$type, 'getRelations']);
$columns = [];
foreach ($fields as $field) {
$columns[] = [$table, $field];
}
$columns = array_merge($columns, $this->getRelationColumns($relations));
$this->leftJoinRelations($select, $table, $relations);
$select->columns($columns);
} else {
$select->columns($fields);
}
//TODO: return with array?
$data = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($data === null) {
return null;
}
$model = new $type();
$this->fillWithData($data, $model);
return $model;
}
public function selectFromDbById($id, string $type, bool $withRelations = false): ?Model
{
$select = new Select(\Container::$dbConnection);
$select->whereId($id);
return $this->selectFromDb($select, $type, $withRelations);
}
public function fillWithData(array $data, Model $model): void
{
$relations = $model::getRelations();
$relationData = [];
foreach ($data as $key => $value) {
if ($this->extractRelationData($key, $value, $relationData, $relations)) {
continue;
}
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($model, $method)) {
$model->$method($value);
}
}
$this->setRelations($model, $relationData);
$model->saveSnapshot();
}
public function loadRelationsFromDb(Model $model, bool $recursive): void
{
foreach ($model::getRelations() as $relation => $relationType) {
$camel = str_replace('_', '', ucwords($relation, '_'));
$methodGet = 'get' . $camel . 'Id';
$methodSet = 'set' . $camel;
$relationId = $model->$methodGet();
if ($relationId !== null) {
$relationModel = $this->selectFromDbById($relationId, $relationType, $recursive);
$model->$methodSet($relationModel);
}
}
}
public function saveToDb(Model $model): void
{
$this->syncRelations($model);
$modified = $model->toArray();
$id = $model->getId();
$modify = new Modify(\Container::$dbConnection, $model::getTable());
if ($id !== null) {
$original = $model->getSnapshot();
foreach ($original as $key => $value) {
if ($value === $modified[$key]) {
unset($modified[$key]);
}
}
if (count($modified) > 0) {
$modify->setId($id);
$modify->fill($modified);
$modify->save();
}
} else {
$modify->fill($modified);
$modify->save();
$model->setId($modify->getId());
}
$model->saveSnapshot();
}
public function deleteFromDb(Model $model): void
{
$modify = new Modify(\Container::$dbConnection, $model::getTable());
$modify->setId($model->getId());
$modify->delete();
$model->setId(null);
$model->resetSnapshot();
}
private function getRelationColumns(array $relations): array
{
$columns = [];
foreach ($relations as $relation => $relationType) {
$relationTable = call_user_func([$relationType, 'getTable']);
foreach (call_user_func([$relationType, 'getFields']) as $relationField) {
$columns[] = [$relationTable, $relationField, $relation . '__' . $relationField];
}
}
return $columns;
}
private function leftJoinRelations(Select $select, string $table, array $relations): void
{
foreach ($relations as $relation => $relationType) {
$relationTable = call_user_func([$relationType, 'getTable']);
$select->leftJoin($relationTable, [$relationTable, 'id'], '=', [$table, $relation . '_id']);
}
}
private function extractRelationData(string $key, $value, array &$relationData, array $relations): bool
{
$found = false;
foreach ($relations as $relation => $relationType) {
if (substr($key, 0, strlen($relation . '__')) === $relation . '__') {
$found = true;
$relationData[$relation][substr($key, strlen($relation . '__'))] = $value;
break;
}
}
return $found;
}
private function setRelations(Model $model, array &$relations): void
{
foreach ($model::getRelations() as $relation => $relationType) {
if (isset($relations[$relation])) {
$object = new $relationType();
$this->fillWithData($relations[$relation], $object);
$method = 'set' . str_replace('_', '', ucwords($relation, '_'));
$model->$method($object);
}
}
}
private function syncRelations(Model $model): void
{
foreach ($model::getRelations() as $relation => $relationType) {
$camel = str_replace('_', '', ucwords($relation, '_'));
$methodGet = 'get' . $camel;
$methodSet = 'set' . $camel . 'Id';
$relationModel = $model->$methodGet();
if ($relationModel !== null) {
$model->$methodSet($relationModel->getId());
}
}
}
}
+11 -8
View File
@@ -1,16 +1,19 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\PersistentDataManager;
class MapRepository
{
public function getById(int $mapId)
{
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns(['id', 'name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area']);
$select->whereId($mapId);
private PersistentDataManager $pdm;
return $select->execute()->fetch(IResultSet::FETCH_ASSOC);
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $mapId): ?Map
{
return $this->pdm->selectFromDbById($mapId, Map::class);
}
}
+17 -112
View File
@@ -1,95 +1,43 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Database\Query\Select;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Database\IResultSet;
use DateTime;
use DateInterval;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
class PlaceRepository
{
public function getById(int $placeId)
private PersistentDataManager $pdm;
public function __construct()
{
$place = $this->selectFromDbById($placeId);
$panoId = $this->requestPanoId($place);
$position = new Position($place['lat'], $place['lng']);
return [
'position' => $position,
'panoId' => $panoId
];
$this->pdm = new PersistentDataManager();
}
public function getForMapWithValidPano(int $mapId, array $exclude = []): array
public function getById(int $placeId): ?Place
{
return $this->pdm->selectFromDbById($placeId, Place::class);
}
public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place
{
$placesWithoutPano = [];
do {
$place = $this->selectFromDbForMap($mapId, $exclude);
$place = $this->selectRandomFromDbForMap($mapId, $exclude);
$panoId = $this->requestPanoId($place);
$panoId = $place->getFreshPanoId();
if ($panoId === null) {
$placesWithoutPano[] = $exclude[] = $place['id'];
$placesWithoutPano[] = $exclude[] = $place->getId();
}
} while ($panoId === null);
$position = new Position($place['lat'], $place['lng']);
return [
'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place['id'],
'position' => $position,
'panoId' => $panoId
];
}
public function addToMap(int $mapId, array $place): int
{
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->set('map_id', $mapId);
$modify->fill($place);
$modify->save();
return $modify->getId();
}
public function modify(int $id, array $place): void
{
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($id);
$modify->set('pano_id_cached', null);
$modify->set('pano_id_cached_timestamp', null);
$modify->fill($place);
$modify->save();
}
public function delete(int $id): void
{
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($id);
$modify->delete();
}
private function selectFromDbById(int $placeId): array
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->whereId($placeId);
$place = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
return $place;
}
private function selectFromDbForMap(int $mapId, array $exclude): array
private function selectRandomFromDbForMap(int $mapId, array $exclude): ?Place
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $mapId);
@@ -99,49 +47,6 @@ class PlaceRepository
$select->orderBy('id');
$select->limit(1, $randomOffset);
$place = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
return $place;
}
private function requestPanoId(array $place, bool $canBeIndoor = false): ?string
{
if (
$place['pano_id_cached_timestamp'] &&
(new DateTime($place['pano_id_cached_timestamp']))->add(new DateInterval('P1D')) > new DateTime()
) {
return $place['pano_id_cached'];
}
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
$request->setQuery([
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
'location' => $place['lat'] . ',' . $place['lng'],
'source' => $canBeIndoor ? 'default' : 'outdoor'
]);
$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->requestPanoId($place, true);
}
$this->saveCachedPanoId($place['id'], $panoId);
return $panoId;
}
private function saveCachedPanoId(int $placeId, ?string $panoId): void
{
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($placeId);
$modify->set('pano_id_cached', $panoId);
$modify->set('pano_id_cached_timestamp', (new DateTime())->format('Y-m-d H:i:s'));
$modify->save();
return $this->pdm->selectFromDb($select, Place::class);
}
}
@@ -0,0 +1,28 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\PersistentDataManager;
class UserConfirmationRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userConfirmationId): ?UserConfirmation
{
return $this->pdm->selectFromDbById($userConfirmationId, UserConfirmation::class);
}
public function getByToken(string $token): ?UserConfirmation
{
$select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token);
return $this->pdm->selectFromDb($select, UserConfirmation::class);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
class UserRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userId): ?User
{
return $this->pdm->selectFromDbById($userId, User::class);
}
public function getByEmail(string $email): ?User
{
$select = new Select(\Container::$dbConnection);
$select->where('email', '=', $email);
return $this->pdm->selectFromDb($select, User::class);
}
public function getByGoogleSub(string $sub): ?User
{
$select = new Select(\Container::$dbConnection);
$select->where('google_sub', '=', $sub);
return $this->pdm->selectFromDb($select, User::class);
}
}
+26 -1
View File
@@ -3,6 +3,9 @@
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Request\ISession;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserRepository;
class Request implements IRequest
{
@@ -16,12 +19,24 @@ class Request implements IRequest
private Session $session;
private UserRepository $userRepository;
private ?User $user = null;
public function __construct(string $base, array &$get, array &$post, array &$session)
{
$this->base = $base;
$this->get = &$get;
$this->post = &$post;
$this->session = new Session($session);
$this->userRepository = new UserRepository();
$userId = $this->session->get('userId');
if ($userId !== null) {
$this->user = $this->userRepository->getById($userId);
}
}
public function setParsedRouteParams(array &$routeParams)
@@ -61,8 +76,18 @@ class Request implements IRequest
return $this->session;
}
public function setUser(?IUser $user): void
{
if ($user === null) {
$this->session->delete('userId');
return;
}
$this->session->set('userId', $user->getUniqueId());
}
public function user(): ?IUser
{
return $this->session->get('user');
return $this->user;
}
}
+5 -5
View File
@@ -4,11 +4,11 @@ use MapGuesser\Interfaces\Response\IRedirect;
class Redirect implements IRedirect
{
private $target;
private string $target;
private int $type;
public function __construct($target, int $type = IRedirect::TEMPORARY)
public function __construct(string $target, int $type = IRedirect::TEMPORARY)
{
$this->target = $target;
$this->type = $type;
@@ -16,10 +16,10 @@ class Redirect implements IRedirect
public function getUrl(): string
{
if (is_array($this->target)) {
$link = $this->target[0]->generateLink($this->target[1]);
} else {
if (preg_match('/^http(s)?/', $this->target)) {
$link = $this->target;
} else {
$link = \Container::$request->getBase() . '/' . $this->target;
}
return $link;
+6 -1
View File
@@ -45,6 +45,11 @@ class RouteCollection
{
$groupNumber = count($uri);
// response to HEAD request with the GET content
if ($method === 'head') {
$method = 'get';
}
if (!isset($this->searchTable[$method][$groupNumber])) {
return null;
}
@@ -64,7 +69,7 @@ class RouteCollection
throw new \Exception('Route already exists: ' . $id);
}
$pattern = array_merge($this->groupStack, explode('/', $pattern));
$pattern = array_merge($this->groupStack, $pattern === '' ? [] : explode('/', $pattern));
$route = new Route($id, $pattern, $handler);
$groupNumber = count($pattern);
+20
View File
@@ -59,6 +59,26 @@ class Bounds
}
}
public function setSouthLat(float $southLat): void
{
$this->southLat = $southLat;
}
public function setWestLng(float $westLng): void
{
$this->westLng = $westLng;
}
public function setNorthLat(float $northLat): void
{
$this->northLat = $northLat;
}
public function setEastLng(float $eastLng): void
{
$this->eastLng = $eastLng;
}
public function getSouthLat(): float
{
return $this->southLat;
+10
View File
@@ -13,6 +13,16 @@ class Position
$this->lng = $lng;
}
public function setLat(float $lat): void
{
$this->lat = $lat;
}
public function setLng(float $lng): void
{
$this->lng = $lng;
}
public function getLat(): float
{
return $this->lat;
+33
View File
@@ -0,0 +1,33 @@
<?php namespace MapGuesser\Util;
class JwtParser
{
private array $token;
public function __construct(?string $token = null)
{
if ($token !== null) {
$this->setToken($token);
}
}
public function setToken(string $token)
{
$this->token = explode('.', str_replace(['_', '-'], ['/', '+'], $token));
}
public function getHeader(): array
{
return json_decode(base64_decode($this->token[0]), true);
}
public function getPayload(): array
{
return json_decode(base64_decode($this->token[1]), true);
}
public function getSignature(): string
{
return base64_decode($this->token[2]);
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php namespace MapGuesser\Tests\Util\Geo;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Geo\Position;
use PHPUnit\Framework\TestCase;
final class BoundsTest extends TestCase
{
public function testCanBeCreatedEmpty(): void
{
$bounds = new Bounds();
$this->assertEquals(90.0, $bounds->getSouthLat());
$this->assertEquals(180.0, $bounds->getWestLng());
$this->assertEquals(-90.0, $bounds->getNorthLat());
$this->assertEquals(-180.0, $bounds->getEastLng());
}
public function testCanBeCreatedWithPosition(): void
{
$position = new Position(45.25, 18.12);
$bounds = new Bounds($position);
$this->assertEquals($position->getLat(), $bounds->getSouthLat());
$this->assertEquals($position->getLng(), $bounds->getWestLng());
$this->assertEquals($position->getLat(), $bounds->getNorthLat());
$this->assertEquals($position->getLng(), $bounds->getEastLng());
}
public function testCanBeCreatedDirectly(): void
{
$bounds = Bounds::createDirectly(44.12, 7.33, 50.12, 15.11);
$this->assertEquals(44.12, $bounds->getSouthLat());
$this->assertEquals(7.33, $bounds->getWestLng());
$this->assertEquals(50.12, $bounds->getNorthLat());
$this->assertEquals(15.11, $bounds->getEastLng());
}
public function testExtendingWithOnePositionIsTheSameAsCreatingWithPosition(): void
{
$position = new Position(45.25, 18.12);
$bounds1 = new Bounds();
$bounds1->extend($position);
$bounds2 = new Bounds($position);
$this->assertEquals($bounds1, $bounds2);
}
public function testCanBeExtendedWithTwoPositions(): void
{
$bounds = new Bounds();
$position1 = new Position(45.25, 18.12);
$position2 = new Position(43.15, 19.28);
$bounds->extend($position1);
$bounds->extend($position2);
$this->assertEquals($position2->getLat(), $bounds->getSouthLat());
$this->assertEquals($position1->getLng(), $bounds->getWestLng());
$this->assertEquals($position1->getLat(), $bounds->getNorthLat());
$this->assertEquals($position2->getLng(), $bounds->getEastLng());
$bounds = new Bounds();
$position1 = new Position(43.15, 18.12);
$position2 = new Position(45.25, 19.28);
$bounds->extend($position1);
$bounds->extend($position2);
$this->assertEquals($position1->getLat(), $bounds->getSouthLat());
$this->assertEquals($position1->getLng(), $bounds->getWestLng());
$this->assertEquals($position2->getLat(), $bounds->getNorthLat());
$this->assertEquals($position2->getLng(), $bounds->getEastLng());
}
public function testCanCalculateApproximateArea(): void
{
$bounds = Bounds::createDirectly(44.12, 7.33, 50.12, 15.11);
$this->assertEqualsWithDelta(391766.09, $bounds->calculateApproximateArea(), 0.01);
}
public function testCanBeConvertedToArray(): void
{
$bounds = Bounds::createDirectly(44.12, 7.33, 50.12, 15.11);
$this->assertEquals(
['south' => 44.12, 'west' => 7.33, 'north' => 50.12, 'east' => 15.11],
$bounds->toArray()
);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php namespace MapGuesser\Tests\Util\Geo;
use MapGuesser\Util\Geo\Position;
use PHPUnit\Framework\TestCase;
final class PositionTest extends TestCase
{
public function testCanBeCreatedWithLatAndLng(): void
{
$position = new Position(45.25, 18.12);
$this->assertEquals(45.25, $position->getLat());
$this->assertEquals(18.12, $position->getLng());
}
public function testCanCalculateDistanceToAnotherPosition(): void
{
$position1 = new Position(44.12, 7.33);
$position2 = new Position(50.12, 15.11);
$this->assertEqualsWithDelta(888785.73, $position1->calculateDistanceTo($position2), 0.01);
}
public function testCanBeConvertedToArray(): void
{
$position = new Position(45.25, 18.12);
$this->assertEquals(
['lat' => 45.25, 'lng' => 18.12],
$position->toArray()
);
}
}
+25 -23
View File
@@ -13,11 +13,11 @@ $jsFiles = [
];
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<div class="header small">
<header class="small">
<h1>
<a href="/maps" title="Back to playable maps">
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>">
<span>MapGuesser</span>
<a href="/" title="<?= $_ENV['APP_NAME'] ?>">
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>"><!--
--><span><?= $_ENV['APP_NAME'] ?></span>
</a>
</h1>
<p class="header">
@@ -28,26 +28,26 @@ $jsFiles = [
<path fill-rule="evenodd" d="M8 3.5a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5H4a.5.5 0 0 1 0-1h3.5V4a.5.5 0 0 1 .5-.5z"/>
<path fill-rule="evenodd" d="M7.5 8a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1H8.5V12a.5.5 0 0 1-1 0V8z"/>
<path fill-rule="evenodd" d="M14 1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
</svg>
<span id="added" class="bold">0</span><!--
</svg><!--
--><span id="added" class="bold">0</span><!--
--></span><!--
--><span><!--
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
--><svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M15.502 1.94a.5.5 0 0 1 0 .706a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456l-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/> <path fill-rule="evenodd" d="M14 1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
</svg>
<span id="edited" class="bold">0</span><!--
</svg><!--
--><span id="edited" class="bold">0</span><!--
--></span><!--
--><span><!--
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
--><svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3.5 8a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1H4a.5.5 0 0 1-.5-.5z"/>
<path fill-rule="evenodd" d="M14 1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
</svg>
<span id="deleted" class="bold">0</span><!--
</svg><!--
--><span id="deleted" class="bold">0</span><!--
--></span>
</p>
</div>
</header>
<div id="metadata" class="modal">
<h2>Edit map data</h2>
<form id="metadataForm" class="marginTop">
@@ -59,18 +59,20 @@ $jsFiles = [
</div>
</form>
</div>
<div id="map"></div>
<div id="control">
<button id="saveButton" class="fullWidth" disabled>Save</button>
</div>
<div id="panorama"></div>
<div id="noPano">
<p class="bold">No panorama is available for this location.</p>
</div>
<div id="placeControl">
<button id="applyButton" class="fullWidth">Apply</button>
<button id="closeButton" class="gray fullWidth marginTop">Close</button>
<button id="deleteButton" class="red fullWidth marginTop">Delete</button>
<div class="full">
<div id="map"></div>
<div id="panorama"></div>
<div id="noPano">
<p class="bold">No panorama is available for this location.</p>
</div>
<div id="control">
<button id="saveButton" class="fullWidth" disabled>Save</button>
</div>
<div id="placeControl">
<button id="applyButton" class="fullWidth">Apply</button>
<button id="closeButton" class="gray fullWidth marginTop">Close</button>
<button id="deleteButton" class="red fullWidth marginTop">Delete</button>
</div>
</div>
<script>
var tileUrl = '<?= $_ENV['LEAFLET_TILESERVER_URL'] ?>';
+2 -3
View File
@@ -1,7 +1,6 @@
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>404 | Page not found</h2>
<p>The requested URL was not found on this server. <a href="/" title="MapGuesser">Back to start.</a></p>
</div>
<p>The requested URL was not found on this server. <a href="/" title="<?= $_ENV['APP_NAME'] ?>">Back to start.</a></p>
<?php require ROOT . '/views/templates/footer.php'; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
+34 -32
View File
@@ -8,11 +8,11 @@ $jsFiles = [
];
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<div class="header small">
<header class="small">
<h1>
<a href="/maps" title="Back to playable maps">
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>">
<span>MapGuesser</span>
<a href="/" title="<?= $_ENV['APP_NAME'] ?>">
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>"><!--
--><span><?= $_ENV['APP_NAME'] ?></span>
</a>
</h1>
<p class="header">
@@ -20,39 +20,41 @@ $jsFiles = [
--><span>Round <span id="currentRound" class="bold"></span></span><!--
--><span>Score <span id="currentScoreSum" class="bold"></span></span>
</p>
</div>
<div id="guessCover"></div>
<div id="panorama"></div>
<div id="showGuessButtonContainer">
<button id="showGuessButton" class="fullWidth">Show guess map</button>
</div>
<div id="guess">
<div id="closeGuessButtonContainer" class="buttonContainer marginBottom">
<button id="closeGuessButton" class="fullWidth gray">Close</button>
</header>
<div class="full">
<div id="panoCover"></div>
<div id="panorama"></div>
<div id="showGuessButtonContainer">
<button id="showGuessButton" class="fullWidth">Show guess map</button>
</div>
<div id="map"></div>
<div id="guessButtonContainer" class="buttonContainer marginTop">
<button id="guessButton" class="fullWidth" disabled>Guess</button>
</div>
<div id="resultInfo">
<div id="distanceInfo">
<p>You were <span id="distance" class="bold"></span> close.</p>
<p class="bold">Game finished.</p>
<div id="guess">
<div id="closeGuessButtonContainer" class="buttonContainer marginBottom">
<button id="closeGuessButton" class="fullWidth gray">Close</button>
</div>
<div id="scoreInfo">
<p>You earned <span id="score" class="bold"></span> points.</p>
<p>You got <span id="scoreSum" class="bold"></span> points in total.</p>
<div id="map"></div>
<div id="guessButtonContainer" class="buttonContainer marginTop">
<button id="guessButton" class="fullWidth" disabled>Guess</button>
</div>
<div>
<div id="scoreBarBase">
<div id="scoreBar"></div>
<div id="resultInfo">
<div id="distanceInfo">
<p>You were <span id="distance" class="bold"></span> close.</p>
<p class="bold">Game finished.</p>
</div>
<div id="scoreInfo">
<p>You earned <span id="score" class="bold"></span> points.</p>
<p>You got <span id="scoreSum" class="bold"></span> points in total.</p>
</div>
<div>
<div id="scoreBarBase">
<div id="scoreBar"></div>
</div>
</div>
</div>
</div>
<div id="continueButtonContainer" class="buttonContainer marginTop">
<button id="continueButton" class="fullWidth">Continue</button>
<button id="showSummaryButton" class="fullWidth">Show summary</button>
<button id="startNewGameButton" class="fullWidth">Play this map again</button>
<div id="continueButtonContainer" class="buttonContainer marginTop">
<button id="continueButton" class="fullWidth">Continue</button>
<button id="showSummaryButton" class="fullWidth">Show summary</button>
<button id="startNewGameButton" class="fullWidth">Play this map again</button>
</div>
</div>
</div>
<script>
@@ -1,9 +1,8 @@
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Account activation</h2>
<div class="box">
<p class="error justify">Activation failed. Please check the link you entered or retry <a href="/signup" title="Sign up">sign up</a>!</p>
</div>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
@@ -1,6 +1,5 @@
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Account cancellation</h2>
<div class="box">
<?php if ($success) : ?>
@@ -9,5 +8,5 @@
<p class="error justify">Cancellation failed. Please check the link you entered! Maybe the account was already deleted, in this case no further action is required.</p>
<?php endif; ?>
</div>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
+8
View File
@@ -0,0 +1,8 @@
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<h2>Login up with Google</h2>
<div class="box">
<p class="error justify">Authenticating with Google failed. Please <a href="/login/google" title="Login with Google">retry</a>!</p>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
+48
View File
@@ -0,0 +1,48 @@
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<h2>Sign up</h2>
<div class="box">
<form id="googleSignupForm" action="/signup/google" method="post">
<?php if ($found): ?>
<p class="justify">Please confirm that you link your account to your Google account.</p>
<?php else: ?>
<p class="justify">Please confirm your sign up request. Your account will be linked to your Google account.</p>
<?php endif; ?>
<input class="big fullWidth marginTop" type="email" name="email" placeholder="Email address" value="<?= $email ?>" disabled>
<div class="right">
<button class="marginTop marginRight" type="submit">
<?php if ($found): ?>
Link
<?php else: ?>
Sign up
<?php endif; ?>
</button><!--
--><button id="cancelGoogleSignupButton" class="gray marginTop" type="button">Cancel</button>
</div>
</form>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<script>
(function () {
var form = document.getElementById('googleSignupForm');
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
MapGuesser.httpRequest('POST', form.action, function () {
window.location.replace('/');
});
};
document.getElementById('cancelGoogleSignupButton').onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/signup/google/reset', function () {
window.location.replace('/signup');
});
};
})();
</script>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
+7 -4
View File
@@ -5,17 +5,20 @@ $jsFiles = [
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Login</h2>
<div class="box">
<form id="loginForm" action="/login" method="post">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" autofocus>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" required autofocus>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6">
<p id="loginFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit">Login</button>
</div>
<hr>
<div class="center">
<a class="button yellow" href="/login/google" title="Login with Google">Login with Google</a>
</div>
</form>
</div>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
+34
View File
@@ -0,0 +1,34 @@
<?php
$jsFiles = [
'js/signup.js',
];
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<h2>Sign up</h2>
<div class="box">
<form id="signupForm" action="/signup" method="post">
<?php if (isset($email)): ?>
<p class="justify">No user found with the given email address. Sign up with one click!</p>
<input class="big fullWidth marginTop" type="email" name="email" placeholder="Email address" value="<?= $email ?>" required>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password confirmation" required minlength="6" autofocus>
<?php else: ?>
<input class="big fullWidth" type="email" name="email" placeholder="Email address" required autofocus>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6">
<input class="big fullWidth marginTop" type="password" name="password_confirm" placeholder="Password confirmation" minlength="6">
<?php endif; ?>
<p id="signupFormError" class="formError justify marginTop"></p>
<div class="right">
<button class="marginTop" type="submit">Sign up</button><!--
--><?php if (isset($email)): ?><!--
--><button id="resetSignupButton" class="gray marginTop marginLeft" type="button">Reset</button>
<?php endif; ?>
</div>
<hr>
<div class="center">
<a class="button yellow" href="/login/google" title="Signup with Google">Signup with Google</a>
</div>
</form>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
+8
View File
@@ -0,0 +1,8 @@
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<h2>Sign up</h2>
<div class="box">
<p class="justify">Sign up was successful. Please check your email and click on the activation link to activate your account!</p>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
+93 -13
View File
@@ -5,36 +5,34 @@ $cssFiles = [
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Playable maps</h2>
<div id="mapContainer">
<?php foreach ($maps as $map): ?>
<div class="mapItem">
<div class="title">
<p class="title"><?= $map['name'] ?></p>
</div>
<div class="imgContainer">
<img src="https://maps.googleapis.com/maps/api/staticmap?size=350x175&visible=<?= $map['bound_south_lat'] . ',' . $map['bound_west_lng'] . '|' . $map['bound_north_lat'] . ',' . $map['bound_east_lng'] ?>&key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>" alt="Map area">
</div>
<div class="imgContainer" data-bound-south-lat="<?= $map['bound_south_lat'] ?>" data-bound-west-lng="<?= $map['bound_west_lng'] ?>" data-bound-north-lat="<?= $map['bound_north_lat'] ?>" data-bound-east-lng="<?= $map['bound_east_lng'] ?>"></div>
<div class="inner">
<div class="info">
<p>
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" />
</svg>
<?= $map['num_places'] ?> places
</svg><!--
--><?= $map['num_places'] ?> places
</p>
<p>
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.5 2h-9V1h9v1zm-10 1.5v9h-1v-9h1zm11 9v-9h1v9h-1zM3.5 14h9v1h-9v-1z" />
<path fill-rule="evenodd" d="M14 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 1a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 1a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM2 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 1a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 1a2 2 0 1 0 0-4 2 2 0 0 0 0 4z" />
</svg>
~ <?= $map['area'][0] ?> <?= $map['area'][1] ?><sup>2</sup>
</svg><!--
-->~ <?= $map['area'][0] ?> <?= $map['area'][1] ?><sup>2</sup>
</p>
</div>
<p class="small justify marginTop"><?= $map['description'] ?></p>
<div class="description marginTop">
<p class="small center"><?= $map['description'] ?></p>
</div>
</div>
<?php if ($isAdmin): ?>
<div class="buttonContainer">
@@ -62,10 +60,90 @@ $cssFiles = [
<?php endfor; ?>
<?php endif; ?>
</div>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<script>
(function () {
const GOOGLE_MAPS_JS_API_KEY = '<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>';
var Maps = {
descriptionDivs: null,
addStaticMaps: function () {
var imgContainers = document.getElementById('mapContainer').getElementsByClassName('imgContainer');
for (var i = 0; i < imgContainers.length; i++) {
var imgContainer = imgContainers[i];
var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' +
'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&' +
'visible=' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng +
'&key=' + GOOGLE_MAPS_JS_API_KEY;
imgContainer.style.backgroundImage = 'url("' + imgSrc + '")';
}
},
initializeDescriptionDivs: function () {
Maps.descriptionDivs = document.getElementById('mapContainer').getElementsByClassName('description');
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
var description = Maps.descriptionDivs[i];
var boundingClientRect = description.getBoundingClientRect();
description.defaultHeight = boundingClientRect.height;
}
},
calculateDescriptionDivHeights: function () {
var currentY;
var rows = [];
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
var description = Maps.descriptionDivs[i];
var boundingClientRect = description.getBoundingClientRect();
if (currentY !== boundingClientRect.y) {
rows.push([]);
}
rows[rows.length - 1].push(description);
currentY = boundingClientRect.y;
}
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var maxHeight = 0;
for (var j = 0; j < row.length; j++) {
var description = row[j];
if (description.defaultHeight > maxHeight) {
maxHeight = description.defaultHeight;
}
}
for (var j = 0; j < row.length; j++) {
var description = row[j];
description.style.height = maxHeight + 'px';
}
}
}
};
Maps.addStaticMaps();
Maps.initializeDescriptionDivs();
Maps.calculateDescriptionDivHeights();
window.onresize = function () {
Maps.calculateDescriptionDivHeights();
};
})();
</script>
<?php if ($isAdmin): ?>
<script>
(function() {
(function () {
Maps = {
deleteMap: function(mapId, mapName) {
MapGuesser.showModalWithContent('Delete map', 'Are you sure you want to delete map \'' + mapName + '\'?', [{
@@ -91,7 +169,9 @@ $cssFiles = [
};
var buttons = document.getElementById('mapContainer').getElementsByClassName('deleteButton');
for (var button of buttons) {
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
button.onclick = function() {
Maps.deleteMap(this.dataset.mapId, this.dataset.mapName);
};
+5 -6
View File
@@ -5,21 +5,20 @@ $jsFiles = [
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Profile</h2>
<div class="box">
<form id="profileForm" action="/profile" method="post">
<input class="big fullWidth" type="password" name="password" placeholder="Current password" autofocus>
<hr>
<?php /* TODO: disabled for the time being, email modification should be implemented */ ?>
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled>
<input class="big fullWidth marginTop" type="password" name="password_new" placeholder="New password" autofocus>
<input class="big fullWidth marginTop" type="password" name="password_new_confirm" placeholder="New password confirmation">
<hr>
<input class="big fullWidth" type="password" name="password" placeholder="Current password">
<input class="big fullWidth marginTop" type="password" name="password_new" placeholder="New password" minlength="6">
<input class="big fullWidth marginTop" type="password" name="password_new_confirm" placeholder="New password confirmation" minlength="6">
<p id="profileFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit" name="save" disabled>Save</button>
</div>
</form>
</div>
</div>
<?php require ROOT . '/views/templates/footer.php'; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
-22
View File
@@ -1,22 +0,0 @@
<?php
$jsFiles = [
'js/signup.js',
];
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Sign up</h2>
<div class="box">
<form id="signupForm" action="/signup" method="post">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" autofocus>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password">
<input class="big fullWidth marginTop" type="password" name="password_confirm" placeholder="Password confirmation">
<p id="signupFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit">Sign up</button>
</div>
</form>
</div>
</div>
<?php require ROOT . '/views/templates/main_footer.php'; ?>
+5
View File
@@ -0,0 +1,5 @@
</main>
<footer>
<p><span class="bold"><?= $_ENV['APP_NAME'] ?></span> <?= str_replace('Release_', '', VERSION) ?></p><!--
--><p>&copy; Pőcze Bence <?= (new DateTime(REVISION_DATE))->format('Y') ?></p>
</footer>
+9 -8
View File
@@ -1,18 +1,18 @@
<div class="header">
<header>
<h1>
<a href="/" title="MapGuesser">
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>">
<span>MapGuesser</span>
<a href="/" title="<?= $_ENV['APP_NAME'] ?>">
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>"><!--
--><span><?= $_ENV['APP_NAME'] ?></span>
</a>
</h1>
<p class="header">
<p>
<?php if (Container::$request->user()) : ?>
<span><a href="/profile" title="Profile">
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>
<?= Container::$request->user()->getDisplayName() ?><!--
</svg><!--
--><?= Container::$request->user()->getDisplayName() ?><!--
--></a></span><!--
--><span><a href="/logout" title="Logout">Logout</a></span>
<?php else : ?>
@@ -20,4 +20,5 @@
--><span><a href="/login" title="Login">Login</a></span>
<?php endif; ?>
</p>
</div>
</header>
<main>
+32 -1
View File
@@ -1,7 +1,10 @@
<script>
const STATIC_ROOT = '<?= $_ENV['STATIC_ROOT'] ?>';
const REVISION = '<?= REVISION ?>';
const ANTI_CSRF_TOKEN = '<?= $_SESSION['anti_csrf_token'] ?>';
var ANTI_CSRF_TOKEN = '<?= \Container::$request->session()->get('anti_csrf_token') ?>';
<?php if (!empty($_ENV['GOOGLE_ANALITICS_ID'])): ?>
const GOOGLE_ANALITICS_ID = '<?= $_ENV['GOOGLE_ANALITICS_ID'] ?>';
<?php endif; ?>
</script>
<script src="<?= $_ENV['STATIC_ROOT'] ?>/js/mapguesser.js?rev=<?= REVISION ?>"></script>
<?php if (isset($jsFiles)) : ?>
@@ -14,5 +17,33 @@
<script src="<?= $jsFile ?>"></script>
<?php endforeach; ?>
<?php endif; ?>
<?php if (!isset($_COOKIE['COOKIES_CONSENT'])): ?>
<script>
(function () {
// 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();
};
})();
</script>
<?php else: ?>
<?php if (!empty($_ENV['GOOGLE_ANALITICS_ID'])): ?>
<script>
(function () {
MapGuesser.initGoogleAnalitics();
})();
</script>
<?php endif; ?>
<?php endif; ?>
</body>
</html>
+9 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MapGuesser</title>
<title><?= $_ENV['APP_NAME'] ?></title>
<link href="<?= $_ENV['STATIC_ROOT'] ?>/css/mapguesser.css?rev=<?= REVISION ?>" rel="stylesheet">
<?php if (isset($cssFiles)) : ?>
<?php foreach ($cssFiles as $cssFile) : ?>
@@ -22,6 +22,14 @@
<link rel="icon" type="image/png" sizes="16x16" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/16x16.png?rev=<?= REVISION ?>">
</head>
<body>
<?php if (!isset($_COOKIE['COOKIES_CONSENT'])): ?>
<div id="cookiesNotice">
<p class="small">
<?= $_ENV['APP_NAME'] ?> uses cookies to improve user experience. By using the app or clicking 'Agree', you consent to our use of cookies.
</p>
<button id="agreeCookiesButton" class="small marginTop">Agree</button>
</div>
<?php endif; ?>
<div id="loading">
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/loading.svg?rev=<?= REVISION ?>" width="64" height="64">
</div>
+36 -18
View File
@@ -12,17 +12,31 @@ if (!empty($_ENV['DEV'])) {
Container::$routeCollection = new MapGuesser\Routing\RouteCollection();
Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']);
Container::$routeCollection->get('login', 'login', [MapGuesser\Controller\LoginController::class, 'getLoginForm']);
Container::$routeCollection->post('login-action', 'login', [MapGuesser\Controller\LoginController::class, 'login']);
Container::$routeCollection->get('signup', 'signup', [MapGuesser\Controller\SignupController::class, 'getSignupForm']);
Container::$routeCollection->post('signup-action', 'signup', [MapGuesser\Controller\SignupController::class, 'signup']);
Container::$routeCollection->get('signup.activate', 'signup/activate/{token}', [MapGuesser\Controller\SignupController::class, 'activate']);
Container::$routeCollection->get('signup.cancel', 'signup/cancel/{token}', [MapGuesser\Controller\SignupController::class, 'cancel']);
Container::$routeCollection->get('index', '', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->get('startSession', 'startSession.json', [MapGuesser\Controller\HomeController::class, 'startSession']);
Container::$routeCollection->group('login', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('login', '', [MapGuesser\Controller\LoginController::class, 'getLoginForm']);
$routeCollection->post('login-action', '', [MapGuesser\Controller\LoginController::class, 'login']);
$routeCollection->get('login-google', 'google', [MapGuesser\Controller\LoginController::class, 'getGoogleLoginRedirect']);
$routeCollection->get('login-google-action', 'google/code', [MapGuesser\Controller\LoginController::class, 'loginWithGoogle']);
});
Container::$routeCollection->group('signup', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('signup', '', [MapGuesser\Controller\LoginController::class, 'getSignupForm']);
$routeCollection->post('signup-action', '', [MapGuesser\Controller\LoginController::class, 'signup']);
$routeCollection->get('signup-google', 'google', [MapGuesser\Controller\LoginController::class, 'getSignupWithGoogleForm']);
$routeCollection->post('signup-google-action', 'google', [MapGuesser\Controller\LoginController::class, 'signupWithGoogle']);
$routeCollection->post('signup.reset', 'reset', [MapGuesser\Controller\LoginController::class, 'resetSignup']);
$routeCollection->post('signup-google.reset', 'google/reset', [MapGuesser\Controller\LoginController::class, 'resetGoogleSignup']);
$routeCollection->get('signup.success', 'success', [MapGuesser\Controller\LoginController::class, 'getSignupSuccess']);
$routeCollection->get('signup.activate', 'activate/{token}', [MapGuesser\Controller\LoginController::class, 'activate']);
$routeCollection->get('signup.cancel', 'cancel/{token}', [MapGuesser\Controller\LoginController::class, 'cancel']);
});
Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
Container::$routeCollection->get('profile', 'profile', [MapGuesser\Controller\UserController::class, 'getProfile']);
Container::$routeCollection->post('profile-action', 'profile', [MapGuesser\Controller\UserController::class, 'saveProfile']);
Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('profile', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('profile', '', [MapGuesser\Controller\UserController::class, 'getProfile']);
$routeCollection->post('profile-action', '', [MapGuesser\Controller\UserController::class, 'saveProfile']);
});
//Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']);
@@ -36,15 +50,19 @@ Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCo
$routeCollection->post('admin.deleteMap', 'deleteMap/{mapId}', [MapGuesser\Controller\MapAdminController::class, 'deleteMap']);
});
Container::$sessionHandler = new MapGuesser\Session\DatabaseSessionHandler();
if (isset($_COOKIE['COOKIES_CONSENT'])) {
Container::$sessionHandler = new MapGuesser\Session\DatabaseSessionHandler();
session_set_save_handler(Container::$sessionHandler, true);
session_start([
'gc_maxlifetime' => 604800,
'cookie_lifetime' => 604800,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax'
]);
session_set_save_handler(Container::$sessionHandler, true);
session_start([
'gc_maxlifetime' => 604800,
'cookie_lifetime' => 604800,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax'
]);
} else {
$_SESSION = [];
}
Container::$request = new MapGuesser\Request\Request($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'], $_GET, $_POST, $_SESSION);