/** * Errors * @return \Touchbase\Data\Store */ public function errors($key = null) { $errors = SessionStore::get("touchbase.key.session.errors", new Store()); if (isset($key)) { return $errors->get($key); } return $errors; }
/** * Route History * @return string */ public static function routeHistory() { return SessionStore::get(self::ROUTE_HISTORY_KEY, []); }
/** * Reflash * NB. Currently not implemented * @return VOID */ public static function reflash() { $aged = SessionStore::get(self::FLASH_KEY . ".aged", []); $values = array_unique(array_merge(SessionStore::get(self::FLASH_KEY, []), $aged)); SessionStore::set(self::FLASH_KEY, $values); }
/** * Validate Post Request * This method adds default validation to input fields based on the served HTML. * If any errors are found, the redirect will be made on the response object with the errors. * @param \Touchbase\Control\HTTPRequest $request * @param \Touchbase\Control\HTTPResponse &$response * @param \Touchbase\Utils\Validation &$validation * @return VOID */ protected function validatePostRequest($request, &$response, &$validation) { if (!$request->isMainRequest()) { return; } $formNameToken = $request->_VAR("tb_form_token"); $formName = substr($formNameToken, 0, strrpos($formNameToken, "_")); if (isset($formNameToken) && ($form = SessionStore::get($formNameToken, false))) { SessionStore::consume($formName, $formNameToken); $data = $request->_VARS(); libxml_use_internal_errors(true); $dom = new \DOMDocument(); $dom->loadHtml(gzinflate(base64_decode($form)), LIBXML_NOWARNING | LIBXML_NOERROR); if ($dom->documentElement->getAttribute("novalidate")) { return; } $formValidation = Validation::create($formName); $privateFields = []; foreach (["input", "textarea", "select"] as $tag) { foreach ($dom->getElementsByTagName($tag) as $input) { if ($input->hasAttributes() && ($inputName = $input->getAttribute("name"))) { $inputType = $input->getAttribute("type"); $inputValidation = Validation::create($inputName); if ($inputType === "password") { $privateFields[] = $inputName; } $inputValidation->type($inputType); if ($inputType === "file" && $input->hasAttribute("accept")) { $validTypes = explode(",", $input->getAttribute("accept")); //File Extentions $fileExt = array_filter($validTypes, function ($value) { return strpos($value, ".") === 0; }); if (!empty($fileExt)) { $inputValidation->addRule(function ($value) use($fileExt) { $name = $value['name']; if (is_array($name)) { foreach ($name as $nme) { if (!in_array(pathinfo($nme, PATHINFO_EXTENSION), $fileExt)) { return false; } } return true; } return in_array(pathinfo($name, PATHINFO_EXTENSION), $fileExt); }, "A file uploaded did not have the correct extension"); } //Mime Types $validTypes = array_diff($validTypes, $fileExt); $implicitFileMime = array_filter($validTypes, function ($value) { return strpos($value, "/*") !== false; }); if (!empty($implicitFileMime)) { $inputValidation->addRule(function ($value) use($implicitFileMime) { $tmpName = $value['tmp_name']; if (is_array($tmpName)) { foreach ($tmpName as $tmp) { $mime = strstr(File::create($tmp)->mime(), "/", true) . "/*"; if (!in_array($mime, $implicitFileMime)) { return false; } } return true; } $mime = strstr(File::create($tmpName)->mime(), "/", true) . "/*"; return in_array($mime, $implicitFileMime); }, "A file uploaded did not have the correct mime type"); } $validTypes = array_diff($validTypes, $implicitFileMime); $fileMime = array_filter($validTypes, function ($value) { return strpos($value, "/") !== false; }); if (!empty($fileMime)) { $inputValidation->addRule(function ($value) use($fileMime) { $tmpName = $value['tmp_name']; if (is_array($tmpName)) { foreach ($tmpName as $tmp) { if (!in_array(File::create($tmp)->mime(), $fileMime)) { return false; } } return true; } return in_array(File::create($tmpName)->mime(), $fileMime); }, "A file uploaded did not have the correct mime type"); } } if ($input->hasAttribute("required")) { $errorMessage = null; if ($placeholder = $input->getAttribute("placeholder")) { $errorMessage = sprintf("Please complete the `%s` field", $placeholder); } $inputValidation->required($errorMessage); } if ($input->hasAttribute("readonly")) { $inputValidation->readonly($input->getAttribute("value")); } if ($input->hasAttribute("disabled")) { $inputValidation->disabled(); } if ($minLength = $input->getAttribute("minlength")) { $inputValidation->minLength($minLength); } if ($maxLength = $input->getAttribute("maxlength")) { $inputValidation->maxLength($maxLength); } if (in_array($inputType, ["number", "range", "date", "datetime", "datetime-local", "month", "time", "week"])) { if ($min = $input->getAttribute("min")) { $inputValidation->min($min, $inputType); } if ($max = $input->getAttribute("max")) { $inputValidation->max($max, $inputType); } } if ($pattern = $input->getAttribute("pattern")) { $inputValidation->pattern($pattern); } if (count($inputValidation)) { $validation->addRule($formValidation->addRule($inputValidation)); } } } } if (!$validation->validate($data)) { $response->withData(array_diff_key($data, array_flip($privateFields))); $response->redirect(-1)->withErrors($validation->errors, $formName); } } else { $response->redirect(-1)->withErrors(["Session timed out, please try again."], $formName); } }
/** * With Data * Attach data to the response * @param array $data * @return \Touchbase\Control\HTTPResponse */ public function withData(array $data) { SessionStore::flash($key = "touchbase.key.session.post", SessionStore::get($key)->set($data)); return $this; }
/** * Validate Html * This method scans the outgoing HTML for any forms, if found it will save the form to the session in order to validate. * If any errors previously existed in the session, this method will apply `bootstrap` style css error classes. * @pararm string &$htmlDocument - The outgoing HTML string * @return VOID */ private function validateHtml(&$htmlDocument) { if (!empty($htmlDocument)) { $dom = new DOMDocument(); $dom->loadHtml($htmlDocument, LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_NOENT | LIBXML_HTML_NOIMPLIED); //Automatically apply an active class to links that relate to the current URL foreach ($dom->getElementsByTagName('a') as $link) { if (Router::isSiteUrl($href = Router::relativeURL($link->getAttribute("href")))) { $currentClasses = explode(" ", $link->getAttribute("class")); if (strcasecmp($href, $this->controller->request()->url()) == 0) { $currentClasses[] = 'active'; } if (strpos($this->controller->request()->url(), $href) === 0) { $currentClasses[] = 'child-active'; } $link->setAttribute('class', implode(" ", $currentClasses)); } } //Save the outgoing form elements for future validation. foreach ($dom->getElementsByTagName('form') as $form) { $savedom = new \DOMDocument(); if ($formAction = $form->getAttribute("action") && !Router::isSiteUrl($formAction)) { continue; } //Add CSRF $rand = function_exists('random_bytes') ? random_bytes(32) : null; $rand = !$rand && function_exists('mcrypt_create_iv') ? mcrypt_create_iv(32, MCRYPT_DEV_URANDOM) : $rand; $rand = $rand ? $rand : openssl_random_pseudo_bytes(32); $csrfToken = bin2hex($rand); $formName = $form->getAttribute("name"); $formNameToken = $formName . "_" . $csrfToken; $csrf = $dom->createDocumentFragment(); $csrf->appendXML(HTML::input()->attr("type", "hidden")->attr("name", "tb_form_token")->attr("value", $formNameToken)->attr("readonly", true)); $form->insertBefore($csrf, $form->firstChild); //TODO: Should we assume the form will allways have content foreach (["input", "textarea", "select"] as $tag) { foreach ($form->getElementsByTagName($tag) as $input) { $savedom->appendChild($savedom->importNode($input->cloneNode())); //Populate form with previous data if (($newValue = SessionStore::get("touchbase.key.session.post")->get($input->getAttribute("name"), false)) !== false) { if (is_scalar($newValue) && $input->getAttribute("type") !== "hidden" && !$input->hasAttribute("readonly")) { $input->setAttribute('value', $newValue); } } //Populate errors if ($errorMessage = $this->controller->errors($formName)->get($input->getAttribute("name"), false)) { $currentClasses = explode(" ", $input->parentNode->getAttribute("class")); foreach (["has-feedback", "has-error"] as $class) { if (!in_array($class, $currentClasses)) { $currentClasses[] = $class; } } $input->parentNode->setAttribute('class', implode(" ", $currentClasses)); $input->setAttribute("data-error", $errorMessage); } } } SessionStore::recycle($formName, $formNameToken, base64_encode(gzdeflate($savedom->saveHTML(), 9))); } //Move body scripts to bottom $bodies = $dom->getElementsByTagName('body'); $body = $bodies->item(0); if ($body) { foreach ($body->getElementsByTagName('script') as $script) { if ($script->parentNode->nodeName === "body") { break; } $body->appendChild($dom->importNode($script)); } } //Look for the special attribute that moves nodes. //This is useful for moving modals from the template files to the bottom output. $xpath = new \DOMXPath($dom); $appendToBodyRef = NULL; foreach ($xpath->query("//*[@tb-append]") as $element) { $appendTo = $xpath->query($element->getAttribute("tb-append"))->item(0); $element->removeAttribute("tb-append"); if ($appendTo) { if ($appendTo->nodeName === "body") { //Special case to append above the included javascript files. if (!$appendToBodyRef) { $appendToBodyRef = $xpath->query('/html/body/comment()[. = " END CONTENT "][1]')->item(0); } $body->insertBefore($dom->importNode($element), $appendToBodyRef); } else { $appendTo->appendChild($dom->importNode($element)); } } } //Save the HTML with the updates. if ($this->controller->request()->isAjax() || !$this->controller->request()->isMainRequest()) { //This will remove the doctype that's automatically appended. $htmlDocument = $dom->saveHTML($dom->documentElement); } else { $htmlDocument = $dom->saveHTML(); } } }