Simple.php

<?php

namespace Lia;

class Simple extends \Lia {

    /**
     * `require` a php file that instantiates a Lia instance & return that instance, without routing, sending a server response, or outputing any text. 
     * May not work with unconventional setups. Requires a variable named `$lia` at `$file_path`
     * @param $file_path absolute path to your deliver script
     * @return \Lia a lia instance
     */
    static public function get_lia_instance(string $file_path): \Lia {
        $_SERVER['DO_NOT_RESPOND'] = true;
        $_SERVER['HTTP_HOST'] = 'localhost';
        $_SERVER['REQUEST_URI'] = '/';
        ob_start();
        require($file_path);
        ob_end_clean();
        return $lia;
    }

    public array $phads = [];
    public bool $debug = false;
    /**
     * The feature currently being enabled or the last feature enabled if one is not currently being enable.
     */
    public ?string $last_feature = null;

    /**
     * array of `$feature_name=>true`
     */
    public array $features = [];

    public function __construct(){
        parent::__construct();

        $server_dir = \Lia\Package\Server::main_dir();
        $server = new \Lia\Package\Server($this, 'server', $server_dir);

        // some basic configs
        $this->set('lia:server.cache.dir',$server->dir.'/cache/');  
        $this->set('lia:server.router.varDelim', '\\.\\/\\:'); //default includes a hyphen, which is dumb as hek
    }
    /**
     * Check if current server is a production server
     * @return false if `$_SERVER['HTTP_HOST'] == 'localhost'` else return true
     */
    public function is_production(){
        if (substr($_SERVER['HTTP_HOST'],0,9)=='localhost')return false;
        return true;
    }

    public function require_feature(string $feature){
        $last = $this->last_feature;
        if (!isset($this->features[$feature]))throw new \Exception("'$last' requires feature '$feature'. Please add '$feature' to your config.json like `features:[\"$feature\", \"$last\"]`");
    }

    public function setup(){
        foreach ($this->packages as $p){
            $p->ready();
        }
    }
    public function deliver_files(...$public_dirs){
        // var_dump($public_dirs);
        foreach ($public_dirs as $d){
            \Lia\FastFileRouter::file($d);
        }
    }
    public function load_apps(...$app_dirs){
        return array_map([$this,'load_app'], $app_dirs);
    }
    public function load_app($app_dir){
        $name = strtolower(basename($app_dir));

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

        $config = [];
        if (file_exists($app_dir.'/config.json')){
            $config = json_decode(file_get_contents($app_dir.'/config.json'),true);
        }
        if (!isset($config['features']))$config['features'] = ['phad'];
        $app->config = $config;

        if (file_exists($init_php=$app_dir.'/init.php')){
            $lia = $this;
            require($init_php);
        }
        if (file_exists($fn_php = $app_dir.'/functions.php')){
            $lia = $this;
            require_once($fn_php);
        }

        foreach ($config['features'] as $feature_name){
            $this->enable($feature_name, $app);
        }

        if (file_exists($post_init_php=$app_dir.'/post-init.php')){
            $lia = $this;
            $env = $this->env ?? null;
            $user = $this->user ?? null;
            require($post_init_php);
        }

        return $app;
    }

    public function enable(string $feature, $app=null){
        if (substr($feature,0,2)=='--')return;
        $this->last_feature = $feature;
        $this->features[$feature] = true;
        if (method_exists($this,$method='enable_'.$feature))return $this->$method($app);
        switch ($feature){
            case "seo_extractor":
                \Lia\Addon\SeoExtractor::enable($this);
                return;
            case "kitchen_sink":
                \Lia\App\KitchenSink::enable($this);
                return;
            case "R":
                $r = \R::setup();
                $dir = $this->root_dir;
                if (file_exists($dir.'/RSettings.json'))$r->load($dir.'/RSettings.json');
                if (file_exists($dir.'/.env/secret.json'))$r->load($dir.'/.env/secret.json');
                return $r;
            case "debug_empty_cache":
                if ($this->debug!==false&&$this->env->is_bool(['host.local'])
                    &&substr(parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH),-1)=='/'
                    ||$_SERVER['REQUEST_METHOD']=='POST'
                ){
                    // echo 'good';
                    // exit;
                    $this->cache->delete_all_cache_files();
                    $this->resources->cache->delete_all_cache_files();
                }
                return;
            case "debug_show_errors":
                if ($this->debug===true){
                    $this->env->showErrors();
                }
                return;

        }
        throw new \Exception("Feature '$feature' does not exist.");
    }


    public function enable_defaults($app){
        $this->set('server.router.varDelim', '\.\\/\\:'); //default includes a hyphen, which is dumb as hek
    }

    public function enable_debug_fake_user_mail($app){
        if ($this->env->is_bool(['host.local'])){
            if (empty(@LIA_DEBUG_FAKE_USER_MAIL_OUT_DIR));
            define('LIA_DEBUG_FAKE_USER_MAIL_OUT_DIR', $this->root_dir);
            require_once(dirname(__DIR__).'/file/fake_user_mail.php');
        }
    }

    public function enable_phad($app){

        if (!class_exists('\Phad', true)){
            throw new \Exception("\nPhad is not installed. Run `composer require taeluf/phad v0.4`");
        }
        $this->require_feature('pdo');

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

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

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

        $phad->handlers['user_has_role'] = 
        function(string $role){
            if (isset($this->user))return $this->user->has_role($role);
            // echo 'generic user has role';
            // exit;
            return true;
        }
        ;

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

        if (isset($app->config['phad.handler']) && isset($this->user)){
            $handler = $app->config['phad.handler'];
            $phad->set_handler(new $handler($this->user));
            // var_dump($handler);
            // exit;
        }

        $this->phads[] = $phad;
        return $phad;
    }

    /**
     * Get a phad instance who's `$phad->item_dir == $target_dir``
     * @param $target_dir the directory your target phad item should be in
     * @return a Phad instance or null if no item dirs match.
     * @note there is no path normalization
     */
    public function phad_from_dir(string $target_dir): ?\Phad {
        $len = strlen($target_dir);
        foreach ($this->phads as $phad){
            if (substr($phad->item_dir, 0,$len)==$target_dir)return $phad;
        }
        return null;
    }

    public function enable_user($app){
        if (!class_exists('\Tlf\User\Lib', true)){
            throw new \Exception("User lib not installed. Run `composer require taeluf/user-gui v0.3`");
        }
        $user_lib_dir = $this->root_dir.'/vendor/taeluf/user-gui/code/';
        $pdo = $this->pdo;

        // var_dump($user_lib_dir);
        // exit;
        $this->set('user.base_url', '/user/');  

        $user_package = new \Lia\Package\Server($this, 'user', $user_lib_dir);    
        $user_package->base_url = '/user/';
          
        // load db settings from env file & make pdo  
        // $settings = json_decode(file_get_contents(dirname(__DIR__).'/db-env.json'),true);
        // $pdo = new \PDO("mysql:dbname=".$settings['db'], $settings['user'], $settings['password']);
          
        // configure the user login library  
        $lib = new \Tlf\User\Lib($pdo);  
        $lib->disabled_pages = $app->config['user.disabled_pages'] ?? [];
        $user_lib = $lib;
        $lib->user_class = $app->config['user.class'] ?? null;
        $user_class = $lib->user_class;
        $lib->config = [  
            'web_address'=>$app->config['user.web_address'], 
            'email_from'=>$app->config['user.email_from'], 
        ];  
          
        // uncomment this line, run once, then re-comment this line  
        // $lib->init_db();
          
        // log the user in  
        $current_user = $lib->user_from_cookie();  
        // if there was no cookie, we'll use an unregistered user  
        if ($current_user===false)$current_user = new $user_class($pdo);  

        $this->set('user', $current_user);
        $this->addon('lia:server.view')->globalArgs['user'] = $current_user;
          
        // passes the user & library to user pages (login, register, etc)  
        $user_package->public_file_params = [  
            'user'=>$current_user,  
            'lib'=>$lib  
        ];  
          
        $user_package->lib = $lib;

        // $this->dump($app);

        $app->public_file_params = [
            'user'=>$current_user,
            'lib'=>$lib
        ];
           
    }

    public function enable_env($app){
        $this->env = new \Env();
        if (file_exists($env_file=$this->root_dir.'/.env/secret.json')){}
        $this->env->load($env_file);
        // $this->dump();
    }

    /**
     * 
     *
     * @deprecated in favor of pdo_mysql & pdo_sqlite to be more explicit
     * @alias enable_pdo_mysql
     */
    public function enable_pdo($app){
        return $this->enable_pdo_mysql($app);
    }
    public function enable_pdo_mysql($app){
        // echo "enable pdo mysql!";
        // exit;
        $this->features['pdo'] = true;
        if (!isset($this->env)){
            throw new \Exception("Feature 'env' is not enabled. Call `\$lia->enable_env()` or add `\"env\"` to the `\"features:[\"env\"]\"` of an app's confing.json");
        }

        $env = $this->env;
        $this->pdo = $pdo = new \PDO(
            'mysql:host='.$env->get('mysql.host').';dbname='.$env->get('mysql.dbname'),
            $env->get('mysql.user'), $env->get('mysql.password')
        );
        $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

        return $pdo;
    }

    public function enable_pdo_sqlite($app){
        $this->features['pdo'] = true;
        if (isset($this->env)&&$this->env->has('sqlite.path')){
            $path = $this->root_dir.'/'.$this->env->get('sqlite.path');
        } else {
            $path = $this->root_dir.'/data.sqlite';
        }

        $this->pdo = $pdo = new \PDO('sqlite:'.$path);
        $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

        return $pdo;

    }
    public function enable_ldb($app){
        if (!isset($this->pdo)){
            throw new \Exception("Feature 'pdo' is not enabled. Call `\$lia->enable_pdo()` or add `\"pdo\"` to the `\"features:[\"pdo\"]\"` of an app's confing.json");
        }

        if (!class_exists('\Tlf\LilDb', true)){
            throw new \Exception("\nLilDb is not installed. Run `composer require taeluf/lildb v0.1`");
        }

        require_once(__DIR__.'/ldb.php');
        $this->addMethod('ldb', 'ldb');

        return $this->ldb = \ldb($this->pdo);
    }



    /**
     * 1. Save the current page to analytics 
     * 2. Enable the analytics route, if phad is enabled
     *
     * Set `$_SERVER['DO_NOT_RESPOND'] = true` to disable the analytics feature. Useful for cli environments
     */
    public function enable_analytics($app){
        if (($_SERVER['DO_NOT_RESPOND']??false)==true)return;
        $pdo = $this->pdo;
        // still need the phad view
        $this->require_feature('pdo');
        $this->require_feature('phad');
        $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;
            }
            $stmt->execute( ['url'=>$url,'ip_hash'=>$ip_hash, 'year_month'=>$year_month]);
        }

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


    /**
     * enable the route /generic-error-page/ to display 'Unknown Error' message. 
     */
    public function enable_error_page($app){
        // route to error page
        if ($_SERVER['REQUEST_URI']=='/generic-error-page/'){ // save a small amount of overhead when this isn't the requested page

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

            if (!$this->addon('lia:server.router')->has_static_route('/generic-error-page/')){
                $this->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.";
                });
            }


            // $this->hook('ResponseReady',
            //     function($response){
            //         if (!file_exists($file = $this->cache->dir.'/generic-error-page.html')){
            //             file_put_contents($file, $response->content);
            //         }
            //     }
            // );
        }
    }


    public function respond(){
        // if (static::$IS_TLFSERV_CLI)return;
        if (isset($_SERVER['DO_NOT_RESPOND']) && $_SERVER['DO_NOT_RESPOND']==true){
            return;
        }
        $debug = $this->debug ?? false;
        try {
            if ($debug && $_SERVER['REQUEST_URI']=='/debug/'){
                $this->debug();
                exit;
            }
            $env = $this->env;

            try {
                $this->deliver();
            } catch (\Exception $e){
                if ($debug || !isset($this->generic_error_page)){
                    \Lia\ExceptionCatcher::throw($e,$debug);
                } else if (!$debug && isset($this->generic_error_page)){
                    echo file_get_contents($this->generic_error_page);
                }
            }
        } catch (\Throwable $e){
            if ($debug){
                echo nl2br("\n\n---------------------------------------\n\nError:\n");
                throw $e;
            } else {
                ob_get_clean();
                try {
                    $host = $_SERVER['HTTP_HOST'];
                    $file = $this->cache->dir.'/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);
                    }

                    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;
        $router = $lia->addon('lia:server.router');
        $routes = $router->routeMap;
        print_r(array_keys($routes['POST']));
        exit;
    }

    /**
     * get an array of all routes for the sitemap
     *
     * @param $sitemap_builder a var to reference the sitemap builder
     */
    public function get_all_sitemap_routes(&$sm_builder){

        $host = R("Site.Url") ?? false;
        if ($host==false){
            throw new \Exception("You must set 'Site.Url' in your RSettings.json file");
        }

        $full_list = [];
        foreach ($this->phads as $phad){

            $items = $phad->get_all_items();

            $sm_builder = $phad->sitemap;
            $sm_list = $sm_builder->get_sitemap_list($items, $phad);
            $full_list = [...$full_list, ...$sm_list];
        }
        // return $this->sitemap_dir.'/sitemap.xml';
        //
        if (isset($this->methods['get_extra_sitemap_entries'])){
            $full_list = array_merge(
                $full_list,
                $this->get_extra_sitemap_entries($full_list)
            );
            // print_r($full_list);
            // exit;
        }
        return $full_list;
    }














    /**
     * Convert markdown to html, if CommonMark is installed
     *
     * @param $markdown
     * @return html, or if CommonMark is not installed, returns the markdown
     */
    public function markdown_to_html(string $markdown){
        
        $class = "League\\CommonMark\\CommonMarkConverter";
        if (class_exists($class,true)){
            $converter = new \League\CommonMark\CommonMarkConverter();
            return $converter->convert($markdown).'';
        }
        throw new \Exception("\nCommonMark is not installed. Run `composer require league/commonmark 2.3`\n");

    }


    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->addResourceFile($f);
                }
                foreach ($item->resource_files()['js'] as $f){
                    $this->addResourceFile($f);
                }
                return $item;
            }
        }
        throw new \Exception("Phad item '$item' not found");
    }


    /**
     * 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;
    }

    /**
     * Generate an sql CREATE TABLE statement from a phad item containing a form.
     *
     * @param $phad 
     * @param $form_item the form-item to scan & use it's inputs to form a CREATE TABLE sql statement
     * @return string sql CREATE TABLE statement
     */
    public function create_phad_table(\Phad $phad, string $form_item): string {
        $item = $phad->item($form_item);
        $info = $item->info();
        if (!is_object($info)||!isset($info->properties)){
            throw new \Exception("'$form_item' does not appear to be a phad item or does not have properties.");
        }
        $properties = $info->properties;
        // print_r($properties);
        $table = strtolower($info->name);
        $statement = "CREATE TABLE `$table` (\n    ";
        $did_first = false;
        foreach ($properties as $name=>$details){
            $col_str = col_str($name, $details);
            if ($did_first)$statement .=",\n    ";
            $statement .= $col_str;
            $did_first = true;
        }

        $statement .= "\n);";

        return $statement;
    }


    /**
     * Send an email through SMTP
     * You MUST set the following settings to R: smtp.host, smtp.user, smtp.password, Business.Name
     
     */
    function send_mail(string $to, string $reply_to, $subject, $body){
        $env = $this->env;

        if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer', true)){
            throw new \Exception("Please install PHPMailer to use send_mail. composer require phpmailer/phpmailer. Commit hash 'a04c4c2' is known to work.");
        }

        //Create an instance; passing `true` enables exceptions
        $mail = new \PHPMailer\PHPMailer\PHPMailer(true);

        try {
            //Server settings
            $mail->SMTPDebug = \PHPMailer\PHPMailer\SMTP::DEBUG_SERVER;                      //Enable verbose debug output
            $mail->isSMTP();                                            //Send using SMTP
            $mail->Host       = R("smtp.host");                     //Set the SMTP server to send through
            $mail->SMTPAuth   = true;                                   //Enable SMTP authentication
            $mail->Username   = R("smtp.user");                     //SMTP username
            $mail->Password   = R("smtp.password");                               //SMTP password
            $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;            //Enable implicit TLS encryption
            $mail->Port       = 465;                                    //TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS`

            //Recipients
            $mail->setFrom(R("smtp.user"), R("Business.Name").' Help');
            $mail->addAddress($to);               //Name is optional
            $mail->addReplyTo($reply_to);
            // $mail->addCC('cc@example.com');
            // $mail->addBCC('bcc@example.com');

            //Attachments
            // $mail->addAttachment('/var/tmp/file.tar.gz');         //Add attachments
            // $mail->addAttachment('/tmp/image.jpg', 'new.jpg');    //Optional name

            //Content
            $mail->isHTML(true);                                  //Set email format to HTML
            $mail->Subject = $subject;
            $mail->Body    = $body;
            // $mail->AltBody = 'This is the body in plain text for non-HTML mail clients';

            ob_start();
            $mail->send();
            ob_get_clean();
            $this->log("mail-success", "Email sent!");
        } catch (\Exception $e) {
            $this->log("mail-error", "Message could not be sent. Mailer Error: {$mail->ErrorInfo}");
        }
    }

    public function log(){
        // @TODO write log method
    }

    public function __isset($prop){
        return isset($this->$prop) ? true : isset($this->props[$prop]);
    }

    /**
     * get the named property or addon if property not found
     * @param $addon the name of the addon
     */
    public function __get($prop){
        if (!isset($this->props[$prop])&&!isset($this->addons[$prop]))throw new \Exception("Property '$prop' not found");
        $a = $this->props[$prop] ?? $this->addons[$prop];
        return $a;
    }

    public function __set($prop, $value){
        $this->props[$prop] = $value;
    }
}