diff options
author | Christian Schneider | 2007-10-11 00:39:30 +0000 |
---|---|---|
committer | Christian Schneider | 2007-10-11 00:39:30 +0000 |
commit | 35fe33f7364329dacf415c950bff01b6de9ef88e (patch) | |
tree | b0e6b018b50038ca20266723c53750268f508df5 /it_html.class | |
parent | 1f95711ff3e9697cd85a54545ab42e5fd3611317 (diff) | |
download | itools-35fe33f7364329dacf415c950bff01b6de9ef88e.tar.gz itools-35fe33f7364329dacf415c950bff01b6de9ef88e.tar.bz2 itools-35fe33f7364329dacf415c950bff01b6de9ef88e.zip |
Populated release branch
Diffstat (limited to 'it_html.class')
-rw-r--r-- | it_html.class | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/it_html.class b/it_html.class new file mode 100644 index 0000000..a007aad --- /dev/null +++ b/it_html.class @@ -0,0 +1,499 @@ +<?php +/* +** $Id$ +** +** Copyright (C) 1995-2007 by the ITools Authors. +** This file is part of ITools - the Internet Tools Library +** +** ITools is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** ITools is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see <http://www.gnu.org/licenses/>. +** +** UltraHTML 3000 tool layer. Create functions for html tags. +** +** new it_html; +** echo html(head('title' => 'hello'), body(h1('hello'), p('Hello world!'))); +**/ + +class it_html +{ + +/** + * 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 it_html($p = array()) +{ + # Default configuration of html class + $this->p = $p + array( + 'charset' => 'iso-8859-1', + 'doctype' => null, # Custom doctype (will usually be calculated from htmltype) + 'head' => '', # Code to put into head() section + 'htmltype' => 'xhtml', # 'html' (=old-style), 'xhtml' or 'xhtml-mobile' + 'lang' => 'de', # Language code to use in <html lang="..."> tag + 'ie_png_fix' => false, # To enable, supply URL of a transparent gif (like /images/0.gif) + 'moretags' => '', # Comma-separated list of tag-functions to generate additionally to 'tags' + 'name' => 'it_html', # Name of global variable $this is assigned to (string), XXX Copy and paste in configure() to keep PHP4 compatibility + 'nonewlinetags' => 'a,b,em,img,input,span', # tags that do not like newlines after them + 'notexported' => 'configure,sanitize',# Those methods are not exported + 'prettyprint' => false, # Should output be prettily indented? + 'show_boot_dom' => false, # If true, append invisible <div id="it_boot_dom"> at the end of body + 'show_content_type' => true, # If true, add <meta http-equiv="Content-Type" ...> header + 'show_favicon' => true, # If true, add <link> tag to /favicon.ico if it exists + '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,input,label,legend,li,meta,noscript,p,span,style,table,td,textarea,th,tr,ul', + 'title' => '', # HTML title (default: no title added) + 'use_it_state' => false, # If true, generate code needed by state.js (aka 'history iframe') + ); + + # We know these doctypes. If you need something else, supply 'doctype' in p + $this->doctypes = array( + 'html' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">', + 'xhtml' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', + 'xhtml-mobile' => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd">' + ); + + $this->hasnonewline = array_flip(explode(',', "dummy," . $this->p['nonewlinetags'])); # dummy keeps values > 0 + $notexported = array_flip(explode(',', "dummy," . $this->p['notexported'])); # dummy keeps values > 0 + + # Create global functions for _tags + foreach (array_merge(explode(',', $this->p['tags']), explode(',', $this->p['moretags'])) as $func) + { + if (!function_exists($func) && $func) + $code[$func] = "function $func() { \$args = func_get_args(); return \$GLOBALS['{$this->p['name']}']->_tag('$func', \$args); }"; + } + + # 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 = func_get_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 = func_get_args(); return call_user_func_array(array(&\$GLOBALS['{$this->p['name']}'], '$func'), \$args); }"; + } + + eval(join('', (array)$code)); + + # Since name is given as param, it is our duty to store it, not our caller's. + $GLOBALS[$this->p['name']] =& $this; +} + + +/** + * 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 + */ +function configure($p) +{ + $p += array('name' => "it_html"); # XXX Copy and paste from constructor to keep PHP4 compatibility + $GLOBALS[$p['name']]->p = $p + (array)$GLOBALS[$p['name']]->p; +} + +/** + * 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 for $p. + * @param named parameter doctype can override <!DOCTYPE line + * @param named parameter lang contains language + */ +function html($args) +{ + list($data, $p) = $this->_parse_args($args); + $p += $this->p; + + $html = ($p['doctype'] ? $p['doctype'] : $this->doctypes[$p['htmltype']]) . "\n" . + '<html ' . ($p['htmltype'] == "xhtml-mobile" ? 'xml:lang' : 'lang') . "=\"{$p['lang']}\">\n" . $data . ($p['omit_endhtml'] ? '' : "</html>\n"); + + return EDC('re') ? it::replace(array('<!DOCTYPE.*<head>' => ''), $html, array('singleline' => true)) : $html; +} + + +/** + * Return HTML header on first call or empty string on subsequent calls + * + * @param any number of text args or array of key => value: + * 'content-type' optional content type (default: "text/html; charset=iso-8859-1") + * 'description' optional data for <meta name="description"> tag + * 'keywords' optional data for <meta name="keywords"> tag + * 'stylesheets' optional array mediatype => url of styleshests + * 'title' optional content of <title>, will be html encoded + */ +function head($args = array()) +{ + if (!$this->head_sent++) + { + list($data, $p) = $this->_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'])) : ""; + + foreach(array('description', 'keywords') as $name) + if (!empty($p[$name])) + $header .= meta(array('name' => $name, 'content' => $p[$name])); + + # Add favicon if file exists + if ($p['show_favicon'] && @file_exists($_SERVER['DOCUMENT_ROOT'] . '/favicon.ico')) + $header .= tag('link', array('rel' => "shortcut icon", 'href' => "/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('type' => "text/css", "\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']); + + $js = $p['jsenv'] ? "var env = " . itjs::serialize($p['jsenv']) . ";\n" : ''; + + if ($p['js']) + { + $checksum = itjs::checksum(itjs::filenames($p['js'])); + $js .= $this->_itjs("boot.js", "inline"); + $js .= "function it_boot_start(){ " . trim($p['jsboot']) . " }\n"; + $js .= "it_boot('/itjs/" . U($p['js'], array('s' => $checksum)) . "');\n"; + } + + $js .= $this->_itjs($p['jsinline'], 'inline'); + + if ($js) + $header .= $this->js(array($js)); + + 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 ($this->p['use_it_state']) + array_unshift($args, tag('iframe', array('id' => "it_state", 'src' => "/itjs/state.html", 'width' => 1, 'height' => 1, 'frameborder' => 0))); + + if ($this->p['show_boot_dom']) + $args[] = div(array('id' => "it_boot_dom", 'style' => "visibility:hidden")); + + return $this->head() . $this->_tag('body', $args); +} + + +/** + * INTERNAL: 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 _parse_args($args) +{ + $p = array(); + foreach ($args as $arg) + { + if (is_array($arg)) + { + foreach ($arg as $key => $value) + { + if (is_int($key)) + $data .= $value; + else + $p[$key] = $value; + } + } + else + $data .= $arg; + } + + return array($data, $p); +} + + +/** + * 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) = $this->_parse_args($args); + + $newline = $this->hasnonewline[$name] ? "" : "\n"; + + # Ultra XML PrettyPrinter 3000 [\] by SCA + if ($this->p['prettyprint'] && $newline && (substr($data, -1, 1) == "\n") && (strpos($data, '<textarea') === false) && ($data != strip_tags($data))) + $data = str_replace("\n", "\n ", "\n" . trim($data)) . "\n"; + + $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 + $result .= " $key=\"" . (preg_match("/[<>&\"'\n\x80-\x9f]/", $value) ? str_replace("\n", " ", Q($value)) : $value) . '"'; + else # true: tag without value + $result .= ($this->p['htmltype'] == 'html') ? " $key" : " $key=\"$key\""; + } + + # Apply a kind of magic... this needs further investigation + if (isset($data) || preg_match('/^(a|div|iframe|script|span|td|textarea)$/i', $name)) + $result .= ">$data</$name>$newline"; + elseif ($this->p['htmltype'] == 'html') + $result .= ">$newline"; + else + $result .= " />$newline"; + + if ($GLOBALS['debug_srclines']) + { + $trace = debug_backtrace(); + $trace = $trace[2]; + $result = "<!-- " . basename($trace['file']) . ":" . $trace['line'] . " -->" . $result; + } + + 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); +} + +/** + * Special img() function patches png transparency for IE 5.5-6 if ie_png_fix is set + * @param ... any number optional data or array of key => value arguments + * @return <img ... /> + */ +function img($args) +{ + if ($this->p['ie_png_fix'] && preg_match('/MSIE [56]/', $_SERVER['HTTP_USER_AGENT'])) + { + foreach($args as $id => $arg) + if (preg_match('/\.png(\?.*)?$/', $arg['src'])) + { + $args[$id]['style'] = "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='{$arg['src']}',sizingMethod='scale');" . $arg['style']; + $args[$id]['src'] = $this->p['ie_png_fix']; + } + } + + return $this->_tag("img", $args); +} + + +/** + * Create a dropdown menu object. Warning: encodes html code within options! + * @param $tags key => value pairs of <select> tag + * @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 = (isset($selected) && !is_array($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($optval, $selected)), Q($opt))); + $html .= $this->_tag("optgroup", array(array('label' => $value, $grouphtml))); + } + else + $html .= $this->_tag("option", array(array('value' => $value, 'selected' => in_array($value, $selected), 'disabled' => $option === ""), (trim($option) === "") ? " " : Q($option))); + } + + return $this->_tag("select", array($tags, $html)); +} + + +/** + * Return HTML with all evil things stripped. Allowed are a coupld of simple + * tags like div, p, i, b, 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 + */ +function sanitize($html) +{ + $result = ""; + $html = it::replace(array('[\0\n\r\s]+' => " "), $html); + $urlpattern = 'https?://[^">]+'; + + if ($tag = it::match("(.*)<(div|p|i|b)[^>]*>(.*?)</\\2>(.*)", $html)) + { + # Simple tags with content, no attributes kept + list($head, $tagname, $content, $tail) = $tag; + $result .= it_html::sanitize($head) . "<$tagname>" . it_html::sanitize($content) . "</$tagname>" . it_html::sanitize($tail); + } + else if ($tag = it::match('(.*)<a[^>]+?href="(' . $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(html_entity_decode($href)) . '">' . it_html::sanitize($content) . "</a>" . it_html::sanitize($tail); + } + else if ($tag = it::match('(.*)<img[^>]+?src="(' . $urlpattern . ')"[^>]*?>(.*)', $html)) + { + # Image tags, keeps only src attribute + list($head, $src, $tail) = $tag; + $result .= it_html::sanitize($head) . '<img src="' . it_html::Q(html_entity_decode($src)) . '" alt="" />' . it_html::sanitize($tail); + } + else if ($tag = it::match("(.*)<(br)[^>]*>(.*)", $html)) + { + # Simple tags without content, no attributes kept + list($head, $tagname, $tail) = $tag; + $result .= it_html::sanitize($head) . "<$tagname />" . it_html::sanitize($tail); + } + else + $result = it_html::Q(it::replace(array('&#\d+;' => ""), html_entity_decode(strip_tags($html)))); + + return $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) + */ +function q($string) +{ + if ($GLOBALS['it_html']->p['charset'] == "iso-8859-1") + $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 htmlspecialchars($string); +} + + +/** + * Build a complete url from base-url and params + * @param ... scalar args and numeric indices build base-url, rest as params + */ +function u(/* ... */) +{ + $args = func_get_args(); + list($base, $params) = it_html::_parse_args($args); + + if (!isset($base)) + $base = $_SERVER['PHP_SELF']; + + $base = preg_replace('|\0|', '', $base); + $base = preg_replace('|[^\w.+!*(),:?@&=/~$-]|e', 'urlencode("$0")', $base); + $base = preg_replace('|^(\w+:)?//[^/]*$|', '$0/', $base); # Add slash if absolute url without a path, e.g. http://gna.ch + $queryparams = it_url::params($params); + $separator = strpos($base, "?") === false ? "?" : "&"; + + return $base . ($queryparams ? "$separator$queryparams" : ""); +} + + +/** + * Insert a javascript script + * @param ... any number optional data or array of key => value arguments + * @return <script type="text/javascript"...>...</script> + */ +function js($args) +{ + if (($this->p['htmltype'] != 'html') && $args[0] && ((array)$args[0] === array_values((array)$args[0]))) + { + array_unshift($args, "<!--//--><![CDATA[//><!--\n"); + $args[] = "\n//--><!]]>"; + } + + array_unshift($args, array('type' => 'text/javascript')); + return $this->_tag('script', $args); +} + + +/** + * Include javascript code or generate HTML to include it + */ +function _itjs($files, $mode) +{ + $result = ""; + + if ($files) + { + $filenames = itjs::filenames($files); + + if ($mode == "files") + { + foreach ($filenames as $file) + $result .= tag('script', array('type' => "text/javascript", 'src' => "/itjs/" . basename($file) . "?v=" . itjs::checksum($file))); + } + else if ($mode == "inline") + { + $jsfile = ""; + + foreach ($filenames as $file) + $jsfile .= @file_get_contents($file); + + $result .= itjs::strip($jsfile); + } + else + $result .= tag('script', array('type' => "text/javascript", 'src' => "/itjs/$files?v=" . itjs::checksum($filenames))); + } + + return $result; +} + +} +?> |