diff --git a/Dockerfile b/Dockerfile index 4fca892..8803505 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,6 +45,7 @@ RUN apk add --no-cache \ php8-json \ php8-opcache \ php8-openssl \ + php8-session \ php8-xml \ php8-zlib diff --git a/jumpapp/api/weatherdata.php b/jumpapp/api/weatherdata.php index 97835bd..d5b2d73 100644 --- a/jumpapp/api/weatherdata.php +++ b/jumpapp/api/weatherdata.php @@ -12,6 +12,31 @@ require __DIR__ .'/../vendor/autoload.php'; $config = new Jump\Config(); $cache = new Jump\Cache($config); +// Output header here so we can return early with a json response if there is a curl error. +header('Content-Type: application/json; charset=utf-8'); + +// Initialise a new session using the request object. +$session = new \Nette\Http\Session((new \Nette\Http\RequestFactory)->fromGlobals(), new \Nette\Http\Response); +$session->setName($config->get('sessionname')); +$session->setExpiration($config->get('sessiontimeout')); + +// Get a Nette session section for CSRF data. +$csrfsection = $session->getSection('csrf'); + +// Has a CSRF token been set up for the session yet? +if (!$csrfsection->offsetExists('token')){ + http_response_code(401); + die(json_encode(['error' => 'Session not fully set up'])); +} + +// Check CSRF token saved in session against token provided via request. +$token = isset($_GET['token']) ? $_GET['token'] : false; +if (!$token || !hash_equals($csrfsection->get('token'), $token)) { + http_response_code(401); + die(json_encode(['error' => 'API token is incorrect or missing'])); +} + +// Start of variables we want to use. $owmapiurlbase = 'https://api.openweathermap.org/data/2.5/weather'; $units = $config->parse_bool($config->get('metrictemp')) ? 'metric' : 'imperial'; @@ -35,9 +60,6 @@ $url = $owmapiurlbase .'&lon=' . $latlong[1] .'&appid=' . $config->get('owmapikey', false); -// Output header here so we can return early with a json response if there is a curl error. -header('Content-Type: application/json; charset=utf-8'); - // Use the cache to store/retrieve data, make an md5 hash of latlong so it is not possible // to track location history form the stored cache. $weatherdata = $cache->load(cachename: 'weatherdata', key: md5(json_encode($latlong)), callback: function() use ($url) { @@ -56,10 +78,11 @@ $weatherdata = $cache->load(cachename: 'weatherdata', key: md5(json_encode($latl curl_close($ch); // If we had an error then return the error message and exit, otherwise return the API response. if (isset($curlerror)) { + http_response_code(400); die(json_encode(['error' => $curlerror])); } return $response; }); // We made it here so output the API response as json. -echo $weatherdata; \ No newline at end of file +echo $weatherdata; diff --git a/jumpapp/assets/js/index.bundle.js b/jumpapp/assets/js/index.bundle.js index ab61c6f..0447efc 100644 --- a/jumpapp/assets/js/index.bundle.js +++ b/jumpapp/assets/js/index.bundle.js @@ -1 +1 @@ -(()=>{"use strict";var t={729:t=>{var e=Object.prototype.hasOwnProperty,n="~";function i(){}function s(t,e,n){this.fn=t,this.context=e,this.once=n||!1}function r(t,e,i,r,o){if("function"!=typeof i)throw new TypeError("The listener must be a function");var h=new s(i,r||t,o),c=n?n+e:e;return t._events[c]?t._events[c].fn?t._events[c]=[t._events[c],h]:t._events[c].push(h):(t._events[c]=h,t._eventsCount++),t}function o(t,e){0==--t._eventsCount?t._events=new i:delete t._events[e]}function h(){this._events=new i,this._eventsCount=0}Object.create&&(i.prototype=Object.create(null),(new i).__proto__||(n=!1)),h.prototype.eventNames=function(){var t,i,s=[];if(0===this._eventsCount)return s;for(i in t=this._events)e.call(t,i)&&s.push(n?i.slice(1):i);return Object.getOwnPropertySymbols?s.concat(Object.getOwnPropertySymbols(t)):s},h.prototype.listeners=function(t){var e=n?n+t:t,i=this._events[e];if(!i)return[];if(i.fn)return[i.fn];for(var s=0,r=i.length,o=new Array(r);s{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var i in e)n.o(e,i)&&!n.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{class t{constructor(t,e=!1){this.set_utc_shift(),this.contentintervalid=null,this.eventemitter=t,this.ampm=e}set_utc_shift(t=0){this.utcshift=t,this.shiftedtimestamp=(new Date).getTime()+this.utcshift,this.shifteddate=new Date(this.shiftedtimestamp)}get_formatted_time(){let t=this.shifteddate.getUTCHours();const e=String(this.shifteddate.getUTCMinutes()).padStart(2,"0");if(!this.ampm)return String(t).padStart(2,"0")+":"+e;const n=t<=12?"AM":"PM";return t=(t+11)%12+1,t+":"+e+""+n+""}get_hour(){return this.shifteddate.getUTCHours()}update_time(){this.set_utc_shift(this.utcshift),this.eventemitter.emit("clock-updated",{formatted_time:this.get_formatted_time(),hour:this.get_hour(),utcshift:this.utcshift})}run(t){this.contentintervalid&&clearInterval(this.contentintervalid),this.update_time(),this.contentintervalid=setInterval((()=>{this.update_time()}),t)}}var e=n(729),i=n.n(e);class s{constructor(t){this.hour=t,this.greetings={0:"morning",12:"afternoon",16:"evening",19:"night"}}get_greeting(){let t=Object.keys(this.greetings).reverse();for(let e of t)if(this.hour>=e)return this.greetings[e]}}class r{constructor(t){this.eventemitter=t}fetch_owm_data(t){let e="/api/weatherdata.php";t.length&&(e+="?lat="+t[0]+"&lon="+t[1]),fetch(e).then((t=>t.json())).then((t=>{401===t.cod&&alert("The OWM API key is invalid, check config.php");var e="night";t.dt>t.sys.sunrise&&t.dt{this.timezoneshift=t.timezoneshift,this.weatherelm.href="https://openweathermap.org/city/"+t.locationcode,this.weathericonelm.classList.add(t.iconclass),this.clientlocationelm.innerHTML=t.locationname,this.tempelm.innerHTML=t.temp,this.weatherdescelm.innerHTML=t.description,this.clientlocationelm.classList.add("enable"),this.eventemitter.emit("show-content")})),this.eventemitter.on("clock-updated",(t=>{if(null!=this.timeelm&&(this.timeelm.innerHTML=t.formatted_time),null!=this.greetingelm){let e=new s(t.hour);this.greetingelm.innerHTML=e.get_greeting()}})),this.eventemitter.on("show-content",(()=>{this.set_clock(),this.show_content()})),this.clientlocationelm.addEventListener("click",(t=>{navigator.geolocation.getCurrentPosition((t=>{this.latlong=[t.coords.latitude,t.coords.longitude],this.storage.setItem("lastrequestedlocation",JSON.stringify(this.latlong)),this.weather.fetch_owm_data(this.latlong)}),(t=>{console.error(t.message)}),{enableHighAccuracy:!0})})),this.showtagsbuttonelm&&this.showtagsbuttonelm.addEventListener("click",(t=>{this.tagselectorelm.classList.add("enable"),t.preventDefault()})),this.tagsselectorclosebuttonelm&&this.tagsselectorclosebuttonelm.addEventListener("click",(t=>{this.tagselectorelm.classList.remove("enable")}))}show_content(){document.querySelectorAll(".hidden").forEach((function(t){t.classList.remove("hidden")}))}set_clock(){this.clock.set_utc_shift(this.timezoneshift),this.clock.run(this.updatefrequency)}}).init()})()})(); \ No newline at end of file +(()=>{"use strict";var t={729:t=>{var e=Object.prototype.hasOwnProperty,n="~";function s(){}function i(t,e,n){this.fn=t,this.context=e,this.once=n||!1}function r(t,e,s,r,o){if("function"!=typeof s)throw new TypeError("The listener must be a function");var h=new i(s,r||t,o),c=n?n+e:e;return t._events[c]?t._events[c].fn?t._events[c]=[t._events[c],h]:t._events[c].push(h):(t._events[c]=h,t._eventsCount++),t}function o(t,e){0==--t._eventsCount?t._events=new s:delete t._events[e]}function h(){this._events=new s,this._eventsCount=0}Object.create&&(s.prototype=Object.create(null),(new s).__proto__||(n=!1)),h.prototype.eventNames=function(){var t,s,i=[];if(0===this._eventsCount)return i;for(s in t=this._events)e.call(t,s)&&i.push(n?s.slice(1):s);return Object.getOwnPropertySymbols?i.concat(Object.getOwnPropertySymbols(t)):i},h.prototype.listeners=function(t){var e=n?n+t:t,s=this._events[e];if(!s)return[];if(s.fn)return[s.fn];for(var i=0,r=s.length,o=new Array(r);i{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var s in e)n.o(e,s)&&!n.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{class t{constructor(t,e=!1){this.set_utc_shift(),this.contentintervalid=null,this.eventemitter=t,this.ampm=e}set_utc_shift(t=0){this.utcshift=t,this.shiftedtimestamp=(new Date).getTime()+this.utcshift,this.shifteddate=new Date(this.shiftedtimestamp)}get_formatted_time(){let t=this.shifteddate.getUTCHours();const e=String(this.shifteddate.getUTCMinutes()).padStart(2,"0");if(!this.ampm)return String(t).padStart(2,"0")+":"+e;const n=t<=12?"AM":"PM";return t=(t+11)%12+1,t+":"+e+""+n+""}get_hour(){return this.shifteddate.getUTCHours()}update_time(){this.set_utc_shift(this.utcshift),this.eventemitter.emit("clock-updated",{formatted_time:this.get_formatted_time(),hour:this.get_hour(),utcshift:this.utcshift})}run(t){this.contentintervalid&&clearInterval(this.contentintervalid),this.update_time(),this.contentintervalid=setInterval((()=>{this.update_time()}),t)}}var e=n(729),s=n.n(e);class i{constructor(t){this.hour=t,this.greetings={0:"morning",12:"afternoon",16:"evening",19:"night"}}get_greeting(){let t=Object.keys(this.greetings).reverse();for(let e of t)if(this.hour>=e)return this.greetings[e]}}class r{constructor(t){this.eventemitter=t}fetch_owm_data(t){let e="/api/weatherdata.php?token="+JUMP.token;t.length&&(e+="&lat="+t[0]+"&lon="+t[1]),fetch(e).then((t=>t.json())).then((t=>{if(t.error)console.error("JUMP ERROR: There was an issue with the OWM API... "+t.error);else if(401!==t.cod){var e="night";t.dt>t.sys.sunrise&&t.dt{this.weather.fetch_owm_data(this.latlong)}),this.weatherfrequency)):this.eventemitter.emit("show-content")}add_event_listeners(){this.eventemitter.on("weather-loaded",(t=>{this.timezoneshift=t.timezoneshift,this.weatherelm.href="https://openweathermap.org/city/"+t.locationcode,this.weathericonelm.classList.add(t.iconclass),this.clientlocationelm.innerHTML=t.locationname,this.tempelm.innerHTML=t.temp,this.weatherdescelm.innerHTML=t.description,this.clientlocationelm.classList.add("enable"),this.eventemitter.emit("show-content")})),this.eventemitter.on("clock-updated",(t=>{if(null!=this.timeelm&&(this.timeelm.innerHTML=t.formatted_time),null!=this.greetingelm){let e=new i(t.hour);this.greetingelm.innerHTML=e.get_greeting()}})),this.eventemitter.on("show-content",(()=>{this.set_clock(),this.show_content()})),this.clientlocationelm.addEventListener("click",(t=>{navigator.geolocation.getCurrentPosition((t=>{this.latlong=[t.coords.latitude,t.coords.longitude],this.storage.setItem("lastrequestedlocation",JSON.stringify(this.latlong)),this.weather.fetch_owm_data(this.latlong)}),(t=>{console.error(t.message)}),{enableHighAccuracy:!0})})),this.showtagsbuttonelm&&this.showtagsbuttonelm.addEventListener("click",(t=>{this.tagselectorelm.classList.add("enable"),t.preventDefault()})),this.tagsselectorclosebuttonelm&&this.tagsselectorclosebuttonelm.addEventListener("click",(t=>{this.tagselectorelm.classList.remove("enable")}))}show_content(){document.querySelectorAll(".hidden").forEach((function(t){t.classList.remove("hidden")}))}set_clock(){this.clock.set_utc_shift(this.timezoneshift),this.clock.run(this.clockfrequency)}}).init()})()})(); \ No newline at end of file diff --git a/jumpapp/assets/js/src/classes/Main.js b/jumpapp/assets/js/src/classes/Main.js index 8ddd0e9..3d2b4ab 100644 --- a/jumpapp/assets/js/src/classes/Main.js +++ b/jumpapp/assets/js/src/classes/Main.js @@ -8,7 +8,8 @@ export default class Main { constructor() { this.latlong = []; this.storage = window.localStorage; - this.updatefrequency = 10000; + this.clockfrequency = 10000; // 10 seconds. + this.weatherfrequency = 300000; // 5 minutes. this.timezoneshift = 0; this.metrictemp = JUMP.metrictemp; // Cache some DOM elements that we will access frequently. @@ -48,6 +49,9 @@ export default class Main { } // Retrieve weather and timezone data from Open Weather Map API. this.weather.fetch_owm_data(this.latlong); + setInterval(() => { + this.weather.fetch_owm_data(this.latlong); + }, this.weatherfrequency); } /** @@ -126,7 +130,7 @@ export default class Main { set_clock() { this.clock.set_utc_shift(this.timezoneshift); - this.clock.run(this.updatefrequency); + this.clock.run(this.clockfrequency); } } diff --git a/jumpapp/assets/js/src/classes/Weather.js b/jumpapp/assets/js/src/classes/Weather.js index c588971..5b5a70c 100644 --- a/jumpapp/assets/js/src/classes/Weather.js +++ b/jumpapp/assets/js/src/classes/Weather.js @@ -16,16 +16,16 @@ export default class Weather { fetch_owm_data(latlong) { // If we are provided with a latlong then the user must have cliecked on the location // button at some point, so let's use this in the api url... - let apiurl = '/api/weatherdata.php'; + let apiurl = '/api/weatherdata.php?token=' + JUMP.token; if (latlong.length) { - apiurl += ('?lat=' + latlong[0] + '&lon=' + latlong[1]); + apiurl += ('&lat=' + latlong[0] + '&lon=' + latlong[1]); } // Get some data from the weather api... fetch(apiurl) .then(response => response.json()) .then(data => { if (data.error) { - console.error('JUMP ERROR: There was a problem contacting the OWM API'); + console.error('JUMP ERROR: There was an issue with the OWM API... ' + data.error); return; } if (data.cod === 401) { diff --git a/jumpapp/classes/Config.php b/jumpapp/classes/Config.php index fcf7f3b..10d9846 100644 --- a/jumpapp/classes/Config.php +++ b/jumpapp/classes/Config.php @@ -42,9 +42,18 @@ class Config { 'noindex' ]; + /** + * Session config params. + */ + private const CONFIG_SESSION = [ + 'sessionname' => 'JUMP', + 'sessiontimeout' => '10 minutes' + ]; + public function __construct() { $this->config = new \PHLAK\Config\Config(__DIR__.'/../config.php'); $this->add_wwwroot_to_base_paths(); + $this->add_session_config(); if ($this->config_params_missing()) { throw new Exception('Config.php must always contain... '.implode(', ', self::CONFIG_PARAMS)); } @@ -63,6 +72,11 @@ class Config { } } + private function add_session_config(): void { + foreach(self::CONFIG_SESSION as $key => $value) { + $this->config->set($key, $value); + } + } /** * Determine if any configuration params are missing in the list loaded * from the config.php. diff --git a/jumpapp/classes/Main.php b/jumpapp/classes/Main.php index 745ef93..2b5523b 100644 --- a/jumpapp/classes/Main.php +++ b/jumpapp/classes/Main.php @@ -14,6 +14,8 @@ class Main { private Cache $cache; private Config $config; + private \Nette\Http\Request $request; + private \Nette\Http\Session $session; public function __construct() { $this->config = new Config(); @@ -27,10 +29,24 @@ class Main { } function init() { + // Create a request object based on globals so we can utilise url rewriting etc. + $this->request = (new \Nette\Http\RequestFactory)->fromGlobals(); + + // Initialise a new session using the request object. + $this->session = new \Nette\Http\Session($this->request, new \Nette\Http\Response); + $this->session->setName($this->config->get('sessionname')); + $this->session->setExpiration($this->config->get('sessiontimeout')); + + // Get a Nette session section for CSRF data. + $csrfsection = $this->session->getSection('csrf'); + + // Create a new CSRF token within the section if one doesn't exist already. + if (!$csrfsection->offsetExists('token')){ + $csrfsection->set('token', bin2hex(random_bytes(32))); + } + // Try to match the correct route based on the HTTP request. - $matchedroute = $this->router->match( - (new \Nette\Http\RequestFactory)->fromGlobals() - ); + $matchedroute = $this->router->match($this->request); // If we do not have a matched route then just serve up the home page. $pageclass = $matchedroute['class'] ?? 'Jump\Pages\HomePage'; @@ -38,7 +54,7 @@ class Main { // Instantiate the correct class to build the requested page, get the // content and return it. - $page = new $pageclass($this->config, $this->cache, $param ?? null); + $page = new $pageclass($this->config, $this->cache, $this->session, $param ?? null); return $page->get_output(); } diff --git a/jumpapp/classes/Pages/AbstractPage.php b/jumpapp/classes/Pages/AbstractPage.php index b23305c..0194e7c 100644 --- a/jumpapp/classes/Pages/AbstractPage.php +++ b/jumpapp/classes/Pages/AbstractPage.php @@ -14,7 +14,12 @@ abstract class AbstractPage { * @param \Jump\Cache $cache * @param string|null $generic param, passed from router. */ - public function __construct(protected \Jump\Config $config, protected \Jump\Cache $cache, protected ?string $param = null) { + public function __construct( + protected \Jump\Config $config, + protected \Jump\Cache $cache, + protected \Nette\Http\Session $session, + protected ?string $param = null + ){ $this->hastags = false; $this->mustache = new \Mustache_Engine([ 'loader' => new \Mustache_Loader_FilesystemLoader($this->config->get('templatedir')), diff --git a/jumpapp/classes/Pages/HomePage.php b/jumpapp/classes/Pages/HomePage.php index dc25fa0..78e100d 100644 --- a/jumpapp/classes/Pages/HomePage.php +++ b/jumpapp/classes/Pages/HomePage.php @@ -10,7 +10,9 @@ class HomePage extends AbstractPage { if (!$this->config->parse_bool($this->config->get('showgreeting'))) { $greeting = 'home'; } + $csrfsection = $this->session->getSection('csrf'); return $template->render([ + 'csrftoken' => $csrfsection->get('token'), 'greeting' => $greeting, 'noindex' => $this->config->parse_bool($this->config->get('noindex')), 'title' => $this->config->get('sitename'), diff --git a/jumpapp/classes/Pages/TagPage.php b/jumpapp/classes/Pages/TagPage.php index 6139c29..04e8d91 100644 --- a/jumpapp/classes/Pages/TagPage.php +++ b/jumpapp/classes/Pages/TagPage.php @@ -10,7 +10,9 @@ class TagPage extends AbstractPage { $template = $this->mustache->loadTemplate('header'); $greeting = $this->param; $title = 'Tag: '.$this->param; + $csrfsection = $this->session->getSection('csrf'); return $template->render([ + 'csrftoken' => $csrfsection->get('token'), 'greeting' => $greeting, 'noindex' => $this->config->parse_bool($this->config->get('noindex')), 'title' => $title, diff --git a/jumpapp/composer.json b/jumpapp/composer.json index 81f381c..d5c748b 100644 --- a/jumpapp/composer.json +++ b/jumpapp/composer.json @@ -9,6 +9,7 @@ "arthurhoaro/favicon": "~1.0", "nette/caching": "^3.1", "nette/routing": "^3.0.2", - "phlak/config": "^7.0" + "phlak/config": "^7.0", + "nette/http": "^3.1" } } diff --git a/jumpapp/composer.lock b/jumpapp/composer.lock index 77ff91b..15d0c0e 100644 --- a/jumpapp/composer.lock +++ b/jumpapp/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "097843a2f00f12e9786893c07a3ae8e3", + "content-hash": "860d4eabaa4ccb80ee0f7d501149c83d", "packages": [ { "name": "arthurhoaro/favicon", diff --git a/jumpapp/templates/header.mustache b/jumpapp/templates/header.mustache index 33a0ab4..3f9c077 100644 --- a/jumpapp/templates/header.mustache +++ b/jumpapp/templates/header.mustache @@ -14,6 +14,7 @@ owmapikey: '{{owmapikey}}', metrictemp: '{{metrictemp}}', ampmclock: '{{ampmclock}}', + token: '{{csrftoken}}' };