Compare commits

..
Author SHA1 Message Date
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
53 changed files with 988 additions and 235 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
+8
View File
@@ -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:
@@ -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();
+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;
} }
+5 -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 {
+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);
}, },
+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..."
+2
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;
@@ -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'));
+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 $persistentDataManager;
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct()
{
parent::__construct();
$this->persistentDataManager = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
}
public function configure()
{
$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->persistentDataManager->deleteFromDb($user);
}
\Container::$dbConnection->commit();
}
private function deleteExpiredPasswordResetters(): void
{
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
$this->persistentDataManager->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()
{ {
$this->setName('migrate') $this->setName('db:migrate')
->setDescription('Migration of database changes.'); ->setDescription('Migration of database changes.');
} }
+4 -4
View File
@@ -21,15 +21,15 @@ 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)
+18 -11
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,23 +107,25 @@ 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(&$state, Place $place, array $placesWithoutPano): void
@@ -128,7 +134,8 @@ class GameFlowController
'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()
{
$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
+50 -31
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,24 +186,28 @@ 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, &$error): bool
+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);
} }
} }
+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
{
}
+5 -2
View File
@@ -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;
} }
+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);
+24 -1
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;
@@ -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,6 +90,16 @@ 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) {
+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');
}
}
+2 -1
View File
@@ -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);
+12
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 = [];
@@ -37,6 +48,7 @@ class PlaceRepository
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);
@@ -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);
}
} }
+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;
} }
+2 -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
+2 -2
View File
@@ -2,9 +2,9 @@
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
+14 -23
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): int
{ {
$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);
} }
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();
+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
];
}
}
+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
+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>
+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)));
} }