Package.php

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