The TryingToScale PHP framework.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
tts_framework/src/bootstrap/safer_io.php

566 lines
19 KiB

<?php
declare(strict_types=1);
/* Sanitize Input, Validate data and Escape output.
* 1) In web development to sanitize means that you remove unsafe
* characters from the input. Makes safer DB inserts/selects, etc...!
* 2) Validation is not sanitization, this step does not remove any bad data,
* validation confirms that the info that is coming to your application meets
* the criteria you want.
* 3) Escape output - to get free from something, or to avoid something. Pay
* attention to not escape the data more than once, you must escape only when
* you received it or when you need to output it. I recommend it on all output only.
*
* Don’t try to sanitize input. Escape output. Perhaps more importantly,
* it gives a false sense of security. What does “unsafe” mean? In what context?
* Sure, <>& are unsafe characters for HTML, but what about CSS, JSON, SQL, or
* even shell scripts? Those have a completely different set of unsafe characters.
*
* Every so often developers talk about “sanitizing user input” to prevent
* cross-site scripting attacks. This is well-intentioned, but leads to a
* false sense of security, and sometimes mangles perfectly good input.
*/
namespace bs_tts;
enum HTML_FLAG {
case raw; // Dangerious XSS attacks...
case strip;
case encode;
case purify; // Allow safe whitelisted HTML elements/tags
case escape; // safely Escape HTML
}
enum INPUTS: int {
case variable = 998; // User Defined VAR
case debugging = 999; // check POST and then if debugging is set, check GET
case json = 1000; // uses JSON on raw POST BODY
case post = 0; // INPUT_POST;
case get = 1; // INPUT_GET;
case cookie = 2; //INPUT_COOKIE;
case env = 4; // INPUT_ENV;
case server = 5; // INPUT_SERVER;
public function resolve(): int {
return match($this) {
self::post => INPUT_POST,
self::get => INPUT_GET,
self::cookie => INPUT_COOKIE,
self::env => INPUT_ENV,
self::server => INPUT_SERVER,
};
}
}
enum DB_FILTER {
case ON; // Tries to Filter out SQL from User Input
case OFF; // Normal pass thourgh...
}
enum FIELD_FILTER: string {
case raw_string = "string";
case array_of_strings = "strings";
case email = "email-address";
case url = "site-url";
case raw = "unfiltered-non-sanitized";
case integer_number = "integer";
case array_of_ints = "integers";
case floating_point = "float";
case array_of_floats = "floats";
public function resolve() {
return match($this) {
self::raw_string => FILTER_UNSAFE_RAW,
self::array_of_strings => [
'filter' => FILTER_UNSAFE_RAW,
'flags' => FILTER_REQUIRE_ARRAY
],
self::email => FILTER_SANITIZE_EMAIL,
self::url => FILTER_SANITIZE_URL,
self::raw => FILTER_DEFAULT, // Unfiltered, non-sanitized!!!
self::integer_number => [
'filter' => FILTER_SANITIZE_NUMBER_INT,
'flags' => FILTER_REQUIRE_SCALAR
],
self::array_of_ints => [
'filter' => FILTER_SANITIZE_NUMBER_INT,
'flags' => FILTER_REQUIRE_ARRAY
],
self::floating_point => [
'filter' => FILTER_SANITIZE_NUMBER_FLOAT,
'flags' => FILTER_FLAG_ALLOW_FRACTION
],
self::array_of_floats => [
'filter' => FILTER_SANITIZE_NUMBER_FLOAT,
'flags' => FILTER_REQUIRE_ARRAY
],
};
}
}
final class use_io {
public $input_var;
public $input_type;
public $field_filter;
public $escape_html;
public $validation_rule;
public $validation_message;
public $skip_the_db;
public $use_db_filter;
}
final class use_iol {
public static function auto_wire(
string $root_folder,
string $file,
string $method = 'index',
string $db_service= 'db_mocker'
) {
$project = rtrim(\bs_tts\site_helper::get_project(), '/');
\main_tts\registry::set('db', \main_tts\registry::get('di')->get_service($db_service) );
$class_name = "\\prj\\{$project}\\inputs\\{$root_folder}\\{$file}_in";
$input = $class_name::$method();
$class_name = "\\prj\\{$project}\\logic\\{$root_folder}\\{$file}_logic";
$class_name::$method($input);
$class_name = "\\prj\\{$project}\\outputs\\{$root_folder}\\{$file}_out";
return $class_name::$method($input);
}
}
final class safer_io {
protected function __construct() {
}
public static function convert_to_utf8(string $in_str): string {
if (! extension_loaded('mbstring')) {
return $in_str;
}
$cur_encoding = mb_detect_encoding($in_str);
if($cur_encoding == "UTF-8" && mb_check_encoding($in_str,"UTF-8")) {
return $in_str;
} else {
return mb_convert_encoding($in_str, 'UTF-8', $cur_encoding);
}
}
// Escape HTML output
public static function h(string $string): string {
$utf8 = self::convert_to_utf8($string);
return htmlspecialchars($utf8, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
// Reverse encode of HTML
public static function html_decode(string $string): string {
return htmlspecialchars_decode($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5);
}
// HTML Purify library
public static function p(string $string): string {
$purifer = \main_tts\registry::get('di')->get_service('html_filter');
if (!$purifer->has_loaded()) {
$purifer->set_defaults();
}
return $purifer->purify($string);
}
// Escape JavaScript output
public static function j($input, int $levels_deep = 512): mixed {
try {
return json_encode($input, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, $levels_deep);
} catch (\JsonException $ex) {
return $ex;
}
}
public static function json_decode(string $string, bool $return_as_an_array = true, int $levels_deep = 512): mixed {
try {
return json_decode($string, $return_as_an_array, $levels_deep, JSON_THROW_ON_ERROR);
} catch (\JsonException $ex) {
return $ex;
}
}
public static function has_json_error($object): bool {
return ($object instanceof \JsonException);
}
// Escape URL output
public static function u(string $string): string {
return urlencode($string);
}
/*
* Encode HTML kindof... The problem with htmlentities() is that it is not
* very powerful, in fact, it does not escape single quotes, cannot detect
* the character set and does not validate HTML as well.
*/
public static function e(string $string): string {
$utf8 = self::convert_to_utf8($string);
return htmlentities($utf8, ENT_QUOTES, 'UTF-8');
}
public static function de(string $data): string {
return html_entity_decode($data);
}
/*
* Note: Generally, "strip_tags" is just the wrong function.
* Never use it. And if you do, absolutely never use the second parameter,
* because sooner or later someone will abuse it.
*/
public static function get_clean_server_var(string $var): mixed {
return filter_input(INPUT_SERVER, $var, FILTER_UNSAFE_RAW);
}
public static function get_bool($in): bool {
return (filter_var($in, FILTER_VALIDATE_BOOLEAN));
}
/**
* Purpose: To decode JQuery encoded objects, arrays, strings, int, bool types.
* The content must be of application/json.
* Returns the JSON encoded POST data, if any....
* @param type $return_as_array (true) -> Array, (false) -> Object
* @retval type Object/Array|null|false
* Note: It will return null if not valid json. false is not application/json
*/
public static function get_json_post_data(bool $return_as_array = true, int $levels_deep = 512): mixed {
$content_type = self::get_clean_server_var('CONTENT_TYPE');
if ($content_type === null) {
return false;
}
if (str_contains($content_type, "application/json")) {
$post_body = trim(file_get_contents("php://input")); // get raw POST data.
$ret_json = self::json_decode($post_body, $return_as_array, $levels_deep);
if (self::has_json_error($ret_json)) {
return false;
}
return $ret_json;
} else {
return false;
}
}
public static function safer_html(string $input, HTML_FLAG $safety_level = HTML_FLAG::escape): string {
switch ($safety_level) {
case HTML_FLAG::raw :
throw new \Exception('Raw HTML not supported!');
case HTML_FLAG::strip :
return strip_tags($input);
case HTML_FLAG::encode :
return self::e($input);
case HTML_FLAG::purify :
return self::p($input);
case HTML_FLAG::escape :
default:
return self::h($input);
}
}
private static function t($item, bool $do_trim = true) {
if ($do_trim) {
if (is_string($item)) {
return trim($item);
}
if (\bs_tts\common::get_count($data)) {
$ret = [];
foreach($data as $text) {
if (is_bool($text) || is_int($text)) {
$ret[] = $text;
continue;
}
if (! is_string($text)) {
continue; // Deny Arrays and Objects here!
}
$ret[] = trim($text);
}
return $ret;
}
}
return $item;
}
static $JSON_POST_DATA = [];
private static function get_input_by_type(
string $input_field_name,
INPUTS $input_type,
): mixed {
if ($input_type == INPUTS::debugging) {
if (isset(self::$JSON_POST_DATA[$input_field_name])) {
return self::$JSON_POST_DATA[$input_field_name];
}
$is_set = filter_has_var(INPUT_POST, $input_field_name);
if ($is_set) {
return filter_input(INPUT_POST, $input_field_name);
}
if (!filter_has_var(INPUT_GET, "debugging")) {
return null;
}
$is_get_set = filter_has_var(INPUT_GET, $input_field_name);
if ($is_get_set) {
return filter_input(INPUT_GET, $input_field_name);
}
return null;
}
if ($input_type === INPUTS::json) {
return (isset(self::$JSON_POST_DATA[$input_field_name])) ? self::$JSON_POST_DATA[$input_field_name] : null;
}
$resolve_input = $input_type->resolve();
$is_set = filter_has_var($resolve_input, $input_field_name);
if ($is_set) {
return filter_input($resolve_input, $input_field_name);
}
return null;
}
/**
*
* @param string $data
* @param array $a['html'] of type HTML_FLAG
* @return string|bool
*/
private static function get_safer_string(string $data, use_io $a): string | bool {
if (isset($a->escape_html) && $a->escape_html instanceof \UnitEnum) {
return self::safer_html($data, $a->escape_html);
}
return self::safer_html($data);
}
private static function get_safer_html($data, use_io $a) {
if (is_string($data)) {
return self::get_safer_string($data, $a);
} else if (\bs_tts\common::get_count($data)) {
$ret = [];
foreach($data as $text) {
if (is_bool($text) || is_int($text)) {
$ret[] = $text;
continue;
}
if (! is_string($text)) {
continue; // Deny Arrays and Objects here!
}
$ret[] = self::get_safer_string($text, $a);
}
return $ret;
}
return $data;
}
/**
* Initialize JSON post data into static array, if used....
* @param int $levels_deep are JSON Levels to use
*/
public static function init_json(int $levels_deep = 512): void {
self::$JSON_POST_DATA = self::get_json_post_data(true, $levels_deep);
}
public static function required_fields_were_NOT_all_submitted(array $data): bool {
$field = $data['name'] ?? false;
$empty = $data['meta'][$field]['empty'] ?? true;
$required = $data['meta'][$field]['validation_rules_set'] ?? false;
return ($empty && $required);
}
private static function sanitize_helper(
string $from,
string $input_field_name,
use_io $a,
FIELD_FILTER $default_filter = FIELD_FILTER::raw_string,
bool $trim = true,
) : array {
$meta = [];
$meta['missing'] = [];
$safer_data = "";
$rules = [];
$messages = [];
if (isset($a->field_filter) && $a->field_filter instanceof \UnitEnum) {
$field_type = $a->field_filter;
} else {
$field_type = $default_filter;
}
if (isset($a->input_var)) {
$user_text = $a->input_var;
} elseif (isset($a->input_type) && $a->input_type instanceof \UnitEnum) {
$user_text = self::get_input_by_type($input_field_name, $a->input_type);
} else {
$ret['name'] = $input_field_name;
$ret['meta']['missing'][] = $input_field_name;
$ret['errors'][$input_field_name] = "Missing Field $input_field_name";
$ret['html'] = null;
$ret['db'] = false;
$ret['logic'] = false;
return $ret;
}
$safer_data = false; // needs to be false to fail the validator
$safer_html_data = null; // should be null for ?? operator to work with it....
if (isset($a->validation_rule)) {
$rules[$input_field_name] = $a->validation_rule;
}
if (isset($a->validation_message) && isset($a->validation_rule)) {
$messages[$input_field_name] = $a->validation_message;
}
$meta[$input_field_name]['validation_rules_set'] = (count($rules)) ? true : false;
$db = (isset($a->skip_the_db)) ? $a->skip_the_db : false;
$meta[$input_field_name]['type'] = $field_type->name;
$meta[$input_field_name]['skip_db'] = $db;
if ($user_text === null) {
$safer_data = null;
$safer_db_data = null;
$safer_html_data = null;
$meta[$input_field_name]['empty'] = true;
} else {
$field_filter_resolved = $field_type->resolve();
$meta[$input_field_name]['empty'] = false;
$safer_data = $user_text;
if ($field_type == FIELD_FILTER::email) {
$safer_data = substr($safer_data, 0, 254);
}
$safer_data = filter_var($safer_data, FILTER_DEFAULT, $field_filter_resolved);
// FallBack: These field types should never allow arrays anyways
if ($field_type == FIELD_FILTER::raw_string ||
$field_type == FIELD_FILTER::raw
) {
if (\bs_tts\common::get_count($safer_data)) {
$safer_data = $safer_data[0];
}
}
if ($from === "html") {
$safer_html = self::get_safer_html($safer_data, $a);
if ($safer_html !== false) {
$safer_html_data = $safer_html;
}
if (isset($safer_html_data)) {
$safer_html_data = self::t($safer_html_data, $trim);
}
} else {
$safer_data = self::t($safer_data, $trim);
}
if ($field_type == FIELD_FILTER::integer_number) {
$safer_data = intval($safer_data);
}
if ($field_type == FIELD_FILTER::floating_point) {
$safer_data = floatval($safer_data);
}
if ($from === "db") {
if ($field_type == FIELD_FILTER::integer_number || $field_type == FIELD_FILTER::floating_point) {
$safer_db_data = $safer_data;
} else {
if (isset($a->use_db_filter) && $a->use_db_filter == DB_FILTER::ON) {
$safe_for_db = \tts\safer_sql::get_safer_sql_text($safer_data);
$text = $safe_for_db["text"];
$meta[$input_field_name]['db_filter_status'] = $safe_for_db["status"] ?? \tts\SQL_SAFETY_FLAG::filtered;
} else {
$text = $safer_data;
}
$safer_db_data = $text;
}
}
}
$ret['name'] = $input_field_name;
$ret['meta'] = $meta;
if ($from === "db") {
$ret['db'] = $safer_db_data;
$data[$input_field_name] = $safer_db_data;
} elseif ($from === "logic") {
$ret['logic'] = $safer_data;
$data[$input_field_name] = $safer_data;
} elseif ($from === "html") {
$ret['html'] = $safer_html_data;
$data[$input_field_name] = $safer_html_data;
}
$ret['errors'] = (count($rules)) ? \bs_tts\validator::validate($data, $rules, $messages) : [];
return $ret;
}
public static function db_sanitize(
array $inputs,
FIELD_FILTER $default_filter = FIELD_FILTER::raw_string,
bool $trim = true,
) : \Generator {
foreach ($inputs as $input_field_name => $a) {
if (! $a instanceof use_io) {
continue;
}
$yield = static::sanitize_helper(
"db",
$input_field_name,
$a,
$default_filter,
$trim
);
yield $yield;
}
}
public static function logic_sanitize(
array $inputs,
FIELD_FILTER $default_filter = FIELD_FILTER::raw_string,
bool $trim = true,
) : \Generator {
foreach ($inputs as $input_field_name => $a) {
if (! $a instanceof use_io) {
continue;
}
$yield = static::sanitize_helper(
"logic",
$input_field_name,
$a,
$default_filter,
$trim
);
yield $yield;
}
}
/**
* Sanitize the inputs based on the rules an optionally trim the string
* @param FIELD_FILTER $default_filter FILTER_SANITIZE_STRING
* @param bool $trim
* @return Generator
*/
public static function html_escape_and_sanitize(
array $inputs,
FIELD_FILTER $default_filter = FIELD_FILTER::raw_string,
bool $trim = true,
) : \Generator {
foreach ($inputs as $input_field_name => $a) {
if (! $a instanceof use_io) {
continue;
}
$yield = static::sanitize_helper(
"html",
$input_field_name,
$a,
$default_filter,
$trim
);
yield $yield;
}
}
}