Server.php

<?php

namespace Tlf;

/**
 * A (somewhat) minimal layer on top of Liaison & Phad to make them much easier to setup.
 */
class Server {

    /** set TRUE to stop deliver() from delivering */
    static public $IS_TLFSERV_CLI = false;

    /**
     * if true, print error messages & always recompile everything
     * See init() for initialization.
     */
    public bool $debug = false;

    /**
     * Set false before calling init() if you want to implement your own /generic-error-page/ route
     */
    public bool $enable_error_route = true;


    /** array of phad objects */
    public array $phads = [];

    /** environment object */
    public \Env $env;
    /** Liaison object */
    public \Lia $lia;
    /** The most recently set Phad object. See $this->phads for list of all phad instances */
    public \Phad $phad;

    /** The main server package */
    public \Lia\Package $main;

    public function __construct(){}

    /**
     * Initialize liaison. 
     * @note This COULD be in the constructor, but then file_route() & php_route() would have a lot of overhead before running
     *
     * @param $debug true to force debug mode, false to force no debug mode, null to use default (yes for localhost, no for non-localhost)
     */
    public function init($debug=null){
        // i could separate this into an init() function so the fast routes are faster
        $this->env = new \Env();
        if ($debug===null&&$this->env->is_bool(['host.local']))$debug = true;
        else if ($debug===null)$debug = false;
        $this->debug = $debug;

        if ($debug)$this->env->showErrors();

        $lia = $this->lia = new \Lia();

        // add the main server package (default addons)
        $server_dir = \Lia\Package\Server::main_dir();
        $this->main = $server = new \Lia\Package\Server($lia, 'server', $server_dir);
        $lia->set('lia:server.cache.dir', $server_dir.'/cache/');

        // some basic configs
        $lia->set('lia:server.router.varDelim', '\\.\\/\\:'); //default includes a hyphen, which is dumb as hek
        $lia->set('lia:server.cache.dir',$server->dir.'/cache/');  

        // if ($debug){
        //     $lia->set('lia:server.resources.recompileJsAfter', 0);
        //     $lia->set('lia:server.resources.recompileCssAfter', 0);
        //     $lia->set('lia:server.resources.useCache', true);
        //     // $lia->set('lia:blog.use_cache', false);
        //     $lia->set('lia:server.resources.forceRecompile', true);
        // } else {
        //     $lia->set('lia:server.resources.useCache', true);
        //     // $lia->set('lia:server.blog.use_cache', true);
        // }
        if ($debug!==false&&$this->env->is_bool(['host.local'])
            &&substr($_SERVER['REQUEST_URI'],-1)=='/'
            ||$_SERVER['REQUEST_METHOD']=='POST'
        ){
            $lia->cache->delete_all_cache_files();
        }

        // route to error page
        if ($this->enable_error_route&&$_SERVER['REQUEST_URI']=='/generic-error-page/'){ // save a small amount of overhead when this isn't the requested page

            $lia->addon('lia:server.seo')->html['meta-noindex'] = ['<meta name="robots" content="noindex" />'];

            if (!$lia->addon('lia:server.router')->has_static_route('/generic-error-page/')){
                $lia->addRoute('/generic-error-page/',function($route,$response){
                    $response->content = "Unknown Error. Please return to the <a href=\"/\">Home Page</a> or try again. If this persists, contact the website owner.";
                });
            }
        }
    }

    /**
     * Load environment settings file
     * @param $file Absolute path to the json file
     */
    public function load_env(string $file){
        $this->env->load($file);
    }

    public function set_cache_dir(string $dir){
        // $this->lia->set('server.cache.dir', $dir);
        // $this->lia->set('lia:cache.dir', $dir);
    }

    /**
     * To disable the theme which generally provides a full HTML doc
     */
    public function disable_theme(){
        $server->addon('lia:server.server')->useTheme = false;
    }

    /** quickly route to a file in $dir */
    public function file_route($dir){
        \Lia\FastFileRouter::file($dir);
    }
    /** quickly route to a script in $dir */
    public function php_route($dir){
        \Lia\FastFileRouter::php($dir);
    }

    /** Check if the request uri starts with the given string
     * @param $url_prefix something like `/blog/`
     */
    public function is_request_to(string $url_prefix){
        return substr($_SERVER['REQUEST_URI'],0,strlen($url_prefix))
                == $url_prefix;
        
    }

    /**
     * Add a standard Liaison server package for the given dir
     */
    public function addServer($dir,$name=null, $url_prefix='/'){

        $name = $name ?? uniqid();

        $app = new \Lia\Package\Server($this->lia, $name, $dir, $url_prefix);
        $app->base_url = $url_prefix;
        $app->dir = $dir;

        return $app;
    }

    /**
     * Get a pdo object with a mysql connection that's defined in your env settings file
     * The settings must define:
     * mysql.host, mysql.dbname, mysql.user, & mysql.password
     */
    public function env_pdo(){
        $env = $this->env;
        $pdo = new \PDO(
            'mysql:host='.$env->get('mysql.host').';dbname='.$env->get('mysql.dbname'),
            $env->get('mysql.user'), $env->get('mysql.password')
        );
        return $pdo;
    }

    public function phad_item(string $item, array $args = []){
        foreach ($this->phads as $phad){
            if ($phad->has_item($item)){
                $item = $phad->item($item, $args);
                foreach ($item->resource_files()['css'] as $f){
                    $this->lia->addResourceFile($f);
                }
                foreach ($item->resource_files()['js'] as $f){
                    $this->lia->addResourceFile($f);
                }
                return $item;
            }
        }
        throw new \Exception("Phad item '$item' not found");
    }

    /**
     * @param $dir the base dir. In it, you MUST have dirs 'phad', 'cache', and 'sitemap'
     * @param $pdo a pdo object, connected to a database
     */
    public function enable_phad(string $dir, \PDO $pdo, $phad_class='Phad', $route_prefix=''){
        $lia = $this->lia;
        $helper = new \Tlf\Server\Helper();

        $options = [
            'item_dir'=>$dir.'/phad/',
            'cache_dir'=>$dir.'/cache/',
            'sitemap_dir'=>$dir.'/sitemap/',
            'pdo' => $pdo,// a pdo object
            'router' => $lia->addon('lia:server.router'),
            'throw_on_query_failure'=>$this->debug,
            'force_compile'=>$this->debug,
        ];
        $phad = $phad_class::main($options);
        $phad->route_prefix = $route_prefix;
        $phad->global_phad_args['lia'] = $this->lia;
        $lia->set('phad', $phad);
        $lia->addMethod('phad', [$this, 'phad_item']);
        $this->phads[] = $phad;

        $phad->filters['markdown'] = [$helper, 'markdown_to_html'];

        $phad->integration->setup_liaison_routes($lia);
        $phad->integration->setup_liaison_route($lia, '/sitemap.xml', $phad->sitemap_dir.'/sitemap.xml');


        $phad->handlers['user_has_role'] = 
        function(string $role){
            return true;
        }
        ;

        $phad->handlers['can_read_row'] = 
            function(array $ItemRow,object $ItemInfo,string $ItemName){
                return true;
            };

        $this->phad = $phad;
        return $phad;
    }


    /**
     * Run `$lia->deliver()` & automatically handle errors
     */
    public function deliver(){
        if (static::$IS_TLFSERV_CLI)return;
        $debug = $this->debug;
        try {
            if ($debug && $_SERVER['REQUEST_URI']=='/debug/'){
                $this->debug();
                exit;
            }
            $env = $this->env;

            try {
                $this->lia->deliver();
            } catch (\Exception $e){
                \Lia\ExceptionCatcher::throw($e,$debug);
            }
        } catch (\Throwable $e){
            if ($debug){
                echo nl2br("\n\n---------------------------------------\n\nError:\n");
                throw $e;
            } else {
                ob_get_clean();
                try {
                    $host = $_SERVER['HTTP_HOST'];
                    $file = $_SERVER['DOCUMENT_ROOT'].'/cache/generic-error-page.html';


                    if (is_file($file)){
                        $i=0;
                        while(ob_get_level()>0&&$i++<10){
                            ob_end_clean();
                        }
                        echo file_get_contents($file);
                    }
                    if ($debug){
                        throw new \Exception("Error page not found at $file");
                    } else {
                        exit;
                    }
                } catch (\Throwable $e){
                    echo "There was an error. Try returning to the "
                        .'<a href="/">Home Page</a> at '.$host;   
                    exit;
                }
            }
        }
    }

    public function debug(){
        $lia = $this->lia;
        $router = $lia->addon('lia:server.router');
        $routes = $router->routeMap;
        print_r(array_keys($routes['POST']));
        exit;
    }

    /**
     * 1. Save the current page to analytics 
     * 2. Enable the analytics route, if phad is enabled
     */
    public function enable_analytics(\PDO $pdo){
        $url = @$_SERVER['REQUEST_URI'];
        $pos = strpos($url, '?');
        $url = str_replace(['<','>'],'-',$url);
        if ($pos!==false)$url = substr($url,0,$pos);
        // $ip_hash = md5(@$_SERVER["REMOTE_ADDR"]);
        $ip_hash = 'na';
        $datetime = new \DateTime();
        $year_month = $datetime->format("Y-m")."-01";

        $end = substr($url,-4);
        // var_dump($end);
        if (substr($url,-4)=='.css'||substr($url,-3)=='.js'){
        } else {
            $stmt = $pdo->prepare(
                'INSERT INTO tlf_analytics (`url`, `ip_hash`, `year_month`)
                VALUES (:url, :ip_hash, :year_month)
                ');
            if ($stmt==false){
                // print_r($pdo->errorInfo());
                // exit;
                return;
            }
            // echo 'execute analytics';
            // exit;
            $stmt->execute( ['url'=>$url,'ip_hash'=>$ip_hash, 'year_month'=>$year_month]);
        }

        if (!isset($this->phad)||!is_object($this->phad))return;
        $args = [
            'pdo'=>$pdo,
        ];
        $item = $this->phad->item_from_file(__DIR__.'/AnalyticsView.php', $args);
        $this->lia->addRoute(
            '/analytics/',
            function($route,$response) use ($item){
                $response->content = $item->html();
            }
        );
    }

    /**
     * Initialize your analytics table. Recommend using a local sqlite database.
     */
    public function init_analytics_table(\PDO $pdo){
        $pdo->exec(
            'CREATE TABLE IF NOT EXISTS `tlf_analytics`(
                `id` int AUTO_INCREMENT,
                `url` varchar(255) NOT NULL,
                `ip_hash` varchar(255) NOT NULL,
                `year_month` varchar(12) NOT NULL,
                PRIMARY KEY(`id`)
            );
            '
        );

        print_r($pdo->errorInfo());
        // exit;
    }
}