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