Server.php

<?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;
    }

}