<?php
namespace Lia\Addon;
/**
* A very bad integration of the Router & other addons & utility classes that makes it easy to send a response
*
* @todo test: the try_redirect_to_corrected_url feature
* @todo add: convenient/easy way to handle 404 requests
* @todo test: base_url configuration
*/
class Server extends \Lia\Addon {
public string $fqn = 'lia:server.server';
/**
* true to flush the output buffer after sending headers & echoing response
* @note Nov 30, 2021: Not currently used
*/
public $bufferResponse = true;
/**
* true to enable theme
* false to disable theme
*
* @note $response->useTheme must ALSO be `true` for theme to be used.
*/
public $useTheme = true;
/**
* name of a theme in `theme` or `view` dir
*/
public string $themeName = 'theme';
public function init_lia(){
$lia = $this->lia;
$lia->methods['getResponse'] = [$this, 'getResponse'];
$lia->methods['deliver'] = [$this,'deliver'];
$lia->methods['urlWithDomain'] = [$this,'urlWithDomain'];
$lia->methods['setTheme'] = [$this, 'setTheme'];
}
/**
* Set theme to use in a web response
*
* @param $name name of a theme in `theme` or `view` dirs
*/
public function setTheme($name){
$this->themeName = $name;
}
/**
* @return http or https depending on $_SERVER['HTTPS']
*/
public function protocol(){
return isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) ? 'https' : 'http';
}
/**
* @param $relativeUrl include the leading `/` !!!
* @return http[s]://domain.tld{$relativeUrl}
*/
public function urlWithDomain($relativeUrl){
return $this->protocol().'://'.$_SERVER['HTTP_HOST'].$relativeUrl;
}
/**
*
* @warning redirects to a corrected url if no routes are found
*/
public function getResponse($url=null, $method=null){
$this->lia->call_hook('ServerStart');
$this->lia->call_hook('PreAllPackagesReady');
$this->lia->call_hook('AllPackagesReady');
$request = new \Lia\Obj\Request($url, $method);
$url = $request->url();
$response = new \Lia\Obj\Response($request);
$this->lia->call_hook('RequestStarted', $request, $response);
$routeList = $this->lia->route($request);
$this->lia->call_hook('RoutesFound', $routeList);
foreach ($routeList as $index=>$r){
$rets = $this->lia->call_hook('FilterRoute', $r);
foreach ($rets as $r){
if ($r===false)unset($routeList[$index]);
}
}
$route = $this->getDistinctRoute($routeList);
if ($route === null){
$this->try_redirect_to_corrected_url($url, $method);
throw new \Exception("No routes were found for this request to `".$url."`");
}
$this->lia->call_hook(\Lia\Hooks::ROUTES_FILTERED, $route);
$response->useTheme = true;
$accidental_output = $this->process_route($route, $response);
// @NOTE if `$_GET['theme']=='json'`, response will be returned as json. Idr the format exactly. You can override the 'themeName' of the Server addon in `RouteResolved`.
if (isset($_GET['theme'])&&$_GET['theme']=='json'){
$this->themeName = 'json';
// generates the compiled files
$this->lia->getResourceHtml();
}
$this->lia->call_hook(\Lia\Hooks::ROUTE_RESOLVED,$route,$response);
$this->apply_theme($route, $response);
$this->lia->call_hook('ResponseReady', $response);
return $response;
}
/**
* Applies the theme (if response->useTheme & server->useTheme are true)
*
*/
public function apply_theme($route, $response){
if (!$response->useTheme || !$this->useTheme)return;
if ($this->themeName=='json'){
$js = $this->lia->addons['resources']->getCompiledFilesUrl('js');
if ($js==false){
$scripts = $this->lia->addons['resources']->urls['js']??[];
} else {
$scripts = [$js, ...$this->lia->addons['resources']->urls['js']??[]];
}
$css = $this->lia->addons['resources']->getCompiledFilesUrl('css');
if ($css==false){
$styles= $this->lia->addons['resources']->urls['css']??[];
} else {
$styles = [$css, ...$this->lia->addons['resources']->urls['css']??[]];
}
$output = [
'content'=>$response->content,
'scripts'=>$scripts,
'stylesheets'=>$styles,
];
$response->content = json_encode($output);
return;
}
$themeView = $this->lia->view($this->themeName, ['response'=>$response, 'content'=>$response->content]);
$this->lia->call_hook('ThemeLoaded',$themeView);
// echo 'ok ok';exit;
$response->content = ''.$themeView;
}
/**
* Process a route & setup the response's content & headers
*/
public function process_route(\Lia\Obj\Route $route,\Lia\Obj\Response $response){
$target = $route->target();
ob_start();
//@TODO add 'route resolvers' to Liaison. Route Resolvers would provide extensibility to the handling of routes.
if ($route->isCallable()){
$response->useTheme = true;
// ob_start();
$target($route, $response);
// $response->content = ob_get_clean();
// $response->addHeader('Cache-Control: no-cache', false);
} else if ($route->fileExt()=='php'){
$response->useTheme = true;
$this->requirePhpFileRoute($route,$response);
// $response->addHeader('Cache-Control: no-cache', false);
} else if ($route->isFile()){
// @NOTE any public file route not ending in `.php` will set `$response->useTheme = false;` and add static file & cache headers.
// echo "IS STATIC FILE";exit;
$response->useTheme = false;
$staticFile = new \Lia\Utility\StaticFile($route->target());
$response->addHeaders($staticFile->getHeaders());
if ($staticFile->userHasFileCached){
$response->sendContent = false;
} else {
$response->content = file_get_contents($route->target());
}
} else {
if ($this->lia!=null){
ob_start();
$this->lia->dump_thing($route->target());
$target = ob_get_clean();
}
throw new \Lia\Exception(\Lia\Exception::REQUEST_TARGET_NOT_HANDLED, $route->url(), $target);
}
$accidentalOutput = ob_get_clean();
return $accidentalOutput;
}
/**
* This is a sloppy bad function that needs rewritten
*
* @warning executes header() if corrected_url route is found, or if the url was not already lower-case
*/
public function try_redirect_to_corrected_url($url, $method){
if ($url == null ) return;
if (substr($url,-1)!='/'){
$request = new \Lia\Obj\Request($url.'/', $method);
$routeList = $this->lia->route($request);
if (count($routeList)>0){
header('Cache-Control: no-cache');
header("Location: ".$url.'/');
exit;
}
}
if (strtolower($url)!==$url){
header('Cache-Control: no-cache');
header("Location: ".strtolower($url));
exit;
}
}
public function send_response($response){
$response->sendHeaders();
if ($response->sendContent){
echo $response->content;
}
$this->lia->call_hook('ResponseSent',$response);
// $this->closeConnection();
$this->lia->call_hook('RequestFinished', $response);
}
public function deliver($url=null, $method=null){
$response = $this->getResponse($url,$method);
$this->send_response($response);
}
/**
* Close the connection with client. May not work on all hosts, due to apache/server configuration, I'm pretty sure.
*
* @note Nov 30, 2021: Not currently in use
*/
protected function closeConnection(){
header("Connection: close");
ignore_user_abort();
session_write_close();
if ($this->bufferResponse){
while (ob_get_level()!=0){
ob_end_flush();
}
flush();
}
// sleep(5);
}
public function requirePhpFileRoute(\Lia\Obj\Route $route,\Lia\Obj\Response $response){
// @NOTE info about which paramaters are passed to public file route
//Use the View class (& probably compo?) to display this file
$lia = $this->lia;
$package = $route->package();
extract($route->paramaters(), EXTR_PREFIX_SAME, 'route');
//@bugfix for package not being given to the route
//@todo check for package class?
if (is_object($package)){
extract($package->public_file_params);
}
// extract($lia->getGlobalParamaters(),EXTR_PREFIX_SAME,'global');
ob_start();
require($route->target());
$response->content = ob_get_clean();
}
public function getDistinctRoute($routeList){
usort($routeList,
function($a, $b){
$l1 = strlen($a->placeholderPattern());
$l2 = strlen($b->placeholderPattern());
if ($l1==$l2)return 0;
else if ($l1>$l2)return -1;
else return 1;
}
);
return $routeList[0] ?? null;
}
}