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
bence e8adc9f730 Merged in feature/MAPG-177-create-unit-tests-for-all-units (pull request #167)
MAPG-177 add tests for View\Parser
2020-06-28 21:13:32 +00:00
bence 149003250c MAPG-177 add tests for View\Parser 2020-06-28 22:55:15 +02:00
bence 171b4278a6 Merged in feature/MAPG-175-unify-styles (pull request #166)
MAPG-175 make map title one liner
2020-06-28 18:16:23 +00:00
bence 091018d22f MAPG-175 make map title one liner 2020-06-28 20:15:11 +02:00
bence 34f0c92eb5 Merged in bugfix/MAPG-187-dont-evaluate-asset-paths (pull request #165)
Bugfix/MAPG-187 dont evaluate asset paths
2020-06-28 17:38:03 +00:00
bence 8ac5d5c6e2 MAPG-187 adapt views to the new parser 2020-06-28 19:35:14 +02:00
bence e883d184b7 MAPG-187 refactor view parsing and linking 2020-06-28 19:35:14 +02:00
bence 71b1e28aa9 Merged in feature/MAPG-188-pagespeed-improvements (pull request #164)
MAPG-188 get rid of inline js
2020-06-28 16:13:16 +00:00
bence 3555be9b63 MAPG-188 get rid of inline js 2020-06-28 17:56:47 +02:00
bence d03a3934c6 Merged in feature/MAPG-181-handle-the-case-when-user-has-no-password (pull request #163)
Feature/MAPG-181 handle the case when user has no password
2020-06-28 08:42:10 +00:00
bence bd5b49b09b MAPG-181 set user active when added from CLI 2020-06-28 03:19:07 +02:00
bence 4333240acc MAPG-181 add stuff to authenticate without password 2020-06-28 03:18:51 +02:00
bence 4099f1d962 MAPG-181 add new layout type (minimal) 2020-06-28 03:16:48 +02:00
bence 2fac828098 MAPG-181 text fixes 2020-06-28 03:16:28 +02:00
bence f3225ae275 MAPG-181 ability to add login_hint to dialog url 2020-06-28 03:15:20 +02:00
bence 48eba75cf4 MAPG-181 fix and unify styles for buttons and inputs 2020-06-28 03:12:16 +02:00
bence a97a6f8977 Merged in bugfix/MAPG-180-dont-echo-void (pull request #162)
MAPG-180 don't echo void
2020-06-27 12:26:57 +00:00
bence 2a3f891aa0 MAPG-180 don't echo void 2020-06-27 14:26:19 +02:00
83 changed files with 1588 additions and 340 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
APP_NAME=MapQuiz
APP_NAME=MapGuesser
DEV=1
DB_HOST=mariadb
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.
### (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
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,
<br><br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}). To activate your account, please click on the following link:<br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
To activate your account, please click on the following link:<br>
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required, your email address will be deleted soon.<br>
However if you want to immediately delete it, please click on the following link:<br>
You can activate your account until {{ACTIVATABLE_UNTIL}}.
If you don't activate your account, your email address will be permanently deleted after this point of time.
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
However if you want to immediately delete your email address, please click on the following link:<br>
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
<br><br>
Have fun on {{APP_NAME}}!
+1 -1
View File
@@ -16,7 +16,7 @@ class Container
{
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
static MapGuesser\Routing\RouteCollection $routeCollection;
static \SessionHandlerInterface $sessionHandler;
static MapGuesser\Interfaces\Session\ISessionHandler $sessionHandler;
static MapGuesser\Interfaces\Request\IRequest $request;
}
+2 -1
View File
@@ -5,8 +5,9 @@ require 'main.php';
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\DatabaseMigration());
$app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
$app->add(new MapGuesser\Cli\AddUserCommand());
$app->add(new MapGuesser\Cli\LinkViewCommand());
$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand());
$app->run();
+4 -3
View File
@@ -26,8 +26,9 @@ if ($match !== null) {
}
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
$content = new MapGuesser\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
header('Content-Type: text/html; charset=UTF-8', true, 403);
echo json_encode(['error' => 'no_valid_anti_csrf_token']);
$content->render();
return;
}
@@ -36,7 +37,7 @@ if ($match !== null) {
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
echo $response->render();
$response->render();
return;
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
@@ -49,4 +50,4 @@ if ($match !== null) {
$content = new MapGuesser\Response\HtmlContent('error/404');
header('Content-Type: text/html; charset=UTF-8', true, 404);
echo $content->render();
$content->render();
+59 -11
View File
@@ -153,13 +153,27 @@ button, a.button {
line-height: 35px;
}
button.small {
button.small, div.inputWithButton>button {
font-size: 14px;
padding: 0 12px;
height: 32px;
line-height: 32px;
}
button.small {
height: 32px;
line-height: 32px;
}
div.inputWithButton>button {
border-radius: 2px;
height: 27px;
line-height: 27px;
width: 75px;
margin-left: -79px;
vertical-align: 2px;
}
button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus {
background-color: #29457f;
outline: none;
@@ -191,7 +205,7 @@ button.gray, a.button.gray {
background-color: #808080;
}
button.gray:hover, button.gray:focus, a.button.gray:hover, a.button.gray:focus {
button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus {
background-color: #555555;
}
@@ -199,7 +213,7 @@ button.red, a.button.red {
background-color: #aa5e5e;
}
button.red:hover, button.red:focus, a.button.red:hover, a.button.red:focus {
button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus {
background-color: #7f2929;
}
@@ -207,7 +221,7 @@ button.yellow, a.button.yellow {
background-color: #e8a349;
}
button.yellow:hover, button.yellow:focus, a.button.yellow:hover, a.button.yellow:focus {
button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus {
background-color: #c37713;
}
@@ -215,7 +229,7 @@ button.green, a.button.green {
background-color: #28a745;
}
button.green:hover, button.green:focus, a.button.green:hover, a.button.green:focus {
button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.button.green:focus {
background-color: #1b7d31;
}
@@ -223,22 +237,36 @@ input, select, textarea {
background-color: #f9fafb;
border: solid #c8d2e1 1px;
border-radius: 2px;
padding: 4px;
box-sizing: border-box;
font-size: 15px;
font-weight: 300;
}
input, select {
height: 30px;
line-height: 30px;
padding: 0 5px;
}
textarea {
font-size: 13px;
padding: 5px;
resize: none;
}
input.big, select.big, textarea.big {
padding: 5px;
input.big, select.big, textarea.big, div.inputWithButton>input {
font-size: 18px;
}
input.big, select.big, div.inputWithButton>input {
height: 35px;
line-height: 35px;
padding: 0 6px;
}
textarea.big {
padding: 6px;
}
input.fullWidth, select.fullWidth, textarea.fullWidth {
display: block;
width: 100%;
@@ -253,14 +281,34 @@ input:disabled, select:disabled, textarea:disabled {
input:focus, select:focus, textarea:focus {
background-color: #ffffff;
border: solid #29457f 2px;
padding: 3px;
outline: none;
}
input.big:focus, select.big:focus, textarea.big:focus {
input:focus, select:focus {
padding: 0 4px;
}
textarea:focus {
padding: 4px;
}
input.big:focus, select.big:focus {
padding: 0 5px;
}
div.inputWithButton>input {
width: 100%;
padding: 0 83px 0 6px;
}
div.inputWithButton>input:focus {
padding: 0 82px 0 5px;
}
textarea.big:focus {
padding: 5px;
}
div.modal {
position: fixed;
background-color: #ffffff;
+10 -1
View File
@@ -18,7 +18,16 @@ div.mapItem>div.title {
color: white;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 4px 8px;
padding: 0 8px;
height: 35px;
line-height: 35px;
}
div.mapItem>div.title>p {
line-height: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
div.mapItem>div.title>p.title {
+69
View File
@@ -0,0 +1,69 @@
var Account = {
original: null,
countdown: null,
openGoogleAuthenticate: function () {
window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600')
},
authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) {
var password = document.getElementsByTagName('form')[0].elements.password;
var button = document.getElementById('authenticateWithGoogleButton');
Account.original = {
type: password.type,
placeholder: password.placeholder,
required: password.required,
disabled: password.disabled
};
password.type = 'text';
password.placeholder = 'Authenticated!'
password.value = '';
password.required = false;
password.disabled = true;
button.disabled = true;
Account.countdown = setInterval(function () {
var timeLeft = Math.ceil((authenticatedWithGoogleUntil.getTime() - new Date().getTime()) / 1000);
if (timeLeft > 30) {
return;
}
if (timeLeft <= 0) {
Account.resetGoogleAuthentication();
return;
}
password.placeholder = 'Authenticated! ' + timeLeft + ' seconds left...';
}, 1000);
},
resetGoogleAuthentication: function () {
if (Account.countdown !== null) {
clearInterval(Account.countdown);
}
var password = document.getElementsByTagName('form')[0].elements.password;
var button = document.getElementById('authenticateWithGoogleButton');
password.type = Account.original.type;
password.placeholder = Account.original.placeholder
password.required = Account.original.required;
password.disabled = Account.original.disabled;
button.disabled = false;
}
};
(function () {
document.getElementById('authenticateWithGoogleButton').onclick = function () {
Account.openGoogleAuthenticate();
};
document.getElementsByTagName('form')[0].onreset = function () {
Account.resetGoogleAuthentication();
};
})();
@@ -0,0 +1,10 @@
(function () {
if (success) {
window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil);
window.close();
} else {
document.getElementById('closeWindowButton').onclick = function () {
window.close();
}
}
})();
+7 -4
View File
@@ -8,6 +8,7 @@
rounds: [],
scoreSum: 0,
panoId: null,
pov: null,
panorama: null,
map: null,
guessMarker: null,
@@ -35,6 +36,7 @@
}
Game.panoId = this.response.panoId;
Game.pov = this.response.pov;
if (this.response.history) {
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);
Game.loadPano(Game.panoId);
Game.loadPano(Game.panoId, Game.pov);
},
handleErrorResponse: function (error) {
@@ -129,13 +131,13 @@
});
},
loadPano: function (panoId) {
loadPano: function (panoId, pov) {
if (Game.adaptGuess) {
document.getElementById('guess').classList.add('adapt');
}
Game.panorama.setPov({ heading: 0, pitch: 0 });
Game.panorama.setZoom(0);
Game.panorama.setPov({ heading: pov.heading, pitch: pov.pitch });
Game.panorama.setZoom(pov.zoom);
Game.panorama.setPano(panoId);
},
@@ -199,6 +201,7 @@
}
Game.panoId = this.response.panoId;
Game.pov = this.response.pov;
}, data);
},
+19
View File
@@ -244,4 +244,23 @@ var MapGuesser = {
document.getElementById('cover').onclick = function () {
MapGuesser.hideModal();
};
if (COOKIES_CONSENT) {
MapGuesser.initGoogleAnalitics();
} else {
// we don't want user to agree cookies when clicking on the notice itself
document.getElementById('cookiesNotice').onclick = function (e) {
e.stopPropagation();
};
document.getElementById('agreeCookiesButton').onclick = function () {
MapGuesser.agreeCookies();
document.getElementById('cookiesNotice').style.display = 'none';
};
window.onclick = function () {
MapGuesser.agreeCookies();
};
}
})();
+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
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate)
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
+1 -1
View File
@@ -15,7 +15,7 @@ echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate)
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
+3
View File
@@ -1,5 +1,6 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command;
@@ -23,6 +24,8 @@ class AddUserCommand extends Command
$user = new User();
$user->setEmail($input->getArgument('email'));
$user->setPlainPassword($input->getArgument('password'));
$user->setActive(true);
$user->setCreatedDate(new DateTime());
if ($input->hasArgument('type')) {
$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\Output\OutputInterface;
class DatabaseMigration extends Command
class MigrateDatabaseCommand extends Command
{
public function configure()
{
$this->setName('migrate')
$this->setName('db:migrate')
->setDescription('Migration of database changes.');
}
+4 -4
View File
@@ -21,15 +21,15 @@ class GameController
public function getGame(): IContent
{
$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
{
$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)
+18 -11
View File
@@ -29,8 +29,7 @@ class GameFlowController
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
return new JsonContent(['error' => 'no_session_found']);
}
if (count($state['rounds']) === 0) {
@@ -41,7 +40,10 @@ class GameFlowController
$session->set('state', $state);
$data = ['panoId' => $place->getPanoIdCached()];
$data = [
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()->toArray()
];
} else {
$rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1];
@@ -59,7 +61,10 @@ class GameFlowController
$data = [
'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();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
return new JsonContent(['error' => 'no_session_found']);
}
$last = $state['rounds'][count($state['rounds']) - 1];
@@ -103,23 +107,25 @@ class GameFlowController
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$panoId = $place->getPanoIdCached();
$pov = $place->getPov()->toArray();
} else {
$state['rounds'] = [];
$panoId = null;
$pov = null;
}
$session->set('state', $state);
$data = [
return new JsonContent([
'result' => [
'position' => $position->toArray(),
'distance' => $distance,
'score' => $score
],
'panoId' => $panoId
];
return new JsonContent($data);
'panoId' => $panoId,
'pov' => $pov
]);
}
private function addNewRoundToState(&$state, Place $place, array $placesWithoutPano): void
@@ -128,7 +134,8 @@ class GameFlowController
'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place->getId(),
'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
$data = ['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')];
return new JsonContent($data);
return new JsonContent(['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')]);
}
}
+282 -67
View File
@@ -1,5 +1,7 @@
<?php namespace MapGuesser\Controller;
use DateInterval;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
@@ -8,8 +10,10 @@ use MapGuesser\Mailing\Mail;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
@@ -26,12 +30,15 @@ class LoginController
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
}
public function getLoginForm()
@@ -40,18 +47,23 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$data = [];
return new HtmlContent('login/login', $data);
return new HtmlContent('login/login');
}
public function getGoogleLoginRedirect(): IRedirect
{
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(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);
}
@@ -75,8 +87,7 @@ class LoginController
public function getSignupSuccess(): IContent
{
$data = [];
return new HtmlContent('login/signup_success', $data);
return new HtmlContent('login/signup_success');
}
public function getSignupWithGoogleForm()
@@ -93,48 +104,96 @@ class LoginController
$user = $this->userRepository->getByEmail($userData['email']);
$data = ['found' => $user !== null, 'email' => $userData['email']];
return new HtmlContent('login/google_signup', $data);
return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email']]);
}
public function getRequestPasswordResetForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
}
public function getRequestPasswordResetSuccess(): IContent
{
return new HtmlContent('login/password_reset_request_success');
}
public function getResetPasswordForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new HtmlContent('login/reset_password', ['success' => false]);
}
$user = $this->userRepository->getById($resetter->getUserId());
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]);
}
public function login(): IContent
{
if ($this->request->user() !== null) {
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) {
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => ['errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
$tmpUser = new User();
$tmpUser->setPlainPassword($this->request->post('password'));
$this->request->session()->set('tmp_user_data', ['email' => $this->request->post('email'), 'password_hashed' => $tmpUser->getPassword()]);
$this->request->session()->set('tmp_user_data', [
'email' => $this->request->post('email'),
'password_hashed' => $tmpUser->getPassword()
]);
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()]];
return new JsonContent($data);
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()
]
]);
}
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!']];
return new JsonContent($data);
$this->resendConfirmationEmail($user);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
}
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given password is wrong.']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function loginWithGoogle()
@@ -144,30 +203,34 @@ class LoginController
}
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
$data = [];
return new HtmlContent('login/google_login', $data);
return new HtmlContent('login/google_login');
}
$oAuth = new GoogleOAuth(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'])) {
$data = [];
return new HtmlContent('login/google_login', $data);
return new HtmlContent('login/google_login');
}
$jwtParser = new JwtParser($tokenData['id_token']);
$userData = $jwtParser->getPayload();
$idToken = $jwtParser->getPayload();
if (!$userData['email_verified']) {
$data = [];
return new HtmlContent('login/google_login', $data);
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('login/google_login');
}
$user = $this->userRepository->getByGoogleSub($userData['sub']);
if (!$idToken['email_verified']) {
return new HtmlContent('login/google_login');
}
$user = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($user === null) {
$this->request->session()->set('google_user_data', $userData);
$this->request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
}
@@ -187,8 +250,7 @@ class LoginController
public function signup(): IContent
{
if ($this->request->user() !== null) {
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]];
return new JsonContent($data);
return new JsonContent(['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
@@ -196,22 +258,31 @@ class LoginController
if ($user !== null) {
if ($user->getActive()) {
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($data);
return new JsonContent([
'error' => [
'errorText' => 'There is a user already registered with the given email address, ' .
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
$this->request->setUser($user);
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]];
} else {
$data = ['error' => ['errorText' => 'There is a user already registered with the given email address. Please check your email and click on the activation link!']];
$data = [
'error' => [
'errorText' => 'There is a user already registered with the given email address. ' .
'Please check your email and click on the activation link!'
]
];
}
return new JsonContent($data);
}
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
$data = ['error' => ['errorText' => 'The given email address is not valid.']];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if ($this->request->session()->has('tmp_user_data')) {
@@ -221,52 +292,53 @@ class LoginController
$tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given passwords do not match.']];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
} else {
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => ['errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
$data = ['error' => ['errorText' => 'The given passwords do not match.']];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
}
$user = new User();
$user->setEmail($this->request->post('email'));
$user->setPlainPassword($this->request->post('password'));
$user->setCreatedDate(new DateTime());
\Container::$dbConnection->startTransaction();
$this->pdm->saveToDb($user);
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime());
$token = bin2hex(random_bytes(16));
$confirmation = new UserConfirmation();
$confirmation->setUser($user);
$confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token);
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
$this->request->session()->delete('tmp_user_data');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function signupWithGoogle(): IContent
{
if ($this->request->user() !== null) {
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
$userData = $this->request->session()->get('google_user_data');
@@ -278,6 +350,7 @@ class LoginController
$user = new User();
$user->setEmail($userData['email']);
$user->setCreatedDate(new DateTime());
} else {
$sendWelcomeEmail = false;
}
@@ -294,24 +367,21 @@ class LoginController
$this->request->session()->delete('google_user_data');
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function resetSignup(): IContent
{
$this->request->session()->delete('tmp_user_data');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function resetGoogleSignup(): IContent
{
$this->request->session()->delete('google_user_data');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function activate()
@@ -320,11 +390,10 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) {
$data = [];
return new HtmlContent('login/activate', $data);
return new HtmlContent('login/activate');
}
\Container::$dbConnection->startTransaction();
@@ -349,11 +418,10 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) {
$data = ['success' => false];
return new HtmlContent('login/cancel', $data);
return new HtmlContent('login/cancel', ['success' => false]);
}
\Container::$dbConnection->startTransaction();
@@ -366,23 +434,156 @@ class LoginController
\Container::$dbConnection->commit();
$data = ['success' => true];
return new HtmlContent('login/cancel', $data);
return new HtmlContent('login/cancel', ['success' => true]);
}
private function sendConfirmationEmail(string $email, string $token): void
public function requestPasswordReset(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) {
return new JsonContent([
'error' => [
'errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!'
]
]);
}
if (!$user->getActive()) {
$this->resendConfirmationEmail($user);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
}
$existingResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($existingResetter !== null && $existingResetter->getExpiresDate() > new DateTime()) {
return new JsonContent([
'error' => [
'errorText' => 'Password reset was recently requested for this account. Please check your email, or try again later!'
]
]);
}
$token = bin2hex(random_bytes(16));
$expires = new DateTime('+1 hour');
$passwordResetter = new UserPasswordResetter();
$passwordResetter->setUser($user);
$passwordResetter->setToken($token);
$passwordResetter->setExpiresDate($expires);
\Container::$dbConnection->startTransaction();
if ($existingResetter !== null) {
$this->pdm->deleteFromDb($existingResetter);
}
$this->pdm->saveToDb($passwordResetter);
\Container::$dbConnection->commit();
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
return new JsonContent(['success' => true]);
}
public function resetPassword(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
]
]);
}
if (strlen($this->request->post('password')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword($this->request->post('password'));
$this->pdm->saveToDb($user);
\Container::$dbConnection->commit();
$this->request->setUser($user);
return new JsonContent(['success' => true]);
}
private function sendConfirmationEmail(string $email, string $token, DateTime $created): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
$mail->setBodyFromTemplate('signup', [
'EMAIL' => $email,
'ACTIVATE_LINK' => $this->request->getBase() . '/'. \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
'ACTIVATE_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T')
]);
$mail->send();
}
private function resendConfirmationEmail(User $user): bool
{
$confirmation = $this->userConfirmationRepository->getByUser($user);
if ($confirmation === null || (clone $confirmation->getLastSentDate())->add(new DateInterval('PT1H')) > new DateTime()) {
return false;
}
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
return true;
}
private function sendWelcomeEmail(string $email): void
{
$mail = new Mail();
@@ -393,4 +594,18 @@ class LoginController
]);
$mail->send();
}
private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
$mail->setBodyFromTemplate('password-reset', [
'EMAIL' => $email,
'RESET_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i T')
]);
$mail->send();
}
}
+36 -49
View File
@@ -1,10 +1,8 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Map;
@@ -15,7 +13,7 @@ use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov;
class MapAdminController implements ISecured
{
@@ -57,8 +55,13 @@ class MapAdminController implements ISecured
$places = [];
}
$data = ['mapId' => $mapId, 'mapName' => $map->getName(), 'mapDescription' => str_replace('<br>', "\n", $map->getDescription()), 'bounds' => $map->getBounds()->toArray(), 'places' => &$places];
return new HtmlContent('admin/map_editor', $data);
return new HtmlContent('admin/map_editor', [
'mapId' => $mapId,
'mapName' => $map->getName(),
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
'bounds' => $map->getBounds()->toArray(),
'places' => &$places
]);
}
public function getPlace(): IContent
@@ -67,8 +70,7 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById($placeId);
$data = ['panoId' => $place->getFreshPanoId()];
return new JsonContent($data);
return new JsonContent(['panoId' => $place->getFreshPanoId()]);
}
public function saveMap(): IContent
@@ -94,6 +96,11 @@ class MapAdminController implements ISecured
$place->setMap($map);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
if ($placeRaw['panoId'] === -1) {
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
@@ -114,6 +121,11 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
$this->pdm->saveToDb($place);
@@ -146,11 +158,11 @@ class MapAdminController implements ISecured
\Container::$dbConnection->commit();
$data = ['mapId' => $map->getId(), 'added' => $addedIds];
return new JsonContent($data);
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
}
public function deleteMap() {
public function deleteMap()
{
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
@@ -163,40 +175,22 @@ class MapAdminController implements ISecured
\Container::$dbConnection->commit();
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
private function deletePlaces(Map $map): void
{
//TODO: relations?
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(Place::getFields());
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
while ($placeData = $result->fetch(IResultSet::FETCH_ASSOC)) {
$place = new Place();
$this->pdm->fillWithData($placeData, $place);
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$this->pdm->deleteFromDb($place);
}
}
private function calculateMapBounds(Map $map): Bounds
{
//TODO: from repository or relations
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['lat', 'lng']);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
$bounds = new Bounds();
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$bounds->extend(new Position($place['lat'], $place['lng']));
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$bounds->extend($place->getPosition());
}
return $bounds;
@@ -204,26 +198,19 @@ class MapAdminController implements ISecured
private function &getPlaces(Map $map): array
{
//TODO: from repository or relations
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
$places = [];
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
//$panoId = ???
//$pov = ???
$noPano = $place['pano_id_cached_timestamp'] && $place['pano_id_cached'] === null;
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$noPano = $place->getPanoIdCachedTimestampDate() !== null && $place->getPanoIdCached() === null;
$places[$place['id']] = [
'id' => $place['id'],
'lat' => $place['lat'],
'lng' => $place['lng'],
$placeId = $place->getId();
$places[$placeId] = [
'id' => $placeId,
'lat' => $place->getLat(),
'lng' => $place->getLng(),
'panoId' => null,
'pov' => ['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
'pov' => $place->getPov()->toArray(),
'noPano' => $noPano
];
}
+5 -3
View File
@@ -19,7 +19,7 @@ class MapsController
public function getMaps(): IContent
{
//TODO: from repository
//TODO: from repository - count should be added
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns([
['maps', 'id'],
@@ -46,8 +46,10 @@ class MapsController
}
$user = $this->request->user();
$data = ['maps' => $maps, 'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)];
return new HtmlContent('maps', $data);
return new HtmlContent('maps', [
'maps' => $maps,
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
]);
}
private function formatMapAreaForHuman(float $area): array
+137 -22
View File
@@ -1,16 +1,20 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
use MapGuesser\Util\JwtParser;
class UserController implements ISecured
{
@@ -20,11 +24,14 @@ class UserController implements ISecured
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
}
public function authorize(): bool
@@ -41,8 +48,76 @@ class UserController implements ISecured
*/
$user = $this->request->user();
$data = ['user' => $user->toArray()];
return new HtmlContent('account/account', $data);
return new HtmlContent('account/account', ['user' => $user->toArray()]);
}
public function getGoogleAuthenticateRedirect(): IRedirect
{
/**
* @var User $user
*/
$user = $this->request->user();
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
$nonce,
$user->getEmail()
);
return new Redirect($url, IRedirect::TEMPORARY);
}
public function authenticateWithGoogle(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken(
$this->request->query('code'),
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
);
if (!isset($tokenData['id_token'])) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
$jwtParser = new JwtParser($tokenData['id_token']);
$idToken = $jwtParser->getPayload();
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
if ($idToken['sub'] !== $user->getGoogleSub()) {
return new HtmlContent('account/google_authenticate', [
'success' => false,
'errorText' => 'This Google account is not linked to your account.'
]);
}
$authenticatedWithGoogleUntil = new DateTime('+45 seconds');
$this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
return new HtmlContent('account/google_authenticate', [
'success' => true,
'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil
]);
}
public function getDeleteAccount(): IContent
@@ -52,8 +127,7 @@ class UserController implements ISecured
*/
$user = $this->request->user();
$data = ['user' => $user->toArray()];
return new HtmlContent('account/delete', $data);
return new HtmlContent('account/delete', ['user' => $user->toArray()]);
}
public function saveAccount(): IContent
@@ -63,20 +137,30 @@ class UserController implements ISecured
*/
$user = $this->request->user();
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given current password is wrong.']];
return new JsonContent($data);
if (!$this->confirmUserIdentity(
$user,
$this->request->session()->get('authenticated_with_google_until'),
$this->request->post('password'),
$error
)) {
return new JsonContent(['error' => ['errorText' => $error]]);
}
if (strlen($this->request->post('password_new')) > 0) {
if (strlen($this->request->post('password_new')) < 6) {
$data = ['error' => ['errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
$data = ['error' => ['errorText' => 'The given new passwords do not match.']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given new passwords do not match.'
]
]);
}
$user->setPlainPassword($this->request->post('password_new'));
@@ -84,8 +168,9 @@ class UserController implements ISecured
$this->pdm->saveToDb($user);
$data = ['success' => true];
return new JsonContent($data);
$this->request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]);
}
public function deleteAccount(): IContent
@@ -95,22 +180,52 @@ class UserController implements ISecured
*/
$user = $this->request->user();
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given current password is wrong.']];
return new JsonContent($data);
if (!$this->confirmUserIdentity(
$user,
$this->request->session()->get('authenticated_with_google_until'),
$this->request->post('password'),
$error
)) {
return new JsonContent(['error' => ['errorText' => $error]]);
}
\Container::$dbConnection->startTransaction();
foreach ($this->userConfirmationRepository->getByUser($user) as $userConfirmation) {
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter);
}
$this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit();
$data = ['success' => true];
return new JsonContent($data);
$this->request->session()->delete('authenticated_with_google_until');
return new JsonContent(['success' => true]);
}
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, &$error): bool
{
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
return true;
}
if ($password !== null) {
if ($user->checkPassword($password)) {
return true;
}
$error = 'The given current password is wrong.';
return false;
}
$error = 'Could not confirm your identity. Please try again!';
return false;
}
}
+1 -1
View File
@@ -135,6 +135,6 @@ class Modify
private function generateKey(): string
{
return substr(hash('sha256', serialize($this->attributes) . random_bytes(10) . microtime()), 0, 7);
return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7);
}
}
+3 -1
View File
@@ -2,7 +2,9 @@
interface IContent
{
public function &getData(): array;
public function setData(array $data): void;
public function getData(): array;
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
{
}
+9 -2
View File
@@ -15,7 +15,7 @@ class GoogleOAuth
$this->request = $request;
}
public function getDialogUrl(string $state, string $redirectUrl): string
public function getDialogUrl(string $state, string $redirectUrl, ?string $nonce = null, ?string $loginHint = null): string
{
$oauthParams = [
'response_type' => 'code',
@@ -23,9 +23,16 @@ class GoogleOAuth
'scope' => 'openid email',
'redirect_uri' => $redirectUrl,
'state' => $state,
'nonce' => hash('sha256', random_bytes(10) . microtime()),
];
if ($nonce !== null) {
$oauthParams['nonce'] = $nonce;
}
if ($loginHint !== null) {
$oauthParams['login_hint'] = $loginHint;
}
return self::$dialogUrlBase . '?' . http_build_query($oauthParams);
}
+46 -2
View File
@@ -5,12 +5,13 @@ use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov;
class Place extends Model
{
protected static string $table = 'places';
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp'];
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp', 'pov_heading', 'pov_pitch', 'pov_zoom'];
protected static array $relations = ['map' => Map::class];
@@ -24,9 +25,12 @@ class Place extends Model
private ?DateTime $panoIdCachedTimestamp = null;
private Pov $pov;
public function __construct()
{
$this->position = new Position(0.0, 0.0);
$this->pov = new Pov(0.0, 0.0, 0.0);
}
public function setMap(Map $map): void
@@ -54,6 +58,26 @@ class Place extends Model
$this->position->setLng($lng);
}
public function setPov(Pov $pov): void
{
$this->pov = $pov;
}
public function setPovHeading(float $heading): void
{
$this->pov->setHeading($heading);
}
public function setPovPitch(float $pitch): void
{
$this->pov->setPitch($pitch);
}
public function setPovZoom(float $zoom): void
{
$this->pov->setZoom($zoom);
}
public function setPanoIdCached(?string $panoIdCached): void
{
$this->panoIdCached = $panoIdCached;
@@ -96,6 +120,26 @@ class Place extends Model
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
{
if (
@@ -122,7 +166,7 @@ class Place extends Model
}
$this->panoIdCached = $panoId;
$this->panoIdCachedTimestamp = new DateTime('now');
$this->panoIdCachedTimestamp = new DateTime();
(new PersistentDataManager())->saveToDb($this);
+24 -1
View File
@@ -1,12 +1,13 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
use MapGuesser\Interfaces\Authentication\IUser;
class User extends Model implements IUser
{
protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub'];
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub', 'created'];
private static array $types = ['user', 'admin'];
@@ -20,6 +21,8 @@ class User extends Model implements IUser
private ?string $googleSub = null;
private DateTime $created;
public function setEmail(string $email): void
{
$this->email = $email;
@@ -52,6 +55,16 @@ class User extends Model implements IUser
$this->googleSub = $googleSub;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getEmail(): string
{
return $this->email;
@@ -77,6 +90,16 @@ class User extends Model implements IUser
return $this->googleSub;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
public function hasPermission(int $permission): bool
{
switch ($permission) {
+25 -1
View File
@@ -1,10 +1,12 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserConfirmation extends Model
{
protected static string $table = 'user_confirmations';
protected static array $fields = ['user_id', 'token'];
protected static array $fields = ['user_id', 'token', 'last_sent'];
protected static array $relations = ['user' => User::class];
@@ -14,6 +16,8 @@ class UserConfirmation extends Model
private string $token = '';
private DateTime $lastSent;
public function setUser(User $user): void
{
$this->user = $user;
@@ -29,6 +33,16 @@ class UserConfirmation extends Model
$this->token = $token;
}
public function setLastSentDate(DateTime $lastSent): void
{
$this->lastSent = $lastSent;
}
public function setLastSent(string $lastSent): void
{
$this->lastSent = new DateTime($lastSent);
}
public function getUser(): ?User
{
return $this->user;
@@ -43,4 +57,14 @@ class UserConfirmation extends Model
{
return $this->token;
}
public function getLastSentDate(): DateTime
{
return $this->lastSent;
}
public function getLastSent(): string
{
return $this->lastSent->format('Y-m-d H:i:s');
}
}
@@ -0,0 +1,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
{
$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();
$this->fillWithData($data, $model);
+12
View File
@@ -1,6 +1,8 @@
<?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
@@ -18,6 +20,15 @@ class PlaceRepository
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
{
$placesWithoutPano = [];
@@ -37,6 +48,7 @@ class PlaceRepository
private function selectRandomFromDbForMap(int $mapId, array $exclude): ?Place
{
//TODO: omit table name here
$select = new Select(\Container::$dbConnection, 'places');
$select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $mapId);
@@ -1,8 +1,6 @@
<?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\PersistentDataManager;
@@ -29,11 +27,11 @@ class UserConfirmationRepository
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->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;
use DateTime;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
@@ -33,4 +35,13 @@ class UserRepository
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;
public function &getData(): array
public function setData(array $data): void
{
$this->data = $data;
}
public function getData(): array
{
return $this->data;
}
+2 -2
View File
@@ -6,10 +6,10 @@ class HtmlContent extends ContentBase
{
private string $view;
public function __construct(string $view, array &$data = [])
public function __construct(string $view, array $data = [])
{
$this->view = $view;
$this->data = &$data;
$this->data = $data;
}
public function render(): void
+2 -2
View File
@@ -2,9 +2,9 @@
class JsonContent extends ContentBase
{
public function __construct(array &$data = [])
public function __construct(array $data = [])
{
$this->data = &$data;
$this->data = $data;
}
public function render(): void
+14 -23
View File
@@ -4,11 +4,9 @@ use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use SessionHandlerInterface;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
use MapGuesser\Interfaces\Session\ISessionHandler;
class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
class DatabaseSessionHandler implements ISessionHandler
{
private bool $exists = false;
@@ -28,7 +26,7 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
{
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['data']);
$select->whereId($id);
$select->whereId(substr($id, 0, 32));
$result = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
@@ -46,16 +44,16 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
$modify = new Modify(\Container::$dbConnection, 'sessions');
if ($this->exists) {
$modify->setId($id);
$modify->setId(substr($id, 0, 32));
} else {
$modify->setExternalId($id);
$modify->setExternalId(substr($id, 0, 32));
}
$modify->set('data', $data);
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
$modify->save();
$written = true;
$this->written = true;
return true;
}
@@ -63,37 +61,30 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
public function destroy($id): bool
{
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($id);
$modify->setId(substr($id, 0, 32));
$modify->delete();
$this->exists = false;
return true;
}
public function gc($maxlifetime): int
{
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']);
$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();
}
// empty on purpose
// old sessions are deleted by MaintainDatabaseCommand
return true;
}
public function create_sid(): string
{
return hash('sha256', random_bytes(10) . microtime());
return bin2hex(random_bytes(16));
}
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
@@ -104,7 +95,7 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
$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->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
];
}
}
+12 -15
View File
@@ -108,7 +108,7 @@ class Linker
private function generateAssets(ParsedFragment $fragment, array &$sections)
{
foreach ($fragment->getCss() as $cssFile) {
$asset = $this->parseAsset($cssFile, 'css');
$asset = $this->parseAsset($cssFile);
if (isset($asset['code'])) {
$sections['inlineCss'] .= '<style>' . PHP_EOL;
$sections['inlineCss'] .= $asset['code'];
@@ -119,7 +119,7 @@ class Linker
}
foreach ($fragment->getJs() as $jsFile) {
$asset = $this->parseAsset($jsFile, 'js');
$asset = $this->parseAsset($jsFile);
if (isset($asset['code'])) {
$sections['inlineJs'] .= '<script>' . PHP_EOL;
$sections['inlineJs'] .= $asset['code'];
@@ -130,24 +130,21 @@ class Linker
}
}
private function parseAsset(string $asset, string $type): array
private function parseAsset(string $asset): array
{
$output = [];
eval('$asset = ' . $asset . ';');
if (
empty($_ENV['DEV']) &&
preg_match('/^' . $type . '\/.*/', $asset) &&
filesize(ROOT . '/public/static/' . $asset) < self::INLINE_ASSET_LIMIT
) {
$output['code'] = file_get_contents(ROOT . '/public/static/' . $asset);
} else {
if (!preg_match('/^http(s)?/', $asset)) {
$output['file'] = $_ENV['STATIC_ROOT'] . '/' . $asset . '?rev=<?= REVISION ?>';
if (preg_match('/^[\w\/\.]+$/', $asset)) {
if (
empty($_ENV['DEV']) &&
filesize(ROOT . '/public/static/' . $asset) < self::INLINE_ASSET_LIMIT
) {
$output['code'] = file_get_contents(ROOT . '/public/static/' . $asset);
} else {
$output['file'] = $asset;
$output['file'] = '<?= $_ENV[\'STATIC_ROOT\'] ?>/' . $asset . '?rev=<?= REVISION ?>';
}
} else {
$output['file'] = $asset;
}
return $output;
+2 -2
View File
@@ -126,7 +126,7 @@ class Parser
private function matchExtends(string $line): ?string
{
if (preg_match('/^\s*@extends\(\'([\w\/]+)\'\)\s*$/', $line, $matches)) {
if (preg_match('/^\s*@extends\(([\w\/]+)\)\s*$/', $line, $matches)) {
return $matches[1];
}
@@ -135,7 +135,7 @@ class Parser
private function matchSection(string $line): ?string
{
if (preg_match('/^\s*@section\(\'(\w+)\'\)\s*$/', $line, $matches)) {
if (preg_match('/^\s*@section\((\w+)\)\s*$/', $line, $matches)) {
return $matches[1];
}
+4 -8
View File
@@ -13,6 +13,7 @@ final class GoogleOAuthTest extends TestCase
{
$_ENV['GOOGLE_OAUTH_CLIENT_ID'] = 'xyz';
$state = 'random_state_string';
$nonce = 'random_nonce_string';
$redirectUrl = 'http://example.com/oauth';
$requestMock = $this->getMockBuilder(IRequest::class)
@@ -20,7 +21,7 @@ final class GoogleOAuthTest extends TestCase
->getMock();
$googleOAuth = new GoogleOAuth($requestMock);
$dialogUrl = $googleOAuth->getDialogUrl($state, $redirectUrl);
$dialogUrl = $googleOAuth->getDialogUrl($state, $redirectUrl, $nonce);
$dialogUrlParsed = explode('?', $dialogUrl);
$this->assertEquals('https://accounts.google.com/o/oauth2/v2/auth', $dialogUrlParsed[0]);
@@ -33,15 +34,10 @@ final class GoogleOAuthTest extends TestCase
'scope' => 'openid email',
'redirect_uri' => $redirectUrl,
'state' => $state,
'nonce' => hash('sha256', random_bytes(10) . microtime()),
'nonce' => $nonce,
];
$this->assertEquals($expectedQueryParams['response_type'], $dialogUrlQueryParams['response_type']);
$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']);
$this->assertEquals($expectedQueryParams, $dialogUrlQueryParams);
}
public function testCanRequestToken(): void
+144
View File
@@ -0,0 +1,144 @@
<?php namespace MapGuesser\Tests\View\Parser;
use MapGuesser\View\ParsedFragment;
use MapGuesser\View\Parser;
use PHPUnit\Framework\TestCase;
final class ParserTest extends TestCase
{
const TEST_VIEWS_PATH = __DIR__ . '/../assets/views';
public function testCanParseViewWithoutExtends(): void
{
$file = realpath(self::TEST_VIEWS_PATH . '/view_without_extends.php');
$parser = new Parser($file);
$fragment = $parser->parse();
$expected = new ParsedFragment(
null,
[],
[],
[],
['', '']
);
$this->assertEquals($expected, $fragment);
}
public function testCanParseViewWithAssets(): void
{
$file = realpath(self::TEST_VIEWS_PATH . '/view_with_assets.php');
$parser = new Parser($file);
$fragment = $parser->parse();
$expected = new ParsedFragment(
null,
[
'test.css'
],
[
'test.js',
'test_<?= $some[\'expression\'] ?>'
],
[],
['', '']
);
$this->assertEquals($expected, $fragment);
}
public function testCanParseViewWithExtends(): void
{
$file = realpath(self::TEST_VIEWS_PATH . '/view_with_extends.php');
$parser = new Parser($file);
$fragment = $parser->parse();
$expected = new ParsedFragment(
'parent',
[],
[],
[
'section1' => '<div>Test HTML with @extends</div>' . "\n"
],
['', '']
);
$this->assertEquals($expected, $fragment);
}
public function testCanParseComplexView(): void
{
$file = realpath(self::TEST_VIEWS_PATH . '/view_complex.php');
$parser = new Parser($file);
$fragment = $parser->parse();
$expected = new ParsedFragment(
'parent',
[
'test1.css'
],
[
'test1.js',
'test2_<?= $some[\'expression\'] ?>'
],
[
'section1' => '<div>Test HTML with @extends - section 1</div>' . "\n",
'section2' => '<div>Test HTML with @extends - section 2</div>' . "\n"
],
[
'<?php phpinfo() ?>' . "\n",
'<?php $a = \'test_string\' ?>' . "\n" . 'EXTRA' . "\n",
]
);
$this->assertEquals($expected, $fragment);
}
public function testFailsIfMultipleExtendsGiven(): void
{
$file = realpath(self::TEST_VIEWS_PATH . '/view_invalid_multiple_extends.php');
$parser = new Parser($file);
$this->expectExceptionMessage('Error in file ' . $file . ' in line 3 - There is already an \'@extends\' declared.');
$parser->parse();
}
public function testFailsIfSectionWithoutExtendsGiven(): void
{
$file = realpath(self::TEST_VIEWS_PATH . '/view_invalid_section_without_extends.php');
$parser = new Parser($file);
$this->expectExceptionMessage('Error in file ' . $file . ' in line 1 - \'@section\' has no meaning if view extends nothing.');
$parser->parse();
}
public function testFailsIfOpeningSectionBeforePreviousClosed(): void
{
$file = realpath(self::TEST_VIEWS_PATH . '/view_invalid_multiple_sections_open.php');
$parser = new Parser($file);
$this->expectExceptionMessage('Parse error in file ' . $file . ' in line 4 - A \'@section\' is already open (no \'@endsection\' found).');
$parser->parse();
}
public function testFailsIfClosingSectionWhenNoSectionIsOpen(): void
{
$file = realpath(self::TEST_VIEWS_PATH . '/view_invalid_section_not_open.php');
$parser = new Parser($file);
$this->expectExceptionMessage('Parse error in file ' . $file . ' in line 4 - Cannot end section until no \'@section\' is open.');
$parser->parse();
}
}
+22
View File
@@ -0,0 +1,22 @@
@css(test1.css)
@js(test1.js)
@js(test2_<?= $some['expression'] ?>)
@extra
<?php phpinfo() ?>
@endextra
@extends(parent)
@section(section1)
<div>Test HTML with @extends - section 1</div>
@endsection
@section(section2)
<div>Test HTML with @extends - section 2</div>
@endsection
@extra
<?php $a = 'test_string' ?>
EXTRA
@endextra
@@ -0,0 +1,5 @@
@extends(parent1)
@extends(parent2)
<div>Test HTML with multiple @extends</div>
@@ -0,0 +1,6 @@
@extends(parent)
@section(section1)
@section(section2)
<div>Test HTML with opening @section before previous closed</div>
@@ -0,0 +1,4 @@
@extends(parent)
<div>Test HTML with @endsection when no section is open</div>
@endsection
@@ -0,0 +1,3 @@
@section(section1)
<div>Test HTML with @section but without @extends</div>
+4
View File
@@ -0,0 +1,4 @@
@css(test.css)
@js(test.js)
@js(test_<?= $some['expression'] ?>)
<div>Test HTML with @css and @js</div>
+5
View File
@@ -0,0 +1,5 @@
@extends(parent)
@section(section1)
<div>Test HTML with @extends</div>
@endsection
@@ -0,0 +1 @@
<div>Test HTML without @extends</div>
+20 -3
View File
@@ -1,10 +1,27 @@
@extends('templates/layout_normal')
@js(js/account/account.js)
@section('main')
@extends(templates/layout_normal)
@section(main)
<h2>Account</h2>
<div class="box">
<form id="accountForm" action="/account" method="post" data-observe-inputs="password_new,password_new_confirm">
<input class="big fullWidth" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with your password or with Google to modify your account.</p>
<div class="inputWithButton">
<input type="password" name="password" placeholder="Current password" required minlength="6" autofocus><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div>
<?php elseif ($user['password'] !== null): ?>
<p class="justify small">Please confirm your identity with your password to modify your account.</p>
<input class="big fullWidth" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
<?php elseif ($user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with Google to modify your account.</p>
<div class="inputWithButton">
<input type="text" name="password" placeholder="Authenticate with Google..." disabled><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div>
<?php endif; ?>
<hr>
<?php /* TODO: disabled for the time being, email modification should be implemented */ ?>
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled>
+23 -5
View File
@@ -1,14 +1,32 @@
@extends('templates/layout_normal')
@js(js/account/account.js)
@section('main')
@extends(templates/layout_normal)
@section(main)
<h2>Delete account</h2>
<div class="box">
<form id="deleteAccountForm" action="/account/delete" method="post" data-redirect-on-success="/">
<p class="justify">Are you sure you want to delete your account? This cannot be undone!</p>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
<p class="justify marginBottom">Are you sure you want to delete your account? This cannot be undone!</p>
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with your password or with Google to delete your account.</p>
<div class="inputWithButton">
<input type="password" name="password" placeholder="Current password" required minlength="6" autofocus><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div>
<?php elseif ($user['password'] !== null): ?>
<p class="justify small">Please confirm your identity with your password to delete your account.</p>
<input class="big fullWidth" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
<?php elseif ($user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with Google to delete your account.</p>
<div class="inputWithButton">
<input type="text" name="password" placeholder="Authenticate with Google..." disabled><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div>
<?php endif; ?>
<p id="deleteAccountFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button class="red" type="submit" name="submit">Delete account</button>
<button class="red marginRight" type="submit" name="submit">Delete account</button><!--
--><a class="button gray marginTop" href="/account" title="Back to account">Cancel</a>
</div>
</form>
</div>
+28
View File
@@ -0,0 +1,28 @@
@js(js/account/google_authenticate.js)
@extends(templates/layout_minimal)
@section(main)
<h2>Authenticate with Google</h2>
<?php if (!$success): ?>
<div class="box">
<p class="error justify">
<?php if (isset($errorText)): ?>
<?= $errorText ?>
<?php else: ?>
Authentication with Google failed.
<?php endif; ?>
Please <a id="closeWindowButton" href="javascript:;" title="Close">close this window/tab</a> and try again!
</p>
</div>
<?php endif; ?>
@endsection
@section(pageScript)
<script>
var success = <?= $success ? 'true' : 'false' ?>;
<?php if (isset($authenticatedWithGoogleUntil)): ?>
var authenticatedWithGoogleUntil = new Date('<?= $authenticatedWithGoogleUntil->format('c') ?>');
<?php endif; ?>
</script>
@endsection
+13 -13
View File
@@ -1,16 +1,16 @@
@css('node_modules/leaflet/dist/leaflet.css')
@css('css/map_editor.css')
@css('node_modules/leaflet.markercluster/dist/MarkerCluster.css')
@css('node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css')
@css(node_modules/leaflet/dist/leaflet.css)
@css(css/map_editor.css)
@css(node_modules/leaflet.markercluster/dist/MarkerCluster.css)
@css(node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css)
@js('node_modules/leaflet/dist/leaflet.js')
@js('node_modules/leaflet.markercluster/dist/leaflet.markercluster.js')
@js('https://maps.googleapis.com/maps/api/js?key=' . $_ENV['GOOGLE_MAPS_JS_API_KEY'])
@js('js/map_editor.js')
@js(node_modules/leaflet/dist/leaflet.js)
@js(node_modules/leaflet.markercluster/dist/leaflet.markercluster.js)
@js(https://maps.googleapis.com/maps/api/js?key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>)
@js(js/map_editor.js)
@extends('templates/layout_full')
@extends(templates/layout_full)
@section('subheader')
@section(subheader)
<span><a href="javascript:;" id="mapName" title="Edit map data"><?= $mapName ?></a></span><!--
--><span><!--
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
@@ -38,7 +38,7 @@
--></span>
@endsection
@section('pagemodal')
@section(pagemodal)
<div id="metadata" class="modal">
<h2>Edit map data</h2>
<form id="metadataForm" class="marginTop" data-no-submit="true">
@@ -52,7 +52,7 @@
</div>
@endsection
@section('main')
@section(main)
<div id="map"></div>
<div id="panorama"></div>
<div id="noPano">
@@ -68,7 +68,7 @@
</div>
@endsection
@section('pageScript')
@section(pageScript)
<script>
var tileUrl = '<?= $_ENV['LEAFLET_TILESERVER_URL'] ?>';
var tileAttribution = '<?= $_ENV['LEAFLET_TILESERVER_ATTRIBUTION'] ?>';
+2 -2
View File
@@ -1,6 +1,6 @@
@extends('templates/layout_normal')
@extends(templates/layout_normal)
@section('main')
@section(main)
<h2>404 | Page not found</h2>
<p>The requested URL was not found on this server. <a href="/" title="<?= $_ENV['APP_NAME'] ?>">Back to start.</a></p>
@endsection
+7 -7
View File
@@ -1,17 +1,17 @@
@css('css/game.css')
@css(css/game.css)
@js('https://maps.googleapis.com/maps/api/js?key=' . $_ENV['GOOGLE_MAPS_JS_API_KEY'])
@js('js/game.js')
@js(https://maps.googleapis.com/maps/api/js?key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>)
@js(js/game.js)
@extends('templates/layout_full')
@extends(templates/layout_full)
@section('subheader')
@section(subheader)
<span id="mapName" class="bold"><?= $mapName ?></span><!--
--><span>Round <span id="currentRound" class="bold"></span></span><!--
--><span>Score <span id="currentScoreSum" class="bold"></span></span>
@endsection
@section('main')
@section(main)
<div id="panoCover"></div>
<div id="panorama"></div>
<div id="showGuessButtonContainer">
@@ -48,7 +48,7 @@
</div>
@endsection
@section('pageScript')
@section(pageScript)
<script>
var mapId = <?= $mapId ?>;
var mapBounds = <?= json_encode($bounds) ?>;
+3 -3
View File
@@ -1,8 +1,8 @@
@extends('templates/layout_normal')
@extends(templates/layout_normal)
@section('main')
@section(main)
<h2>Account activation</h2>
<div class="box">
<p class="error justify">Activation failed. Please check the link you entered or retry <a href="/signup" title="Sign up">sign up</a>!</p>
<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>
@endsection
+2 -2
View File
@@ -1,6 +1,6 @@
@extends('templates/layout_normal')
@extends(templates/layout_normal)
@section('main')
@section(main)
<h2>Account cancellation</h2>
<div class="box">
<?php if ($success) : ?>
+3 -3
View File
@@ -1,8 +1,8 @@
@extends('templates/layout_normal')
@extends(templates/layout_normal)
@section('main')
@section(main)
<h2>Login up with Google</h2>
<div class="box">
<p class="error justify">Authenticating with Google failed. Please <a href="/login/google" title="Login with Google">retry</a>!</p>
<p class="error justify">Authentication with Google failed. Please <a href="/login/google" title="Login with Google">try again</a>!</p>
</div>
@endsection
+3 -3
View File
@@ -1,8 +1,8 @@
@js('js/login/google_signup.js')
@js(js/login/google_signup.js)
@extends('templates/layout_normal')
@extends(templates/layout_normal)
@section('main')
@section(main)
<h2>Sign up</h2>
<div class="box">
<form id="googleSignupForm" action="/signup/google" method="post" data-redirect-on-success="/">
+4 -2
View File
@@ -1,6 +1,6 @@
@extends('templates/layout_normal')
@extends(templates/layout_normal)
@section('main')
@section(main)
<h2>Login</h2>
<div class="box">
<form id="loginForm" action="/login" method="post" data-redirect-on-success="/">
@@ -10,6 +10,8 @@
<div class="right marginTop">
<button type="submit">Login</button>
</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>
<div class="center">
<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
+4 -3
View File
@@ -1,8 +1,8 @@
@js('js/login/signup.js')
@js(js/login/signup.js)
@extends('templates/layout_normal')
@extends(templates/layout_normal)
@section('main')
@section(main)
<h2>Sign up</h2>
<div class="box">
<form id="signupForm" action="/signup" method="post" data-redirect-on-success="/signup/success">
@@ -22,6 +22,7 @@
--><button id="resetSignupButton" class="gray marginTop marginLeft" type="button">Reset</button>
<?php endif; ?>
</div>
<p class="center marginTop"><a href="/login" title="Login">Already have an account?</a></p>
<hr>
<div class="center">
<a class="button yellow" href="/login/google" title="Signup with Google">Signup with Google</a>
+2 -2
View File
@@ -1,6 +1,6 @@
@extends('templates/layout_normal')
@extends(templates/layout_normal)
@section('main')
@section(main)
<h2>Sign up</h2>
<div class="box">
<p class="justify">Sign up was successful. Please check your email and click on the activation link to activate your account!</p>
+5 -5
View File
@@ -1,11 +1,11 @@
@css('css/maps.css')
@js('js/maps.js')
@css(css/maps.css)
@js(js/maps.js)
TODO: condition!
@js('js/maps_admin.js')
@js(js/maps_admin.js)
@extends('templates/layout_normal')
@extends(templates/layout_normal)
@section('main')
@section(main)
<div id="mapContainer">
<?php foreach ($maps as $map): ?>
<div class="mapItem">
+2 -2
View File
@@ -1,6 +1,6 @@
@extends('templates/mapguesser')
@extends(templates/mapguesser)
@section('content')
@section(content)
<header class="small">
<h1>
<a href="/" title="<?= $_ENV['APP_NAME'] ?>">
+13
View File
@@ -0,0 +1,13 @@
@extends(templates/mapguesser)
@section(content)
<header>
<h1>
<img class="inline" width="1em" height="1em" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>"><!--
--><?= $_ENV['APP_NAME'] ?>
</h1>
</header>
<main>
@yields('main')
</main>
@endsection
+3 -2
View File
@@ -1,5 +1,6 @@
@extends('templates/mapguesser')
@section('content')
@extends(templates/mapguesser)
@section(content)
<header>
<h1>
<a href="/" title="<?= $_ENV['APP_NAME'] ?>">
+1 -28
View File
@@ -42,38 +42,11 @@
<?php if (!empty($_ENV['GOOGLE_ANALITICS_ID'])): ?>
const GOOGLE_ANALITICS_ID = '<?= $_ENV['GOOGLE_ANALITICS_ID'] ?>';
<?php endif; ?>
const COOKIES_CONSENT = <?= isset($_COOKIE['COOKIES_CONSENT']) ? 'true' : 'false' ?>;
</script>
@yields('pageScript')
<script src="<?= $_ENV['STATIC_ROOT'] ?>/js/mapguesser.js?rev=<?= REVISION ?>"></script>
@yields('externalJs')
@yields('inlineJs')
<?php if (!isset($_COOKIE['COOKIES_CONSENT'])): ?>
<script>
(function () {
// we don't want user to agree cookies when clicking on the notice itself
document.getElementById('cookiesNotice').onclick = function (e) {
e.stopPropagation();
};
document.getElementById('agreeCookiesButton').onclick = function () {
MapGuesser.agreeCookies();
document.getElementById('cookiesNotice').style.display = 'none';
};
window.onclick = function () {
MapGuesser.agreeCookies();
};
})();
</script>
<?php else: ?>
<?php if (!empty($_ENV['GOOGLE_ANALITICS_ID'])): ?>
<script>
(function () {
MapGuesser.initGoogleAnalitics();
})();
</script>
<?php endif; ?>
<?php endif; ?>
</body>
</html>
+17 -2
View File
@@ -31,12 +31,21 @@ Container::$routeCollection->group('signup', function (MapGuesser\Routing\RouteC
$routeCollection->get('signup.activate', 'activate/{token}', [MapGuesser\Controller\LoginController::class, 'activate']);
$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->group('account', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('account', '', [MapGuesser\Controller\UserController::class, 'getAccount']);
$routeCollection->post('account-action', '', [MapGuesser\Controller\UserController::class, 'saveAccount']);
$routeCollection->get('account.delete', 'delete', [MapGuesser\Controller\UserController::class, 'getDeleteAccount']);
$routeCollection->post('account.delete-action', 'delete', [MapGuesser\Controller\UserController::class, 'deleteAccount']);
$routeCollection->get('account.googleAuthenticate', 'googleAuthenticate', [MapGuesser\Controller\UserController::class, 'getGoogleAuthenticateRedirect']);
$routeCollection->get('account.googleAuthenticate-action', 'googleAuthenticate/code', [MapGuesser\Controller\UserController::class, 'authenticateWithGoogle']);
});
//Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
@@ -58,10 +67,16 @@ if (isset($_COOKIE['COOKIES_CONSENT'])) {
session_set_save_handler(Container::$sessionHandler, true);
session_start([
'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_samesite' => 'Lax'
]);
// this is needed to handle old type of session IDs
if (!Container::$sessionHandler->validateId(session_id())) {
session_regenerate_id(true);
}
} else {
$_SESSION = [];
}
@@ -69,5 +84,5 @@ if (isset($_COOKIE['COOKIES_CONSENT'])) {
Container::$request = new MapGuesser\Request\Request($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'], $_GET, $_POST, $_SESSION);
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)));
}