FormToolsWithRDB.php

<?php

require(__DIR__.'/JSLikeHTMLElement.php');

class FormToolsWithRDB
{
    protected $html = '';
    protected $doc;

    public function __construct($formHtml,$hideXmlErrors=true)
    {
        $this->html = $formHtml;
        libxml_use_internal_errors($hideXmlErrors);
        $domDoc = new \DomDocument();
        $domDoc->registerNodeClass('DOMElement', 'JSLikeHTMLElement');
        $domDoc->loadHtml($formHtml);
        libxml_use_internal_errors(false);
        $this->doc = $domDoc;
    }

    public function resultsFromLookup($type,$lookup){
        $queries = explode(';',$lookup);
        $qs = [];
        $binds = [];
        $addColumns = [];
        foreach ($queries as $q){
            $parts = explode(':',$q);
            $key = $parts[0];
            $value = $parts[1] ?? null;
            if ($value===null)continue;
            $qs[] = $key.' = :'.$key;
            $binds[':'.$key] = $value;
            $addColumns[$key] = $value;
        }
        $where = implode(' AND ',$qs);
        // var_dump($where);
        // print_r($binds);
        // echo "\n\n";
        $results = \RDB::find($type,$where,$binds);
        return ['addcolumns'=>$addColumns,'results'=>$results];
    }
    public function SchemaPrepareDisplay(){
        $hostXPath='//*[@typeof]';
        $hostAttribute='typeof';
        $childAttribute='property';

        $xPath = new DOMXpath($this->doc);
        $entities = $xPath->query($hostXPath);
        $inputs = [];
        foreach ($entities as $input) {
            $type = $input->getAttribute($hostAttribute);
            $lookup = $input->getAttribute('lookup');
            $ret = $this->resultsFromLookup($type,$lookup);
            $results = $ret['results'];
            $addColumns = $ret['addcolumns'];
            $obj = null;
            if (count($results)>1) throw new \Exception ("There were multiple matches. Contact the web developer.");
            else $obj = array_shift($results);//[0];
            // var_dump($obj);
            // exit;
            $xPath = new DOMXpath($this->doc);
            $props = $xPath->query($hostXPath.'//*[@'.$childAttribute.']');
            if ($obj==null){
                $obj = \RDB::dispense($type);
                foreach ($props as $node){
                    // var_dump($node);
                    $propName = $node->getAttribute($childAttribute);
                    $obj->$propName = $this->nodeInnerHTML($node);
                }
                foreach ($addColumns as $key=>$value){
                    $obj->$key=$value;
                }
                \RDB::store($obj);
            } else {
                // var_dump($obj);
                foreach ($props as $node){
                    $prop = $node->getAttribute($childAttribute);
                    $newVal = $obj->$prop;
                    $node->innerHTML = $newVal;
                }
            }
        }
        return $inputs;
    }
    public function SchemaPrepareEdit($popupDialog){
        $this->SchemaPrepareDisplay();
        $xPath = new DOMXpath($this->doc);
        $entities = $xPath->query('//*[@typeof]');
        $inputs = [];
        foreach ($entities as $node) {
            $class = $node->getAttribute('class');
            $list = explode(' ',$class);
            $list[] = 'SchemaEditable';
            $node->setAttribute('class',implode(' ',$list));
        }
        $bodyXPath = new \DOMXpath($this->doc);
        $bodies = $bodyXPath->query('//body');
        $body = $bodies[0];
        $body->innerHTML = $body->innerHTML . $popupDialog;
        // return $inputs;
    }

    public function SchemaPrepareForm(){
        $xPath = new DOMXpath($this->doc);
        $entities = $xPath->query('//form[@name]');
        $form = $entities[0];
        $type = $form->getAttribute('name');
        $lookup = $form->getAttribute('lookup');
        $view = $form->getAttribute('view');
        $ret = $this->resultsFromLookup($type,$lookup);
        $results = $ret['results'];
        $obj = reset($results);
        // var_dump($obj);
        // exit;
        $this->RDBPrepareEdit($type,$obj->id??null);

        // $form->innerHTML = "\n".'<input type="hidden" name="id" value="'.$obj->id.'"/>'."\n".$form->innerHTML;
        // $form->innerHTML = "\n".'<input type="hidden" name="_type" value="'.$type.'"/>'."\n".$form->innerHTML;
        $form->innerHTML = "\n".'<input type="hidden" name="object_view" value="'.$view.'"/>'."\n".$form->innerHTML;
        $form->setAttribute('method','POST');
        $form->setAttribute('action','/submit_auto_form/');
        // $xPath = new DOMXpath($this->doc);
        // $entities = $xPath->query('//form[@name]');
        // $inputs = [];
        // foreach ($entities as $form) {
            // $name = $form->getAttribute('name');
            // $lookup = $form->getAttribute('lookup');
            // $form->setAttribute('data-lookup',$name.'      '.$lookup);
        // }

    }

    public function getValidSubmissions($userSubmissions,$ignore=[]){
        $inputs = $this->getInputs();

        $validSubmissions = [];
        foreach ($ignore as $index=>$ignoreKey){
            unset($userSubmissions[$ignoreKey]);
        }
        foreach ($inputs as $index=>$inputData){
            extract($inputData);
            $pass = $this->verify($inputData,$userSubmissions[$name]??null);
            if (!$pass){
                throw new \Exception("Input '{$name}' failed verification");
            }
            $validSubmissions[$name] = $userSubmissions[$name];
            unset($userSubmissions[$name]);
        }
        if (count($userSubmissions)>0){
            throw new \Exception("More data was sent to the server than was requested.");
        }
        return $validSubmissions;
    }

    protected function verify($inputData, $value)
    {
        //see $this->getInputs() to see what is extracted
        extract($inputData);
        if (empty($value) && $required) {
            return false;
        }

        if ($type == 'phone') {
            $clean = preg_replace('/[^0-9]/', '', $value);
            if (strlen($clean) == 10
                || strlen($clean) == 11) {
                return true;
            }
            return false;
        }
        if ($type == 'text') {
            return true;
        }
        if ($type == 'email') {
            $email = filter_var($value, FILTER_VALIDATE_EMAIL);
            if ($email === false) {
                return false;
            }

            return true;
        }

        return true;
    }

    public function getInputs()
    {
        $xPath = new DOMXpath($this->doc);
        $htmlInputs = $xPath->query('//*[@name]');
        $inputs = [];
        foreach ($htmlInputs as $input) {
            if ($input->tagName == 'form') {
                continue;
            }

            $data = [];
            $data['name'] = $input->getAttribute('name');
            $data['required'] = $input->hasAttribute('required');
            $data['type'] = $input->getAttribute('type');
            $data['tag'] = $input->tagName;
            $data['input'] = $input;
            if (isset($inputs[$data['name']])) {
                throw new \Exception("Input '" . $data['name'] . "' occurs twice on the form, which FormTools doesn't know how to handle.");
            }
            $inputs[$data['name']] = $data;
            continue;
        }
        return $inputs;
    }
    public function getFormName()
    {
        $forms = $this->doc->getElementsByTagName('form');
        if ($forms->count() > 1) {
            throw new \Exception("There are two forms... Form name cannot be retrieved.");
        } else if ($forms->count() == 0) {
            throw new \Exception("There are no forms. Cannot get form name.");
        }

        $name = $forms->item(0)->getAttribute('name');
        if ($name == null) {
            $name = null;
        }

        return $name;
    }

    public function insertPlaceholders($placeholders)
    {
        $xPath = new DOMXpath($this->doc);
        $htmlInputs = $xPath->query('//*[@name]');

        $formName = $this->getFormName();
        foreach ($htmlInputs as $input) {
            if ($input->tagName == 'form') {
                // $input->getAttribute('name');
                continue;
            }
            $name = $input->getAttribute('name');
            $ph = $placeholders[$name] ?? null;
            // var_dump($name,$ph);
            if ($ph === null) {
                continue;
            }

            $input->setAttribute('placeholder', $ph);
        }
    }

    public function nodeInnerHtml($node){
        $innerHTML = '';
        $children = $node->childNodes;
        foreach ($children as $child) {
            $innerHTML .= $child->ownerDocument->saveXML( $child );
        } 
        return $innerHTML;
    }
    public function innerBodyHTML(){
        // this was in my old FormsPlus code...
        $output = $this->doc->saveHtml($this->doc->childNodes[1]->childNodes[0]);
        $output = substr($output,strlen('<body>'),-strlen('</body>'));
        return $output;
    }
    public function __toString()
    {
        return $this->doc->saveHtml();
    }
    public function RDBPrepareEdit($tableName = null, $id = null)
    {
        // Saving this from my old FormsPlus... sets the source for an img element that relates to an input[type=file]
        // if ($tagName=='input'&&strtolower($input->getAttribute('type'))=='file'){
        //     $queryStr = "//*[@data-inputname='{$attr}']";
        //     // echo "\n{$queryStr}\n\n";
        //     $holderList = $xPath->query($queryStr);
        //     // print_r($imgHolder);
        //     $fileHolder = $holderList[0];
        //     $fileHolder->setAttribute('src',$this->src->$attr);
        //     // exit;
        // }
        $tableName = $tableName===null?null: preg_replace('/[^a-z]/','',strtolower($tableName));


        $domDoc = $this->doc;
        $xPath = new DOMXpath($domDoc);
        $htmlInputs = $xPath->query('//*[@name]');
        $formName = $tableName ?? $this->getFormName();
        $bean = \RDB::findOne($formName, 'id = ?', [$id ?? $_GET['id'] ?? null]);
        if ($bean == null) {
            $bean = \RDB::dispense($formName);
        }

        foreach ($htmlInputs as $input) {
            if ($input->tagName == 'form') {
                continue;
            }
            $name = $input->getAttribute('name');
            $value = $bean->$name ?? null;
            if ($value !== null) {
                // this MIGHT need to be textContent for a <textarea>
                $this->setFormInputValue($input,$value);
            }

        }
        // return $domDoc->saveHTML();
    }
    public function setFormInputValue($node,$value){
        $tagName = strtolower($node->tagName);
        $type = strtolower($node->getAttribute('type'));
        if ($tagName=='textarea'){
            $node->innerHTML = $value;
        } else if ($tagName=='input'){
            $node->setAttribute('value',$value);
        }
        return true;
    }
    public function RDBSubmit($postData, $formName = null)
    {
        $fields = $this->getInputs();
        $formName = $formName ?? $this->getFormName();

        foreach ($fields as $index => $data) {
            extract($data);
            $pass = $this->verify($data, $postData[$name] ?? null);
            if (!$pass) {
                throw new \Exception("Input '{$name}' failed verification");
            }
            $saveData[$name] = $postData[$name];
            unset($postData[$name]);
        }
        if (count($postData) > 0) {
            throw new \Exception("More data was sent to the server than was requested.");
        }
        $bean = \RDB::dispense($formName);
        $bean->import($saveData);
        \RDB::store($bean);
    }

    public static function uploadFile($file, $destinationFolder, $validExts = ['jpg', 'png'], $maxMB = 15)
    {
        if (!is_array($file) || $file == []
            || $file['size'] == 0
            || $file['name'] == ''
            || $file['tmp_name'] == ''
            || !is_int($file['error'])) {
            return false;
        }

        try {
            if (!isset($file['error']) ||
                is_array($file['error'])
            ) {
                throw new RuntimeException('Invalid parameters.');
            }

            switch ($file['error']) {
                case UPLOAD_ERR_OK:
                    break;
                case UPLOAD_ERR_NO_FILE:
                    throw new RuntimeException('No file sent.');
                case UPLOAD_ERR_INI_SIZE:
                case UPLOAD_ERR_FORM_SIZE:
                    throw new RuntimeException('Exceeded filesize limit.');
                default:
                    throw new RuntimeException('Unknown errors.');
            }

            // You should also check filesize here.
            if ($file['size'] > ($maxMB * 1024 * 1024)) {
                throw new RuntimeException('Exceeded filesize limit.');
            }

            $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
            if (!in_array($ext, $validExts)) {
                // var_dump($ext);
                // var_dump($validExts);
                // var_dump($file);
                // exit;
                throw new RuntimeException('Invalid file format.');
            }

            if (!file_exists($destinationFolder)) {
                mkdir($destinationFolder, 0775, true);
            }

            $fileName = sha1_file($file['tmp_name']) . '.' . $ext;
            if (!move_uploaded_file(
                $file['tmp_name'],
                $destinationFolder . '/' . $fileName
            )
            ) {
                throw new RuntimeException('Failed to move uploaded file.');
            }

            return $fileName;

        } catch (RuntimeException $e) {

            throw $e;

        }
    }

}