. ** ** UltraHTML 3000 tool layer. Create functions for html tags. ** ** new it_html; ** echo html(head('title' => 'hello'), body(h1('hello'), p('Hello world!'))); **/ /** * Parse an arg array (mixed key=>value pairs and strings) and return it * as array(0 => all numerical args concatenated, 1 => array(key=>value pairs) */ function it_parse_args($args) { $p = array(); foreach ($args as $arg) { if (is_array($arg)) { foreach ($arg as $key => $value) { if (is_int($key)) $data .= is_scalar($value) ? it_taintcheck($value) : ""; else $p[$key] = $value; } } else $data .= it_taintcheck($arg); } return array($data, $p); } class it_html { # these tags have no content and need no closing tag in html or can be shortened in xhtml static $voidtags = array('area' => 1, 'base' => 1, 'br' => 1, 'col' => 1, 'command' => 1, 'embed' => 1, 'hr' => 1, 'img' => 1, 'input' => 1, 'keygen' => 1, 'link' => 1, 'meta' => 1, 'param' => 1, 'source' => 1, 'track' => 1, 'wbr' => 1); var $p; # constructor params plus defaults var $doctypes; var $alltags; var $hasnonewline; /** * Create a HTML object and global functions for all methods (exlcluding * methods starting with '_') in this class plus the default tags (see below). * * @param $p Configuration settings. Can be set/overridden in constructor, configure(), html() or head(). * See source code for a list of supported values */ function __construct($p = array()) { # Default configuration of html class $this->p = $p + array( 'charset' => ini_get('default_charset') ?: 'iso-8859-1', 'doctype' => null, # Custom doctype (will usually be calculated from htmltype) 'head' => '', # Code to put into head() section 'htmltype' => 'html5', # 'html5', 'html' (=old-style), 'xhtml' or 'xhtml-mobile' for xhtml, or 'xml' for plain xml without magic 'lang' => 'de', # Language code to use in tag 'moretags' => '', # Comma-separated list of tag-functions to generate additionally to 'tags' 'name' => $p['htmltype'] == 'xml' ? 'it_html_xml' : 'it_html', # Name of global variable $this is assigned to (string), XXX Copy and paste in configure() 'nonewlinetags' => 'a,b,em,img,input,label,span,noscript', # tags that do not like newlines after them 'prettyprint' => it::is_devel(), # Should output be prettily indented? 'show_content_type' => true, # If true, add header 'show_favicon' => true, # If true, add tag to /favicon.ico if it exists 'favicon' => '', # If set, add favicon tag to this url 'staticallycallable' => 'Q,U,select', # Those methods are statically callable (have same arguments as global stubs) but are a bit slower 'tags' => "a,b,br,button,div,em,fieldset,form,h1,h2,h3,h4,h5,h6,hr,img,input,label,legend,li,meta,noscript,p,pre,span,style,table,tbody,td,textarea,tfoot,th,thead,tr,ul,ol,article,section", 'title' => '', # HTML title (default: no title added) 'srclines' => $GLOBALS['debug_srclines'], # append stackdump to each tag 'error_on_redefine' => false, # Generate it::error when trying to redefine function for a tag ); $this->p['notexported'] = trim($p['notexported'] . ',configure,sanitize,comment', ','); # We know these doctypes. If you need something else, supply 'doctype' in p $this->doctypes = array( 'html5' => '', 'html' => '', 'xhtml' => '', 'xhtml-mobile' => '', 'xml' => '' ); # @@@ Hack: Manually copy for new instance without custom value to keep setting from global auto_prepend instance if (!isset($p['error_on_redefine']) && isset($GLOBALS[$this->p['name']])) $this->p['error_on_redefine'] = $GLOBALS[$this->p['name']]->p['error_on_redefine']; # Since name is given as param, it is our duty to store it, not our caller's. $GLOBALS[$this->p['name']] =& $this; it_html::configure(array('name' => $this->p['name'])); $notexported = array_flip(explode(',', "dummy," . $this->p['notexported'])); # dummy keeps values > 0 # Create global functions for _tags foreach (array_keys($this->alltags) as $func) { if (!function_exists($func) && $func) $code[$func] = "function $func(...\$args) { return \$GLOBALS['{$this->p['name']}']->_tag('$func', \$args); }"; else if ($this->p['error_on_redefine']) it::error("Trying to redefine existing function '$func' in it_html"); } # Create global functions for it_html methods foreach (get_class_methods(get_class($this)) as $func) { if (!preg_match('/^_/', $func) && !is_a($this, $func) && $func && !function_exists($func) && !$notexported[$func]) $code[$func] = "function $func(...\$args) { return \$GLOBALS['{$this->p['name']}']->$func(\$args); }"; } # Create global functions for methods that are statically callable (have same arguments as global stubs) foreach (explode(',', $this->p['staticallycallable']) as $func) { if ($func && !function_exists($func)) $code[$func] = "function $func(...\$args) { return \$GLOBALS['{$this->p['name']}']->$func(...\$args); }"; } eval(implode('', (array)$code)); } /** * Modify configuration of it_html, e.g. htmltype after it was instantiated. * @param $p Configuration settings. Can be set/overridden in constructor, configure(), html() or head(). * See constructor for a list of supported values */ static function configure($p) { $ithtml = $GLOBALS[$p['name'] ?: ($p['htmltype'] == 'xml' ? 'it_html_xml' : 'it_html')]; $ithtml->p = $p + (array)$ithtml->p; $ithtml->alltags = array_flip(explode(',', trim($ithtml->p['tags'] . ',' . $ithtml->p['moretags'], ','))); $ithtml->hasnonewline = array_flip(explode(',', $ithtml->p['nonewlinetags'])); } /** * Return doctype and entire HTML page. * Example application code to render page: * echo html(head(...), body(...)); * * @param any number of text args or array of key => value pairs * Defaults for key => value parameters are inherited from the it_html constructor and should be set there. * The parameters are $p['lang'], $p['htmltype'], $p['doctype'], $p['class'] and $p['manifest'] */ function html($args) { list($data, $p) = it_parse_args($args); $p += $this->p; foreach (array('class', 'manifest') as $attr) $attrs .= $p[$attr] ? " $attr=" . '"' . htmlspecialchars($p[$attr], ENT_COMPAT, $GLOBALS['it_html']->p['charset']) . '"' : ""; $html = ($p['doctype'] ? $p['doctype'] : $this->doctypes[$p['htmltype']]) . "\n" . '\n" . $data . ($p['omit_endhtml'] ? '' : "\n"); return EDC('upd') ? it::replace(array('' => ''), $html, array('singleline' => true)) : $html; } /** * Return HTML header on first call or empty string on subsequent calls * * @param args... any number of assoc arrays and strings. strings will be content of * @param $p['content-type'] content type (default: "text/html; charset=iso-8859-1") * @param $p['headers'] Array of HTTP headers (e.g. [ 'Vary' => "User-Agent,Accept-Language" ]) * @param $p['cssinline'] stylesheet to be put in header * @param $p['description'] data for tag * @param $p['keywords'] data for tag * @param $p['stylesheets'] array mediatype => url of styleshests * @param $p['title'] content of , will be html encoded */ function head($args = array()) { if (!$this->head_sent++) { list($data, $p) = it_parse_args($args); $p += $this->p; $this->p = ($p += array('content-type' => "text/html; charset={$p['charset']}")); $header = $p['show_content_type'] ? meta(array('http-equiv' => "Content-Type", 'content' => $p['content-type'])) : ""; # Enable latest IE mode if(strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE')) $header .= meta(array('http-equiv' => "X-UA-Compatible", 'content' => "IE=edge")); foreach(array('description', 'keywords') as $name) if (!empty($p[$name])) $header .= meta(array('name' => $name, 'content' => $p[$name])); # Add favicon if ($p['favicon']) $header .= tag('link', array('rel' => "shortcut icon", 'href' => $p['favicon'])); else if ($p['show_favicon'] && @file_exists($_SERVER['DOCUMENT_ROOT'] . '/favicon.ico')) $header .= tag('link', array('rel' => "shortcut icon", 'href' => U("/favicon.ico"))); foreach((array)$p['stylesheets'] as $type => $url) $header .= tag('link', array('rel' => "stylesheet", 'type' => "text/css", 'href' => $url) + (is_int($type) ? array() : array('media' => $type))); if (!empty($p['cssinline'])) $header .= tag('style', array("\n" . preg_replace(array('/\s*\/\*[^\*]+\*\//Um', '/\s*\{\s*/', '/;\s+/'), array('', '{', ';'), $p['cssinline']))); $header .= $p['head'] . ($p['title'] ? tag('title', Q($p['title'])) : ""); if($this->p['htmltype'] == "xhtml-mobile" && strpos($_SERVER['HTTP_USER_AGENT'], 'W3C_Validator')) header("Content-Type: application/xhtml+xml; charset={$this->p['charset']}"); # for validation else if (!headers_sent()) # prevent warnings when ED() in use { header("Content-Type: " . $p['content-type']); foreach ((array)$p['headers'] as $key => $value) header("$key: $value"); } $js = isset($p['jsenv']) ? "var env = " . itjs::serialize($p['jsenv']) . ";\n" : ''; $js .= $this->_itjs($p['jsinline']); if ($js) $data .= $this->js(array(self::_cleanup($js, $p['charset']))); return tag('head', $header, $data); } } /** * Return HTML head (if not already sent) and body with it_state and it_boot magic code */ function body($args) { if (EDC('what')) { foreach (it::sort((array)$GLOBALS['ULTRADEBUGVARS'] + ['srclines' => 1, 'texts' => 1], "k") as $var => $dummy) { if (EDC($var)) $toggled_host = preg_replace("/\.$var(-[^.-]+)?/", "", $_SERVER['HTTP_HOST']); else $toggled_host = preg_replace('/^(\w+(-\w+)*)/', "\$1.$var", $_SERVER['HTTP_HOST']); $debug_links[] = a(array('href' => U('http://' . $toggled_host . $_SERVER['REQUEST_URI']), 'style' => 'font-weight:' . (EDC($var) ? 'bold' : 'normal') . ';'), $var); } $args[] = div(array('style' => 'position:fixed; bottom:0; z-index:9999; font-family:monospace; background:white'), "Debugvars: " . implode(" ", $debug_links)); } return $this->head() . $this->_tag('body', $args); } /** * function div($args...) * Return a <div>...</div> element * Any strings in the argument list will be content of the <div> * Any associative arrays among the arguments will create attributes for <div>. * Attributes with values false or null will be omitted completely. * Attributes with value true can be used for boolean attributes like 'checked' * Attribute values will be html encoded, content values won't so you may need to use Q(). * @see _tag() */ #:} /** * INTERNAL: Create html tag from name and args array (the arguments of the parent function) */ function _tag($name, $args) { list($data, $attr) = it_parse_args($args); $newline = isset($this->hasnonewline[$name]) ? "" : "\n"; # Ultra XML PrettyPrinter 3000 [\] by SCA if ($this->p['prettyprint'] && $newline && (substr($data, -1, 1) == "\n") && (strpos($data, '<textarea') === false && strpos($data, '<pre') === false) && ($data != strip_tags($data))) $data = str_replace("\n", "\n ", "\n" . trim($data)) . "\n"; # debugging aid: add backtrace if (($levels = intval($this->p['srclines'])) && !it::match('^(head|meta|title|script|style|link)', $name)) $attr = array('title' => it_debug::backtrace(array('levels' => max(3, $levels), 'skipfiles' => "_html\\.class"))) + $attr; $charset = $GLOBALS['it_html']->p['charset']; $result .= "<$name"; # add attributes. If $value === true, use key only (<td nowrap> instead of <td nowrap=""> for old html, <td nowrap="nowrap"> for xhtml style) foreach($attr as $key => $value) { if (($value === null) || ($value === false)) # null or false: omit whole tag ; else if (isset($value) && $value !== true) # normal case: value { if (preg_match('/[<>&"\x00-\x08\x0a-\x0c\x0e-\x1f\x80-\x9f\n\t\r]/', $value)) # WARNING: copy/pasted from Q() $result .= " $key=\"" . str_replace(["\n", "\t", "\r"], [" ", " ", " "], htmlspecialchars(self::_cleanup($value, $charset), ENT_COMPAT, $charset)) . '"'; else $result .= " $key=\"$value\""; } else # true: tag without value $result .= ($this->p['htmltype'][0] != 'x') ? " $key" : " $key=\"$key\""; } # close tag according to html dialect if ($this->p['htmltype'] == 'xml') # plain xml $result .= isset($data) ? ">$data</$name>$newline" : " />$newline"; else if ($this->p['htmltype'][0] == 'x') # xhtml: only voidtags can be shortened $result .= isset($data) || !self::$voidtags[$name] ? ">$data</$name>$newline" : " />$newline"; else $result .= isset($data) || !self::$voidtags[$name] ? ">$data</$name>$newline" : ">$newline"; if ($GLOBALS['debug_utf8check'] && $charset == "utf-8") $result = it::any2utf8($result, "error in $name()"); return $result; } /** * Return a <tag> containing optional data. * @param $name tag name ('style', etc.) * @param ... any number optional data or array of key => value arguments * @return string containing XML/HTML tag */ function tag($args) { $name = array_shift($args); return $this->_tag($name, $args); } /** * Create a dropdown menu object. Warning: encodes html code within options! * @param $tags key => value pairs of <select> tag * note: add brackets after name (eg. 'name' => "var[]") when using multiple attribute * @param $options array (value => text) of available options or * string key:val{,key:val} where key will be rawurldecoded so it may contain %2C as comma * supports optgroups as array (value => optgroup => array(value => text)) * @param $selected optional currently selected value, or comma-separated list or array for multi-select * Note: use tag('select') and tag('option') if you want do roll your own */ function select($tags, $options, $selected = null) { # Transmogrify key:val{,key:val} to array(key => val) if (!is_array($options)) { $opts = explode(',', $options); $options = array(); foreach($opts as $opt) { list($key, $value) = explode(':', $opt); $options[rawurldecode($key)] = $value; } } $selected = ($tags['multiple'] && is_string($selected)) ? explode(',', $selected) : (array)$selected; $html = ""; foreach($options as $value => $option) { if (is_array($option)) { $grouphtml = ""; foreach($option as $optval => $opt) $grouphtml .= $this->_tag("option", array(array('value' => $optval, 'selected' => in_array((string)$optval, $selected)), self::_strip_tags(Q(self::_strip_tags($opt))))); $html .= $this->_tag("optgroup", array(array('label' => $value, $grouphtml))); } else $html .= $this->_tag("option", array(array('value' => $value, 'selected' => in_array((string)$value, $selected), 'disabled' => $option === ""), (trim($option) === "") ? " " : self::_strip_tags(Q(self::_strip_tags($option))))); # self::_strip_tags removes .q debug param coloring } return $this->_tag("select", array($tags, $html)); } /** * Outputs string as correctly quoted HTML comment */ static function comment($string) { return "<!-- " . it::replace(array('--' => "--"), $string) . " -->"; } # internal: strip spans added by debug params function _strip_tags($html) { return preg_replace('!</?(span|a)\b[^>]*>!', '', $html); } /** * Return HTML with all evil things stripped. Allowed are a coupld of simple * tags like div, p, i, b, strong, h1 - h6, br without attributes, a with absolute href, * img with absolute src url. Also ensures that tags are balanced. * @param $html HTML string to be sanitized * @return Sanitized HTML */ static function sanitize($html) { $result = ""; $charset = $GLOBALS['it_html']->p['charset'] ? $GLOBALS['it_html']->p['charset'] : 'iso-8859-1'; if ($charset == "utf-8") $html = it::any2utf8($html); $html = it::replace(array('[\0\s]+' => " "), $html); # \s also matches \r and \n $urlpattern = 'https?://[^">]+'; if ($tag = it::match("(.*?)<(div|p|ol|ul|li|i|b|strong|h[1-6])\b[^>]*>(.*?)</\\2>(.*)", $html)) { # Simple tags with content, no attributes kept list($head, $tagname, $content, $tail) = $tag; $tagname = strtolower($tagname); $result .= it_html::sanitize($head) . "<$tagname>" . it_html::sanitize($content) . "</$tagname>" . it_html::sanitize($tail); } else if ($tag = it::match('(.*)<a\b[^>]+?\bhref\s*=\s*"(' . $urlpattern . ')"[^>]*?>(.*?)</a>(.*)', $html)) { # Link tags, keeps only href attribute list($head, $href, $content, $tail) = $tag; $result .= it_html::sanitize($head) . '<a href="' . it_html::Q(it_html::U(html_entity_decode($href, ENT_COMPAT, $charset))) . '">' . it_html::sanitize($content) . "</a>" . it_html::sanitize($tail); } else if ($tag = it::match('(.*)<img\b[^>]+?\bsrc\s*=\s*"(' . $urlpattern . ')"[^>]*?>(.*)', $html)) { # Image tags, keeps only src attribute list($head, $src, $tail) = $tag; $result .= it_html::sanitize($head) . '<img src="' . it_html::Q(it_html::U(html_entity_decode($src, ENT_COMPAT, $charset))) . '" alt="" />' . it_html::sanitize($tail); } else if ($tag = it::match("(.*)<(br|/tr)\b[^>]*>(.*)", $html)) { # brs and table rows are converted so simple line breaks list($head, $tagname, $tail) = $tag; $result .= it_html::sanitize($head) . "<br />" . it_html::sanitize($tail); } else $result = it::replace(array('&(#\d+;)' => '&$1'), it_html::Q(html_entity_decode(strip_tags($html), ENT_COMPAT, $charset))); return $GLOBALS['debug_q'] ? "<span style='background:#8FF'>$result</span>" : it::replace(array('<(div|p|i|b|a)></\1>' => ""), $result); # remove empty tags } /** * Decode all entities to encoding set for it_html */ static function entity_decode($string) { $charset = $GLOBALS['it_html']->p['charset']; $string = preg_replace('/&#(8217|65533);/', "'", html_entity_decode($string, ENT_COMPAT, $charset)); $string = preg_replace('/&#[^;]*;/i', " ", $string); # remove remaining illegal numeric entities, e.g. 0x80-0x9f return self::_cleanup($string, $charset); } /** * Replace or remove all illegal characters from a HTML string (knows utf-8 and latin1) */ static function _cleanup($string, $charset) { $result = $charset == "utf-8" ? preg_replace('/\xc2[\x80-\x9f]/', ' ', $string) : preg_replace('/[\x80-\x9f]/', ' ', strtr($string, array("\x80" => "EUR", "\x82" => "'", "\x84" => "\"", "\x85" => "...", "\x8a" => "S", "\x8c" => "OE", "\x8e" => "Z", "\x91" => "'", "\x92" => "'", "\x93" => "\"", "\x94" => "\"", "\x96" => "-", "\x97" => "-", "\x9a" => "s", "\x9e" => "z"))); return preg_replace('/[\x00-\x08\x0b-\x0c\x0e-\x1f]/', ' ', $result); } /** * Shortcut: return htmlspecialchars($string) and encode forbidden characters 80-9f if latin1 is output * @param $string String to encode with htmlspecialchars() * @return htmlspecialchars($string) */ static function Q($string) { $string = @strval($string); if (preg_match('/[<>&"\x00-\x08\x0a-\x0c\x0e-\x1f\x80-\xff]/', $string)) # WARNING: copy/pasted to _tag() { $charset = $GLOBALS['it_html']->p['charset'] ?: ini_get('default_charset'); if ($GLOBALS['debug_utf8check'] && $charset == "utf-8") $string = it::any2utf8($string, "error in Q()"); $origstring = $string; $string = @htmlspecialchars(self::_cleanup($string, $charset), ENT_COMPAT, $charset); if ($string === "" && $origstring) it::error("invalid utf-8 '$origstring'"); } return $GLOBALS['debug_q'] && $string ? "<span style='background:#8FF'>$string</span>" : $string; } /** * Build a complete url from base-url and params * @param ... scalar args and numeric indices build base-url, rest as params */ static function U(...$args) { list($base, $params) = it_parse_args($args); if (!isset($base)) $base = preg_replace('/\?.*/', '', $_SERVER['REQUEST_URI']); $base = preg_replace('|\0|', '', $base); # kill null chars if (!($u = @parse_url($base))) { $u = []; list($u['path'], $u['query']) = explode("?", $base, 2); } $u['host'] = preg_match('/[^-_.0-9a-z]/i', $u['host']) && function_exists('idn_to_ascii') && ($idnahost = idn_to_ascii($GLOBALS['it_html']->p['charset'] == "iso-8859-1" ? it::utf8_encode($u['host']) : $u['host'])) ? $idnahost : $u['host']; # Punycode hostname to include into webpage $u['host'] = preg_replace_callback('/[^-_.0-9a-z\x80-\xff]/i', function($m) { return rawurlencode($m[0]); }, $u['host']); # Encode garbage chars in host # handle scheme, user (urlencoded), password, host $hostpart = ($u['user'] ? preg_replace_callback('|[^-a-z0-9_.+!*(),:?@&=/~$%#]|i', function($m) { return rawurlencode($m[0]); }, $u['user'] . ($u['pass'] ? ":" . $u['pass'] : "") . "@") : "") . ($u['host'] ? $u['host'] : "") . ($u['port'] ? ":" . intval($u['port']) : ""); # Remove unsupported javascript: scheme as it leads to security problems if (it::match('javascript', $u['scheme'])) $u['scheme'] = ''; $schemepart = $hostpart ? ($u['scheme'] ? $u['scheme'] . ":" : "") . "//$hostpart" : ($u['scheme'] == "mailto" ? $u['scheme'] . ":" : ""); # remove strings that will be interpreted as scheme from path if (!$schemepart && !$hostpart) $u['path'] = preg_replace('|^([^/]*:)+|', '', $u['path']); # sanitize path and fragment $u['path'] = preg_replace('|\\\\|', '/', $u['path']); # turn \ to / foreach (['path', 'query', 'fragment'] as $key) $u[$key] = preg_replace_callback('/[^-a-z0-9_.+!*(),:?@&=\/~$%#]|%(?![0-9a-f]{2})/i', function($m) { return rawurlencode($m[0]); }, $u[$key]); # convert empty http url path to / if (!$u['path'] && $hostpart && preg_match('/^(https?)?$/', $u['scheme'])) $u['path'] = '/'; # if we supplied params, add/replace them in the existing url, removing empty existing params such as foo=&bar if ($params) { $u['params'] = it_url::parse_str($u['query']); # if params replace url parameters that are present in base, replace them but keep their order foreach ($u['params'] as $key => $dummy) if (isset($params[$key])) $u['params'][$key] = $params[$key]; $u['query'] = it_url::params($u['params'] + $params); } return $schemepart . $u['path'] . ($u['query'] ? "?" . $u['query'] : "") . ($u['fragment'] ? "#" . $u['fragment'] : ""); } /** * Insert a javascript script * @param ... any number optional data or array of key => value arguments * @return <script type="text/javascript"...>...</script> */ function js($args) { $args = it::map(fn($v) => it::replace(['<!--' => '\\x3C!--', '<script' => '\\x3Cscript', '</script' => '\\x3C/script'], $v), $args); if (($this->p['htmltype'][0] == 'x') && $args[0] && ((array)$args[0] === array_values((array)$args[0]))) { array_unshift($args, "<!--//--><![CDATA[//><!--\n"); $args[] = "\n//--><!]]>"; } if ($this->p['htmltype'] != "html5") array_unshift($args, array('type' => 'text/javascript')); return $this->_tag('script', $args); } /** * Include javascript code or generate HTML to include it */ function _itjs($files) { $result = ""; if ($files) { $filenames = itjs::filenames($files); $jsfile = ""; foreach ($filenames as $file) $jsfile .= @it::file_get_contents($file); $result .= itjs::strip($jsfile); } return $result; } /** * Permit calling it_html::div() or parent::div() in a derived class. */ function __call($name, $args) { if (isset($this->alltags[$name])) return $this->_tag($name, $args[0]); else it::error("unknown method '$name' called."); } }