Compare commits

...
Author SHA1 Message Date
bence 22f4abd82d MAPG-80 mark guessable area on static maps
default-pipeline default-pipeline #198
2021-01-01 22:41:17 +01:00
bence bf27f15709 Merge pull request 'MAPG-2020 run PHPStan with 1G memory limit' (#4) from bugfix/MAPG-2020-phpstan-needs-bigger-memory-limit into develop
default-pipeline default-pipeline #12
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/4
2021-01-01 22:36:56 +01:00
bence db3094eb62 MAPG-2020 run PHPStan with 1G memory limit
default-pipeline default-pipeline #11
2021-01-01 22:34:32 +01:00
bence 77c71ab2da Merge pull request 'MAPG-201 update Git link to gitea.e5tv.hu' (#2) from feature/MAPG-201-migrate-to-own-git-server into develop
Check 2 Check 2 was successful
Check 3 Check 3 was not successful
Check 4 Check 4 was successful
Check 5 Check 5 was successful
Check Check was successful
Pipeline Pipeline #1
test_pipeline test_pipeline #3
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/2
2020-12-27 22:12:33 +00:00
bence 8327cd975a MAPG-201 update Git link to gitea.e5tv.hu 2020-12-27 23:08:30 +01:00
bence cc8148fcf9 Merged in bugfix/MAPG-196-fix-remote-static-root-detection (pull request #181)
MAPG-196 fix regex that recognizes remote static root
2020-07-06 20:00:49 +00:00
bence 2a7b5fb992 MAPG-196 fix regex that recognizes remote static root 2020-07-06 21:59:25 +02:00
bence a4c77c3d32 Merged in bugfix/MAPG-196-fix-compatibility-issues (pull request #180)
MAPG-196 revert link preload to plain stylesheet because it is not supported everywhere
2020-07-06 19:15:32 +00:00
bence 317329ae84 MAPG-196 revert link preload to plain stylesheet because it is not supported everywhere 2020-07-06 21:12:56 +02:00
bence 234cb7f086 Merged in feature/MAPG-196-fix-issues-reported-by-lighthouse (pull request #179)
Feature/MAPG-196 fix issues reported by lighthouse
2020-07-06 18:05:51 +00:00
bence 66c709f00d MAPG-196 reposition of cookie notice 2020-07-06 20:04:39 +02:00
bence 2737c7d6d6 MAPG-196 speed up asset loading, add missing description, add missing image alts 2020-07-06 20:04:39 +02:00
bence 2ccee26424 Merged in feature/MAPG-199-static-analyser-save-artifact (pull request #178)
MAPG-199 save static code analysis results as artifact
2020-07-06 16:52:21 +00:00
bence 8cbf611189 MAPG-199 save static code analysis results as artifact 2020-07-06 18:50:17 +02:00
bence 2ac9794e3f Merged in feature/MAPG-199-introduce-static-php-analyser (pull request #177)
Feature/MAPG-199 introduce static php analyser
2020-07-05 21:23:24 +00:00
bence 9e5f7405d1 MAPG-199 move test views to another folder to prevent code analysis errors 2020-07-05 23:18:57 +02:00
bence af98d71924 MAPG-199 fix issues found by PHPStan 2020-07-05 23:18:56 +02:00
bence 39d691fd7f MAPG-199 add static code analysis step to pipeline 2020-07-05 22:51:36 +02:00
bence 405488cf4e MAPG-199 add PHPStan to require-dev 2020-07-05 21:35:49 +02:00
bence f2736d4d6e Merged in hotfix/MAPG-198-fix-not-working-game (pull request #176)
MAPG-198 re-add table name to Select
2020-07-05 19:19:53 +00:00
bence 88b9821c61 MAPG-198 re-add table name to Select
(table name is used when counting)
2020-07-05 21:17:29 +02:00
bence da2c8d96a4 Merged in bugfix/MAPG-142-fix-timestamp-onupdate (pull request #175)
MAPG-142 fix timestamp onupdate issue
2020-07-05 16:24:17 +00:00
bence cd581c6fcd MAPG-142 fix timestamp onupdate issue 2020-07-05 18:23:02 +02:00
bence f7b268e10f Merged in feature/MAPG-185-create-cron-script (pull request #174)
Feature/MAPG-185 create cron script
2020-07-05 15:56:12 +00:00
bence a8b1ab3057 MAPG-185 adapt README 2020-07-05 17:52:13 +02:00
bence 8403c21ec0 MAPG-185 consolidation of email templates 2020-07-05 17:37:02 +02:00
bence 0ba1cda884 MAPG-185 fix getter of User 2020-07-05 17:36:27 +02:00
bence b2e09c394b MAPG-185 switch off builtin session gc 2020-07-05 16:48:37 +02:00
bence 0bcb0187c1 MAPG-185 rename migrate database command 2020-07-05 16:45:38 +02:00
bence f3d1608164 MAPG-185 add maintain database command 2020-07-05 16:43:36 +02:00
bence b254603fb1 MAPG-185 add getter for expired users and password resetters 2020-07-05 16:37:37 +02:00
bence 776561a41c Merged in feature/MAPG-193-replace-direct-selects-to-repository-calls (pull request #173)
Feature/MAPG-193 replace direct selects to repository calls
2020-07-05 13:20:08 +00:00
bence 27fc687043 MAPG-193 replace Select usage to repository calls where it is easily possible 2020-07-05 15:17:28 +02:00
bence 45869321ac MAPG-193 fix PersistentDataManager::selectMultipleFromDb 2020-07-05 15:15:29 +02:00
bence b58137acc9 Merged in feature/MAPG-142-resend-activation-link-functionality (pull request #172)
Feature/MAPG-142 resend activation link functionality
2020-07-05 12:32:32 +00:00
bence dd6bb5ef9a MAPG-142 limit password reset query if the existing is not expired 2020-07-05 14:24:06 +02:00
bence 37094e552b MAPG-142 modify UserPasswordResetterRepository to return only one resetter for user
(it is not possible for an user to have multiple)
2020-07-05 14:24:06 +02:00
bence 32392590bd MAPG-142 save user creation date 2020-07-05 14:24:06 +02:00
bence 38885849de MAPG-142 remove unnecessary 'now' from DateTime constructor 2020-07-05 14:24:05 +02:00
bence ef013b8b9e MAPG-142 rename app to MapGuesser in .env.example 2020-07-05 14:24:05 +02:00
bence ab23b37b97 MAPG-142 redefine tokens and increase OAuth security with nonce 2020-07-05 14:24:05 +02:00
bence 7e3315fc88 MAPG-142 implemenet confirmation mail resend 2020-07-05 13:39:56 +02:00
bence 6cafac1b65 MAPG-142 modify UserConfirmationRepository to return only one confirmation for user
(it is not possible for an user to have multiple)
2020-07-05 13:33:02 +02:00
bence 631582a912 Merged in feature/MAPG-191-get-rid-of-unnecessary-passing-by-reference (pull request #171)
Feature/MAPG-191 get rid of unnecessary passing by reference
2020-07-04 23:02:57 +00:00
bence 6db6208896 MAPG-191 don't create separate variable for data where not necessary 2020-07-05 00:58:03 +02:00
bence e17cf68007 MAPG-191 don't pass data by reference to IContent 2020-07-05 00:25:34 +02:00
bence 091afb0aab Merged in feature/MAPG-141-password-forgotten-functionality (pull request #170)
Feature/MAPG-141 password forgotten functionality
2020-07-04 22:15:50 +00:00
bence 954f111254 MAPG-141 delete password resetters when deleting account 2020-07-05 00:09:45 +02:00
bence de1d7338a4 MAPG-141 add reset password functionality 2020-07-05 00:09:11 +02:00
bence 7539f637b0 MAPG-141 add and fix some links to login/signup 2020-07-05 00:04:35 +02:00
bence 67534a22f5 MAPG-141 add password resetter table, model and repository 2020-07-05 00:03:56 +02:00
bence 43aef22c4e Merged in feature/MAPG-44-possibility-to-add-pov-data-to-locations (pull request #169)
Feature/MAPG-44 possibility to add pov data to locations
2020-07-03 23:12:56 +00:00
bence 3123643bb7 MAPG-44 modify game to use pov of place 2020-07-04 01:11:04 +02:00
bence b736ce8970 MAPG-44 modify MapAdminController to get/save place pov data 2020-07-04 01:10:03 +02:00
bence 4e48e6ae73 MAPG-44 add Pov class and adjust Place model to store pov 2020-07-04 01:09:00 +02:00
bence 028d8a22a2 MAPG-44 modify DB structure to store pov data 2020-07-04 01:06:37 +02:00
bence a73e60ae5a Merged in bugfix/MAPG-189-fix-inputwithbuttons-input-padding (pull request #168)
MAPG-189 fix div.inputWithButton>input's padding when it is focused
2020-07-03 21:51:38 +00:00
bence 5383da81b3 MAPG-189 fix div.inputWithButton>input's padding when it is focused 2020-07-03 23:50:22 +02:00
92 changed files with 1189 additions and 318 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
APP_NAME=MapQuiz APP_NAME=MapGuesser
DEV=1 DEV=1
DB_HOST=mariadb DB_HOST=mariadb
DB_USER=mapguesser DB_USER=mapguesser
-1
View File
@@ -1,4 +1,3 @@
.env .env
installed installed
unit_test_results.xml
vendor vendor
+9 -1
View File
@@ -9,7 +9,7 @@ This is the MapGuesser Application project. This is a game about guessing where
The first step is obviously cloning the repository to your machine: The first step is obviously cloning the repository to your machine:
``` ```
git clone https://bitbucket.org/esoko/mapguesser.git git clone https://gitea.e5tv.hu/esoko/mapguesser.git
``` ```
All the commands listed here should be executed from the repository root. All the commands listed here should be executed from the repository root.
@@ -46,6 +46,14 @@ One very important variable is `DEV`. This indicates that the application operat
If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, etc.) are fine with the default value. If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, etc.) are fine with the default value.
### (Production only) Create cron job
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommened to create a cron job that runs every hour:
```
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1
```
### Finalize installation ### Finalize installation
After you set the environment variables in the `.env` file, execute the following command: After you set the environment variables in the `.env` file, execute the following command:
+11
View File
@@ -13,3 +13,14 @@ pipelines:
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install - composer install
- vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests - vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests
- step:
name: Static Code Analysis
caches:
- composer
artifacts:
- static_code_analysis_results.json
script:
- apt-get update && apt-get install -y unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install
- php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json
+2 -1
View File
@@ -9,7 +9,8 @@
"phpmailer/phpmailer": "^6.1" "phpmailer/phpmailer": "^6.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9" "phpunit/phpunit": "^9",
"phpstan/phpstan": "^0.12.32"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
Generated
+57 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "fb37b0b958cdf02d82468cb4596613ad", "content-hash": "34563bfc619f47473b2e37a5639dd63e",
"packages": [ "packages": [
{ {
"name": "phpmailer/phpmailer", "name": "phpmailer/phpmailer",
@@ -1379,6 +1379,62 @@
], ],
"time": "2020-03-05T15:02:03+00:00" "time": "2020-03-05T15:02:03+00:00"
}, },
{
"name": "phpstan/phpstan",
"version": "0.12.32",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "d03863f504c8432b3de4d1881f73f6acb8c0e67c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03863f504c8432b3de4d1881f73f6acb8c0e67c",
"reference": "d03863f504c8432b3de4d1881f73f6acb8c0e67c",
"shasum": ""
},
"require": {
"php": "^7.1"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.12-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://www.patreon.com/phpstan",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
"time": "2020-07-01T11:57:52+00:00"
},
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "8.0.2", "version": "8.0.2",
@@ -0,0 +1,8 @@
ALTER TABLE
`places`
ADD
`pov_heading` decimal(6, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_pitch` decimal(5, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_zoom` decimal(5, 4) NOT NULL DEFAULT 0.0;
@@ -0,0 +1,10 @@
CREATE TABLE `user_password_resetters` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`token` varchar(32) CHARACTER SET ascii NOT NULL,
`expires` timestamp NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `token` (`token`),
CONSTRAINT `user_password_resetters_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -0,0 +1,5 @@
UPDATE `user_confirmations` SET token=SUBSTRING(token, 1, 32);
ALTER TABLE `user_confirmations`
ADD `last_sent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `token` varchar(32) CHARACTER SET ascii NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE `sessions`
MODIFY `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,4 @@
UPDATE `sessions` SET id=SUBSTRING(id, 1, 32);
ALTER TABLE `sessions`
MODIFY `id` varchar(32) CHARACTER SET ascii NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE `users`
ADD `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
+13
View File
@@ -0,0 +1,13 @@
Hi,
<br><br>
You recently requested password reset on {{APP_NAME}} with this email address ({{EMAIL}}).
To reset the password to your account, please click on the following link:<br>
<a href="{{RESET_LINK}}" title="Reset password">{{RESET_LINK}}</a>
<br><br>
You can reset your password with this link util {{EXPIRES}}.
<br><br>
If you did not requested password reset, no further action is required, your account is not touched.
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
+7 -3
View File
@@ -1,10 +1,14 @@
Hi, Hi,
<br><br> <br><br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}). To activate your account, please click on the following link:<br> You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
To activate your account, please click on the following link:<br>
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a> <a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
<br><br> <br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required, your email address will be deleted soon.<br> You can activate your account until {{ACTIVATABLE_UNTIL}}.
However if you want to immediately delete it, please click on the following link:<br> If you don't activate your account, your email address will be permanently deleted after this point of time.
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
However if you want to immediately delete your email address, please click on the following link:<br>
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a> <a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
<br><br> <br><br>
Have fun on {{APP_NAME}}! Have fun on {{APP_NAME}}!
+1 -1
View File
@@ -16,7 +16,7 @@ class Container
{ {
static MapGuesser\Interfaces\Database\IConnection $dbConnection; static MapGuesser\Interfaces\Database\IConnection $dbConnection;
static MapGuesser\Routing\RouteCollection $routeCollection; static MapGuesser\Routing\RouteCollection $routeCollection;
static \SessionHandlerInterface $sessionHandler; static MapGuesser\Interfaces\Session\ISessionHandler $sessionHandler;
static MapGuesser\Interfaces\Request\IRequest $request; static MapGuesser\Interfaces\Request\IRequest $request;
} }
+2 -1
View File
@@ -5,8 +5,9 @@ require 'main.php';
$app = new Symfony\Component\Console\Application('MapGuesser Console', ''); $app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\DatabaseMigration()); $app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
$app->add(new MapGuesser\Cli\AddUserCommand()); $app->add(new MapGuesser\Cli\AddUserCommand());
$app->add(new MapGuesser\Cli\LinkViewCommand()); $app->add(new MapGuesser\Cli\LinkViewCommand());
$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand());
$app->run(); $app->run();
+9
View File
@@ -0,0 +1,9 @@
parameters:
level: 5
checkMissingIterableValueType: false
paths:
- main.php
- mapg
- web.php
- src
- tests
+2 -1
View File
@@ -26,8 +26,9 @@ if ($match !== null) {
} }
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) { if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
$content = new MapGuesser\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
header('Content-Type: text/html; charset=UTF-8', true, 403); header('Content-Type: text/html; charset=UTF-8', true, 403);
echo json_encode(['error' => 'no_valid_anti_csrf_token']); $content->render();
return; return;
} }
+13 -1
View File
@@ -298,7 +298,11 @@ input.big:focus, select.big:focus {
div.inputWithButton>input { div.inputWithButton>input {
width: 100%; width: 100%;
padding: 0 83px 0 5px; padding: 0 83px 0 6px;
}
div.inputWithButton>input:focus {
padding: 0 82px 0 5px;
} }
textarea.big:focus { textarea.big:focus {
@@ -400,9 +404,17 @@ div.buttonContainer>button {
} }
#cookiesNotice { #cookiesNotice {
position: fixed;
left: 0;
bottom: 0;
right: 0;
margin: 20px;
background-color: #eeeeee; background-color: #eeeeee;
border: solid #888888 1px;
border-radius: 3px;
padding: 10px; padding: 10px;
text-align: center; text-align: center;
z-index: 10;
} }
#loading { #loading {
+7 -4
View File
@@ -8,6 +8,7 @@
rounds: [], rounds: [],
scoreSum: 0, scoreSum: 0,
panoId: null, panoId: null,
pov: null,
panorama: null, panorama: null,
map: null, map: null,
guessMarker: null, guessMarker: null,
@@ -35,6 +36,7 @@
} }
Game.panoId = this.response.panoId; Game.panoId = this.response.panoId;
Game.pov = this.response.pov;
if (this.response.history) { if (this.response.history) {
for (var i = 0; i < this.response.history.length; ++i) { for (var i = 0; i < this.response.history.length; ++i) {
@@ -116,7 +118,7 @@
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS); document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
Game.loadPano(Game.panoId); Game.loadPano(Game.panoId, Game.pov);
}, },
handleErrorResponse: function (error) { handleErrorResponse: function (error) {
@@ -129,13 +131,13 @@
}); });
}, },
loadPano: function (panoId) { loadPano: function (panoId, pov) {
if (Game.adaptGuess) { if (Game.adaptGuess) {
document.getElementById('guess').classList.add('adapt'); document.getElementById('guess').classList.add('adapt');
} }
Game.panorama.setPov({ heading: 0, pitch: 0 }); Game.panorama.setPov({ heading: pov.heading, pitch: pov.pitch });
Game.panorama.setZoom(0); Game.panorama.setZoom(pov.zoom);
Game.panorama.setPano(panoId); Game.panorama.setPano(panoId);
}, },
@@ -199,6 +201,7 @@
} }
Game.panoId = this.response.panoId; Game.panoId = this.response.panoId;
Game.pov = this.response.pov;
}, data); }, data);
}, },
+6 -3
View File
@@ -8,9 +8,12 @@
var imgContainer = imgContainers[i]; var imgContainer = imgContainers[i];
var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' + var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' +
'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&' + 'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&path=color:0x0000ff40|weight:3|fillcolor:0x0000ff20|' +
'visible=' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng + imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng + '|' +
imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundEastLng + '|' +
imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng +
'&key=' + GOOGLE_MAPS_JS_API_KEY; '&key=' + GOOGLE_MAPS_JS_API_KEY;
imgContainer.style.backgroundImage = 'url("' + imgSrc + '")'; imgContainer.style.backgroundImage = 'url("' + imgSrc + '")';
+1 -1
View File
@@ -16,7 +16,7 @@ echo "Installing MapGuesser DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
echo "Migrating DB..." echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate) (cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..." echo "Minifying JS, CSS and SVG files..."
+1 -1
View File
@@ -15,7 +15,7 @@ echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install) (cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..." echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate) (cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..." echo "Minifying JS, CSS and SVG files..."
+3 -1
View File
@@ -1,5 +1,6 @@
<?php namespace MapGuesser\Cli; <?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@@ -9,7 +10,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class AddUserCommand extends Command class AddUserCommand extends Command
{ {
public function configure() public function configure(): void
{ {
$this->setName('user:add') $this->setName('user:add')
->setDescription('Adding of user.') ->setDescription('Adding of user.')
@@ -24,6 +25,7 @@ class AddUserCommand extends Command
$user->setEmail($input->getArgument('email')); $user->setEmail($input->getArgument('email'));
$user->setPlainPassword($input->getArgument('password')); $user->setPlainPassword($input->getArgument('password'));
$user->setActive(true); $user->setActive(true);
$user->setCreatedDate(new DateTime());
if ($input->hasArgument('type')) { if ($input->hasArgument('type')) {
$user->setType($input->getArgument('type')); $user->setType($input->getArgument('type'));
+2 -2
View File
@@ -11,7 +11,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class LinkViewCommand extends Command class LinkViewCommand extends Command
{ {
public function configure() public function configure(): void
{ {
$this->setName('view:link') $this->setName('view:link')
->setDescription('Linking of views.') ->setDescription('Linking of views.')
@@ -39,7 +39,7 @@ class LinkViewCommand extends Command
$view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php'); $view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php');
if (strpos($view, 'templates') === 0) { if (strpos($view, 'templates') === 0 || strpos($view, 'tests') === 0) {
continue; continue;
} }
+107
View File
@@ -0,0 +1,107 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MaintainDatabaseCommand extends Command
{
private PersistentDataManager $pdm;
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct()
{
parent::__construct();
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
}
public function configure(): void
{
$this->setName('db:maintain')
->setDescription('Maintain database.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
try {
$this->deleteInactiveExpiredUsers();
$this->deleteExpiredPasswordResetters();
$this->deleteExpiredSessions();
} catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>Maintenance was successful!</info>');
$output->writeln('');
return 0;
}
private function deleteInactiveExpiredUsers(): void
{
\Container::$dbConnection->startTransaction();
foreach ($this->userRepository->getAllInactiveExpired() as $user) {
//TODO: these can be in some wrapper class
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter);
}
$this->pdm->deleteFromDb($user);
}
\Container::$dbConnection->commit();
}
private function deleteExpiredPasswordResetters(): void
{
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
$this->pdm->deleteFromDb($passwordResetter);
}
}
private function deleteExpiredSessions(): void
{
//TODO: model may be used for sessions too
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']);
$select->where('updated', '<', (new DateTime('-7 days'))->format('Y-m-d H:i:s'));
$result = $select->execute();
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($session['id']);
$modify->delete();
}
}
}
@@ -7,11 +7,11 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
class DatabaseMigration extends Command class MigrateDatabaseCommand extends Command
{ {
public function configure() public function configure(): void
{ {
$this->setName('migrate') $this->setName('db:migrate')
->setDescription('Migration of database changes.'); ->setDescription('Migration of database changes.');
} }
@@ -88,6 +88,10 @@ class DatabaseMigration extends Command
$path = ROOT . '/database/migrations/' . $type; $path = ROOT . '/database/migrations/' . $type;
$dir = opendir($path); $dir = opendir($path);
if ($dir === false) {
throw new \Exception('Cannot open dir: ' . $path);
}
$files = []; $files = [];
while ($file = readdir($dir)) { while ($file = readdir($dir)) {
$filePath = $path . '/' . $file; $filePath = $path . '/' . $file;
+5 -5
View File
@@ -21,18 +21,18 @@ class GameController
public function getGame(): IContent public function getGame(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new HtmlContent('game', $data); return new HtmlContent('game', $this->prepareGame($mapId));
} }
public function getGameJson(): IContent public function getGameJson(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new JsonContent($data); return new JsonContent($this->prepareGame($mapId));
} }
private function prepareGame(int $mapId) private function prepareGame(int $mapId): array
{ {
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
+19 -12
View File
@@ -29,8 +29,7 @@ class GameFlowController
$session = $this->request->session(); $session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found']; return new JsonContent(['error' => 'no_session_found']);
return new JsonContent($data);
} }
if (count($state['rounds']) === 0) { if (count($state['rounds']) === 0) {
@@ -41,7 +40,10 @@ class GameFlowController
$session->set('state', $state); $session->set('state', $state);
$data = ['panoId' => $place->getPanoIdCached()]; $data = [
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()->toArray()
];
} else { } else {
$rounds = count($state['rounds']); $rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1]; $last = $state['rounds'][$rounds - 1];
@@ -59,7 +61,10 @@ class GameFlowController
$data = [ $data = [
'history' => $history, 'history' => $history,
'panoId' => $last['panoId'] 'panoId' => $last['panoId'],
'pov' => isset($last['pov']) ? // should be checked not to break with old sessions
$last['pov']->toArray() :
['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
]; ];
} }
@@ -73,8 +78,7 @@ class GameFlowController
$session = $this->request->session(); $session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found']; return new JsonContent(['error' => 'no_session_found']);
return new JsonContent($data);
} }
$last = $state['rounds'][count($state['rounds']) - 1]; $last = $state['rounds'][count($state['rounds']) - 1];
@@ -103,32 +107,35 @@ class GameFlowController
$this->addNewRoundToState($state, $place, $placesWithoutPano); $this->addNewRoundToState($state, $place, $placesWithoutPano);
$panoId = $place->getPanoIdCached(); $panoId = $place->getPanoIdCached();
$pov = $place->getPov()->toArray();
} else { } else {
$state['rounds'] = []; $state['rounds'] = [];
$panoId = null; $panoId = null;
$pov = null;
} }
$session->set('state', $state); $session->set('state', $state);
$data = [ return new JsonContent([
'result' => [ 'result' => [
'position' => $position->toArray(), 'position' => $position->toArray(),
'distance' => $distance, 'distance' => $distance,
'score' => $score 'score' => $score
], ],
'panoId' => $panoId 'panoId' => $panoId,
]; 'pov' => $pov
return new JsonContent($data); ]);
} }
private function addNewRoundToState(&$state, Place $place, array $placesWithoutPano): void private function addNewRoundToState(array &$state, Place $place, array $placesWithoutPano): void
{ {
$state['rounds'][] = [ $state['rounds'][] = [
'placesWithoutPano' => $placesWithoutPano, 'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place->getId(), 'placeId' => $place->getId(),
'position' => $place->getPosition(), 'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached() 'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()
]; ];
} }
+1 -2
View File
@@ -24,7 +24,6 @@ class HomeController
{ {
// session starts with the request, this method just sends valid data to the client // session starts with the request, this method just sends valid data to the client
$data = ['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')]; return new JsonContent(['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')]);
return new JsonContent($data);
} }
} }
+282 -67
View File
@@ -1,5 +1,7 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateInterval;
use DateTime;
use MapGuesser\Http\Request; use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
@@ -8,8 +10,10 @@ use MapGuesser\Mailing\Mail;
use MapGuesser\OAuth\GoogleOAuth; use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation; use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserRepository; use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
@@ -26,12 +30,15 @@ class LoginController
private UserConfirmationRepository $userConfirmationRepository; private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
$this->pdm = new PersistentDataManager(); $this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
} }
public function getLoginForm() public function getLoginForm()
@@ -40,18 +47,23 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
} }
$data = []; return new HtmlContent('login/login');
return new HtmlContent('login/login', $data);
} }
public function getGoogleLoginRedirect(): IRedirect public function getGoogleLoginRedirect(): IRedirect
{ {
$state = bin2hex(random_bytes(16)); $state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state); $this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl($state, $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()); $url = $oAuth->getDialogUrl(
$state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
$nonce
);
return new Redirect($url, IRedirect::TEMPORARY); return new Redirect($url, IRedirect::TEMPORARY);
} }
@@ -75,8 +87,7 @@ class LoginController
public function getSignupSuccess(): IContent public function getSignupSuccess(): IContent
{ {
$data = []; return new HtmlContent('login/signup_success');
return new HtmlContent('login/signup_success', $data);
} }
public function getSignupWithGoogleForm() public function getSignupWithGoogleForm()
@@ -93,48 +104,96 @@ class LoginController
$user = $this->userRepository->getByEmail($userData['email']); $user = $this->userRepository->getByEmail($userData['email']);
$data = ['found' => $user !== null, 'email' => $userData['email']]; return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email']]);
return new HtmlContent('login/google_signup', $data); }
public function getRequestPasswordResetForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
}
public function getRequestPasswordResetSuccess(): IContent
{
return new HtmlContent('login/password_reset_request_success');
}
public function getResetPasswordForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new HtmlContent('login/reset_password', ['success' => false]);
}
$user = $this->userRepository->getById($resetter->getUserId());
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]);
} }
public function login(): IContent public function login(): IContent
{ {
if ($this->request->user() !== null) { if ($this->request->user() !== null) {
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
$user = $this->userRepository->getByEmail($this->request->post('email')); $user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) { if ($user === null) {
if (strlen($this->request->post('password')) < 6) { if (strlen($this->request->post('password')) < 6) {
$data = ['error' => ['errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!']]; return new JsonContent([
return new JsonContent($data); 'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
} }
$tmpUser = new User(); $tmpUser = new User();
$tmpUser->setPlainPassword($this->request->post('password')); $tmpUser->setPlainPassword($this->request->post('password'));
$this->request->session()->set('tmp_user_data', ['email' => $this->request->post('email'), 'password_hashed' => $tmpUser->getPassword()]); $this->request->session()->set('tmp_user_data', [
'email' => $this->request->post('email'),
'password_hashed' => $tmpUser->getPassword()
]);
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()]]; return new JsonContent([
return new JsonContent($data); 'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()
]
]);
} }
if (!$user->getActive()) { if (!$user->getActive()) {
$data = ['error' => ['errorText' => 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!']]; $this->resendConfirmationEmail($user);
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
} }
if (!$user->checkPassword($this->request->post('password'))) { if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given password is wrong.']]; return new JsonContent([
return new JsonContent($data); 'error' => [
'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
} }
$this->request->setUser($user); $this->request->setUser($user);
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
public function loginWithGoogle() public function loginWithGoogle()
@@ -144,30 +203,34 @@ class LoginController
} }
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) { if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
$data = []; return new HtmlContent('login/google_login');
return new HtmlContent('login/google_login', $data);
} }
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()); $tokenData = $oAuth->getToken(
$this->request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
);
if (!isset($tokenData['id_token'])) { if (!isset($tokenData['id_token'])) {
$data = []; return new HtmlContent('login/google_login');
return new HtmlContent('login/google_login', $data);
} }
$jwtParser = new JwtParser($tokenData['id_token']); $jwtParser = new JwtParser($tokenData['id_token']);
$userData = $jwtParser->getPayload(); $idToken = $jwtParser->getPayload();
if (!$userData['email_verified']) { if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
$data = []; return new HtmlContent('login/google_login');
return new HtmlContent('login/google_login', $data);
} }
$user = $this->userRepository->getByGoogleSub($userData['sub']); if (!$idToken['email_verified']) {
return new HtmlContent('login/google_login');
}
$user = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($user === null) { if ($user === null) {
$this->request->session()->set('google_user_data', $userData); $this->request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
} }
@@ -187,8 +250,7 @@ class LoginController
public function signup(): IContent public function signup(): IContent
{ {
if ($this->request->user() !== null) { if ($this->request->user() !== null) {
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]; return new JsonContent(['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]);
return new JsonContent($data);
} }
$user = $this->userRepository->getByEmail($this->request->post('email')); $user = $this->userRepository->getByEmail($this->request->post('email'));
@@ -196,22 +258,31 @@ class LoginController
if ($user !== null) { if ($user !== null) {
if ($user->getActive()) { if ($user->getActive()) {
if (!$user->checkPassword($this->request->post('password'))) { if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'There is a user already registered with the given email address, but the given password is wrong.']]; return new JsonContent([
return new JsonContent($data); 'error' => [
'errorText' => 'There is a user already registered with the given email address, ' .
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
} }
$this->request->setUser($user); $this->request->setUser($user);
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]]; $data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]];
} else { } else {
$data = ['error' => ['errorText' => 'There is a user already registered with the given email address. Please check your email and click on the activation link!']]; $data = [
'error' => [
'errorText' => 'There is a user already registered with the given email address. ' .
'Please check your email and click on the activation link!'
]
];
} }
return new JsonContent($data); return new JsonContent($data);
} }
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) { if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
$data = ['error' => ['errorText' => 'The given email address is not valid.']]; return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
return new JsonContent($data);
} }
if ($this->request->session()->has('tmp_user_data')) { if ($this->request->session()->has('tmp_user_data')) {
@@ -221,52 +292,53 @@ class LoginController
$tmpUser->setPassword($tmpUserData['password_hashed']); $tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword($this->request->post('password'))) { if (!$tmpUser->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given passwords do not match.']]; return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
return new JsonContent($data);
} }
} else { } else {
if (strlen($this->request->post('password')) < 6) { if (strlen($this->request->post('password')) < 6) {
$data = ['error' => ['errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!']]; return new JsonContent([
return new JsonContent($data); 'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
} }
if ($this->request->post('password') !== $this->request->post('password_confirm')) { if ($this->request->post('password') !== $this->request->post('password_confirm')) {
$data = ['error' => ['errorText' => 'The given passwords do not match.']]; return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
return new JsonContent($data);
} }
} }
$user = new User(); $user = new User();
$user->setEmail($this->request->post('email')); $user->setEmail($this->request->post('email'));
$user->setPlainPassword($this->request->post('password')); $user->setPlainPassword($this->request->post('password'));
$user->setCreatedDate(new DateTime());
\Container::$dbConnection->startTransaction(); \Container::$dbConnection->startTransaction();
$this->pdm->saveToDb($user); $this->pdm->saveToDb($user);
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime()); $token = bin2hex(random_bytes(16));
$confirmation = new UserConfirmation(); $confirmation = new UserConfirmation();
$confirmation->setUser($user); $confirmation->setUser($user);
$confirmation->setToken($token); $confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation); $this->pdm->saveToDb($confirmation);
\Container::$dbConnection->commit(); \Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token); $this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
$this->request->session()->delete('tmp_user_data'); $this->request->session()->delete('tmp_user_data');
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
public function signupWithGoogle(): IContent public function signupWithGoogle(): IContent
{ {
if ($this->request->user() !== null) { if ($this->request->user() !== null) {
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
$userData = $this->request->session()->get('google_user_data'); $userData = $this->request->session()->get('google_user_data');
@@ -278,6 +350,7 @@ class LoginController
$user = new User(); $user = new User();
$user->setEmail($userData['email']); $user->setEmail($userData['email']);
$user->setCreatedDate(new DateTime());
} else { } else {
$sendWelcomeEmail = false; $sendWelcomeEmail = false;
} }
@@ -294,24 +367,21 @@ class LoginController
$this->request->session()->delete('google_user_data'); $this->request->session()->delete('google_user_data');
$this->request->setUser($user); $this->request->setUser($user);
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
public function resetSignup(): IContent public function resetSignup(): IContent
{ {
$this->request->session()->delete('tmp_user_data'); $this->request->session()->delete('tmp_user_data');
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
public function resetGoogleSignup(): IContent public function resetGoogleSignup(): IContent
{ {
$this->request->session()->delete('google_user_data'); $this->request->session()->delete('google_user_data');
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
public function activate() public function activate()
@@ -320,11 +390,10 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
} }
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token')); $confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) { if ($confirmation === null) {
$data = []; return new HtmlContent('login/activate');
return new HtmlContent('login/activate', $data);
} }
\Container::$dbConnection->startTransaction(); \Container::$dbConnection->startTransaction();
@@ -349,11 +418,10 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
} }
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token')); $confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) { if ($confirmation === null) {
$data = ['success' => false]; return new HtmlContent('login/cancel', ['success' => false]);
return new HtmlContent('login/cancel', $data);
} }
\Container::$dbConnection->startTransaction(); \Container::$dbConnection->startTransaction();
@@ -366,23 +434,156 @@ class LoginController
\Container::$dbConnection->commit(); \Container::$dbConnection->commit();
$data = ['success' => true]; return new HtmlContent('login/cancel', ['success' => true]);
return new HtmlContent('login/cancel', $data);
} }
private function sendConfirmationEmail(string $email, string $token): void public function requestPasswordReset(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) {
return new JsonContent([
'error' => [
'errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!'
]
]);
}
if (!$user->getActive()) {
$this->resendConfirmationEmail($user);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
}
$existingResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($existingResetter !== null && $existingResetter->getExpiresDate() > new DateTime()) {
return new JsonContent([
'error' => [
'errorText' => 'Password reset was recently requested for this account. Please check your email, or try again later!'
]
]);
}
$token = bin2hex(random_bytes(16));
$expires = new DateTime('+1 hour');
$passwordResetter = new UserPasswordResetter();
$passwordResetter->setUser($user);
$passwordResetter->setToken($token);
$passwordResetter->setExpiresDate($expires);
\Container::$dbConnection->startTransaction();
if ($existingResetter !== null) {
$this->pdm->deleteFromDb($existingResetter);
}
$this->pdm->saveToDb($passwordResetter);
\Container::$dbConnection->commit();
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
return new JsonContent(['success' => true]);
}
public function resetPassword(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
]
]);
}
if (strlen($this->request->post('password')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword($this->request->post('password'));
$this->pdm->saveToDb($user);
\Container::$dbConnection->commit();
$this->request->setUser($user);
return new JsonContent(['success' => true]);
}
private function sendConfirmationEmail(string $email, string $token, DateTime $created): void
{ {
$mail = new Mail(); $mail = new Mail();
$mail->addRecipient($email); $mail->addRecipient($email);
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account'); $mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
$mail->setBodyFromTemplate('signup', [ $mail->setBodyFromTemplate('signup', [
'EMAIL' => $email, 'EMAIL' => $email,
'ACTIVATE_LINK' => $this->request->getBase() . '/'. \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]), 'ACTIVATE_LINK' => $this->request->getBase() . '/' .
'CANCEL_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]), \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T')
]); ]);
$mail->send(); $mail->send();
} }
private function resendConfirmationEmail(User $user): bool
{
$confirmation = $this->userConfirmationRepository->getByUser($user);
if ($confirmation === null || (clone $confirmation->getLastSentDate())->add(new DateInterval('PT1H')) > new DateTime()) {
return false;
}
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
return true;
}
private function sendWelcomeEmail(string $email): void private function sendWelcomeEmail(string $email): void
{ {
$mail = new Mail(); $mail = new Mail();
@@ -393,4 +594,18 @@ class LoginController
]); ]);
$mail->send(); $mail->send();
} }
private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
$mail->setBodyFromTemplate('password-reset', [
'EMAIL' => $email,
'RESET_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i T')
]);
$mail->send();
}
} }
+36 -49
View File
@@ -1,10 +1,8 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Authentication\IUser; use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured; use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
@@ -15,7 +13,7 @@ use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
use MapGuesser\Util\Geo\Bounds; use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Panorama\Pov;
class MapAdminController implements ISecured class MapAdminController implements ISecured
{ {
@@ -57,8 +55,13 @@ class MapAdminController implements ISecured
$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', [
return new HtmlContent('admin/map_editor', $data); 'mapId' => $mapId,
'mapName' => $map->getName(),
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
'bounds' => $map->getBounds()->toArray(),
'places' => &$places
]);
} }
public function getPlace(): IContent public function getPlace(): IContent
@@ -67,8 +70,7 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById($placeId); $place = $this->placeRepository->getById($placeId);
$data = ['panoId' => $place->getFreshPanoId()]; return new JsonContent(['panoId' => $place->getFreshPanoId()]);
return new JsonContent($data);
} }
public function saveMap(): IContent public function saveMap(): IContent
@@ -94,6 +96,11 @@ class MapAdminController implements ISecured
$place->setMap($map); $place->setMap($map);
$place->setLat((float) $placeRaw['lat']); $place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']); $place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
if ($placeRaw['panoId'] === -1) { if ($placeRaw['panoId'] === -1) {
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day')); $place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
@@ -114,6 +121,11 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById((int) $placeRaw['id']); $place = $this->placeRepository->getById((int) $placeRaw['id']);
$place->setLat((float) $placeRaw['lat']); $place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']); $place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day')); $place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
$this->pdm->saveToDb($place); $this->pdm->saveToDb($place);
@@ -146,11 +158,11 @@ class MapAdminController implements ISecured
\Container::$dbConnection->commit(); \Container::$dbConnection->commit();
$data = ['mapId' => $map->getId(), 'added' => $addedIds]; return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
return new JsonContent($data);
} }
public function deleteMap() { public function deleteMap(): IContent
{
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId); $map = $this->mapRepository->getById($mapId);
@@ -163,40 +175,22 @@ class MapAdminController implements ISecured
\Container::$dbConnection->commit(); \Container::$dbConnection->commit();
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
private function deletePlaces(Map $map): void private function deletePlaces(Map $map): void
{ {
//TODO: relations? foreach ($this->placeRepository->getAllForMap($map) as $place) {
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(Place::getFields());
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
while ($placeData = $result->fetch(IResultSet::FETCH_ASSOC)) {
$place = new Place();
$this->pdm->fillWithData($placeData, $place);
$this->pdm->deleteFromDb($place); $this->pdm->deleteFromDb($place);
} }
} }
private function calculateMapBounds(Map $map): 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', '=', $map->getId());
$result = $select->execute();
$bounds = new Bounds(); $bounds = new Bounds();
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$bounds->extend(new Position($place['lat'], $place['lng'])); foreach ($this->placeRepository->getAllForMap($map) as $place) {
$bounds->extend($place->getPosition());
} }
return $bounds; return $bounds;
@@ -204,26 +198,19 @@ class MapAdminController implements ISecured
private function &getPlaces(Map $map): 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', '=', $map->getId());
$result = $select->execute();
$places = []; $places = [];
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) { foreach ($this->placeRepository->getAllForMap($map) as $place) {
//$panoId = ??? $noPano = $place->getPanoIdCachedTimestampDate() !== null && $place->getPanoIdCached() === null;
//$pov = ???
$noPano = $place['pano_id_cached_timestamp'] && $place['pano_id_cached'] === null;
$places[$place['id']] = [ $placeId = $place->getId();
'id' => $place['id'],
'lat' => $place['lat'], $places[$placeId] = [
'lng' => $place['lng'], 'id' => $placeId,
'lat' => $place->getLat(),
'lng' => $place->getLng(),
'panoId' => null, 'panoId' => null,
'pov' => ['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0], 'pov' => $place->getPov()->toArray(),
'noPano' => $noPano 'noPano' => $noPano
]; ];
} }
+5 -3
View File
@@ -19,7 +19,7 @@ class MapsController
public function getMaps(): IContent public function getMaps(): IContent
{ {
//TODO: from repository //TODO: from repository - count should be added
$select = new Select(\Container::$dbConnection, 'maps'); $select = new Select(\Container::$dbConnection, 'maps');
$select->columns([ $select->columns([
['maps', 'id'], ['maps', 'id'],
@@ -46,8 +46,10 @@ class MapsController
} }
$user = $this->request->user(); $user = $this->request->user();
$data = ['maps' => $maps, 'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)]; return new HtmlContent('maps', [
return new HtmlContent('maps', $data); 'maps' => $maps,
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
]);
} }
private function formatMapAreaForHuman(float $area): array private function formatMapAreaForHuman(float $area): array
+51 -32
View File
@@ -1,18 +1,16 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use MapGuesser\Database\Query\Select;
use MapGuesser\Http\Request; use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Authorization\ISecured; use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect; use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\OAuth\GoogleOAuth; use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect; use MapGuesser\Response\Redirect;
@@ -26,11 +24,14 @@ class UserController implements ISecured
private UserConfirmationRepository $userConfirmationRepository; private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
$this->pdm = new PersistentDataManager(); $this->pdm = new PersistentDataManager();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
} }
public function authorize(): bool public function authorize(): bool
@@ -47,8 +48,7 @@ class UserController implements ISecured
*/ */
$user = $this->request->user(); $user = $this->request->user();
$data = ['user' => $user->toArray()]; return new HtmlContent('account/account', ['user' => $user->toArray()]);
return new HtmlContent('account/account', $data);
} }
public function getGoogleAuthenticateRedirect(): IRedirect public function getGoogleAuthenticateRedirect(): IRedirect
@@ -59,14 +59,17 @@ class UserController implements ISecured
$user = $this->request->user(); $user = $this->request->user();
$state = bin2hex(random_bytes(16)); $state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state); $this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl( $url = $oAuth->getDialogUrl(
$state, $state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
$nonce,
$user->getEmail() $user->getEmail()
); );
@@ -81,31 +84,40 @@ class UserController implements ISecured
$user = $this->request->user(); $user = $this->request->user();
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) { if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
$data = ['success' => false]; return new HtmlContent('account/google_authenticate', ['success' => false]);
return new HtmlContent('account/google_authenticate', $data);
} }
$oAuth = new GoogleOAuth(new Request()); $oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()); $tokenData = $oAuth->getToken(
$this->request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
);
if (!isset($tokenData['id_token'])) { if (!isset($tokenData['id_token'])) {
$data = ['success' => false]; return new HtmlContent('account/google_authenticate', ['success' => false]);
return new HtmlContent('account/google_authenticate', $data);
} }
$jwtParser = new JwtParser($tokenData['id_token']); $jwtParser = new JwtParser($tokenData['id_token']);
$userData = $jwtParser->getPayload(); $idToken = $jwtParser->getPayload();
if ($userData['sub'] !== $user->getGoogleSub()) { if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
$data = ['success' => false, 'errorText' => 'This Google account is not linked to your account.']; return new HtmlContent('account/google_authenticate', ['success' => false]);
return new HtmlContent('account/google_authenticate', $data); }
if ($idToken['sub'] !== $user->getGoogleSub()) {
return new HtmlContent('account/google_authenticate', [
'success' => false,
'errorText' => 'This Google account is not linked to your account.'
]);
} }
$authenticatedWithGoogleUntil = new DateTime('+45 seconds'); $authenticatedWithGoogleUntil = new DateTime('+45 seconds');
$this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil); $this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
$data = ['success' => true, 'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil]; return new HtmlContent('account/google_authenticate', [
return new HtmlContent('account/google_authenticate', $data); 'success' => true,
'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil
]);
} }
public function getDeleteAccount(): IContent public function getDeleteAccount(): IContent
@@ -115,8 +127,7 @@ class UserController implements ISecured
*/ */
$user = $this->request->user(); $user = $this->request->user();
$data = ['user' => $user->toArray()]; return new HtmlContent('account/delete', ['user' => $user->toArray()]);
return new HtmlContent('account/delete', $data);
} }
public function saveAccount(): IContent public function saveAccount(): IContent
@@ -132,19 +143,24 @@ class UserController implements ISecured
$this->request->post('password'), $this->request->post('password'),
$error $error
)) { )) {
$data = ['error' => ['errorText' => $error]]; return new JsonContent(['error' => ['errorText' => $error]]);
return new JsonContent($data);
} }
if (strlen($this->request->post('password_new')) > 0) { if (strlen($this->request->post('password_new')) > 0) {
if (strlen($this->request->post('password_new')) < 6) { if (strlen($this->request->post('password_new')) < 6) {
$data = ['error' => ['errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!']]; return new JsonContent([
return new JsonContent($data); 'error' => [
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
]
]);
} }
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) { if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
$data = ['error' => ['errorText' => 'The given new passwords do not match.']]; return new JsonContent([
return new JsonContent($data); 'error' => [
'errorText' => 'The given new passwords do not match.'
]
]);
} }
$user->setPlainPassword($this->request->post('password_new')); $user->setPlainPassword($this->request->post('password_new'));
@@ -154,8 +170,7 @@ class UserController implements ISecured
$this->request->session()->delete('authenticated_with_google_until'); $this->request->session()->delete('authenticated_with_google_until');
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
public function deleteAccount(): IContent public function deleteAccount(): IContent
@@ -171,27 +186,31 @@ class UserController implements ISecured
$this->request->post('password'), $this->request->post('password'),
$error $error
)) { )) {
$data = ['error' => ['errorText' => $error]]; return new JsonContent(['error' => ['errorText' => $error]]);
return new JsonContent($data);
} }
\Container::$dbConnection->startTransaction(); \Container::$dbConnection->startTransaction();
foreach ($this->userConfirmationRepository->getByUser($user) as $userConfirmation) { $userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation); $this->pdm->deleteFromDb($userConfirmation);
} }
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter);
}
$this->pdm->deleteFromDb($user); $this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit(); \Container::$dbConnection->commit();
$this->request->session()->delete('authenticated_with_google_until'); $this->request->session()->delete('authenticated_with_google_until');
$data = ['success' => true]; return new JsonContent(['success' => true]);
return new JsonContent($data);
} }
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, &$error): bool private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, string &$error): bool
{ {
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) { if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
return true; return true;
+2 -2
View File
@@ -12,11 +12,11 @@ class Connection implements IConnection
public function __construct(string $host, string $user, string $password, string $db, int $port = -1, string $socket = null) public function __construct(string $host, string $user, string $password, string $db, int $port = -1, string $socket = null)
{ {
if ($port < 0) { if ($port < 0) {
$port = ini_get('mysqli.default_port'); $port = (int) ini_get('mysqli.default_port');
} }
if ($socket === null) { if ($socket === null) {
$socket = ini_get('mysqli.default_socket'); $socket = (string) ini_get('mysqli.default_socket');
} }
$this->connection = new mysqli($host, $user, $password, $db, $port, $socket); $this->connection = new mysqli($host, $user, $password, $db, $port, $socket);
+1 -1
View File
@@ -135,6 +135,6 @@ class Modify
private function generateKey(): string private function generateKey(): string
{ {
return substr(hash('sha256', serialize($this->attributes) . random_bytes(10) . microtime()), 0, 7); return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7);
} }
} }
+5 -5
View File
@@ -30,7 +30,7 @@ class Select
private array $orders = []; private array $orders = [];
private array $limit; private ?array $limit;
public function __construct(IConnection $connection, ?string $table = null) public function __construct(IConnection $connection, ?string $table = null)
{ {
@@ -144,7 +144,7 @@ class Select
$this->limit = null; $this->limit = null;
} }
public function paginate(int $page, int $itemsPerPage) public function paginate(int $page, int $itemsPerPage): Select
{ {
$this->limit($itemsPerPage, ($page - 1) * $itemsPerPage); $this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
@@ -308,7 +308,7 @@ class Select
return implode(' ', $joins); return implode(' ', $joins);
} }
private function generateConditions(string $type): array private function generateConditions(int $type): array
{ {
$conditions = ''; $conditions = '';
$params = []; $params = [];
@@ -376,9 +376,9 @@ class Select
return [$conditionsString, $params]; return [$conditionsString, $params];
} }
private function generateComplexConditionFragment(string $type, Closure $conditionCallback): array private function generateComplexConditionFragment(int $type, Closure $conditionCallback): array
{ {
$instance = new static($this->connection, $this->table); $instance = new self($this->connection, $this->table);
$instance->tableAliases = $this->tableAliases; $instance->tableAliases = $this->tableAliases;
$conditionCallback($instance); $conditionCallback($instance);
+2 -1
View File
@@ -1,7 +1,8 @@
<?php namespace MapGuesser\Database; <?php namespace MapGuesser\Database;
class Utils { class Utils {
public static function backtick(string $name) { public static function backtick(string $name): string
{
return '`' . $name . '`'; return '`' . $name . '`';
} }
} }
+5 -5
View File
@@ -29,7 +29,7 @@ class Request implements IRequest
$this->method = $method; $this->method = $method;
} }
public function setQuery($query) public function setQuery($query): void
{ {
if (is_string($query)) { if (is_string($query)) {
$this->query = $query; $this->query = $query;
@@ -38,7 +38,7 @@ class Request implements IRequest
} }
} }
public function setHeaders(array $headers) public function setHeaders(array $headers): void
{ {
$this->headers = array_merge($this->headers, $headers); $this->headers = array_merge($this->headers, $headers);
} }
@@ -47,13 +47,13 @@ class Request implements IRequest
{ {
$ch = curl_init(); $ch = curl_init();
if ($this->method === self::HTTP_GET) { if ($this->method === self::HTTP_POST) {
$url = $this->url . '?' . $this->query;
} elseif ($this->method === self::HTTP_POST) {
$url = $this->url; $url = $this->url;
curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query); curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query);
} else {
$url = $this->url . '?' . $this->query;
} }
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
+2 -2
View File
@@ -14,12 +14,12 @@ class Response implements IResponse
$this->headers = $headers; $this->headers = $headers;
} }
public function getBody() public function getBody(): string
{ {
return $this->body; return $this->body;
} }
public function getHeaders() public function getHeaders(): array
{ {
return $this->headers; return $this->headers;
} }
+2 -2
View File
@@ -10,9 +10,9 @@ interface IRequest
public function setMethod(int $method): void; public function setMethod(int $method): void;
public function setQuery($query); public function setQuery($query): void;
public function setHeaders(array $headers); public function setHeaders(array $headers): void;
public function send(): IResponse; public function send(): IResponse;
} }
+2 -2
View File
@@ -2,7 +2,7 @@
interface IResponse interface IResponse
{ {
public function getBody(); public function getBody(): string;
public function getHeaders(); public function getHeaders(): array;
} }
+1 -1
View File
@@ -4,7 +4,7 @@ use MapGuesser\Interfaces\Authentication\IUser;
interface IRequest interface IRequest
{ {
public function setParsedRouteParams(array &$routeParams); public function setParsedRouteParams(array &$routeParams): void;
public function getBase(): string; public function getBase(): string;
+3 -1
View File
@@ -2,7 +2,9 @@
interface IContent interface IContent
{ {
public function &getData(): array; public function setData(array $data): void;
public function getData(): array;
public function render(): void; public function render(): void;
@@ -0,0 +1,9 @@
<?php namespace MapGuesser\Interfaces\Session;
use SessionHandlerInterface;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
interface ISessionHandler extends SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
{
}
+1 -1
View File
@@ -77,7 +77,7 @@ class Mail
} }
} }
private function sendMail(PHPMailer $mailer, array $recipient) private function sendMail(PHPMailer $mailer, array $recipient): void
{ {
$mailer->clearAddresses(); $mailer->clearAddresses();
$mailer->addAddress($recipient[0], $recipient[1]); $mailer->addAddress($recipient[0], $recipient[1]);
+8 -5
View File
@@ -4,9 +4,9 @@ use MapGuesser\Interfaces\Http\IRequest;
class GoogleOAuth class GoogleOAuth
{ {
private static $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth'; private static string $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
private static $tokenUrlBase = 'https://oauth2.googleapis.com/token'; private static string $tokenUrlBase = 'https://oauth2.googleapis.com/token';
private IRequest $request; private IRequest $request;
@@ -15,7 +15,7 @@ class GoogleOAuth
$this->request = $request; $this->request = $request;
} }
public function getDialogUrl(string $state, string $redirectUrl, ?string $loginHint = null): string public function getDialogUrl(string $state, string $redirectUrl, ?string $nonce = null, ?string $loginHint = null): string
{ {
$oauthParams = [ $oauthParams = [
'response_type' => 'code', 'response_type' => 'code',
@@ -23,9 +23,12 @@ class GoogleOAuth
'scope' => 'openid email', 'scope' => 'openid email',
'redirect_uri' => $redirectUrl, 'redirect_uri' => $redirectUrl,
'state' => $state, 'state' => $state,
'nonce' => hash('sha256', random_bytes(10) . microtime()),
]; ];
if ($nonce !== null) {
$oauthParams['nonce'] = $nonce;
}
if ($loginHint !== null) { if ($loginHint !== null) {
$oauthParams['login_hint'] = $loginHint; $oauthParams['login_hint'] = $loginHint;
} }
@@ -33,7 +36,7 @@ class GoogleOAuth
return self::$dialogUrlBase . '?' . http_build_query($oauthParams); return self::$dialogUrlBase . '?' . http_build_query($oauthParams);
} }
public function getToken(string $code, string $redirectUrl) public function getToken(string $code, string $redirectUrl): array
{ {
$tokenParams = [ $tokenParams = [
'code' => $code, 'code' => $code,
+46 -2
View File
@@ -5,12 +5,13 @@ use DateTime;
use MapGuesser\Http\Request; use MapGuesser\Http\Request;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov;
class Place extends Model class Place extends Model
{ {
protected static string $table = 'places'; protected static string $table = 'places';
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']; protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp', 'pov_heading', 'pov_pitch', 'pov_zoom'];
protected static array $relations = ['map' => Map::class]; protected static array $relations = ['map' => Map::class];
@@ -24,9 +25,12 @@ class Place extends Model
private ?DateTime $panoIdCachedTimestamp = null; private ?DateTime $panoIdCachedTimestamp = null;
private Pov $pov;
public function __construct() public function __construct()
{ {
$this->position = new Position(0.0, 0.0); $this->position = new Position(0.0, 0.0);
$this->pov = new Pov(0.0, 0.0, 0.0);
} }
public function setMap(Map $map): void public function setMap(Map $map): void
@@ -54,6 +58,26 @@ class Place extends Model
$this->position->setLng($lng); $this->position->setLng($lng);
} }
public function setPov(Pov $pov): void
{
$this->pov = $pov;
}
public function setPovHeading(float $heading): void
{
$this->pov->setHeading($heading);
}
public function setPovPitch(float $pitch): void
{
$this->pov->setPitch($pitch);
}
public function setPovZoom(float $zoom): void
{
$this->pov->setZoom($zoom);
}
public function setPanoIdCached(?string $panoIdCached): void public function setPanoIdCached(?string $panoIdCached): void
{ {
$this->panoIdCached = $panoIdCached; $this->panoIdCached = $panoIdCached;
@@ -96,6 +120,26 @@ class Place extends Model
return $this->position->getLng(); return $this->position->getLng();
} }
public function getPov(): Pov
{
return $this->pov;
}
public function getPovHeading(): float
{
return $this->pov->getHeading();
}
public function getPovPitch(): float
{
return $this->pov->getPitch();
}
public function getPovZoom(): float
{
return $this->pov->getZoom();
}
public function getFreshPanoId(bool $canBeIndoor = false): ?string public function getFreshPanoId(bool $canBeIndoor = false): ?string
{ {
if ( if (
@@ -122,7 +166,7 @@ class Place extends Model
} }
$this->panoIdCached = $panoId; $this->panoIdCached = $panoId;
$this->panoIdCachedTimestamp = new DateTime('now'); $this->panoIdCachedTimestamp = new DateTime();
(new PersistentDataManager())->saveToDb($this); (new PersistentDataManager())->saveToDb($this);
+32 -5
View File
@@ -1,12 +1,13 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime;
use MapGuesser\Interfaces\Authentication\IUser; use MapGuesser\Interfaces\Authentication\IUser;
class User extends Model implements IUser class User extends Model implements IUser
{ {
protected static string $table = 'users'; protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub']; protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub', 'created'];
private static array $types = ['user', 'admin']; private static array $types = ['user', 'admin'];
@@ -20,6 +21,8 @@ class User extends Model implements IUser
private ?string $googleSub = null; private ?string $googleSub = null;
private DateTime $created;
public function setEmail(string $email): void public function setEmail(string $email): void
{ {
$this->email = $email; $this->email = $email;
@@ -42,9 +45,9 @@ class User extends Model implements IUser
} }
} }
public function setActive($active): void public function setActive(bool $active): void
{ {
$this->active = (bool) $active; $this->active = $active;
} }
public function setGoogleSub(?string $googleSub): void public function setGoogleSub(?string $googleSub): void
@@ -52,6 +55,16 @@ class User extends Model implements IUser
$this->googleSub = $googleSub; $this->googleSub = $googleSub;
} }
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getEmail(): string public function getEmail(): string
{ {
return $this->email; return $this->email;
@@ -77,15 +90,25 @@ class User extends Model implements IUser
return $this->googleSub; return $this->googleSub;
} }
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
public function hasPermission(int $permission): bool public function hasPermission(int $permission): bool
{ {
switch ($permission) { switch ($permission) {
case IUser::PERMISSION_NORMAL: case IUser::PERMISSION_NORMAL:
return true; return true;
break;
case IUser::PERMISSION_ADMIN: case IUser::PERMISSION_ADMIN:
return $this->type === 'admin'; return $this->type === 'admin';
break; default:
throw new \Exception('Permission does not exist: ' . $permission);
} }
} }
@@ -101,6 +124,10 @@ class User extends Model implements IUser
public function checkPassword(string $password): bool public function checkPassword(string $password): bool
{ {
if ($this->password === null) {
return false;
}
return password_verify($password, $this->password); return password_verify($password, $this->password);
} }
} }
+25 -1
View File
@@ -1,10 +1,12 @@
<?php namespace MapGuesser\PersistentData\Model; <?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserConfirmation extends Model class UserConfirmation extends Model
{ {
protected static string $table = 'user_confirmations'; protected static string $table = 'user_confirmations';
protected static array $fields = ['user_id', 'token']; protected static array $fields = ['user_id', 'token', 'last_sent'];
protected static array $relations = ['user' => User::class]; protected static array $relations = ['user' => User::class];
@@ -14,6 +16,8 @@ class UserConfirmation extends Model
private string $token = ''; private string $token = '';
private DateTime $lastSent;
public function setUser(User $user): void public function setUser(User $user): void
{ {
$this->user = $user; $this->user = $user;
@@ -29,6 +33,16 @@ class UserConfirmation extends Model
$this->token = $token; $this->token = $token;
} }
public function setLastSentDate(DateTime $lastSent): void
{
$this->lastSent = $lastSent;
}
public function setLastSent(string $lastSent): void
{
$this->lastSent = new DateTime($lastSent);
}
public function getUser(): ?User public function getUser(): ?User
{ {
return $this->user; return $this->user;
@@ -43,4 +57,14 @@ class UserConfirmation extends Model
{ {
return $this->token; return $this->token;
} }
public function getLastSentDate(): DateTime
{
return $this->lastSent;
}
public function getLastSent(): string
{
return $this->lastSent->format('Y-m-d H:i:s');
}
} }
@@ -0,0 +1,70 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserPasswordResetter extends Model
{
protected static string $table = 'user_password_resetters';
protected static array $fields = ['user_id', 'token', 'expires'];
protected static array $relations = ['user' => User::class];
private ?User $user = null;
private ?int $userId = null;
private string $token = '';
private DateTime $expires;
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setToken(string $token): void
{
$this->token = $token;
}
public function setExpiresDate(DateTime $expires): void
{
$this->expires = $expires;
}
public function setExpires(string $expires): void
{
$this->expires = new DateTime($expires);
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getToken(): string
{
return $this->token;
}
public function getExpiresDate(): DateTime
{
return $this->expires;
}
public function getExpires(): string
{
return $this->expires->format('Y-m-d H:i:s');
}
}
+4 -3
View File
@@ -8,7 +8,7 @@ use MapGuesser\PersistentData\Model\Model;
class PersistentDataManager class PersistentDataManager
{ {
public function selectFromDb(Select $select, string $type, bool $withRelations = false): ?Model public function selectFromDb(Select $select, string $type, bool $withRelations = false)
{ {
$select = $this->createSelect($select, $type, $withRelations); $select = $this->createSelect($select, $type, $withRelations);
@@ -27,8 +27,9 @@ class PersistentDataManager
public function selectMultipleFromDb(Select $select, string $type, bool $withRelations = false): Generator public function selectMultipleFromDb(Select $select, string $type, bool $withRelations = false): Generator
{ {
$select = $this->createSelect($select, $type, $withRelations); $select = $this->createSelect($select, $type, $withRelations);
$result = $select->execute();
while ($data = $select->execute()->fetch(IResultSet::FETCH_ASSOC)) { while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) {
$model = new $type(); $model = new $type();
$this->fillWithData($data, $model); $this->fillWithData($data, $model);
@@ -36,7 +37,7 @@ class PersistentDataManager
} }
} }
public function selectFromDbById($id, string $type, bool $withRelations = false): ?Model public function selectFromDbById($id, string $type, bool $withRelations = false)
{ {
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->whereId($id); $select->whereId($id);
+20 -2
View File
@@ -1,6 +1,8 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
@@ -18,6 +20,15 @@ class PlaceRepository
return $this->pdm->selectFromDbById($placeId, Place::class); return $this->pdm->selectFromDbById($placeId, Place::class);
} }
public function getAllForMap(Map $map): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('map_id', '=', $map->getId());
yield from $this->pdm->selectMultipleFromDb($select, Place::class);
}
//TODO: use Map instead of id
public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place
{ {
$placesWithoutPano = []; $placesWithoutPano = [];
@@ -35,13 +46,20 @@ class PlaceRepository
return $place; return $place;
} }
private function selectRandomFromDbForMap(int $mapId, array $exclude): ?Place private function selectRandomFromDbForMap(int $mapId, array $exclude): Place
{ {
//TODO: omit table name here
$select = new Select(\Container::$dbConnection, 'places'); $select = new Select(\Container::$dbConnection, 'places');
$select->where('id', 'NOT IN', $exclude); $select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $mapId); $select->where('map_id', '=', $mapId);
$numberOfPlaces = $select->count(); // TODO: what if 0 $numberOfPlaces = $select->count();
//TODO: prevent this
if ($numberOfPlaces === 0) {
throw new \Exception('There is no selectable place (count was 0).');
}
$randomOffset = random_int(0, $numberOfPlaces - 1); $randomOffset = random_int(0, $numberOfPlaces - 1);
$select->orderBy('id'); $select->orderBy('id');
@@ -1,8 +1,6 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation; use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
@@ -29,11 +27,11 @@ class UserConfirmationRepository
return $this->pdm->selectFromDb($select, UserConfirmation::class); return $this->pdm->selectFromDb($select, UserConfirmation::class);
} }
public function getByUser(User $user): Generator public function getByUser(User $user): ?UserConfirmation
{ {
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId()); $select->where('user_id', '=', $user->getId());
yield from $this->pdm->selectMultipleFromDb($select, UserConfirmation::class); return $this->pdm->selectFromDb($select, UserConfirmation::class);
} }
} }
@@ -0,0 +1,47 @@
<?php namespace MapGuesser\Repository;
use DateTime;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager;
class UserPasswordResetterRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userConfirmationId): ?UserPasswordResetter
{
return $this->pdm->selectFromDbById($userConfirmationId, UserPasswordResetter::class);
}
public function getByToken(string $token): ?UserPasswordResetter
{
$select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token);
return $this->pdm->selectFromDb($select, UserPasswordResetter::class);
}
public function getByUser(User $user): ?UserPasswordResetter
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
return $this->pdm->selectFromDb($select, UserPasswordResetter::class);
}
public function getAllExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, UserPasswordResetter::class);
}
}
+11
View File
@@ -1,5 +1,7 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use DateTime;
use Generator;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
@@ -33,4 +35,13 @@ class UserRepository
return $this->pdm->selectFromDb($select, User::class); return $this->pdm->selectFromDb($select, User::class);
} }
public function getAllInactiveExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('active', '=', false);
$select->where('created', '<', (new DateTime('-1 day'))->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, User::class);
}
} }
+1 -1
View File
@@ -39,7 +39,7 @@ class Request implements IRequest
} }
} }
public function setParsedRouteParams(array &$routeParams) public function setParsedRouteParams(array &$routeParams): void
{ {
$this->routeParams = &$routeParams; $this->routeParams = &$routeParams;
} }
+4 -4
View File
@@ -11,12 +11,12 @@ class Session implements ISession
$this->data = &$data; $this->data = &$data;
} }
public function has($key): bool public function has(string $key): bool
{ {
return isset($this->data[$key]); return isset($this->data[$key]);
} }
public function get($key) public function get(string $key)
{ {
if (isset($this->data[$key])) { if (isset($this->data[$key])) {
return $this->data[$key]; return $this->data[$key];
@@ -25,12 +25,12 @@ class Session implements ISession
return null; return null;
} }
public function set($key, $value): void public function set(string $key, $value): void
{ {
$this->data[$key] = $value; $this->data[$key] = $value;
} }
public function delete($key): void public function delete(string $key): void
{ {
unset($this->data[$key]); unset($this->data[$key]);
} }
+6 -1
View File
@@ -6,7 +6,12 @@ abstract class ContentBase implements IContent
{ {
protected array $data; protected array $data;
public function &getData(): array public function setData(array $data): void
{
$this->data = $data;
}
public function getData(): array
{ {
return $this->data; return $this->data;
} }
+3 -2
View File
@@ -6,10 +6,10 @@ class HtmlContent extends ContentBase
{ {
private string $view; private string $view;
public function __construct(string $view, array &$data = []) public function __construct(string $view, array $data = [])
{ {
$this->view = $view; $this->view = $view;
$this->data = &$data; $this->data = $data;
} }
public function render(): void public function render(): void
@@ -23,6 +23,7 @@ class HtmlContent extends ContentBase
require ROOT . '/cache/views/' . $this->view . '.php'; require ROOT . '/cache/views/' . $this->view . '.php';
// @phpstan-ignore-next-line - SCRIPT_STARTED is defined in main.php
echo '<!-- __debug__runtime: ' . round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1) . ' -->'; echo '<!-- __debug__runtime: ' . round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1) . ' -->';
} }
+3 -2
View File
@@ -2,13 +2,14 @@
class JsonContent extends ContentBase class JsonContent extends ContentBase
{ {
public function __construct(array &$data = []) public function __construct(array $data = [])
{ {
$this->data = &$data; $this->data = $data;
} }
public function render(): void public function render(): void
{ {
// @phpstan-ignore-next-line - SCRIPT_STARTED is defined in main.php
$this->data['__debug__runtime'] = round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1); $this->data['__debug__runtime'] = round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1);
echo json_encode($this->data); echo json_encode($this->data);
+1 -1
View File
@@ -16,7 +16,7 @@ class Redirect implements IRedirect
public function getUrl(): string public function getUrl(): string
{ {
if (preg_match('/^http(s)?/', $this->target)) { if (preg_match('/^http(s)?/', $this->target) === 1) {
$link = $this->target; $link = $this->target;
} else { } else {
$link = \Container::$request->getBase() . '/' . $this->target; $link = \Container::$request->getBase() . '/' . $this->target;
+2 -2
View File
@@ -30,7 +30,7 @@ class Route
$link = []; $link = [];
foreach ($this->pattern as $fragment) { foreach ($this->pattern as $fragment) {
if (preg_match('/^{(\\w+)(\\?)?}$/', $fragment, $matches)) { if (preg_match('/^{(\\w+)(\\?)?}$/', $fragment, $matches) === 1) {
if (isset($parameters[$matches[1]])) { if (isset($parameters[$matches[1]])) {
$link[] = $parameters[$matches[1]]; $link[] = $parameters[$matches[1]];
unset($parameters[$matches[1]]); unset($parameters[$matches[1]]);
@@ -61,7 +61,7 @@ class Route
$parameters = []; $parameters = [];
foreach ($path as $i => $fragment) { foreach ($path as $i => $fragment) {
if (preg_match('/^{(\\w+)(?:\\?)?}$/', $this->pattern[$i], $matches)) { if (preg_match('/^{(\\w+)(?:\\?)?}$/', $this->pattern[$i], $matches) === 1) {
$parameters[$matches[1]] = $fragment; $parameters[$matches[1]] = $fragment;
} elseif ($fragment != $this->pattern[$i]) { } elseif ($fragment != $this->pattern[$i]) {
return null; return null;
+1 -1
View File
@@ -76,7 +76,7 @@ class RouteCollection
$this->searchTable[$method][$groupNumber][] = $route; $this->searchTable[$method][$groupNumber][] = $route;
while (preg_match('/^{\\w+\\?}$/', end($pattern))) { while (preg_match('/^{\\w+\\?}$/', end($pattern)) === 1) {
$groupNumber--; $groupNumber--;
array_pop($pattern); array_pop($pattern);
+15 -24
View File
@@ -4,11 +4,9 @@ use DateTime;
use MapGuesser\Database\Query\Modify; use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
use SessionHandlerInterface; use MapGuesser\Interfaces\Session\ISessionHandler;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface class DatabaseSessionHandler implements ISessionHandler
{ {
private bool $exists = false; private bool $exists = false;
@@ -28,7 +26,7 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
{ {
$select = new Select(\Container::$dbConnection, 'sessions'); $select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['data']); $select->columns(['data']);
$select->whereId($id); $select->whereId(substr($id, 0, 32));
$result = $select->execute()->fetch(IResultSet::FETCH_ASSOC); $result = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
@@ -46,16 +44,16 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
$modify = new Modify(\Container::$dbConnection, 'sessions'); $modify = new Modify(\Container::$dbConnection, 'sessions');
if ($this->exists) { if ($this->exists) {
$modify->setId($id); $modify->setId(substr($id, 0, 32));
} else { } else {
$modify->setExternalId($id); $modify->setExternalId(substr($id, 0, 32));
} }
$modify->set('data', $data); $modify->set('data', $data);
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s')); $modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
$modify->save(); $modify->save();
$written = true; $this->written = true;
return true; return true;
} }
@@ -63,37 +61,30 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
public function destroy($id): bool public function destroy($id): bool
{ {
$modify = new Modify(\Container::$dbConnection, 'sessions'); $modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($id); $modify->setId(substr($id, 0, 32));
$modify->delete(); $modify->delete();
$this->exists = false;
return true; return true;
} }
public function gc($maxlifetime): int public function gc($maxlifetime): bool
{ {
$select = new Select(\Container::$dbConnection, 'sessions'); // empty on purpose
$select->columns(['id']); // old sessions are deleted by MaintainDatabaseCommand
$select->where('updated', '<', (new DateTime('-' . $maxlifetime . ' seconds'))->format('Y-m-d H:i:s'));
$result = $select->execute();
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($session['id']);
$modify->delete();
}
return true; return true;
} }
public function create_sid(): string public function create_sid(): string
{ {
return hash('sha256', random_bytes(10) . microtime()); return bin2hex(random_bytes(16));
} }
public function validateId($id): bool public function validateId($id): bool
{ {
return preg_match('/^[a-f0-9]{64}$/', $id); return preg_match('/^[a-f0-9]{32}$/', $id) === 1;
} }
public function updateTimestamp($id, $data): bool public function updateTimestamp($id, $data): bool
@@ -104,7 +95,7 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
$modify = new Modify(\Container::$dbConnection, 'sessions'); $modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($id); $modify->setId(substr($id, 0, 32));
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s')); $modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
$modify->save(); $modify->save();
+1 -1
View File
@@ -27,7 +27,7 @@ class Bounds
public static function createDirectly(float $southLat, float $westLng, float $northLat, float $eastLng): Bounds public static function createDirectly(float $southLat, float $westLng, float $northLat, float $eastLng): Bounds
{ {
$instance = new static(); $instance = new self();
$instance->southLat = $southLat; $instance->southLat = $southLat;
$instance->westLng = $westLng; $instance->westLng = $westLng;
+1 -1
View File
@@ -11,7 +11,7 @@ class JwtParser
} }
} }
public function setToken(string $token) public function setToken(string $token): void
{ {
$this->token = explode('.', str_replace(['_', '-'], ['/', '+'], $token)); $this->token = explode('.', str_replace(['_', '-'], ['/', '+'], $token));
} }
+56
View File
@@ -0,0 +1,56 @@
<?php namespace MapGuesser\Util\Panorama;
class Pov
{
private float $heading;
private float $pitch;
private float $zoom;
public function __construct(float $heading, float $pitch, float $zoom)
{
$this->heading = $heading;
$this->pitch = $pitch;
$this->zoom = $zoom;
}
public function setHeading(float $heading): void
{
$this->heading = $heading;
}
public function setPitch(float $pitch): void
{
$this->pitch = $pitch;
}
public function setZoom(float $zoom): void
{
$this->zoom = $zoom;
}
public function getHeading(): float
{
return $this->heading;
}
public function getPitch(): float
{
return $this->pitch;
}
public function getZoom(): float
{
return $this->zoom;
}
public function toArray(): array
{
return [
'heading' => $this->heading,
'pitch' => $this->pitch,
'zoom' => $this->zoom
];
}
}
+3 -3
View File
@@ -65,7 +65,7 @@ class Linker
while (($line = fgets($inputFileHandle)) !== false) { while (($line = fgets($inputFileHandle)) !== false) {
++$lineNumber; ++$lineNumber;
if (preg_match('/^\s*@yields\(\'([\w\/]+)\'\)\s*$/', $line, $matches)) { if (preg_match('/^\s*@yields\(\'([\w\/]+)\'\)\s*$/', $line, $matches) === 1) {
if (isset($sections[$matches[1]])) { if (isset($sections[$matches[1]])) {
fwrite($outputFileHandle, $sections[$matches[1]]); fwrite($outputFileHandle, $sections[$matches[1]]);
} }
@@ -105,7 +105,7 @@ class Linker
fclose($outputFileHandle); fclose($outputFileHandle);
} }
private function generateAssets(ParsedFragment $fragment, array &$sections) private function generateAssets(ParsedFragment $fragment, array &$sections): void
{ {
foreach ($fragment->getCss() as $cssFile) { foreach ($fragment->getCss() as $cssFile) {
$asset = $this->parseAsset($cssFile); $asset = $this->parseAsset($cssFile);
@@ -134,7 +134,7 @@ class Linker
{ {
$output = []; $output = [];
if (preg_match('/^[\w\/\.]+$/', $asset)) { if (preg_match('/^[\w\/\.]+$/', $asset) === 1) {
if ( if (
empty($_ENV['DEV']) && empty($_ENV['DEV']) &&
filesize(ROOT . '/public/static/' . $asset) < self::INLINE_ASSET_LIMIT filesize(ROOT . '/public/static/' . $asset) < self::INLINE_ASSET_LIMIT
+7 -7
View File
@@ -108,7 +108,7 @@ class Parser
private function matchCss(string $line): ?string private function matchCss(string $line): ?string
{ {
if (preg_match('/^\s*@css\((.*)\)\s*$/', $line, $matches)) { if (preg_match('/^\s*@css\((.*)\)\s*$/', $line, $matches) === 1) {
return $matches[1]; return $matches[1];
} }
@@ -117,7 +117,7 @@ class Parser
private function matchJs(string $line): ?string private function matchJs(string $line): ?string
{ {
if (preg_match('/^\s*@js\((.*)\)\s*$/', $line, $matches)) { if (preg_match('/^\s*@js\((.*)\)\s*$/', $line, $matches) === 1) {
return $matches[1]; return $matches[1];
} }
@@ -126,7 +126,7 @@ class Parser
private function matchExtends(string $line): ?string private function matchExtends(string $line): ?string
{ {
if (preg_match('/^\s*@extends\(([\w\/]+)\)\s*$/', $line, $matches)) { if (preg_match('/^\s*@extends\(([\w\/]+)\)\s*$/', $line, $matches) === 1) {
return $matches[1]; return $matches[1];
} }
@@ -135,7 +135,7 @@ class Parser
private function matchSection(string $line): ?string private function matchSection(string $line): ?string
{ {
if (preg_match('/^\s*@section\((\w+)\)\s*$/', $line, $matches)) { if (preg_match('/^\s*@section\((\w+)\)\s*$/', $line, $matches) === 1) {
return $matches[1]; return $matches[1];
} }
@@ -144,16 +144,16 @@ class Parser
private function matchEndSection(string $line): bool private function matchEndSection(string $line): bool
{ {
return preg_match('/^\s*@endsection(?:\(\))?\s*$/', $line); return preg_match('/^\s*@endsection(?:\(\))?\s*$/', $line) === 1;
} }
private function matchExtra(string $line): bool private function matchExtra(string $line): bool
{ {
return preg_match('/^\s*@extra(?:\(\))?\s*$/', $line); return preg_match('/^\s*@extra(?:\(\))?\s*$/', $line) === 1;
} }
private function matchEndExtra(string $line): bool private function matchEndExtra(string $line): bool
{ {
return preg_match('/^\s*@endextra(?:\(\))?\s*$/', $line); return preg_match('/^\s*@endextra(?:\(\))?\s*$/', $line) === 1;
} }
} }
+4 -8
View File
@@ -13,6 +13,7 @@ final class GoogleOAuthTest extends TestCase
{ {
$_ENV['GOOGLE_OAUTH_CLIENT_ID'] = 'xyz'; $_ENV['GOOGLE_OAUTH_CLIENT_ID'] = 'xyz';
$state = 'random_state_string'; $state = 'random_state_string';
$nonce = 'random_nonce_string';
$redirectUrl = 'http://example.com/oauth'; $redirectUrl = 'http://example.com/oauth';
$requestMock = $this->getMockBuilder(IRequest::class) $requestMock = $this->getMockBuilder(IRequest::class)
@@ -20,7 +21,7 @@ final class GoogleOAuthTest extends TestCase
->getMock(); ->getMock();
$googleOAuth = new GoogleOAuth($requestMock); $googleOAuth = new GoogleOAuth($requestMock);
$dialogUrl = $googleOAuth->getDialogUrl($state, $redirectUrl); $dialogUrl = $googleOAuth->getDialogUrl($state, $redirectUrl, $nonce);
$dialogUrlParsed = explode('?', $dialogUrl); $dialogUrlParsed = explode('?', $dialogUrl);
$this->assertEquals('https://accounts.google.com/o/oauth2/v2/auth', $dialogUrlParsed[0]); $this->assertEquals('https://accounts.google.com/o/oauth2/v2/auth', $dialogUrlParsed[0]);
@@ -33,15 +34,10 @@ final class GoogleOAuthTest extends TestCase
'scope' => 'openid email', 'scope' => 'openid email',
'redirect_uri' => $redirectUrl, 'redirect_uri' => $redirectUrl,
'state' => $state, 'state' => $state,
'nonce' => hash('sha256', random_bytes(10) . microtime()), 'nonce' => $nonce,
]; ];
$this->assertEquals($expectedQueryParams['response_type'], $dialogUrlQueryParams['response_type']); $this->assertEquals($expectedQueryParams, $dialogUrlQueryParams);
$this->assertEquals($expectedQueryParams['client_id'], $dialogUrlQueryParams['client_id']);
$this->assertEquals($expectedQueryParams['scope'], $dialogUrlQueryParams['scope']);
$this->assertEquals($expectedQueryParams['redirect_uri'], $dialogUrlQueryParams['redirect_uri']);
$this->assertEquals($expectedQueryParams['state'], $dialogUrlQueryParams['state']);
$this->assertMatchesRegularExpression('/^[a-f0-9]{64}$/', $dialogUrlQueryParams['nonce']);
} }
public function testCanRequestToken(): void public function testCanRequestToken(): void
+4
View File
@@ -3,6 +3,10 @@
use MapGuesser\PersistentData\Model\Model; use MapGuesser\PersistentData\Model\Model;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class OtherModel
{
}
class DummyModel extends Model class DummyModel extends Model
{ {
protected static string $table = 'test_table'; protected static string $table = 'test_table';
+1 -1
View File
@@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase;
final class ParserTest extends TestCase final class ParserTest extends TestCase
{ {
const TEST_VIEWS_PATH = __DIR__ . '/../assets/views'; const TEST_VIEWS_PATH = __DIR__ . '/../../views/tests';
public function testCanParseViewWithoutExtends(): void public function testCanParseViewWithoutExtends(): void
{ {
+1 -1
View File
@@ -3,6 +3,6 @@
@section(main) @section(main)
<h2>Account activation</h2> <h2>Account activation</h2>
<div class="box"> <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> <p class="error justify">Activation failed. Please check the link you entered, or retry <a href="/signup" title="Sign up">signing up</a>!</p>
</div> </div>
@endsection @endsection
+2
View File
@@ -10,6 +10,8 @@
<div class="right marginTop"> <div class="right marginTop">
<button type="submit">Login</button> <button type="submit">Login</button>
</div> </div>
<p class="center marginTop"><a href="/password/requestReset" title="Request password reset">Forgot your password?</a></p>
<p class="center marginTop"><a href="/signup" title="Sign up">New to <?= $_ENV['APP_NAME'] ?>?</a></p>
<hr> <hr>
<div class="center"> <div class="center">
<a class="button yellow" href="/login/google" title="Login with Google">Login with Google</a> <a class="button yellow" href="/login/google" title="Login with Google">Login with Google</a>
+14
View File
@@ -0,0 +1,14 @@
@extends(templates/layout_normal)
@section(main)
<h2>Request password reset</h2>
<div class="box">
<form id="passwordResetForm" action="/password/requestReset" method="post" data-redirect-on-success="/password/requestReset/success">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= isset($email) ? $email : '' ?>" required autofocus>
<p id="passwordResetFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit">Continue</button>
</div>
</form>
</div>
@endsection
@@ -0,0 +1,8 @@
@extends(templates/layout_normal)
@section(main)
<h2>Request password reset</h2>
<div class="box">
<p class="justify">Password reset was successfully requested. Please check your email and click on the link to reset your password!</p>
</div>
@endsection
+20
View File
@@ -0,0 +1,20 @@
@extends(templates/layout_normal)
@section(main)
<h2>Reset password</h2>
<div class="box">
<?php if ($success) : ?>
<form id="resetPasswordForm" action="/password/reset/<?= $token ?>" method="post" data-redirect-on-success="/">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $email ?>" disabled>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6" autofocus>
<input class="big fullWidth marginTop" type="password" name="password_confirm" placeholder="Password confirmation" required minlength="6">
<p id="resetPasswordFormError" class="formError justify marginTop"></p>
<div class="right">
<button class="marginTop" type="submit">Reset password</button>
</div>
</form>
<?php else: ?>
<p class="error justify">Confirming your identity failed. Please check the link you entered, or retry <a href="/password/requestReset" title="Request password reset">requesting password reset</a>!</p>
<?php endif; ?>
</div>
@endsection
+1
View File
@@ -22,6 +22,7 @@
--><button id="resetSignupButton" class="gray marginTop marginLeft" type="button">Reset</button> --><button id="resetSignupButton" class="gray marginTop marginLeft" type="button">Reset</button>
<?php endif; ?> <?php endif; ?>
</div> </div>
<p class="center marginTop"><a href="/login" title="Login">Already have an account?</a></p>
<hr> <hr>
<div class="center"> <div class="center">
<a class="button yellow" href="/login/google" title="Signup with Google">Signup with Google</a> <a class="button yellow" href="/login/google" title="Signup with Google">Signup with Google</a>
+1 -1
View File
@@ -4,7 +4,7 @@
<header class="small"> <header class="small">
<h1> <h1>
<a href="/" title="<?= $_ENV['APP_NAME'] ?>"> <a href="/" title="<?= $_ENV['APP_NAME'] ?>">
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>"><!-- <img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>" alt="<?= $_ENV['APP_NAME'] ?>"><!--
--><span><?= $_ENV['APP_NAME'] ?></span> --><span><?= $_ENV['APP_NAME'] ?></span>
</a> </a>
</h1> </h1>
+1 -1
View File
@@ -3,7 +3,7 @@
@section(content) @section(content)
<header> <header>
<h1> <h1>
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>"><!-- <img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>" alt="<?= $_ENV['APP_NAME'] ?>"><!--
--><?= $_ENV['APP_NAME'] ?> --><?= $_ENV['APP_NAME'] ?>
</h1> </h1>
</header> </header>
+1 -1
View File
@@ -4,7 +4,7 @@
<header> <header>
<h1> <h1>
<a href="/" title="<?= $_ENV['APP_NAME'] ?>"> <a href="/" title="<?= $_ENV['APP_NAME'] ?>">
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>"><!-- <img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>" alt="<?= $_ENV['APP_NAME'] ?>"><!--
--><span><?= $_ENV['APP_NAME'] ?></span> --><span><?= $_ENV['APP_NAME'] ?></span>
</a> </a>
</h1> </h1>
+21 -11
View File
@@ -1,13 +1,23 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="description" content="<?= $_ENV['APP_NAME'] ?> – A game about guessing where you are based on a street view panorama.">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#198231"> <meta name="theme-color" content="#198231">
<title><?= $_ENV['APP_NAME'] ?></title> <title><?= $_ENV['APP_NAME'] ?></title>
<?php if (preg_match('/^(http(s)?:)?\/\//', $_ENV['STATIC_ROOT']) === 1): ?>
<link href="<?= $_ENV['STATIC_ROOT'] ?>" rel="preconnect">
<?php endif; ?>
<link href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://maps.googleapis.com" rel="preconnect">
<?php if (!empty($_ENV['GOOGLE_ANALITICS_ID'])): ?>
<link href="https://www.googletagmanager.com" rel="preconnect">
<link href="https://www.google-analytics.com" rel="preconnect">
<?php endif; ?>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&amp;display=swap" rel="stylesheet">
<link href="<?= $_ENV['STATIC_ROOT'] ?>/css/mapguesser.css?rev=<?= REVISION ?>" rel="stylesheet"> <link href="<?= $_ENV['STATIC_ROOT'] ?>/css/mapguesser.css?rev=<?= REVISION ?>" rel="stylesheet">
@yields('externalCss') @yields('externalCss')
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&display=swap" rel="stylesheet">
@yields('inlineCss') @yields('inlineCss')
<link rel="icon" type="image/png" sizes="192x192" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/192x192.png?rev=<?= REVISION ?>"> <link rel="icon" type="image/png" sizes="192x192" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/192x192.png?rev=<?= REVISION ?>">
<link rel="icon" type="image/png" sizes="96x96" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/96x96.png?rev=<?= REVISION ?>"> <link rel="icon" type="image/png" sizes="96x96" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/96x96.png?rev=<?= REVISION ?>">
@@ -15,16 +25,8 @@
<link rel="icon" type="image/png" sizes="16x16" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/16x16.png?rev=<?= REVISION ?>"> <link rel="icon" type="image/png" sizes="16x16" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/16x16.png?rev=<?= REVISION ?>">
</head> </head>
<body> <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"> <div id="loading">
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/loading.svg?rev=<?= REVISION ?>" width="64" height="64"> <img src="<?= $_ENV['STATIC_ROOT'] ?>/img/loading.svg?rev=<?= REVISION ?>" width="64" height="64" alt="Loading">
</div> </div>
<div id="cover"></div> <div id="cover"></div>
<div id="modal" class="modal"> <div id="modal" class="modal">
@@ -34,6 +36,14 @@
</div> </div>
@yields('pagemodal') @yields('pagemodal')
@yields('content') @yields('content')
<?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; ?>
<script> <script>
const STATIC_ROOT = '<?= $_ENV['STATIC_ROOT'] ?>'; const STATIC_ROOT = '<?= $_ENV['STATIC_ROOT'] ?>';
const REVISION = '<?= REVISION ?>'; const REVISION = '<?= REVISION ?>';
File renamed without changes.
File renamed without changes.
File renamed without changes.
+15 -2
View File
@@ -31,6 +31,13 @@ Container::$routeCollection->group('signup', function (MapGuesser\Routing\RouteC
$routeCollection->get('signup.activate', 'activate/{token}', [MapGuesser\Controller\LoginController::class, 'activate']); $routeCollection->get('signup.activate', 'activate/{token}', [MapGuesser\Controller\LoginController::class, 'activate']);
$routeCollection->get('signup.cancel', 'cancel/{token}', [MapGuesser\Controller\LoginController::class, 'cancel']); $routeCollection->get('signup.cancel', 'cancel/{token}', [MapGuesser\Controller\LoginController::class, 'cancel']);
}); });
Container::$routeCollection->group('password', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('password-requestReset', 'requestReset', [MapGuesser\Controller\LoginController::class, 'getRequestPasswordResetForm']);
$routeCollection->post('password-requestReset-action', 'requestReset', [MapGuesser\Controller\LoginController::class, 'requestPasswordReset']);
$routeCollection->get('password-requestReset.success', 'requestReset/success', [MapGuesser\Controller\LoginController::class, 'getRequestPasswordResetSuccess']);
$routeCollection->get('password-reset', 'reset/{token}', [MapGuesser\Controller\LoginController::class, 'getResetPasswordForm']);
$routeCollection->post('password-reset.action', 'reset/{token}', [MapGuesser\Controller\LoginController::class, 'resetPassword']);
});
Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']); Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
Container::$routeCollection->group('account', function (MapGuesser\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('account', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('account', '', [MapGuesser\Controller\UserController::class, 'getAccount']); $routeCollection->get('account', '', [MapGuesser\Controller\UserController::class, 'getAccount']);
@@ -60,10 +67,16 @@ if (isset($_COOKIE['COOKIES_CONSENT'])) {
session_set_save_handler(Container::$sessionHandler, true); session_set_save_handler(Container::$sessionHandler, true);
session_start([ session_start([
'gc_maxlifetime' => 604800, 'gc_maxlifetime' => 604800,
'cookie_lifetime' => 604800, 'gc_probability' => 0, // old sessions are deleted by MaintainDatabaseCommand
'cookie_lifetime' => 604800, // TODO: cookie is not renewed so session can be lost
'cookie_httponly' => true, 'cookie_httponly' => true,
'cookie_samesite' => 'Lax' 'cookie_samesite' => 'Lax'
]); ]);
// this is needed to handle old type of session IDs
if (!Container::$sessionHandler->validateId(session_id())) {
session_regenerate_id(true);
}
} else { } else {
$_SESSION = []; $_SESSION = [];
} }
@@ -71,5 +84,5 @@ if (isset($_COOKIE['COOKIES_CONSENT'])) {
Container::$request = new MapGuesser\Request\Request($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'], $_GET, $_POST, $_SESSION); Container::$request = new MapGuesser\Request\Request($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'], $_GET, $_POST, $_SESSION);
if (!Container::$request->session()->has('anti_csrf_token')) { if (!Container::$request->session()->has('anti_csrf_token')) {
Container::$request->session()->set('anti_csrf_token', hash('sha256', random_bytes(10) . microtime())); Container::$request->session()->set('anti_csrf_token', bin2hex(random_bytes(16)));
} }