Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa3cdc7f3 | ||
|
|
5760e31e29 | ||
|
|
14fd723f26 | ||
|
|
a6d5214166 | ||
|
|
d1d9dede9c | ||
|
|
171064816c | ||
|
|
f8bb7bd092 | ||
|
|
466f065718 | ||
|
|
6d68f247e6 | ||
|
|
e4c7aadc64 | ||
|
|
25262fb5fe | ||
|
|
8e0c1ce08b | ||
|
|
9e196ebf33 | ||
|
|
013668e3d6 | ||
|
|
dbd16d6cbe | ||
|
|
6a191d5c28 | ||
|
|
39e49724da | ||
|
|
6c8a3c6a90 | ||
|
|
7159867db0 | ||
|
|
4d7c86fd1b |
No files matched your search
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns(['id', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
|
||||
|
||||
$modify = new Modify(\Container::$dbConnection, 'maps');
|
||||
$modify->setId($map['id']);
|
||||
$modify->set('area', $bounds->calculateApproximateArea());
|
||||
$modify->save();
|
||||
}
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE
|
||||
`maps`
|
||||
ADD
|
||||
`area` decimal(13, 4) NOT NULL DEFAULT 0.0;
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE `sessions` (
|
||||
`id` varchar(64) NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
`updated` timestamp NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -10,21 +10,11 @@ const REVISION_DATE = '';
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(ROOT);
|
||||
$dotenv->load();
|
||||
|
||||
if (!empty($_ENV['DEV'])) {
|
||||
error_reporting(E_ALL);
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
} else {
|
||||
ini_set('display_errors', '0');
|
||||
}
|
||||
|
||||
class Container
|
||||
{
|
||||
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
|
||||
static MapGuesser\Routing\RouteCollection $routeCollection;
|
||||
static \SessionHandlerInterface $sessionHandler;
|
||||
}
|
||||
|
||||
Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
|
||||
Container::$routeCollection = new MapGuesser\Routing\RouteCollection();
|
||||
|
||||
session_start();
|
||||
+7
-19
@@ -1,8 +1,7 @@
|
||||
<?php
|
||||
|
||||
require '../main.php';
|
||||
require '../web.php';
|
||||
|
||||
// very basic routing
|
||||
$host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'];
|
||||
$method = strtolower($_SERVER['REQUEST_METHOD']);
|
||||
$url = substr($_SERVER['REQUEST_URI'], strlen('/'));
|
||||
@@ -11,23 +10,6 @@ if (($pos = strpos($url, '?')) !== false) {
|
||||
}
|
||||
$url = rawurldecode($url);
|
||||
|
||||
Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']);
|
||||
Container::$routeCollection->get('login', 'login', [MapGuesser\Controller\LoginController::class, 'getLoginForm']);
|
||||
Container::$routeCollection->post('login-action', 'login', [MapGuesser\Controller\LoginController::class, 'login']);
|
||||
Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
|
||||
Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
|
||||
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
|
||||
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']);
|
||||
$routeCollection->get('newPlace-json', '{mapId}/newPlace.json', [MapGuesser\Controller\GameFlowController::class, 'getNewPlace']);
|
||||
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'evaluateGuess']);
|
||||
});
|
||||
Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('admin.mapEditor', 'mapEditor/{mapId?}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']);
|
||||
$routeCollection->get('admin.place', 'place.json/{placeId}', [MapGuesser\Controller\MapAdminController::class, 'getPlace']);
|
||||
$routeCollection->post('admin.saveMap', 'saveMap/{mapId}/json', [MapGuesser\Controller\MapAdminController::class, 'saveMap']);
|
||||
});
|
||||
|
||||
$match = Container::$routeCollection->match($method, explode('/', $url));
|
||||
|
||||
if ($match !== null) {
|
||||
@@ -44,6 +26,12 @@ if ($match !== null) {
|
||||
$authorized = true;
|
||||
}
|
||||
|
||||
if ($method === 'post' && $request->post('anti_csrf_token') !== $request->session()->get('anti_csrf_token')) {
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 403);
|
||||
echo json_encode(['error' => 'no_valid_anti_csrf_token']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($authorized) {
|
||||
$response = call_user_func([$controller, $handler[1]]);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#cover {
|
||||
#guessCover {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
#metadata {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 10px;
|
||||
padding: 10px;
|
||||
background-color: #eeeeee;
|
||||
border: solid 1px #555555;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
opacity: 0.95;
|
||||
z-index: 2;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
@@ -61,9 +47,6 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: 999px) and (min-height: 600px) {
|
||||
#metadata {
|
||||
width: calc(100% - 155px);
|
||||
}
|
||||
#map.selected {
|
||||
height: calc(50% - 20px);
|
||||
}
|
||||
@@ -79,12 +62,6 @@
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px), (max-height: 599px) {
|
||||
#metadata {
|
||||
width: calc(50% - 20px);
|
||||
}
|
||||
#metadata.selected {
|
||||
top: 95px;
|
||||
}
|
||||
#map.selected {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@@ -89,10 +89,18 @@ sub {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.marginLeft {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.marginBottom {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.marginRight {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -230,6 +238,29 @@ input.big:focus, select.big:focus, textarea.big:focus {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
div.modal {
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 3px;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
z-index: 3;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#cover {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #000000;
|
||||
opacity: 0.5;
|
||||
z-index: 2;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p.formError {
|
||||
color: #7f2929;
|
||||
font-weight: 500;
|
||||
@@ -313,7 +344,19 @@ div.box {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
div.modal {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
div.box {
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
div.modal {
|
||||
width: 540px;
|
||||
left: 50%;
|
||||
margin-left: -270px;
|
||||
}
|
||||
}
|
||||
+10
-25
@@ -16,7 +16,7 @@
|
||||
|
||||
initialize: function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
document.getElementById('cover').style.visibility = 'visible';
|
||||
document.getElementById('guessCover').style.visibility = 'visible';
|
||||
document.getElementById('currentRound').innerHTML = '1/' + String(Game.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentScoreSum').innerHTML = '0/0';
|
||||
|
||||
@@ -25,11 +25,9 @@
|
||||
});
|
||||
Game.map.fitBounds(mapBounds);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
MapGuesser.httpRequest('GET', '/game/' + mapId + '/newPlace.json', function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
document.getElementById('cover').style.visibility = 'hidden';
|
||||
document.getElementById('guessCover').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
//TODO: handle this error
|
||||
@@ -51,10 +49,7 @@
|
||||
}
|
||||
|
||||
Game.startNewRound();
|
||||
};
|
||||
|
||||
xhr.open('GET', '/game/' + mapId + '/newPlace.json', true);
|
||||
xhr.send();
|
||||
});
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
@@ -103,7 +98,7 @@
|
||||
lastRound.line.setVisible(false);
|
||||
}
|
||||
|
||||
document.getElementById('cover').style.visibility = 'hidden';
|
||||
document.getElementById('guessCover').style.visibility = 'hidden';
|
||||
document.getElementById('showGuessButton').style.visibility = null;
|
||||
document.getElementById('guess').style.visibility = null;
|
||||
document.getElementById('guess').classList.remove('result')
|
||||
@@ -127,16 +122,11 @@
|
||||
handleErrorResponse: function (error) {
|
||||
// for the time being we only handle the "no_session_found" error and reset the game
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
|
||||
mapBounds = this.response.bounds;
|
||||
|
||||
Game.reset();
|
||||
};
|
||||
|
||||
xhr.open('GET', '/game/' + mapId + '/json', true);
|
||||
xhr.send();
|
||||
});
|
||||
},
|
||||
|
||||
loadPano: function (panoId) {
|
||||
@@ -162,15 +152,13 @@
|
||||
document.getElementById('guess').classList.remove('adapt');
|
||||
}
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
document.getElementById('cover').style.visibility = 'visible';
|
||||
document.getElementById('guessCover').style.visibility = 'visible';
|
||||
|
||||
var data = new FormData();
|
||||
data.append('lat', String(guessPosition.lat));
|
||||
data.append('lng', String(guessPosition.lng));
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
MapGuesser.httpRequest('POST', '/game/' + mapId + '/guess.json', function () {
|
||||
if (this.response.error) {
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
@@ -211,10 +199,7 @@
|
||||
}
|
||||
|
||||
Game.panoId = this.response.panoId;
|
||||
};
|
||||
|
||||
xhr.open('POST', '/game/' + mapId + '/guess.json', true);
|
||||
xhr.send(data);
|
||||
}, data);
|
||||
},
|
||||
|
||||
addRealGuessPair: function (position, guessPosition, hidden) {
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
|
||||
var formData = new FormData(form);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
MapGuesser.httpRequest('POST', form.action, function () {
|
||||
if (this.response.error) {
|
||||
var errorText;
|
||||
switch (this.response.error) {
|
||||
@@ -34,9 +32,6 @@
|
||||
}
|
||||
|
||||
window.location.replace('/');
|
||||
};
|
||||
|
||||
xhr.open('POST', form.action, true);
|
||||
xhr.send(formData);
|
||||
}, formData);
|
||||
};
|
||||
})();
|
||||
@@ -22,15 +22,13 @@
|
||||
|
||||
document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
|
||||
|
||||
document.getElementById('metadata').style.visibility = 'hidden';
|
||||
MapGuesser.hideModal();
|
||||
|
||||
document.getElementById('saveButton').disabled = false;
|
||||
},
|
||||
|
||||
getPlace: function (placeId, marker) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
MapGuesser.httpRequest('GET', '/admin/place.json/' + placeId, function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (!this.response.panoId) {
|
||||
@@ -46,10 +44,7 @@
|
||||
places[marker.placeId].noPano = false;
|
||||
|
||||
MapEditor.loadPano(this.response.panoId, places[marker.placeId].pov);
|
||||
};
|
||||
|
||||
xhr.open('GET', '/admin/place.json/' + placeId, true);
|
||||
xhr.send();
|
||||
});
|
||||
},
|
||||
|
||||
loadPano: function (panoId, pov) {
|
||||
@@ -114,7 +109,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('metadata').classList.add('selected');
|
||||
document.getElementById('map').classList.add('selected');
|
||||
document.getElementById('control').classList.add('selected');
|
||||
document.getElementById('noPano').style.visibility = 'hidden';
|
||||
@@ -220,7 +214,6 @@
|
||||
},
|
||||
|
||||
closePlace: function (del) {
|
||||
document.getElementById('metadata').classList.remove('selected')
|
||||
document.getElementById('map').classList.remove('selected');
|
||||
document.getElementById('control').classList.remove('selected');
|
||||
document.getElementById('noPano').style.visibility = 'hidden';
|
||||
@@ -284,9 +277,7 @@
|
||||
data.append('deleted[]', JSON.stringify(MapEditor.deleted[placeId]));
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
MapGuesser.httpRequest('POST', '/admin/saveMap/' + mapId + '/json', function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
@@ -310,10 +301,7 @@
|
||||
document.getElementById('deleted').innerHTML = '0';
|
||||
|
||||
document.getElementById('saveButton').disabled = true;
|
||||
};
|
||||
|
||||
xhr.open('POST', '/admin/saveMap/' + mapId + '/json', true);
|
||||
xhr.send(data);
|
||||
}, data);
|
||||
},
|
||||
|
||||
replacePlaceIdsToReal: function (addedPlaces) {
|
||||
@@ -423,14 +411,8 @@
|
||||
document.getElementById('mapName').onclick = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var metadata = document.getElementById('metadata');
|
||||
|
||||
if (metadata.style.visibility === 'visible') {
|
||||
metadata.style.visibility = 'hidden';
|
||||
} else {
|
||||
metadata.style.visibility = 'visible';
|
||||
document.getElementById('metadataForm').elements.name.select();
|
||||
}
|
||||
MapGuesser.showModal('metadata');
|
||||
document.getElementById('metadataForm').elements.name.select();
|
||||
};
|
||||
|
||||
document.getElementById('metadataForm').onsubmit = function (e) {
|
||||
@@ -440,7 +422,7 @@
|
||||
};
|
||||
|
||||
document.getElementById('closeMetadataButton').onclick = function () {
|
||||
document.getElementById('metadata').style.visibility = 'hidden';
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
document.getElementById('saveButton').onclick = function () {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
var MapGuesser = {
|
||||
httpRequest: function (method, url, callback, data) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = callback;
|
||||
|
||||
xhr.open(method, url, true);
|
||||
|
||||
if (method === 'POST') {
|
||||
if (typeof data === 'undefined') {
|
||||
data = new FormData();
|
||||
}
|
||||
|
||||
data.append('anti_csrf_token', ANTI_CSRF_TOKEN);
|
||||
|
||||
xhr.send(data);
|
||||
} else {
|
||||
xhr.send();
|
||||
}
|
||||
},
|
||||
|
||||
showModal: function (id) {
|
||||
document.getElementById(id).style.visibility = 'visible';
|
||||
document.getElementById('cover').style.visibility = 'visible';
|
||||
},
|
||||
|
||||
showModalWithContent: function (title, body, extraButtons) {
|
||||
if (typeof extraButtons === 'undefined') {
|
||||
extraButtons = [];
|
||||
}
|
||||
|
||||
MapGuesser.showModal('modal');
|
||||
|
||||
document.getElementById('modalTitle').textContent = title;
|
||||
|
||||
if (typeof body === 'object') {
|
||||
document.getElementById('modalText').appendChild(body);
|
||||
} else {
|
||||
document.getElementById('modalText').textContent = body;
|
||||
}
|
||||
|
||||
var buttons = document.getElementById('modalButtons');
|
||||
buttons.textContent = '';
|
||||
|
||||
for (extraButton of extraButtons) {
|
||||
var button = document.createElement(extraButton.type);
|
||||
|
||||
if (extraButton.type === 'a') {
|
||||
button.classList.add('button');
|
||||
}
|
||||
|
||||
for (className of extraButton.classNames) {
|
||||
button.classList.add(className);
|
||||
}
|
||||
|
||||
button.classList.add('marginTop');
|
||||
button.classList.add('marginRight');
|
||||
button.textContent = extraButton.text;
|
||||
|
||||
if (extraButton.type === 'a') {
|
||||
button.href = extraButton.href;
|
||||
} else if (extraButton.type === 'button') {
|
||||
button.onclick = extraButton.onclick;
|
||||
}
|
||||
|
||||
buttons.appendChild(button);
|
||||
}
|
||||
|
||||
var closeButton = document.createElement('button');
|
||||
|
||||
closeButton.classList.add('gray');
|
||||
closeButton.classList.add('marginTop');
|
||||
closeButton.textContent = 'Cancel';
|
||||
closeButton.onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
buttons.appendChild(closeButton);
|
||||
},
|
||||
|
||||
hideModal: function () {
|
||||
var modals = document.getElementsByClassName('modal');
|
||||
|
||||
for (modal of modals) {
|
||||
modal.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
document.getElementById('cover').style.visibility = 'hidden';
|
||||
}
|
||||
};
|
||||
|
||||
(function () {
|
||||
var anchors = document.getElementsByTagName('a');
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
var a = anchors[i];
|
||||
if (a.href !== 'javascript:;' && a.target !== '_blank') {
|
||||
a.onclick = function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('cover').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
})();
|
||||
@@ -44,7 +44,7 @@ class GameController
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
$session->set('state', [
|
||||
'mapId' => $mapId,
|
||||
'area' => $bounds->calculateApproximateArea(),
|
||||
'area' => $map['area'],
|
||||
'rounds' => []
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class GameFlowController
|
||||
|
||||
private function calculateScore(float $distance, float $area): int
|
||||
{
|
||||
$goodness = 1.0 - ($distance / sqrt($area));
|
||||
$goodness = 1.0 - ($distance / (sqrt($area) * 1000));
|
||||
|
||||
return (int) round(pow(static::MAX_SCORE, $goodness));
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ class MapAdminController implements ISecured
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
if (!$mapId) {
|
||||
$mapId = $this->addNewMap();
|
||||
}
|
||||
@@ -118,7 +120,8 @@ class MapAdminController implements ISecured
|
||||
'bound_south_lat' => $mapBounds->getSouthLat(),
|
||||
'bound_west_lng' => $mapBounds->getWestLng(),
|
||||
'bound_north_lat' => $mapBounds->getNorthLat(),
|
||||
'bound_east_lng' => $mapBounds->getEastLng()
|
||||
'bound_east_lng' => $mapBounds->getEastLng(),
|
||||
'area' => $mapBounds->calculateApproximateArea(),
|
||||
];
|
||||
|
||||
if (isset($_POST['name'])) {
|
||||
@@ -130,10 +133,44 @@ class MapAdminController implements ISecured
|
||||
|
||||
$this->saveMapData($mapId, $map);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$data = ['mapId' => $mapId, 'added' => $addedIds];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
public function deleteMap() {
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$this->deletePlaces($mapId);
|
||||
|
||||
$modify = new Modify(\Container::$dbConnection, 'maps');
|
||||
$modify->setId($mapId);
|
||||
$modify->delete();
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
private function deletePlaces(int $mapId): void
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['id']);
|
||||
$select->where('map_id', '=', $mapId);
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$modify = new Modify(\Container::$dbConnection, 'places');
|
||||
$modify->setId($place['id']);
|
||||
$modify->delete();
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateMapBounds(int $mapId): Bounds
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
|
||||
@@ -6,7 +6,6 @@ use MapGuesser\Interfaces\Authentication\IUser;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
|
||||
class MapsController
|
||||
@@ -29,6 +28,7 @@ class MapsController
|
||||
['maps', 'bound_west_lng'],
|
||||
['maps', 'bound_north_lat'],
|
||||
['maps', 'bound_east_lng'],
|
||||
['maps', 'area'],
|
||||
new RawExpression('COUNT(places.id) AS num_places')
|
||||
]);
|
||||
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
|
||||
@@ -39,9 +39,7 @@ class MapsController
|
||||
|
||||
$maps = [];
|
||||
while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
|
||||
|
||||
$map['area'] = $this->formatMapAreaForHuman($bounds->calculateApproximateArea());
|
||||
$map['area'] = $this->formatMapAreaForHuman($map['area']);
|
||||
|
||||
$maps[] = $map;
|
||||
}
|
||||
@@ -53,29 +51,29 @@ class MapsController
|
||||
|
||||
private function formatMapAreaForHuman(float $area): array
|
||||
{
|
||||
if ($area < 100.0) {
|
||||
if ($area < 0.01) {
|
||||
$digits = 0;
|
||||
$rounded = round($area * 1000000.0, -2);
|
||||
$unit = 'm';
|
||||
} elseif ($area < 0.1) {
|
||||
$digits = 0;
|
||||
$rounded = round($area * 1000000.0, -3);
|
||||
$unit = 'm';
|
||||
} elseif ($area < 1.0) {
|
||||
$digits = 2;
|
||||
$rounded = round($area, 2);
|
||||
$unit = 'km';
|
||||
} elseif ($area < 100.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area, 0);
|
||||
$unit = 'm';
|
||||
} elseif ($area < 100000.0) {
|
||||
$unit = 'km';
|
||||
} elseif ($area < 10000.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area, -2);
|
||||
$unit = 'm';
|
||||
} elseif ($area < 1000000.0) {
|
||||
$digits = 2;
|
||||
$rounded = round($area / 1000000.0, 2);
|
||||
$unit = 'km';
|
||||
} elseif ($area < 100000000.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area / 1000000.0, 0);
|
||||
$unit = 'km';
|
||||
} elseif ($area < 10000000000.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area / 1000000.0, -2);
|
||||
$unit = 'km';
|
||||
} else {
|
||||
$digits = 0;
|
||||
$rounded = round($area / 1000000.0, -4);
|
||||
$rounded = round($area, -4);
|
||||
$unit = 'km';
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ class Modify
|
||||
|
||||
private array $original = [];
|
||||
|
||||
private ?string $externalId = null;
|
||||
|
||||
private bool $autoIncrement = true;
|
||||
|
||||
public function __construct(IConnection $connection, string $table)
|
||||
@@ -31,6 +33,13 @@ class Modify
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setExternalId($id): Modify
|
||||
{
|
||||
$this->externalId = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAutoIncrement(bool $autoIncrement = true): Modify
|
||||
{
|
||||
$this->autoIncrement = $autoIncrement;
|
||||
@@ -87,7 +96,9 @@ class Modify
|
||||
|
||||
private function insert(): void
|
||||
{
|
||||
if (!$this->autoIncrement) {
|
||||
if ($this->externalId !== null) {
|
||||
$this->attributes[$this->idName] = $this->externalId;
|
||||
} elseif (!$this->autoIncrement) {
|
||||
$this->attributes[$this->idName] = $this->generateKey();
|
||||
}
|
||||
|
||||
@@ -105,11 +116,14 @@ class Modify
|
||||
|
||||
private function update(): void
|
||||
{
|
||||
$diff = $this->generateDiff();
|
||||
/*$diff = $this->generateDiff();
|
||||
|
||||
if (count($diff) === 0) {
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
|
||||
$diff = $this->attributes;
|
||||
unset($diff[$this->idName]);
|
||||
|
||||
$set = $this->generateColumnsWithBinding(array_keys($diff));
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class MapRepository
|
||||
public function getById(int $mapId)
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns(['id', 'name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
|
||||
$select->columns(['id', 'name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area']);
|
||||
$select->whereId($mapId);
|
||||
|
||||
return $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php namespace MapGuesser\Session;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use SessionHandlerInterface;
|
||||
use SessionIdInterface;
|
||||
use SessionUpdateTimestampHandlerInterface;
|
||||
|
||||
class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
|
||||
{
|
||||
private bool $exists = false;
|
||||
|
||||
private bool $written = false;
|
||||
|
||||
public function open($savePath, $sessionName): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function close(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function read($id): string
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'sessions');
|
||||
$select->columns(['data']);
|
||||
$select->whereId($id);
|
||||
|
||||
$result = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
if ($result === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$this->exists = true;
|
||||
|
||||
return $result['data'];
|
||||
}
|
||||
|
||||
public function write($id, $data): bool
|
||||
{
|
||||
$modify = new Modify(\Container::$dbConnection, 'sessions');
|
||||
|
||||
if ($this->exists) {
|
||||
$modify->setId($id);
|
||||
} else {
|
||||
$modify->setExternalId($id);
|
||||
}
|
||||
|
||||
$modify->set('data', $data);
|
||||
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
|
||||
$modify->save();
|
||||
|
||||
$written = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function destroy($id): bool
|
||||
{
|
||||
$modify = new Modify(\Container::$dbConnection, 'sessions');
|
||||
$modify->setId($id);
|
||||
$modify->delete();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function create_sid(): string
|
||||
{
|
||||
return hash('sha256', random_bytes(10) . microtime());
|
||||
}
|
||||
|
||||
public function validateId($id): bool
|
||||
{
|
||||
return preg_match('/^[a-f0-9]{64}$/', $id);
|
||||
}
|
||||
|
||||
public function updateTimestamp($id, $data): bool
|
||||
{
|
||||
if ($this->written) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$modify = new Modify(\Container::$dbConnection, 'sessions');
|
||||
|
||||
$modify->setId($id);
|
||||
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
|
||||
$modify->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class Bounds
|
||||
{
|
||||
const ONE_DEGREE_OF_LATITUDE_IN_METER = 111132.954;
|
||||
const ONE_DEGREE_OF_LATITUDE_IN_KM = 111.132954;
|
||||
|
||||
private float $southLat = 90.0;
|
||||
private float $westLng = 180.0;
|
||||
@@ -84,9 +84,9 @@ class Bounds
|
||||
$dLat = $this->northLat - $this->southLat;
|
||||
$dLng = $this->eastLng - $this->westLng;
|
||||
|
||||
$m = $dLat * static::ONE_DEGREE_OF_LATITUDE_IN_METER;
|
||||
$a = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_METER * cos(deg2rad($this->northLat));
|
||||
$c = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_METER * cos(deg2rad($this->southLat));
|
||||
$m = $dLat * static::ONE_DEGREE_OF_LATITUDE_IN_KM;
|
||||
$a = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_KM * cos(deg2rad($this->northLat));
|
||||
$c = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_KM * cos(deg2rad($this->southLat));
|
||||
|
||||
return $m * ($a + $c) / 2;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ $jsFiles = [
|
||||
</a>
|
||||
</h1>
|
||||
<p>
|
||||
<span class="bold"><a href="javascript:;" id="mapName"><?= $mapName ?></a></span><!--
|
||||
<span class="bold"><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'. */ ?>
|
||||
--><svg class="inline" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -50,13 +50,14 @@ $jsFiles = [
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="metadata">
|
||||
<form id="metadataForm">
|
||||
<div id="metadata" class="modal">
|
||||
<h2>Edit map data</h2>
|
||||
<form id="metadataForm" class="marginTop">
|
||||
<input class="fullWidth" type="text" name="name" value="<?= $mapName ?>" placeholder="Name of the map">
|
||||
<textarea class="fullWidth marginTop" name="description" rows="4" placeholder="Description of the map"><?= $mapDescription ?></textarea>
|
||||
<div style="text-align: right;">
|
||||
<button id="closeMetadataButton" class="gray marginTop" type="button">Close</button>
|
||||
<button class="marginTop" type="submit">Apply</button>
|
||||
<div class="right">
|
||||
<button class="marginTop marginRight" type="submit">Apply</button><!--
|
||||
--><button id="closeMetadataButton" class="gray marginTop" type="button">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ $jsFiles = [
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cover"></div>
|
||||
<div id="guessCover"></div>
|
||||
<div id="panorama"></div>
|
||||
<div id="showGuessButtonContainer">
|
||||
<button id="showGuessButton" class="fullWidth">Show guess map</button>
|
||||
|
||||
+42
-6
@@ -8,7 +8,7 @@ $cssFiles = [
|
||||
<div class="main">
|
||||
<h2>Playable maps</h2>
|
||||
<div id="mapContainer">
|
||||
<?php foreach ($maps as $map) : ?>
|
||||
<?php foreach ($maps as $map): ?>
|
||||
<div class="mapItem">
|
||||
<div class="title">
|
||||
<p class="title"><?= $map['name'] ?></p>
|
||||
@@ -36,18 +36,18 @@ $cssFiles = [
|
||||
</div>
|
||||
<?php if ($isAdmin): ?>
|
||||
<div class="buttonContainer">
|
||||
<a class="button fullWidth noRightRadius" href="game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
|
||||
<a class="button yellow fullWidth noLeftRadius noRightRadius" href="admin/mapEditor/<?= $map['id']; ?>" title="Edit map '<?= $map['name'] ?>'">Edit</a>
|
||||
<button class="button red fullWidth noLeftRadius" title="Delete map '<?= $map['name'] ?>'">Delete</button>
|
||||
<a class="button fullWidth noRightRadius" href="/game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
|
||||
<a class="button yellow fullWidth noLeftRadius noRightRadius" href="/admin/mapEditor/<?= $map['id']; ?>" title="Edit map '<?= $map['name'] ?>'">Edit</a>
|
||||
<button class="button red fullWidth noLeftRadius deleteButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Delete map '<?= $map['name'] ?>'">Delete</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<a class="button fullWidth" href="game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
|
||||
<a class="button fullWidth" href="/game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($isAdmin): ?>
|
||||
<div class="mapItem new">
|
||||
<a class="button green fullWidth" href="admin/mapEditor" title="Add new map">
|
||||
<a class="button green fullWidth" href="/admin/mapEditor" title="Add new map">
|
||||
Add new map
|
||||
</a>
|
||||
</div>
|
||||
@@ -61,4 +61,40 @@ $cssFiles = [
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($isAdmin): ?>
|
||||
<script>
|
||||
(function() {
|
||||
Maps = {
|
||||
deleteMap: function(mapId, mapName) {
|
||||
MapGuesser.showModalWithContent('Delete map', 'Are you sure you want to delete map \'' + mapName + '\'?', [{
|
||||
type: 'button',
|
||||
classNames: ['red'],
|
||||
text: 'Delete',
|
||||
onclick: function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/admin/deleteMap/' + mapId, function () {
|
||||
if (this.response.error) {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
//TODO: handle this error
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
var buttons = document.getElementById('mapContainer').getElementsByClassName('deleteButton');
|
||||
for (var button of buttons) {
|
||||
button.onclick = function() {
|
||||
Maps.deleteMap(this.dataset.mapId, this.dataset.mapName);
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php require ROOT . '/views/templates/main_footer.php'; ?>
|
||||
@@ -1,7 +1,9 @@
|
||||
<script>
|
||||
const STATIC_ROOT = '<?= $_ENV['STATIC_ROOT'] ?>';
|
||||
const REVISION = '<?= REVISION ?>';
|
||||
const ANTI_CSRF_TOKEN = '<?= $_SESSION['anti_csrf_token'] ?>';
|
||||
</script>
|
||||
<script src="<?= $_ENV['STATIC_ROOT'] ?>/js/mapguesser.js?rev=<?= REVISION ?>"></script>
|
||||
<?php if (isset($jsFiles)) : ?>
|
||||
<?php foreach ($jsFiles as $jsFile) : ?>
|
||||
<?php
|
||||
@@ -12,18 +14,5 @@
|
||||
<script src="<?= $jsFile ?>"></script>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<script>
|
||||
(function () {
|
||||
var anchors = document.getElementsByTagName('a');
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
var a = anchors[i];
|
||||
if (a.href !== 'javascript:;' && a.target !== '_blank') {
|
||||
a.onclick = function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -24,4 +24,10 @@
|
||||
<body>
|
||||
<div id="loading">
|
||||
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/loading.svg?rev=<?= REVISION ?>">
|
||||
</div>
|
||||
<div id="cover"></div>
|
||||
<div id="modal" class="modal">
|
||||
<h2 id="modalTitle"></h2>
|
||||
<p id="modalText" class="marginTop"></p>
|
||||
<div id="modalButtons" class="right"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
require 'main.php';
|
||||
|
||||
if (!empty($_ENV['DEV'])) {
|
||||
error_reporting(E_ALL);
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
} else {
|
||||
ini_set('display_errors', '0');
|
||||
}
|
||||
|
||||
Container::$routeCollection = new MapGuesser\Routing\RouteCollection();
|
||||
|
||||
Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']);
|
||||
Container::$routeCollection->get('login', 'login', [MapGuesser\Controller\LoginController::class, 'getLoginForm']);
|
||||
Container::$routeCollection->post('login-action', 'login', [MapGuesser\Controller\LoginController::class, 'login']);
|
||||
Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
|
||||
Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
|
||||
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
|
||||
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']);
|
||||
$routeCollection->get('newPlace-json', '{mapId}/newPlace.json', [MapGuesser\Controller\GameFlowController::class, 'getNewPlace']);
|
||||
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'evaluateGuess']);
|
||||
});
|
||||
Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('admin.mapEditor', 'mapEditor/{mapId?}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']);
|
||||
$routeCollection->get('admin.place', 'place.json/{placeId}', [MapGuesser\Controller\MapAdminController::class, 'getPlace']);
|
||||
$routeCollection->post('admin.saveMap', 'saveMap/{mapId}/json', [MapGuesser\Controller\MapAdminController::class, 'saveMap']);
|
||||
$routeCollection->post('admin.deleteMap', 'deleteMap/{mapId}', [MapGuesser\Controller\MapAdminController::class, 'deleteMap']);
|
||||
});
|
||||
|
||||
Container::$sessionHandler = new MapGuesser\Session\DatabaseSessionHandler();
|
||||
|
||||
session_set_save_handler(Container::$sessionHandler, true);
|
||||
session_start([
|
||||
'gc_maxlifetime' => 604800,
|
||||
'cookie_lifetime' => 604800,
|
||||
'cookie_httponly' => true,
|
||||
'cookie_samesite' => 'Lax'
|
||||
]);
|
||||
|
||||
if (!isset($_SESSION['anti_csrf_token'])) {
|
||||
$_SESSION['anti_csrf_token'] = hash('sha256', random_bytes(10) . microtime());
|
||||
}
|
||||
Reference in new issue
Block a user