<?php
namespace Fresh;
class Package extends \Lia\Package {
protected $findCallable;
protected $submitCallable;
protected $patterns;
protected $fresh;
protected $showEditContentButton = false;
protected $freshCompo;
protected $baseCompo;
public function __construct($options){
parent::__construct($options);
$this->findCallable = [$this,'rdbFind'];
$this->submitCallable = [$this,'autoSubmit'];
$this->patterns = [];
$this->lia->schedule('Route_Display',$this);
$this->lia->schedule('Request_Setup',$this);
$this->lia->schedule('Fresh_GetUrl',$this);
$this->lia->schedule('Body_Extra_End_Print', $this);
$this->freshCompo = new \Fresh\Component($this->name,$this->dir);
$packageOptions = array_replace($options['conf'],$options['options']);
if ($relDir=isset($packageOptions['Upload.Dir'])){
$url = '/'.$relDir.'/';
$url = str_replace('//','/',$url);
$dir = $this->dir('public').'/'.$relDir.'/';
$dir = str_replace(['///','//'], '/', $dir);
$this->freshCompo->setUpload($dir,$url);
// $this->freshCompo->setFileUploadDir($dir);
// var_dump($packageOptions);exit;
}
$this->baseCompo = new \Fresh\Component($this->name.'_base', __DIR__.'/../6-res/');
}
/**
* Override the default finder, which views use to query for information
* The default finder uses RDB, which is a library built upon Redbean
*
* This must be called BEFORE a freshcompo is added to Liaison
*
* @param mixed $callable - Must accept two paramaters: $table, $lookup
* - $table is a string table name
* - $lookup is a lookup string, which will generally need to be parsed
* @return void
*/
public function setFinder($callable){
$this->findCallable = $callable;
}
/**
* Override the default submitter, which is used to submit auto-forms
*
* @param mixed $callable - takes the following paramaters: $table, $data, $itemId, $passthru
* - $table: string, the name of the table to save to
* - $data: array, the $_POST data, with special entries (like fresh_table) removed
* - $itemId: The id of the item being edited (or null, if it's a new item to be created)
* - $passthru: Data that gets passed through Liaison (dynamic properties in the URL). I doubt this is used in most cases.
* @return void
*/
public function setSubmitter($callable){
$this->submitCallable = $callable;
}
public function onIsRouteThemeable($event,$route){
if (is_callable($route->callback))return true;
}
/**
* Allows routes to point to callbacks instead of just pointing to files
*/
public function onRoute_Display($event,$route){
if (!is_callable($route->callback))return;
// echo 'callback here';
// exit;
$callback = $route->callback;
// echo 'callback route';exit;
$callback($event,$route,$route->extractables);
}
public function addResources($view,$resources){
$file = $view->file;
foreach ($resources['css'] as $cssFile){
$this->lia->send('Res_AddStyleFile',$cssFile);
}
foreach ($resources['js'] as $jsFile){
$this->lia->send('Res_AddScriptFile',$jsFile);
}
foreach ($resources['php'] as $phpFile){
$event = $this->Event;
require($phpFile);
}
}
public function addPattern($view,$pattern){
$this->patterns[$view->file][] = $pattern;
}
public function onFresh_GetUrl($event,$view){
$patterns = $this->patterns[$view->file];
if ($view instanceof \Fresh\Form){
foreach ($patterns as $pattern){
if (strpos($pattern,'/edit/')!==false)return $pattern;
}
}
return null;
}
public function autoSubmit($table,$data,$itemId,$passthru){
$item = null;
if ($itemId==null){
$item = \RDB::dispense($table);
if (!$this->callHandler('Fresh.Item.Create',$item,$table,$itemId)){
$this->lia->send('Error_Forbidden','');
echo "You do not have permission to create this item. Return to <a href=\"/\">The home page</a>";
return;
}
}
else {
$item = \RDB::findOne($table,'id = ?',[$itemId]);
if (!$this->callHandler('Fresh.Item.Edit',$item,$table,$itemId)){
$this->lia->send('Error_Forbidden','');
echo "You do not have permission to edit this item. Return to <a href=\"/\">The home page</a>";
return;
}
}
if ($item==null){
throw new \Exception("An Item could not be found for id: '{$itemId}'");
}
if (isset($data['fresh_delete'])
&&$data['fresh_delete']==['Delete']){
if (!$this->callHandler('Fresh.Item.Delete',$item,$table,$itemId)){
$this->lia->send('Error_Forbidden','');
echo "You do not have permission to delete this item. Return to <a href=\"/\">The home page</a>";
return;
}
unset($data['fresh_delete']);
$delItem = $item;
$values = [];
foreach ($delItem->props() as $name=>$value){
$values[$name] = is_numeric($value) ? $value+0 : $value.'';
}
$values['removedItemId'] = $delItem->id+0;
unset($values['id']);
\RDB::trash($delItem);
$archiveItem = \RDB::dispense('deleted'.$table);
foreach ($values as $name=>$val){
$archiveItem->$name = $val;
}
\RDB::store($archiveItem);
} else {
unset($data['fresh_delete']);
foreach ($data as $key=>$valueList){
$item->$key = $valueList[0];
}
\RDB::store($item);
}
if (isset($passthru['redirect_url'])){
header("Location: ".$passthru['redirect_url']);
exit;
} else {
echo "Submission complete. We had a problem redirecting you.";
exit;
}
}
public function rdbFind($table,$lookup){
// var_dump($table);
// var_dump($lookup);
// exit;
$queries = explode(';',$lookup);
$qs = [];
$binds = [];
// $addColumns = [];
foreach ($queries as $q){
$parts = explode(':',$q);
$key = $parts[0];
$value = $parts[1] ?? null;
if ($value=='*')continue;
if ($value===null)continue;
$qs[] = $key.' = :'.$key;
$binds[':'.$key] = $value;
// $addColumns[$key] = $value;
}
if (($binds[':id']??null)=='new'){
$item = \RDB::dispense($table);
$item->id = '';
$results = [$item];
} else {
$where = implode(' AND ',$qs);
$results = \RDB::find($table,$where,$binds);
}
/**
* @TODO maybe... do permissions checks here, instead of in the find loop??? (but then I can't `continue`)...
*
* @export(TODO.SimplifyPermissions)
*/
return $results;
}
public function filterInputValue($name,$value,$input){
return $value;
}
/**
* If your package does not contain the named view in your view directory, then load views from Liaison with the given $staleViewDir
* call `$package->view($name,$data=[], $staleViewDir = 'view-stale')`
* To disable stale view dir backup, pass `false`
*
* @export(Package.GetView)
*/
public function view($name,$data=[], $staleViewDir = 'view-stale'){
$data['lia'] = $this->lia;
$data['fresh'] = $this;
$freshView = $this->freshCompo->view($name,$data);
if ($freshView!=null)return $freshView;
$baseView = $this->baseCompo->view($name,$data);
if ($baseView!=null){
// echo $name;
// exit;
return $baseView;
}
if ($staleViewDir!==false)
return parent::view($name,$data,$staleViewDir);
}
// public function form($name){
// }
protected function initComponents(){
// $this->compos[] = $freshCompo;
}
public function onRequest_Setup($event,$url){
$recompile = $this->lia->get('FreshPackage.forceRecompile',false);
$this->setupFreshCompo($this->freshCompo,$recompile);
}
protected function setupFreshCompo($compo,$forceRecompile=false){
$lia = $this->lia;
// throw new \Exception("The callback route creates a new instance of note, instead of using the existing instance. This new instance has not been run through this onAddFreshCompo event... so it doesn't have 'find' or any other handlers");
$compo->setRuntimeHandler('find',$this->findCallable);
// var_dump($this->findCallable);
// exit;
$compo->setRuntimeHandler('submit',$this->submitCallable);
$compo->addRuntimeHandler('filterInputValue',[$this,'filterInputValue']);
$compo->addRuntimeHandler('addResources',[$this,'addResources']);
$compo->addRuntimeHandler('addPattern',[$this,'addPattern']);
$compo->setRuntimeHandler('enableClickToEdit',[$this,'enableClickToEdit']);
$compo->addViewQuery('//*[@tlf-clicktoedit]',[$this,'setupClickToEdit']);
$this->AccessItemDisplay($compo);
$this->AccessItemEdit($compo);
$this->AccessItemDelete($compo);
$this->AccessItemCreate($compo);
$compo->addCompileHandler('preDoc',
function($cleanSource,$dirtySource,$compiler,$view){
$cleaner = preg_replace('/(\<lia\-route[^\>]*[^\/\>])\>/','$1/>',$cleanSource);
return $cleaner;
// return $cleanSource;
}
);
$compo->addViewQuery('//lia-route',
function($doc,$view,$compiler,$node) use ($lia,$compo){
if ($node->boolAttribute('isform'))return;
$escPattern = var_export($node->getAttribute('pattern'),true);
$escClass = var_export(get_class($compo),true);
$escViewName = var_export($view->name,true);
$phpCode =
<<<PHP
<?php
\$pattern = {$escPattern};
\$this->callHandler('addPattern',\$this,\$pattern);
\$rtr = \$lia->compo('Router');
\$rtr->addCallbackRoute(\$pattern, \$package,
function(\$event,\$route,\$passthru){
// var_dump(get_class(\$this));
// exit;
// \$class = {$escClass};
// \$viewName = {$escViewName};
// \$compo = new \$class();
\$passthru['pattern'] = {$escPattern};
\$passthru['event'] = \$event;
\$passthru['lia'] = \$event->lia;
\$passthru['route'] = \$route;
// \$view = \$compo->view(\$viewName,\$passthru);
\$this->setPassthru(\$passthru);
echo \$this;
}
);
?>
PHP;
$view->appendSetupCode($phpCode);
if ($node->hasAttribute('onmatch')){
$onmatch = $node->getAttribute('onmatch');
$path = realpath(__DIR__.'/../').'/public/compiled/php-bitties/';
$filePath = $compiler->fileForCode($path,$compiler->codeForId($onmatch));
$pattern = $node->getAttribute('pattern');
$compiler->prependCode("<?php if (\$pattern=={$escPattern}) require('{$filePath}'); ?>");
}
$node->parentNode->removeChild($node);
}
);
$compo->setRuntimeHandler('getRoutes',
function($view){
$viewContent = file_get_contents($view->file);
$compiler = new \RBDoc\Compiler();
$cleanSource = $compiler->cleanSource($viewContent);
// echo $cleanSource."\n\n";
// exit;
$doc = new \RBDoc\DOMDoc($cleanSource);
$routeNodes = $doc->xpath('//lia-route');
return $routeNodes;
}
);
$compo->addCompileHandler('form',
function($form,$doc,$compiler,$node) use ($lia,$compo){
$view = $form->view();
$routes = $view->callHandler('getRoutes',$view);//getRoutes($view);
$formRoutes = $form->callHandler('getRoutes',$form);
$routes = array_merge($routes,$formRoutes);
$patterns = [];
foreach ($routes as $route){
if ($route->boolAttribute('clicktoedit')){
$pattern = $route->getAttribute('pattern');
// $edit = '/edit-'.substr($pattern,1);
$edit = $pattern.'edit/';
$submit = $pattern.'submit/';
$patterns[] = ['edit'=>$edit,'submit'=>$submit,'view'=>$pattern];
} else if ($route->boolAttribute('isform')){
$pattern = $route->getAttribute('pattern');
// $edit = '/edit-'.substr($pattern,1);
$submit = $pattern.'submit/';
$patterns[] = ['edit'=>$pattern,'submit'=>$submit, 'view'=>$route->redirect??'/#redirect-failure'];
}
}
foreach ($patterns as $pattern){
$editPattern = $pattern['edit'];
$submitPattern = $pattern['submit'];
$viewPattern = $pattern['view'] ?? null;
$escPattern = var_export($editPattern,true);
$escClass = var_export(get_class($compo),true);
$escViewName = var_export(substr($form->name,0,-strlen('Form')),true);
$escSubmitUrl = '"'.$submitPattern.'"';
$phpCode =
<<<PHP
<?php
\$pattern = {$escPattern};
\$this->callHandler('addPattern',\$this,\$pattern);
\$rtr = \$lia->compo('Router');
\$rtr->addCallbackRoute(\$pattern, \$package,
function(\$event,\$route,\$passthru) {
extract(\$passthru);
\$passthru['pattern'] = {$escPattern};
\$passthru['submit_url'] = {$escSubmitUrl};
\$passthru['id'] = \$_GET['id'] ?? 'new';
\$passthru['event'] = \$event;
\$passthru['lia'] = \$event->lia;
\$passthru['route'] = \$route;
// print_r(\$passthru['id']);
// exit;
foreach (\$route->extractables as \$key=>\$value){
\$passthru[\$key] = \$value;
}
// var_dump(\$passthru);
// exit;
\$view = \$this;
\$view->setPassthru(\$passthru);
echo \$view;
}
);
?>
PHP;
$form->appendSetupCode($phpCode);
$submitPattern = '@POST.'.$submitPattern;
$escPattern = var_export($submitPattern,true);
$escClass = var_export(get_class($compo),true);
// $escViewName = var_export(substr($form->name,0,-strlen('Form')),true);
$escViewName = var_export($form->name,true);
$escRedirectUrl = '"'.$viewPattern.'"';
$phpCode =
<<<PHP
<?php
\$pattern = {$escPattern};
\$this->callHandler('addPattern',\$this,\$pattern);
\$rtr = \$lia->compo('Router');
\$rtr->addCallbackRoute(\$pattern, \$package,
function(\$event,\$route,\$passthru) {
// var_dump(get_class(\$this));
\$compo = \$this->component;
\$passthru['pattern'] = {$escPattern};
\$passthru['redirect_url'] = {$escRedirectUrl};
\$passthru['event'] = \$event;
\$passthru['lia'] = \$event->lia;
\$passthru['route'] = \$route;
foreach (\$route->extractables as \$key=>\$value){
\$passthru[\$key] = \$value;
}
\$compo->submit({$escViewName}, \$passthru, \$_POST);
}
);
?>
PHP;
$form->appendSetupCode($phpCode);
}
}
);
$compo->compile($forceRecompile);
$compo->setup(['lia'=>$lia,'package'=>$this]);
}
public function callHandler($method,...$args){
return $this->freshCompo->handler->callMethod('runtime',$method,$args);
}
/**
* Set an access control to determine if individual items can be displayed.
* ```php
* $liaison->set('Fresh.Item.Display',
* function($item, $table, $id) use ($liaison){
* // do your custom code. Maybe look up a user. Maybe just return true for displaying.
* return false;
* }
* );
* ```
*
* @TODO Allow bool setting for Fresh.Item.Display, when a callable is not needed.
* @TODO MAYBE pass `$lia` along to the Fresh.Item.Display handler
*
* @export(Access.DisplayItem)
*/
protected function AccessItemDisplay($compo){
$freshItemDisplay = $this->lia->get('Fresh.Item.Display', null);
if ($freshItemDisplay != null
&&is_callable($freshItemDisplay)){
$compo->setRuntimeHandler('Fresh.Item.Display',$freshItemDisplay);
} else {
trigger_error("You should set Fresh.Item.Display function on `\$lia`. Default is to always display items.");
$compo->setRuntimeHandler('Fresh.Item.Display',function(){return true;});
}
$compo->addCompileHandler('View.EntityLoop.Start',
function(){
$phpCode =
<<<PHP
\$allow = \$this->callHandler('Fresh.Item.Display', \$rb_object, \$table, \$rb_object->id);
if (!\$allow)continue;
PHP;
return $phpCode;
}
);
$compo->addCompileHandler('Form.EntityLoop.Start',
function(){
/**
* @TODO Provide some kind of error reporting, instead of a simple `continue`
* @export(TODO.FormEditCreateCheck)
*/
$phpCode =
<<<PHP
if (\$rb_object->id==null
&&!\$this->callHandler('Fresh.Item.Create', \$table)){
continue;
} else if (\$rb_object->id!=null
&&!\$this->callHandler('Fresh.Item.Edit', \$rb_object, \$table, \$rb_object->id)){
continue;
}
PHP;
return $phpCode;
}
);
}
protected function AccessItemDelete($compo){
// check if item can be edited
// if yes set marker that there is an editable item
// later on, check that marker & show the edit prompt.
$freshItemEdit = $this->lia->get('Fresh.Item.Delete', null);
if ($freshItemEdit != null
&&is_callable($freshItemEdit)){
$compo->setRuntimeHandler('Fresh.Item.Delete',$freshItemEdit);
} else {
trigger_error("You should set Fresh.Item.Delete function on `\$lia`. Default is to never allow deletion.");
$compo->setRuntimeHandler('Fresh.Item.Delete',function(){return false;});
}
}
protected function AccessItemEdit($compo){
// check if item can be edited
// if yes set marker that there is an editable item
// later on, check that marker & show the edit prompt.
$freshItemEdit = $this->lia->get('Fresh.Item.Edit', null);
if ($freshItemEdit != null
&&is_callable($freshItemEdit)){
$compo->setRuntimeHandler('Fresh.Item.Edit',$freshItemEdit);
} else {
trigger_error("You should set Fresh.Item.Edit function on `\$lia`. Default is to never allow editing.");
$compo->setRuntimeHandler('Fresh.Item.Edit',function(){return false;});
}
}
protected function AccessItemCreate($compo){
// check if item can be edited
// if yes set marker that there is an editable item
// later on, check that marker & show the Create prompt.
$freshItemCreate = $this->lia->get('Fresh.Item.Create', null);
if ($freshItemCreate != null
&&is_callable($freshItemCreate)){
$compo->setRuntimeHandler('Fresh.Item.Create',$freshItemCreate);
} else {
trigger_error("You should set Fresh.Item.Create function on `\$lia`. Default is to never allow creating.");
$compo->setRuntimeHandler('Fresh.Item.Create',function(){return false;});
}
}
/**
* Add 'tlf-clicktoedit' to any entity node which you want to have editable.
*
* @export(Edit.Auto)
*/
public function setupClickToEdit($doc,$view,$compiler,$node){
$ph = $compiler->placeholderFor("<?php \$this->callHandler('enableClickToEdit',\$rb_object,\$table, \$rb_object->id, \$this); ?>");
$node->setAttributeNode(new \DOMAttr($ph));
$node->removeAttribute('tlf-clicktoedit');
// $node->setAttribute('tlf-autoform','edited');
// throw new \Exception("\n\nI need to set up the auto-form functionality. See ".__FILE__." on line 15\n\n");
}
/**
* Declare a 'Fresh.Item.Edit' callback.
* - Return true to allow, false to not allow
* - accept paramaters `$item`, `$tableName`, & `$itemId`
* - Will be called for every single item that is loaded via an entity node during a request.
* ```php
* $liaison->set('Fresh.Item.Edit',
* function($item, $tableName, $itemId){
* // or check that however you want...
* if (currently_logged_in_user()->hasAccessTo($item))return true;
* return false;
* }
* );
* ```
*
* @export(Edit.Access)
*/
protected function isItemEditAllowed($item, $table, $id){
$freshItemEdit = $this->lia->get('Fresh.Item.Edit', null);
if ($freshItemEdit != null
&&is_callable($freshItemEdit)){
$isAllowed = $freshItemEdit($item, $table, $id);
return ($isAllowed===true);
} else {
trigger_error("You should set Fresh.Item.Edit function on `\$lia`. Default is to never allow editing.");
// $compo->setRuntimeHandler('Fresh.Item.Display',function(){return false;});
return false;
}
// $lia->set('Fresh.ShowEditPrompt', true);
}
public function enableClickToEdit($item, $table, $id, $view){
$lia = $this->lia;
// check if edit allowed
$isEditAllowed = $this->isItemEditAllowed($item,$table,$id);
if (!$isEditAllowed)return;
$this->showEditContentButton = true;
// add autowire dependency and AutoformLoader script
$jsauto = $lia->view('depend/JSAutowire');
$lia->send('Res_AddScriptFile',__DIR__.'/../6-res/AutoformLoader.js');
$lia->send('Res_AddStyleFile',dirname(__DIR__).'/6-res/EditButton.css');
if (trim(strtolower($_GET['edit']??null))!=='true')return;
// add attribute to mark as a click-to-edit item
$form = $view->form();
$url = $lia->send('Fresh_GetUrl',$form);
echo ' tlf-clicktoedit-id="'.$id.'" tlf-clicktoedit-url="'.$url.'" ';
}
/**
* Call `$lia->send('Body_Extra_End_Print')` at the end of your `<body>` tag, so the 'Edit Content' button can be displayed.
*
* @export(Edit.HTML)
*/
public function onBody_Extra_End_Print($event){
if (!$this->showEditContentButton)return;
$view = file_get_contents(dirname(__DIR__).'/6-res/EditButton.html');
echo $view;
}
}