From ccfd50278baa306abee1acd1b310a168f8ae4925 Mon Sep 17 00:00:00 2001 From: Chuck Hagenbuch Date: Fri, 13 Feb 2009 10:53:38 -0500 Subject: [PATCH] import Horde_View --- framework/View/examples/Horde/View/template.php | 27 +++ framework/View/examples/Horde/View/view.php | 28 +++ framework/View/lib/Horde/View.php | 28 +++ framework/View/lib/Horde/View/Base.php | 269 +++++++++++++++++++++++ framework/View/lib/Horde/View/Exception.php | 13 ++ framework/View/lib/Horde/View/Helper.php | 46 ++++ framework/View/lib/Horde/View/Helper/Block.php | 129 +++++++++++ framework/View/lib/Horde/View/Helper/Url.php | 179 +++++++++++++++ framework/View/lib/Horde/View/Interface.php | 119 ++++++++++ framework/View/package.xml | 70 ++++++ framework/View/test/Horde/View/AllTests.php | 46 ++++ framework/View/test/Horde/View/InterfaceTest.php | 19 ++ 12 files changed, 973 insertions(+) create mode 100644 framework/View/examples/Horde/View/template.php create mode 100644 framework/View/examples/Horde/View/view.php create mode 100644 framework/View/lib/Horde/View.php create mode 100644 framework/View/lib/Horde/View/Base.php create mode 100644 framework/View/lib/Horde/View/Exception.php create mode 100644 framework/View/lib/Horde/View/Helper.php create mode 100644 framework/View/lib/Horde/View/Helper/Block.php create mode 100644 framework/View/lib/Horde/View/Helper/Url.php create mode 100644 framework/View/lib/Horde/View/Interface.php create mode 100644 framework/View/package.xml create mode 100644 framework/View/test/Horde/View/AllTests.php create mode 100644 framework/View/test/Horde/View/InterfaceTest.php diff --git a/framework/View/examples/Horde/View/template.php b/framework/View/examples/Horde/View/template.php new file mode 100644 index 000000000..77afb5fad --- /dev/null +++ b/framework/View/examples/Horde/View/template.php @@ -0,0 +1,27 @@ +books): +?> + + + + + + + + +books as $key => $val): ?> + + + + + + +
AuthorTitle
escape($val['author']) ?>escape($val['title']) ?>
+ + +

There are no books to display.

+ diff --git a/framework/View/examples/Horde/View/view.php b/framework/View/examples/Horde/View/view.php new file mode 100644 index 000000000..fc6262586 --- /dev/null +++ b/framework/View/examples/Horde/View/view.php @@ -0,0 +1,28 @@ + 'Hernando de Soto', + 'title' => 'The Mystery of Capitalism' + ), + array( + 'author' => 'Henry Hazlitt', + 'title' => 'Economics in One Lesson' + ), + array( + 'author' => 'Milton Friedman', + 'title' => 'Free to Choose' + ) + ); + +$view = new Horde_View; +$view->books = $data; + +// and render a template called "template.php" +echo $view->render('template.php'); diff --git a/framework/View/lib/Horde/View.php b/framework/View/lib/Horde/View.php new file mode 100644 index 000000000..d7820edc7 --- /dev/null +++ b/framework/View/lib/Horde/View.php @@ -0,0 +1,28 @@ +setEscape($config['escape']); + } + + // encoding + if (!empty($config['encoding'])) { + $this->setEncoding($config['encoding']); + } + + // user-defined template path + if (!empty($config['templatePath'])) { + $this->addTemplatePath($config['templatePath']); + } + } + + /** + * Return a view variable + * + * @param string $name Variable name to retrieve + */ + public function __get($name) + { + return isset($this->name) ? $this->name : ''; + } + + /** + * Assign a single view variable + * + * @param string $name Variable name to set + * @param mixed $value The value of $name + */ + public function __set($name, $value) + { + $this->$name = $value; + } + + /** + * Accesses a helper object from within a template. + * + * @param string $method The helper method. + * @param array $args The parameters for the helper. + * + * @return string The result of the helper method. + */ + public function __call($method, $args) + { + if (isset($this->_helpers[$method])) { + return call_user_func_array(array($this->_helpers[$method], $method), $args); + } + + throw new Horde_View_Exception('Helper for ' . $method . ' not found.'); + } + + /** + * Adds to the stack of template paths in LIFO order. + * + * @param string|array The directory (-ies) to add. + */ + public function addTemplatePath($path) + { + foreach ((array)$path as $dir) { + // Attempt to strip any possible separator and append the + // system directory separator. + $dir = rtrim($dir, '\\/' . DIRECTORY_SEPARATOR) + . DIRECTORY_SEPARATOR; + + // Add to the top of the stack. + array_unshift($this->_templatePath, $dir); + } + } + + /** + * Resets the stack of template paths. + * + * To clear all paths, use Horde_View::setTemplatePath(null). + * + * @param string|array The directory (-ies) to set as the path. + */ + public function setTemplatePath($path) + { + $this->_templatePath = array(); + $this->addTemplatePath($path); + } + + /** + * Adds to the stack of helpers in LIFO order. + * + * @param Horde_View_Helper $helper The helper instance to add. + */ + public function addHelper($helper) + { + foreach (get_class_methods($helper) as $method) { + $this->_helpers[$method] = $helper; + } + } + + /** + * Sets the escape() callback. + * + * @param mixed $spec The callback for escape() to use. + */ + public function setEscape($spec) + { + $this->_escape = $spec; + } + + /** + * Assigns multiple variables to the view. + * + * The array keys are used as names, each assigned their + * corresponding array value. + * + * @param array $array The array of key/value pairs to assign. + * + * @see __set() + */ + public function assign($array) + { + foreach ($array as $key => $val) { + $this->$key = $val; + } + } + + /** + * Processes a template and returns the output. + * + * @param string $name The template to process. + * + * @return string The template output. + */ + public function render($name) + { + // Find the template file name. + $this->_file = $this->_template($name); + + // remove $name from local scope + unset($name); + + ob_start(); + $this->_run($this->_file); + return ob_get_clean(); + } + + /** + * Escapes a value for output in a template. + * + * If escaping mechanism is one of htmlspecialchars or htmlentities, uses + * {@link $_encoding} setting. + * + * @param mixed $var The output to escape. + * + * @return mixed The escaped value. + */ + public function escape($var) + { + if (in_array($this->_escape, array('htmlspecialchars', 'htmlentities'))) { + return call_user_func($this->_escape, $var, ENT_QUOTES, $this->_encoding); + } + + return call_user_func($this->_escape, $var); + } + + /** + * Set encoding to use with htmlentities() and htmlspecialchars() + * + * @param string $encoding + */ + public function setEncoding($encoding) + { + $this->_encoding = $encoding; + } + + /** + * Return current escape encoding + * + * @return string + */ + public function getEncoding() + { + return $this->_encoding; + } + + /** + * Finds a template from the available directories. + * + * @param $name string The base name of the template. + */ + protected function _template($name) + { + if (!count($this->_templatePath)) { + throw new Horde_View_Exception('No template directory set; unable to locate ' . $name); + } + + foreach ($this->_templatePath as $dir) { + if (is_readable($dir . $name)) { + return $dir . $name; + } + } + + throw new Horde_View_Exception("\"$name\" not found in template path (\"" . implode(':', $this->_templatePath) . '")'); + } + + /** + * Use to include the template in a scope that only allows public + * members. + * + * @return mixed + */ + abstract protected function _run(); + +} diff --git a/framework/View/lib/Horde/View/Exception.php b/framework/View/lib/Horde/View/Exception.php new file mode 100644 index 000000000..9f2d56a07 --- /dev/null +++ b/framework/View/lib/Horde/View/Exception.php @@ -0,0 +1,13 @@ +_view = $view; + $view->addHelper($this); + } + + /** + * Call chaining so other helpers can be called transparently. + * + * @param string $method The helper method. + * @param array $args The parameters for the helper. + * + * @return string The result of the helper method. + */ + public function __call($method, $args) + { + return call_user_func_array(array($this->_view, $method), $args); + } + +} diff --git a/framework/View/lib/Horde/View/Helper/Block.php b/framework/View/lib/Horde/View/Helper/Block.php new file mode 100644 index 000000000..f08784369 --- /dev/null +++ b/framework/View/lib/Horde/View/Helper/Block.php @@ -0,0 +1,129 @@ +_args($args); + + return $this->_block($app, $block, $params)->getTitle(); + } + + /** + * Return the content of the specified block. + * + * @param string $app The application the block is from. + * @param string $block The name of the block to get the content for. + * @param mixed $arg1 (optional) The first argument to the Block constructor. + * @param mixed $arg2 (optional) The first argument to the Block constructor. + * @param mixed $arg3 (optional) The first argument to the Block constructor. + * + * ... + * + * @return string The requested Block's content. + * + * @throws Horde_View_Exception, InvalidArgumentException + */ + public function blockContent() + { + $args = func_get_args(); + list($app, $block, $params) = $this->_args($args); + + return $this->_block($app, $block, $params)->getContent(); + } + + /** + * Instantiate and cache Block objects + * + * @param string $app The application the block is from. + * @param string $block The name of the block to fetch. + * @param array $params (option) Any arguments to the Block constructor. + * + * ... + * + * @return Horde_Block The requested Block object + * + * @throws Horde_View_Exception, InvalidArgumentException + */ + protected function _block($app, $block, $params) + { + $hash = sha1(serialize(array($app, $block, $params))); + if (!isset($this->_blockCache[$hash])) { + $block = Horde_Block_Collection::getBlock($app, $block, $params); + if (!$block instanceof Horde_Block) { + if (is_callable(array($block, 'getMessage'))) { + throw new Horde_View_Exception($block->getMessage()); + } else { + throw new Horde_View_Exception('Unknown error instantiating Block object'); + } + } + + $this->_blockCache[$hash] = $block; + } + + return $this->_blockCache[$hash]; + } + + /** + * Parse any argument style for the Block-fetching functions + * + * @param array $args + */ + protected function _args($args) + { + $argc = count($args); + + if ($argc == 1) { + if (is_array($args[0])) { + $args = $args[0]; + $argc = count($args); + } + } + + if ($argc < 2) { + throw new InvalidArgumentException('You must provide at least an application name and a block name.'); + } + $app = array_shift($args); + $block = array_shift($args); + + return array($app, $block, $args); + } + +} diff --git a/framework/View/lib/Horde/View/Helper/Url.php b/framework/View/lib/Horde/View/Helper/Url.php new file mode 100644 index 000000000..27e449e21 --- /dev/null +++ b/framework/View/lib/Horde/View/Helper/Url.php @@ -0,0 +1,179 @@ +:confirm => 'question?' -- This will add a JavaScript confirm + * prompt with the question specified. If the user accepts, the link is + * processed normally, otherwise no action is taken. + * * :popup => true || array of window options -- This will force the + * link to open in a popup window. By passing true, a default browser window + * will be opened with the URL. You can also specify an array of options + * that are passed-thru to JavaScripts window.open method. + * * :method => symbol of HTTP verb -- This modifier will dynamically + * create an HTML form and immediately submit the form for processing using + * the HTTP verb specified. Useful for having links perform a POST operation + * in dangerous actions like deleting a record (which search bots can follow + * while spidering your site). Supported verbs are :post, :delete and :put. + * Note that if the user has JavaScript disabled, the request will fall back + * to using GET. If you are relying on the POST behavior, you should check + * for it in your controller's action by using the request object's methods + * for post?, delete? or put?. + * * The +html_options+ will accept a hash of html attributes for the link tag. + * + * Note that if the user has JavaScript disabled, the request will fall back + * to using GET. If :href=>'#' is used and the user has JavaScript disabled + * clicking the link will have no effect. If you are relying on the POST + * behavior, your should check for it in your controller's action by using the + * request object's methods for post?, delete? or put?. + */ + public function linkTo($name, $url, $htmlOptions = array()) + { + if ($htmlOptions) { + $href = isset($htmlOptions['href']) ? $htmlOptions['href'] : null; + // @todo convert_otpions_to_javascript!(html_options, url) + $tagOptions = $this->tagOptions($htmlOptions); + } else { + $tagOptions = null; + } + + $hrefAttr = isset($href) ? null : 'href="' . $url . '"'; + $nameOrUrl = isset($name) ? $name : $url; + return '' . $this->escape($nameOrUrl) . ''; + } + + /** + * Creates a link tag of the given $name using $url unless the current + * request URI is the same as the links, in which case only the name is + * returned. + */ + public function linkToUnlessCurrent($name, $url, $htmlOptions = array()) + { + return $this->linkToUnless($this->isCurrentPage($url), + $name, $url, $htmlOptions); + } + + /** + * Creates a link tag of the given +name+ using a URL created by the set of + * +options+ unless +condition+ is true, in which case only the name is + * returned. To specialize the default behavior (i.e., show a login link rather + * than just the plaintext link text), you can pass a block that + * accepts the name or the full argument list for link_to_unless. + */ + public function linkToUnless($condition, $name, $url, $htmlOptions = array()) + { + return $condition ? $name : $this->linkTo($name, $url, $htmlOptions); + } + + /** + * Creates a link tag of the given +name+ using a URL created by the set of + * +options+ if +condition+ is true, in which case only the name is + * returned. To specialize the default behavior, you can pass a block that + * accepts the name or the full argument list for link_to_unless (see the examples + * in link_to_unless). + */ + public function linkToIf($condition, $name, $url, $htmlOptions = array()) + { + return $this->linkToUnless(!$condition, $name, $url, $htmlOptions); + } + + /** + * True if the current request URI is the same as the current URL. + * + * @TODO Get REQUEST_URI from somewhere other than the global environment. + */ + public function isCurrentPage($url) + { + return $url == $_SERVER['REQUEST_URI']; + } + + // @TODO Move these methods to a generic HTML/Tag helper + + /** + * HTML attributes that get converted from boolean to the attribute name: + * array('disabled' => true) becomes array('disabled' => 'disabled') + * + * @var array + */ + private $_booleanAttributes = array('disabled', 'readonly', 'multiple', 'selected', 'checked'); + + /** + * Converts an associative array of $options into + * a string of HTML attributes + * + * @param array $options key/value pairs + * @param string key1="value1" key2="value2" + */ + public function tagOptions($options) + { + foreach ($options as $k => &$v) { + if ($v === null || $v === false) { + unset($options[$k]); + } else { + if (in_array($k, $this->_booleanAttributes)) { + $v = $k; + } + } + } + + if (!empty($options)) { + foreach ($options as $k => &$v) { + $v = $k . '="' . $this->escapeOnce($v) . '"'; + } + sort($options); + return ' ' . implode(' ', $options); + } else { + return ''; + } + } + + /** + * Returns the escaped $html without affecting existing escaped entities. + * + * $this->escapeOnce("1 > 2 & 3") + * => "1 < 2 & 3" + * + * @param string $html HTML to be escaped + * + * @return string Escaped HTML without affecting existing escaped entities + */ + public function escapeOnce($html) + { + return $this->_fixDoubleEscape(htmlspecialchars($html, ENT_QUOTES, $this->getEncoding())); + } + + /** + * Fix double-escaped entities, such as &amp; + * + * @param string $escaped Double-escaped entities + * @return string Entities fixed + */ + private function _fixDoubleEscape($escaped) + { + return preg_replace('/&([a-z]+|(#\d+));/i', '&\\1;', $escaped); + } + +} diff --git a/framework/View/lib/Horde/View/Interface.php b/framework/View/lib/Horde/View/Interface.php new file mode 100644 index 000000000..c5c2c58e6 --- /dev/null +++ b/framework/View/lib/Horde/View/Interface.php @@ -0,0 +1,119 @@ + + + View + pear.horde.org + Horde View API + The Horde_View:: class provides a simple View pattern implementation. + + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + 2008-02-12 + + + 0.2.1 + 0.2.0 + + + beta + beta + + LGPL + - Better Helper architecture, improved View/Helper registration + + + + + + + + + + + + + + + + + + + + + + + + 5.2.0 + + + 1.5.0 + + + + + + + + + + + + + + + diff --git a/framework/View/test/Horde/View/AllTests.php b/framework/View/test/Horde/View/AllTests.php new file mode 100644 index 000000000..dbebda25d --- /dev/null +++ b/framework/View/test/Horde/View/AllTests.php @@ -0,0 +1,46 @@ +isFile() && preg_match('/Test.php$/', $file->getFilename())) { + $pathname = $file->getPathname(); + require $pathname; + + $class = str_replace(DIRECTORY_SEPARATOR, '_', + preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname)); + $suite->addTestSuite('Horde_View_' . $class); + } + } + + return $suite; + } + +} + +if (PHPUnit_MAIN_METHOD == 'Horde_View_AllTests::main') { + Horde_View_AllTests::main(); +} diff --git a/framework/View/test/Horde/View/InterfaceTest.php b/framework/View/test/Horde/View/InterfaceTest.php new file mode 100644 index 000000000..bcbc15834 --- /dev/null +++ b/framework/View/test/Horde/View/InterfaceTest.php @@ -0,0 +1,19 @@ +