Created
October 19, 2022 11:44
-
-
Save larsloQ/a30471b3d42dbc7f72e69455ffe90719 to your computer and use it in GitHub Desktop.
SLIM-Framework based Micro-Proxy for accessing private Google Calendar URLS via JS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Background: | |
* A website I made wanted to show the pricing and availablity of some houses the offer for rent. | |
* they managed these data via simple google calendars, one for bookings/availabiliy and | |
* one where they enter prices for each day via recurring events a couple of weeks in advance. | |
* | |
* since google private calendar urls can not be access via JS (due to CORS) | |
* We need a proxy where we can controll CORS-Headers. | |
* Also we do not want to expose the private urls. | |
* So we needed a proxy. | |
* | |
* This is a typical https://www.slimframework.com/ kind of mirco-backend / micro-proxy | |
* preventing cors and returning ics / iCalendar data as strings | |
* with the following composer.json: | |
* { | |
"require": { | |
"slim/slim": "4.*", | |
"slim/psr7": "^1.5", | |
"guzzlehttp/psr7": "^2", | |
"selective/basepath": "^2.1", | |
"guzzlehttp/guzzle": "^7.0", | |
"monolog/monolog": "^2.8" | |
} | |
} | |
* | |
*/ | |
use Psr\Http\Message\ResponseInterface as Response; | |
use Psr\Http\Message\ServerRequestInterface as Request; | |
use Selective\BasePath\BasePathMiddleware; | |
use GuzzleHttp\Exception\RequestException; | |
use GuzzleHttp\Psr7\Request as G7Req; | |
use Slim\Exception\HttpNotFoundException; | |
use Slim\Factory\AppFactory; | |
require __DIR__ . '/../vendor/autoload.php'; | |
$app = AppFactory::create(); | |
// Set the base path to run the app in a subdirectory. | |
// This path is used in urlFor(). | |
$app->add(new BasePathMiddleware($app)); | |
/* print errors, log them, with details */ | |
// $app->addErrorMiddleware( true, true, true ); | |
/* for production: no display of errors */ | |
$app->addErrorMiddleware(false, true, true); | |
$app->get( | |
'/', | |
function (Request $request, Response $response, $args) { | |
$response->getBody()->write('Hello world! '); | |
return $response; | |
} | |
); | |
$logger_settings = array( | |
'timezone' => 'UTC', | |
'log_method_not_allowed' => true, | |
'log_404' => true, | |
'path' => '../logs/error.log', | |
'name' => 'error', | |
); | |
// calendar_map.php | |
/* | |
* private urls of goggle calendar | |
* you can access them via route /{first_key}/{second_key}/ (see index.php) | |
*/ | |
/** | |
*configure your private calendar urls there | |
*can be accessed via route house_1/bookings (see below) | |
*/ | |
$calendar_map = array( | |
'house_1' => array( | |
'bookings' => '{YOUR_PRIVATE_GOOGLE_OR_OTHER_ICS_ICAL_CALENDAR_URL}', | |
'prices' => '{YOUR_PRIVATE_GOOGLE_OR_OTHER_ICS_ICAL_CALENDAR_URL}', | |
'third_calendar' => '{YOUR_PRIVATE_GOOGLE_OR_OTHER_ICS_ICAL_CALENDAR_URL}', | |
), | |
); | |
/* | |
* configure CORS | |
* when accessing this via browser/JS you need to set allowed_domain to the domain your JS runs at | |
* because of CORS | |
*/ | |
$allowed_domain = 'http://localhost'; | |
/** | |
* handling errors for this app. DRY | |
* | |
* @param Response $response The response | |
* @param array $error The error, gets logged, gets json_encode | |
* @param bool $with_calendar The with calendard, use our "Error.ics" calendar to return to show message inside of calendar | |
* @param int $http_status The http status, responding with 200 and having '$with_calendar' true makes frontend think that things went fine, so we can show the error message inside calendar | |
* @param bool $log log? | |
* | |
* @return <type> ( description_of_the_return_value ) | |
*/ | |
function handle_error(Response $response, array $error, bool $with_calendar = true, int $http_status = 200, bool $log = true) | |
{ | |
if ($log) { | |
global $logger_settings; | |
$logger = new App\Services\LoggerFactory($logger_settings); | |
$logger->getLogger()->error(json_encode($error)); | |
} | |
/* wrong key, return error calendar */ | |
$error_content = file_get_contents(__DIR__ . '/ERROR.ics'); | |
$response->getBody()->write($error_content); | |
return $response->withStatus($http_status); | |
} | |
/** | |
* access private calendar urls and return ics data as string | |
* house_key string must match one of first-level keys in config/calendar_map.php / $calendar_map | |
* cal_key string must match one of second-level keys in config/calendar_map.php / $calendar_map | |
*/ | |
$app->get( | |
'/{house_key}/{cal_key}', | |
function (Request $request, Response $response, $args) use ($calendar_map) { | |
$urls = $calendar_map[ $args['house_key'] ]; | |
$ical_url = $urls[ $args['cal_key'] ]; | |
$error = array(); | |
if (empty($urls)) { | |
$error = array( | |
'type' => 'wrong house key', | |
'wrong_key' => filter_var($args['house_key'], FILTER_SANITIZE_FULL_SPECIAL_CHARS), | |
); | |
} | |
$ical_url = $urls[ $args['cal_key'] ]; | |
if (empty($ical_url)) { | |
$error = array( | |
'type' => 'wrong calendar key', | |
'wrong_key' => filter_var($args['cal_key'], FILTER_SANITIZE_FULL_SPECIAL_CHARS), | |
'house' => filter_var($args['house_key'], FILTER_SANITIZE_FULL_SPECIAL_CHARS), | |
); | |
} | |
if (! empty($error)) { | |
return handle_error($response, $error, true, 200); | |
} | |
try { | |
$client = new GuzzleHttp\Client(); | |
$res = $client->request( | |
'GET', | |
$ical_url, | |
array( | |
'headers' => array(), | |
'http_errors' => false, // do not FATAL ERROR on http errors | |
) | |
); | |
$calendar_ics = $res->getBody()->getContents(); | |
$response->getBody()->write($calendar_ics); | |
return $response; | |
} catch (Exception $e) { | |
$error = array( | |
'type' => 'http-error', | |
'error' => $e->getMessage(), | |
'request' => $ical_url, | |
); | |
return handle_error($response, $error, false, 400, true); | |
} | |
} | |
); | |
/** | |
* CORS STUFF, JS PREFLIGHT, OPTIONS REQUEST | |
* see https://www.slimframework.com/docs/v4/cookbook/enable-cors.html | |
*/ | |
$app->options( | |
'/{routes:.+}', | |
function ($request, $response, $args) { | |
return $response; | |
} | |
); | |
$app->add( | |
function ($request, $handler) use ($allowed_domain) { | |
$response = $handler->handle($request); | |
return $response | |
->withHeader('Access-Control-Allow-Origin', $allowed_domain) | |
->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization') | |
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'); | |
} | |
); | |
/** | |
* Catch-all route to serve a 404 Not Found page if none of the routes match | |
* NOTE: make sure this route is defined last | |
*/ | |
$app->map( | |
array( 'GET', 'POST', 'PUT', 'DELETE', 'PATCH' ), | |
'/{routes:.+}', | |
function ($request, $response) { | |
throw new HttpNotFoundException($request); | |
} | |
); | |
$app->run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment