FormTools.php
<?php
// require_once(__DIR__.'/JSLikeHTMLElement.php');
class FormTools
{
protected $html = '';
protected $doc;
protected $event;
protected $imageDestination;
protected $imageUrlPrefix;
public function __construct($formHtml,$hideXmlErrors=true)
{
$this->html = $formHtml;
$this->doc = $this->docFromHTML($formHtml,$hideXmlErrors);
}
public function setImagePaths($destinationFolder,$urlPrefix){
$this->imageDestination = $destinationFolder;
$this->imageUrlPrefix = $urlPrefix;
}
public function setEvent($event){
$this->event = $event;
// var_dump($event);
// exit;
}
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];
}
protected function removeAutoSchemaProps($node){
$node->removeAttribute('lookup');
$node->removeAttribute('form');
$node->removeAttribute('view');
$node->removeAttribute('typeof');
}
public function SchemaPrepareDisplay($removeSchemaProps=true){
$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');
if ($lookup=='new'){
continue;
}
$ret = $this->resultsFromLookup($type,$lookup);
$results = $ret['results'];
$addColumns = $ret['addcolumns'];
$obj = null;
if (count($results)>1){
// echo "\n{$lookup}\n\n";
// print_r($results);
// exit;
throw new \Exception ("There were multiple matches. Contact the web developer.");
}
else $obj = array_shift($results);//[0];
$parentTag = $input->tagName;
$singleParentXpath = "//{$parentTag}[@{$hostAttribute}=\"{$type}\"][@lookup=\"{$lookup}\"]";
$xPath = new DOMXpath($this->doc);
$props = $xPath->query($singleParentXpath.'//*[@'.$childAttribute.']');
// echo $singleParentXpath.'//*[@'.$childAttribute.']';
if ($obj==null){
$obj = \RDB::dispense($type);
foreach ($props as $node){
$tag = strtolower($node->tagName);
$propName = $node->getAttribute($childAttribute);
if ($tag=='img'){
$obj->$propName = $node->getAttribute('src');
} else {
$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;
$this->setNodeValue($node,$newVal);
}
}
if ($removeSchemaProps){
$this->removeAutoSchemaProps($input);
}
}
return $inputs;
}
public function SchemaPrepareEdit($popupDialog){
$this->SchemaPrepareDisplay(FALSE);
$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));
$viewName = $node->getAttribute('view');
if ($viewName!=null){
$formName = $viewName .= 'Form';
if (!$node->hasAttribute('view'))$node->setAttribute('view',$viewName);
if (!$node->hasAttribute('form'))$node->setAttribute('form',$formName);
// var_dump($formName);
// exit;
}
}
$bodyXPath = new \DOMXpath($this->doc);
$bodies = $bodyXPath->query('//body');
$body = $bodies[0];
$body->innerHTML = $body->innerHTML . $popupDialog;
// return $inputs;
}
protected function docFromHTML($html,$hideXmlErrors=true){
libxml_use_internal_errors($hideXmlErrors);
$domDoc = new \DomDocument();
$domDoc->registerNodeClass('DOMElement', 'JSLikeHTMLElement');
$domDoc->loadHtml($html);
libxml_use_internal_errors(false);
return $domDoc;
}
// protected function docFromNode($node,$hideXmlErrors=true){
// $html = $this->nodeOuterHTML($node);
// $doc
// }
protected function findLookupFromForm($form){
$lookup = $form->getAttribute('lookup');
if ($lookup!='')return $lookup;
$viewName = $form->getAttribute('view');
$event = $this->event;
if ($event==null)return null;
$view = $event->View($viewName);
$doc = $this->docFromHTML($view);
$xPath = new \DOMXPath($doc);
$formName = $form->getAttribute('form');
$nodes = $xPath->query('//*[@form="'.$formName.'"]');
if ($nodes->count()>1){
throw new \Exception("Could not find lookup from form '{$formName}' because there were multiple matches in the view '{$viewName}'");
} else if ($nodes->count()==0){
throw new \Exception("Could not find lookup from form '{$formName}' because there were no matches in the view '{$viewName}'");
}
$node = $nodes->item(0);
$lookup = $node->getAttribute('lookup');
if ($lookup!='')return $lookup;
return null;
}
public function SchemaPrepareForm($formName){
$form = $this->getFormNode();
if ($form==null)return;
if ($formName!=null){
$viewName = substr($form,0,-strlen('Form'));//$viewName .= 'Form';
if (!$form->hasAttribute('view'))$form->setAttribute('view',$viewName);
if (!$form->hasAttribute('form'))$form->setAttribute('form',$formName);
// var_dump($formName);
// exit;
}
$this->insertHiddenIdInput($form);
$type = $form->getAttribute('name');
$initData = $form->getAttribute('data-init');
$lookup = $this->findLookupFromForm($form);
if ($lookup==null){
echo json_encode(['error'=>"Could not find a 'lookup' for the form."]);
exit;
}
$viewName = $form->getAttribute('view');
$formName = $form->getAttribute('form');
if ($lookup=='new'){
$obj = null;
} else {
$ret = $this->resultsFromLookup($type,$lookup);
$results = $ret['results'];
$obj = reset($results);
}
if ($obj==null){
// echo json_encode(['error'=>'null object']);
// exit;
}
$this->RDBPrepareEdit($type,$obj->id??null);
if ($viewName!=null){
$form->innerHTML = "\n".'<input type="hidden" name="view_name" value="'.$viewName.'"/>'."\n".$form->innerHTML;
}
if ($formName!=null){
$form->innerHTML = "\n".'<input type="hidden" name="form_name" value="'.$formName.'"/>'."\n".$form->innerHTML;
}
$form->setAttribute('method','POST');
$form->setAttribute('action','/submit_auto_form/');
if ($this->doesHaveFileInput($form)){
$form->setAttribute('enctype','multipart/form-data');
}
if ($type!=null){
$this->addHiddenInput($form,'autoschema_type',$type);
}
if ($lookup!=null){
$this->addHiddenInput($form,'autoschema_lookup',$lookup);
}
if ($lookup=='new'&&$initData!=null){
$data = $this->decodeDataString($initData);
foreach ($data as $key=>$value){
$this->addHiddenInput($form,$key,$value);
}
}
}
protected function decodeDataString($str){
$entries = explode(';',$str);
$data = [];
foreach ($entries as $e){
$parts = explode(':',$e);
$key = $parts[0];
if ($key==null)continue;
$value = $parts[1] ?? null;
// if ($value===null)continue;
// $qs[] = $key.' = :'.$key;
// $binds[':'.$key] = $value;
$data[$key] = $value;
}
// $where = implode(' AND ',$qs);
return $data;
}
protected function addHiddenInput($form,$name,$value){
$xPath = new \DOMXpath($this->doc);
$existing = $xPath->query('//input[@type="hidden"][@name="'.$name.'"]', $form);
if ($existing->count()>1){
throw new \Exception("There are multiple hidden inputs with the same name");
} else if ($existing->count()==0){
$form->innerHTML = "\n".'<input type="hidden" name="'.$name.'" value="'.$value.'"/>'."\n".$form->innerHTML;
} else {
$input = $existing->item(0);
$input->value = $value;
}
}
protected function doesHaveFileInput($form){
$x = new \DOMXpath($this->doc);
$inputs = $x->query('//input[@type="file"]');
foreach ($inputs as $input){
$parent = $input;
while ($parent==$parent->parentNode){
if ($parent==$form)return true;
}
}
return false;
}
protected function insertHiddenIdInput($form){
$xPath = new DOMXpath($this->doc);
$inputs = $xPath->query('//input[@name="id"][@type="hidden"]');
if (count($inputs)!=0)return;
$form->innerHTML = "\n".'<input type="hidden" name="id" />'."\n".$form->innerHTML;
}
protected function getFormNode() {
$xPath = new DOMXpath($this->doc);
$entities = $xPath->query('//form[@name]');
if (count($entities)==0)return null;
$form = $entities[0];
return $form;
}
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 nodeOuterHtml($node){
$outerHTML = $node->ownerDocument->saveXML($node);
// $children = $node->childNodes;
// foreach ($children as $child) {
// $outerHTML .= $child->ownerDocument->saveXML( $child );
// }
return $outerHTML;
}
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)
{
$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();
}
protected function setNodeValue($node,$newValue){
$tagName = strtolower($node->tagName);
$format = $node->getAttribute('format');
if ($tagName=='input'){
$this->setFormInputValue($node,$newValue);
return;
} else if ($tagName=='img'){
$node->src = $newValue;
return;
} else if ($format=='md'){
$converter = new \League\CommonMark\CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
$newValue = $converter->convertToHtml($newValue);
}
$node->innerHTML = $newValue;
}
public function setFormInputValue($node,$value){
$tagName = strtolower($node->tagName);
$type = strtolower($node->getAttribute('type'));
// echo "\n\n---------".$tagName.':'.$type."\n\n";
// exit;
if ($tagName=='textarea'){
$node->innerHTML = $value;
} else if ($tagName=='input'&&$type=='file'){
// echo "it's a file input";
$attr = $node->getAttribute('name');
$queryStr = "//img[@for='{$attr}']";
$xPath = new \DOMXPath($this->doc);
$holderList = $xPath->query($queryStr);
$fileHolder = $holderList[0];
$fileHolder->setAttribute('src',$value);
$attr = $fileHolder->getAttribute($attr);
$fileHolder->removeAttribute($attr);
$fileHolder->setAttribute('data-name',$attr);
// exit;
// echo 'fileholder';
// exit;
} else if ($tagName=='input'){
$node->setAttribute('value',$value);
}
return true;
}
public function RDBSubmit($postData, $formName = null)
{
$this->insertHiddenIdInput($this->getFormNode());
$fields = $this->getInputs();
$formName = $formName ?? $this->getFormName();
$filesToUpload = [];
foreach ($fields as $index => $data) {
$name = $data['name'];
$required = $data['required'];
$type = $data['type'];
$tag = $data['tag'];
$input = $data['input'];
if ($type=='file'){
$file = $_FILES[$name]?? null;
if ($file==null)$pass=false;
$filesToUpload[] = ['file'=>$_FILES[$name],
'inputData'=>$data
];
unset($_FILES[$name]);
if ($pass)continue;
} else {
$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.");
}
foreach ($filesToUpload as $index=>$data){
$file = $data['file'];
$inputData = $data['inputData'];
$fileName = static::uploadFile($file,$this->imageDestination);
if ($fileName===false)continue;
$urlPath = '/'.$this->imageUrlPrefix.'/'.$fileName;
$urlPath = str_replace(['///','//'],'/',$urlPath);
$saveData[$inputData['name']] = $urlPath;
}
$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;
}
}
}