diff --git a/includes/kohana/modules/pagination/.gitignore b/includes/kohana/modules/pagination/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/includes/kohana/modules/pagination/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/includes/kohana/modules/pagination/classes/Kohana/Pagination.php b/includes/kohana/modules/pagination/classes/Kohana/Pagination.php new file mode 100644 index 0000000..8f5d533 --- /dev/null +++ b/includes/kohana/modules/pagination/classes/Kohana/Pagination.php @@ -0,0 +1,335 @@ + array('source' => 'query_string', 'key' => 'page'), + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ); + + /** + * @var array Members that have access methods + */ + protected $_properties = array( + 'current_page', 'total_items', 'items_per_page', 'total_pages', 'current_first_item', 'current_last_item', + 'previous_page', 'next_page', 'first_page', 'last_page', 'offset', + ); + + // Current page number + protected $_current_page; + + // Total item count + protected $_total_items; + + // How many items to show per page + protected $_items_per_page; + + // Total page count + protected $_total_pages; + + // Item offset for the first item displayed on the current page + protected $_current_first_item; + + // Item offset for the last item displayed on the current page + protected $_current_last_item; + + // Previous page number; FALSE if the current page is the first one + protected $_previous_page; + + // Next page number; FALSE if the current page is the last one + protected $_next_page; + + // First page number; FALSE if the current page is the first one + protected $_first_page; + + // Last page number; FALSE if the current page is the last one + protected $_last_page; + + // Query offset + protected $_offset; + + /** + * Creates a new Pagination object. + * + * @param array configuration + * @return Pagination + */ + public static function factory(array $config = array()) + { + return new Pagination($config); + } + + /** + * Creates a new Pagination object. + * + * @param array configuration + * @return void + */ + public function __construct(array $config = array()) + { + // Overwrite system defaults with application defaults + $this->config = $this->config_group() + $this->config; + + // Pagination setup + $this->setup($config); + } + + /** + * Retrieves a pagination config group from the config file. One config group can + * refer to another as its parent, which will be recursively loaded. + * + * @param string pagination config group; "default" if none given + * @return array config settings + */ + public function config_group($group = 'default') + { + // Load the pagination config file + $config_file = Kohana::$config->load('pagination'); + + // Initialize the $config array + $config['group'] = (string) $group; + + // Recursively load requested config groups + while (isset($config['group']) AND isset($config_file->$config['group'])) + { + // Temporarily store config group name + $group = $config['group']; + unset($config['group']); + + // Add config group values, not overwriting existing keys + $config += $config_file->$group; + } + + // Get rid of possible stray config group names + unset($config['group']); + + // Return the merged config group settings + return $config; + } + + /** + * Loads configuration settings into the object and (re)calculates pagination if needed. + * Allows you to update config settings after a Pagination object has been constructed. + * + * @param array configuration + * @return object Pagination + */ + public function setup(array $config = array()) + { + if (isset($config['group'])) + { + // Recursively load requested config groups + $config += $this->config_group($config['group']); + } + + // Overwrite the current config settings + $this->config = $config + $this->config; + + // Only (re)calculate pagination when needed + if ($this->_current_page === NULL + OR isset($config['current_page']) + OR isset($config['total_items']) + OR isset($config['items_per_page'])) + { + // Retrieve the current page number + if ( ! empty($this->config['current_page']['page'])) + { + // The current page number has been set manually + $this->_current_page = (int) $this->config['current_page']['page']; + } + else + { + switch ($this->config['current_page']['source']) + { + case 'query_string': + $this->_current_page = isset($_GET[$this->config['current_page']['key']]) + ? (int) $_GET[$this->config['current_page']['key']] + : 1; + break; + + case 'route': + $this->_current_page = (int) Request::current()->param($this->config['current_page']['key'], 1); + break; + } + } + + // Calculate and clean all pagination variables + $this->_total_items = (int) max(0, $this->config['total_items']); + $this->_items_per_page = (int) max(1, $this->config['items_per_page']); + $this->_total_pages = (int) ceil($this->_total_items / $this->_items_per_page); + $this->_current_page = (int) min(max(1, $this->_current_page), max(1, $this->_total_pages)); + $this->_current_first_item = (int) min((($this->_current_page - 1) * $this->_items_per_page) + 1, $this->_total_items); + $this->_current_last_item = (int) min($this->_current_first_item + $this->_items_per_page - 1, $this->_total_items); + $this->_previous_page = ($this->_current_page > 1) ? $this->_current_page - 1 : FALSE; + $this->_next_page = ($this->_current_page < $this->_total_pages) ? $this->_current_page + 1 : FALSE; + $this->_first_page = ($this->_current_page === 1) ? FALSE : 1; + $this->_last_page = ($this->_current_page >= $this->_total_pages) ? FALSE : $this->_total_pages; + $this->_offset = (int) (($this->_current_page - 1) * $this->_items_per_page); + } + + // Chainable method + return $this; + } + + /** + * Generates the full URL for a certain page. + * + * @param integer page number + * @return string page URL + */ + public function url($page = 1) + { + // Clean the page number + $page = max(1, (int) $page); + + // No page number in URLs to first page + if ($page === 1 AND ! $this->config['first_page_in_url']) + { + $page = NULL; + } + + switch ($this->config['current_page']['source']) + { + case 'query_string': + return URL::site(Request::current()->uri()).URL::query(array($this->config['current_page']['key'] => $page)); + + case 'route': + return URL::site(Request::current()->uri(array($this->config['current_page']['key'] => $page))).URL::query(); + } + + return '#'; + } + + /** + * Checks whether the given page number exists. + * + * @param integer page number + * @return boolean + * @since 3.0.7 + */ + public function valid_page($page) + { + // Page number has to be a clean integer + if ( ! Valid::digit($page)) + return FALSE; + + return $page > 0 AND $page <= $this->_total_pages; + } + + /** + * Renders the pagination links. + * + * @param mixed string of the view to use, or a Kohana_View object + * @return string pagination output (HTML) + */ + public function render($view = NULL) + { + // Automatically hide pagination whenever it is superfluous + if ($this->config['auto_hide'] === TRUE AND $this->_total_pages <= 1) + return ''; + + if ($view === NULL) + { + // Use the view from config + $view = $this->config['view']; + } + + if ( ! $view instanceof View) + { + // Load the view file + $view = View::factory($view); + } + + // Pass on the whole Pagination object + return $view->set(get_object_vars($this))->set('page', $this)->render(); + } + + /** + * Renders the pagination links. + * + * @return string pagination output (HTML) + */ + public function __toString() + { + try + { + return $this->render(); + } + catch(Exception $e) + { + Kohana_Exception::handler($e); + return ''; + } + } + + /** + * Handles loading and setting properties. + * + * @param string $method Method name + * @param array $args Method arguments + * @return mixed + */ + public function __call($method, array $args) + { + if (in_array($method, $this->_properties)) + { + if (!count($args)) + { + return $this->{'_'.$method}; + } + } + else + { + throw new Kohana_Exception('Invalid method :method called in :class', + array(':method' => $method, ':class' => get_class($this))); + } + } + + /** + * Handles setting of property + * + * @param string $key Property name + * @param mixed $value Property value + * @return void + */ + public function __set($key, $value) + { + if (isset($this->{'_'.$key})) + { + $this->setup(array($key => $value)); + } + else + { + throw new Kohana_Exception('The :property: property does not exist in the :class: class', + array(':property:' => $key, ':class:' => get_class($this))); + } + } + +} // End Pagination diff --git a/includes/kohana/modules/pagination/classes/Pagination.php b/includes/kohana/modules/pagination/classes/Pagination.php new file mode 100644 index 0000000..6be4b15 --- /dev/null +++ b/includes/kohana/modules/pagination/classes/Pagination.php @@ -0,0 +1,3 @@ + array( + 'current_page' => array('source' => 'query_string', 'key' => 'page'), // source: "query_string" or "route" + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ), + +); diff --git a/includes/kohana/modules/pagination/config/userguide.php b/includes/kohana/modules/pagination/config/userguide.php new file mode 100644 index 0000000..936b209 --- /dev/null +++ b/includes/kohana/modules/pagination/config/userguide.php @@ -0,0 +1,23 @@ + array( + + // This should be the path to this modules userguide pages, without the 'guide/'. Ex: '/guide/modulename/' would be 'modulename' + 'pagination' => array( + + // Whether this modules userguide pages should be shown + 'enabled' => TRUE, + + // The name that should show up on the userguide index page + 'name' => 'Pagination', + + // A short description of this module, shown on the index page + 'description' => 'Tool for creating paginated links and viewing pages of results.', + + // Copyright message, shown in the footer for this module + 'copyright' => '© 2008–2010 Kohana Team', + ) + ) +); \ No newline at end of file diff --git a/includes/kohana/modules/pagination/guide/pagination/config.md b/includes/kohana/modules/pagination/guide/pagination/config.md new file mode 100644 index 0000000..4a5713b --- /dev/null +++ b/includes/kohana/modules/pagination/guide/pagination/config.md @@ -0,0 +1,94 @@ +# Pagination Configuration + +[Pagination] uses 6 settings: `current_page`, `total_items`, `items_per_page`, `view`, `auto_hide` and `first_page_in_url`. + +## Configuration Examples + +This example shows the default configuration: + + return array( + + // Application defaults + 'default' => array( + 'current_page' => array('source' => 'query_string', 'key' => 'page'), // source: "query_string" or "route" + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ), + ); + +This is an example with multiple configurations: + + return array( + + // Application defaults + 'default' => array( + 'current_page' => array('source' => 'query_string', 'key' => 'page'), + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ), + + // Second configuration + 'pretty' => array( + 'current_page' => array('source' => 'route', 'key' => 'page'), + 'total_items' => 0, + 'items_per_page' => 20, + 'view' => 'pagination/pretty', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ), + ); + + + +## Settings + +### current_page + +The `current_page` setting tells Pagination where to look to find the current page number. +There are two options for the `source` of the page number: `query_string` and `route`. +The `key` index in the configuration array tells Pagination what name to look for when it's searching in the query string or route. + +This configuration informs Pagination to look in the query string for a value named `page`: + + 'current_page' => array('source' => 'query_string', 'key' => 'page'), + +If you have a route setup with the page number in the actual URL like this: + + Route::set('city_listings', 'listings(/)', array('page_num' => '[0-9]+')) + ->defaults(array( + 'controller' => 'city', + 'action' => 'listings' + )); + +then you would use a setting like this: + + 'current_page' => array('source' => 'route', 'key' => 'page_num'), + + +### total_items + +`total_items` is a setting you will most likely pass in during runtime after figuring out exactly how many items you have. It can be set to zero in the configuration for now. + +### items_per_page + +Self explanatory. This is the maximum items to show on each page. Pagination determines the total number of pages based off of this number. + +### view + +The `view` setting should be a path to a Pagination view file. + +### auto_hide + +If `auto_hide` is set to `TRUE` then Pagination will automatically hide whenever there's only one page of items. + +### first_page_in_url + +If you want Pagination to add the page number to the first page's link then set this setting to `TRUE` otherwise leave it as `FALSE`. + + diff --git a/includes/kohana/modules/pagination/guide/pagination/examples.md b/includes/kohana/modules/pagination/guide/pagination/examples.md new file mode 100644 index 0000000..e69de29 diff --git a/includes/kohana/modules/pagination/guide/pagination/index.md b/includes/kohana/modules/pagination/guide/pagination/index.md new file mode 100644 index 0000000..e69de29 diff --git a/includes/kohana/modules/pagination/guide/pagination/menu.md b/includes/kohana/modules/pagination/guide/pagination/menu.md new file mode 100644 index 0000000..08c39c8 --- /dev/null +++ b/includes/kohana/modules/pagination/guide/pagination/menu.md @@ -0,0 +1,4 @@ +## [Pagination]() +- [Config](config) +- [Usage](usage) +- [Examples](examples) \ No newline at end of file diff --git a/includes/kohana/modules/pagination/guide/pagination/usage.md b/includes/kohana/modules/pagination/guide/pagination/usage.md new file mode 100644 index 0000000..e69de29 diff --git a/includes/kohana/modules/pagination/views/pagination/basic.php b/includes/kohana/modules/pagination/views/pagination/basic.php new file mode 100644 index 0000000..f0d414d --- /dev/null +++ b/includes/kohana/modules/pagination/views/pagination/basic.php @@ -0,0 +1,37 @@ +

+ + first_page() !== FALSE): ?> + + + + + + previous_page() !== FALSE): ?> + + + + + + total_pages(); $i++): ?> + + current_page()): ?> + + + + + + + + next_page() !== FALSE): ?> + + + + + + last_page() !== FALSE): ?> + + + + + +

diff --git a/includes/kohana/modules/pagination/views/pagination/floating.php b/includes/kohana/modules/pagination/views/pagination/floating.php new file mode 100644 index 0000000..52e3fc2 --- /dev/null +++ b/includes/kohana/modules/pagination/views/pagination/floating.php @@ -0,0 +1,94 @@ +total_pages()); + +// Ending group of pages: $n7...$n8 +$n7 = max(1, $page->total_pages() - $count_out + 1); +$n8 = $page->total_pages(); + +// Middle group of pages: $n4...$n5 +$n4 = max($n2 + 1, $page->current_page() - $count_in); +$n5 = min($n7 - 1, $page->current_page() + $count_in); +$use_middle = ($n5 >= $n4); + +// Point $n3 between $n2 and $n4 +$n3 = (int) (($n2 + $n4) / 2); +$use_n3 = ($use_middle && (($n4 - $n2) > 1)); + +// Point $n6 between $n5 and $n7 +$n6 = (int) (($n5 + $n7) / 2); +$use_n6 = ($use_middle && (($n7 - $n5) > 1)); + +// Links to display as array(page => content) +$links = array(); + +// Generate links data in accordance with calculated numbers +for ($i = $n1; $i <= $n2; $i++) +{ + $links[$i] = $i; +} +if ($use_n3) +{ + $links[$n3] = '…'; +} +for ($i = $n4; $i <= $n5; $i++) +{ + $links[$i] = $i; +} +if ($use_n6) +{ + $links[$n6] = '…'; +} +for ($i = $n7; $i <= $n8; $i++) +{ + $links[$i] = $i; +} + +?> +

+ + first_page() !== FALSE): ?> + + + + + + previous_page() !== FALSE): ?> + + + + + + $content): ?> + + current_page()): ?> + + + + + + + + next_page() !== FALSE): ?> + + + + + + last_page() !== FALSE): ?> + + + + + +

diff --git a/includes/kohana/modules/xml/LICENSE b/includes/kohana/modules/xml/LICENSE new file mode 100644 index 0000000..82b7457 --- /dev/null +++ b/includes/kohana/modules/xml/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2010 Cédric de Saint Léger + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/includes/kohana/modules/xml/README.md b/includes/kohana/modules/xml/README.md new file mode 100644 index 0000000..c4b5dcb --- /dev/null +++ b/includes/kohana/modules/xml/README.md @@ -0,0 +1,31 @@ +Kohana_XML is a XML modules to generate and read XML documents in Kohana. +It is build for KO3, but there are barely one or two lines that makes it KO3 specific, +so I guess it should work for KO2.x without much trouble. + +## Notable Features + +* **Extendible, configurable drivers** — You can use the XML class to write simple XML, +or use the Atom driver to generate Atom compliant XML, or write your own driver (extending XML +or another driver) to generate XML compliant to any specs you want. Driver support initial +configuration, which will be used when using native functions, and your own function. +Namespaces and prefix, value filters, default attributes, node name abstraction are all part +of driver configuration and are then used as such by native functions, so they are dealt with +on the fly. But you can also write your own function very easily in your drivers, and writing +an add_author($user_model) function in the Atom driver would take a second. + +* **Dealing with objects of the same class whatever function you use** – $xml→add_node(“test”); +generates another XML instance of the same driver you can add nodes to, import array or XML files +to, search in, modify, export, combine… The whole XML document becomes modular, easy to read and +to modify, and to run through with method chaining. Just play Lego with your XML. + +* **Magic get and get()** — allows to easily run through the document. For instance +$atom→author→name will return an atom document author’s name, this regardless of your driver +configuration. As another example of node name abstraction, if you’ve decided to abstract “pubDate” +with “updated” in your RSS2 driver configuration and “published” with “updated” in you Atom driver, +then $atom→updated will give you the same result as $rss→updated. + +* **Jelly-style driver configuration** — I liked the way Jelly initializes its models, so you can +configure yours just the same way. Driver configuration then goes into a static meta class, which +improves performance. + +* You can still use **DOM functions** if you wish and reintegrate in Kohana_XML \ No newline at end of file diff --git a/includes/kohana/modules/xml/classes/xml.php b/includes/kohana/modules/xml/classes/xml.php new file mode 100644 index 0000000..c4c3cd1 --- /dev/null +++ b/includes/kohana/modules/xml/classes/xml.php @@ -0,0 +1,13 @@ + + * + * Description: + * XML class. Use this class to override XML_Core. + * Extend this class to make your own XML based driver (Atom, XRDS, GData, RSS, PodCast RSS, or your own brewed XML format) + */ + + class XML extends XML_Core + {} \ No newline at end of file diff --git a/includes/kohana/modules/xml/classes/xml/core.php b/includes/kohana/modules/xml/classes/xml/core.php new file mode 100644 index 0000000..1432e05 --- /dev/null +++ b/includes/kohana/modules/xml/classes/xml/core.php @@ -0,0 +1,732 @@ + + * + * Description: + * XML_Core class. + */ + + class XML_Core + { + /** + * @var string XML document version + */ + public static $xml_version = "1.0"; + + /** + * @var string Root Node name + */ + public $root_node; + + /** + * The DOM_Element corresponding to this XML instance + * This is made public to use DOM functions directly if desired. + * @var DOM_Element XML instance DOM node. + */ + public $dom_node; + + /** + * Basically a handy shortcut of $this->dom_node->ownerDocument + * All XML instance belonging to the same document will have this attribute in common + * @var DOM_Document XML instance DOM document, owner of dom_node + */ + public $dom_doc; + + /** + * @var array Array of XML_Meta, containing metadata about XML drivers config + */ + protected static $_metas = array(); + + + /** + * This creates an XML object from the specified driver. + * Specify the driver name, or if there is no specific driver, the root node name + * @param string $driver [optional] Driver Name + * @param string $root_node [optional] Root Node name. Force the root node name. Must be used if no driver nor element is specified. + * @param string $element [optional] XML string or file to generate XML from. Must be used if no driver nor root_node is specified. + * @return XML XML object + */ + public static function factory($driver = NULL, $root_node = NULL, $element = NULL) + { + if ($driver) + { + // Let's attempt to generate a new instance of the subclass corresponding to the driver provided + $class = 'XML_Driver_'.ucfirst($driver); + + // Register a new meta object + XML::$_metas[strtolower($class)] = $meta = new XML_Meta; + + // Override the meta with driver-specific attributes + call_user_func(array($class, "initialize"), $meta); + + // Set content type to default if it is not already set, and report it as initialized + $meta->content_type("text/xml")->set_initialized(); + + return new $class($element, $root_node); + } + else + { + // Register a new meta object in the root node + XML::$_metas["xml"] = $meta = new XML_Meta; + + // Set content type to default if it is not already set, and report it as initialized + $meta->content_type("text/xml")->set_initialized(); + + return new XML($element, $root_node); + } + } + + + /** + * Class constructor. You should use the factory instead. + * @param string $element [optional] What to construct from. Could be some xml string, a file name, or a DOMNode + * @param string $root_node [optional] The root node name. To be specified if no driver are used. + * @return XML XML object instance + */ + public function __construct($element = NULL, $root_node = NULL) + { + // Create the initial DOMDocument + $this->dom_doc = new DOMDocument(XML::$xml_version, Kohana::$charset); + + if ($root_node) + { + // If a root node is specified, overwrite the current_one + $this->root_node = $root_node; + } + + // Initialize the document with the given element + if (is_string($element)) + { + if (is_file($element) OR Valid::url($element)) + { + // Generate XML from a file + $this->dom_doc->load($element); + } + else + { + // Generate XML from a string + $this->dom_doc->loadXML($element); + } + // Node is the root node of the document, containing the whole tree + $this->dom_node = $this->dom_doc->documentElement; + } + elseif ($element instanceof DOMNode) + { + // This is called from another XML instance ( through add_node, or else...) + // Let's add that node to the new object node + $this->dom_node = $element; + + // And overwrite the document with that node's owner document + $this->dom_doc = $this->dom_node->ownerDocument; + } + elseif ( ! is_null($this->root_node)) + { + // Create the Root Element from the driver attributes + if ($this->meta()->get("namespace", $this->root_node)) + { + $root_node_name = $this->meta()->get("prefix", $this->root_node) ? $this->meta()->get("prefix", $this->root_node).":$this->root_node" : $this->root_node; + + // Create the root node in its prefixed namespace + $root_node = $this->dom_doc->createElementNS($this->meta()->get("namespace", $this->root_node), $root_node_name); + } + else + { + // Create the root node + $root_node = $this->dom_doc->createElement($this->root_node); + } + + // Append the root node to the object DOMDocument, and set the resulting DOMNode as it's node + $this->dom_node = $this->dom_doc->appendChild($root_node); + + // Add other attributes + $this->add_attributes($this->dom_node); + } + else + { + throw new Kohana_Exception("You have to specify a root_node, either in your driver or in the constructor if you're not using any."); + } + } + + + /** + * Adds a node to the document + * @param string $name Name of the node. Prefixed namespaces are handled automatically. + * @param value $value [optional] value of the node (will be filtered). If value is not valid CDATA, + * it will be wrapped into a CDATA section + * @param array $attributes [optional] array of attributes. Prefixed namespaces are handled automatically. + * @return XML instance for the node that's been added. + */ + public function add_node($name, $value = NULL, $attributes = array()) + { + // Trim the name + $name = trim($name); + + // Create the element + $node = $this->create_element($name); + + // Add the attributes + $this->add_attributes($node, $attributes); + + // Add the value if provided + if ($value !== NULL) + { + $value = strval($this->filter($name, $value, $node)); + + if (str_replace(array('<', '>', '&'), "", $value) === $value) + { + // Value is valid CDATA, let's add it as a new text node + $value = $this->dom_doc->createTextNode($value); + } + else + { + // We shall create a CDATA section to wrap the text provided + $value = $this->dom_doc->createCDATASection($value); + } + $node->appendChild($value); + } + + // return a new XML instance of the same class from the child node + $class = get_class($this); + return new $class($this->dom_node->appendChild($node)); + } + + + + /** + * Magic get returns the first child node matching the value + * @param string $node_name + * @return mixed If trying to get a node: + * NULL will be return if nothing is matched, + * A string value is returned if it a text/cdata node is matched + * An XML instance is returned otherwise, allowing chaining. + */ + public function __get($value) + { + if ( ! isset($this->$value)) + { + $node = current($this->get($value)); + + if ($node instanceof XML) + { + // Return the whole XML document + return $node; + } + // We did not match any child nodes + return NULL; + } + parent::__get($value); + } + + + + /** + * Gets all nodes matching a name and returns them as an array. + * Can also be used to get a pointer to a particular node and then deal with that node as an XML instance. + * @param string $value name of the nodes desired + * @param bool $as_array [optional] whether or not the nodes should be returned as an array + * @return array Multi-dimensional array or array of XML instances + */ + public function get($value, $as_array = FALSE) + { + $return = array(); + + $value = $this->meta()->alias($value); + + foreach ($this->dom_node->getElementsByTagName($value) as $item) + { + if ($as_array) + { + // Return as array but ignore root node + $array = $this->_as_array($item); + foreach ($array as $val) + { + $return[] = $val; + } + } + else + { + $class = get_class($this); + $return[] = new $class($item); + } + } + return $return; + } + + + /** + * Queries the document with an XPath query + * @param string $query XPath query + * @param bool $as_array [optional] whether or not the nodes should be returned as an array + * @return array Multi-dimensional array or array of XML instances + */ + public function xpath($query, $as_array = TRUE) + { + $return = array(); + + $xpath = new DOMXPath($this->dom_doc); + + foreach ($xpath->query($query) as $item) + { + if ($as_array) + { + $array = $this->_as_array($item); + foreach ($array as $val) + { + $return[] = $val; + } + } + else + { + $class = get_class($this); + $return[] = new $class($item); + } + } + return $return; + } + + + + /** + * Exports the document as a multi-dimensional array. + * Handles element with the same name. + * + * Root node is ignored, as it is known and available in the driver. + * Example : + * + * + * value1 + * + * + * value2 + * + * + * + * Here's the resulting array structure : + * array ("node_name" => array( + * // array of nodes called "node_name" + * 0 => array( + * // Attributes of that node + * "xml_attributes" => array( + * "attr_name" => "val", + * ) + * // node contents + * "child_node_name" => array( + * // array of nodes called "child_node_name" + * 0 => value1, + * 1 => value2, + * ) + * The output is retro-actively convertible to XML using from_array(). + * @return array + */ + public function as_array() + { + $dom_element = $this->dom_node; + + $return = array(); + + // This function is run on a whole XML document and this is the root node. + // That root node shall be ignored in the array as it driven by the driver and handles document namespaces. + foreach($dom_element->childNodes as $dom_child) + { + if ($dom_child->nodeType == XML_ELEMENT_NODE) + { + // Let's run through the child nodes + $child = $this->_as_array($dom_child); + + foreach ($child as $key => $val) + { + $return[$key][]=$val; + } + } + } + + return $return; + } + + + + /** + * Recursive as_array for child nodes + * @param DOMNode $dom_node + * @return Array + */ + private function _as_array(DOMNode $dom_node) + { + // All other nodes shall be parsed normally : attributes then text value and child nodes, running through the XML tree + $object_element = array(); + + // Get the desired node name for this node + $node_name = $this->meta()->key($dom_node->tagName); + + // Get children, run through XML tree + if ($dom_node->hasChildNodes()) + { + if (!$dom_node->firstChild->hasChildNodes()) + { + // Get text value + $object_element[$node_name] = trim($dom_node->firstChild->nodeValue); + } + + foreach($dom_node->childNodes as $dom_child) + { + if ($dom_child->nodeType === XML_ELEMENT_NODE) + { + $child = $this->_as_array($dom_child); + + foreach ($child as $key=>$val) + { + $object_element[$node_name][$key][]=$val; + } + } + } + } + + // Get attributes + if ($dom_node->hasAttributes()) + { + $object_element[$dom_node->nodeName]['xml_attributes'] = array(); + foreach($dom_node->attributes as $att_name => $dom_attribute) + { + // Get the desired name for this attribute + $att_name = $this->meta()->key($att_name); + $object_element[$node_name]['xml_attributes'][$att_name] = $dom_attribute->value; + } + } + return $object_element; + } + + + /** + * Converts an array to XML. Expected structure is given in as_array(). + * However, from_array() is essentially more flexible regarding to the input array structure, + * as we don't have to bother about nodes having the same name. + * Try something logical, that should work as expected. + * @param object $mixed + * @return XML + */ + public function from_array($array) + { + $this->_from_array($array, $this->dom_node); + + return $this; + } + + + /** + * Array shall be like : array('element_name' => array( 0 => text, 'xml_attributes' => array())); + * @param object $mixed + * @param DOMElement $dom_element + * @return + */ + protected function _from_array($mixed, DOMElement $dom_element) + { + if (is_array($mixed)) + { + foreach( $mixed as $index => $mixed_element ) + { + if ( is_numeric($index) ) + { + // If we have numeric keys, we're having multiple children of the same node. + // Append the new node to the current node's parent + // If this is the first node to add, $node = $dom_element + $node = $dom_element; + if ( $index != 0 ) + { + // If not, lets create a copy of the node with the same name + $node = $this->create_element($dom_element->tagName); + // And append it to the parent node + $node = $dom_element->parentNode->appendChild($node); + } + $this->_from_array($mixed_element, $node); + } + elseif ($index == "xml_attributes") + { + // Add attributes to the node + $this->add_attributes($dom_element, $mixed_element); + } + else + { + // Create a new element with the key as the element name. + // Create the element corresponding to the key + $node = $this->create_element($index); + + // Add the driver attributes + $this->add_attributes($node); + + // Append it + $dom_element->appendChild($node); + + // Treat the array by recursion + $this->_from_array($mixed_element, $node); + } + } + } + elseif ($mixed) + { + // This is a string value that shall be appended as such + $mixed = $this->filter($dom_element->tagName, $mixed, $dom_element); + $dom_element->appendChild($this->dom_doc->createTextNode($mixed)); + } + } + + + /** + * This function is used to import another XML instance, or whatever we can construct XML from (string, filename, DOMNode...) + * + * $xml1 = XML::factory("atom", "bla"); + * $xml2 = XML::factory("rss", ""); + * $node_xml2 = $xml2->add_node("key"); + * + * // outputs "bla" + * $node_xml2->import($xml1)->render(); + * + * // outputs "blabla" + * $xml1->import($xml2->get("key"))->render(); + * + * @param object $xml XML instance or DOMNode + * @return object $this Chainable function + */ + public function import($xml) + { + if (! $xml instanceof XML) + { + // Attempt to construct XML from the input + $class = get_class($this); + $xml = new $class($xml); + } + // Import the node, and all its children, to the document + $node = $this->dom_doc->importNode($xml->dom_node, TRUE); + $this->dom_node->appendChild($node); + + return $this; + } + + + /** + * Creates an element, sorts out namespaces (default / prefixed) + * @param string $name element name + * @return DOMElement + */ + private function create_element($name) + { + $name = $this->meta()->alias($name); + + // Let's check if the element name has a namespace, and if this prefix is defined in our driver + if ($namespace_uri = $this->meta()->get("namespace", $name)) + { + if (stristr($name, ":")) + { + // Separate the namespace prefix and the name + list($prefix, $name) = explode(":", $name); + + // Register the prefixed namespace in the document root + $this->dom_doc->documentElement->setAttributeNS("http://www.w3.org/2000/xmlns/" ,"xmlns:".$prefix, $namespace_uri); + + // Create the prefixed element within that namespace + $node = $this->dom_doc->createElementNS($namespace_uri, $prefix.":".$name); + } + else + { + // Create the element normally + $node = $this->dom_doc->createElement($name); + + // Add the new default namespace as an attribute. + $node->setAttribute("xmlns", $namespace_uri); + } + } + else + { + // Simply create the element + $node = $this->dom_doc->createElement($name); + } + return $node; + } + + + /** + * Applies attributes to a node + * @param DOMNode $node + * @param array $attributes as key => value + * @return DOMNode + */ + private function add_attributes(DOMNode $node, $attributes = array()) + { + $node_name = $this->meta()->alias($node->tagName); + + if ($this->meta()->get("attributes", $node_name)) + { + $attributes = array_merge($this->meta()->get("attributes", $node_name), $attributes); + } + + foreach ($attributes as $key => $val) + { + // Trim elements + $key = $this->meta()->alias(trim($key)); + $val = $this->filter($key, trim($val), $node); + + // Set the attribute + // Let's check if the attribute name has a namespace prefix, and if this prefix is defined in our driver + if ($namespace_uri = $this->meta()->get("namespace", $key) + AND stristr($name, ":")) + { + // Separate the namespace prefix and the name + list($prefix, $name) = explode(":", $name); + + // Register the prefixed namespace + $this->dom_node->setAttributeNS("http://www.w3.org/2000/xmlns/" ,"xmlns:".$prefix, $namespace_uri); + + // Add the prefixed attribute within that namespace + $node->setAttributeNS($namespace_uri, $key, $val); + } + else + { + // Simply add the attribute + $node->setAttribute($key, $val); + } + } + return $node; + } + + + /** + * Applies filter on a value. + * These filters are callbacks usually defined in the driver. + * They allow to format dates, links, standard stuff, and play + * as you wish with the value before it is added to the document. + * + * You could even extend it and modify the node name. + * + * @param string $name + * @param string $value + * @return string $value formatted value + */ + protected function filter($name, $value, &$node) + { + $name = $this->meta()->alias($name); + + if ($this->meta()->get("filter", $name)) + { + return call_user_func(array($this, $this->meta()->get("filter", $name)), $value, $node); + } + return $value; + } + + + /** + * This is a classic filter that takes a uri and makes a proper link + * @param object $value + * @return $value + */ + public function normalize_uri($value, $node) + { + if (strpos($value, '://') === FALSE) + { + if (strlen(URL::base()) > 1 AND stristr($value, URL::base())) + { + // Make sure the path is not base related + $value = str_replace(URL::base(), '', $value); + } + // Convert URIs to URLs + $value = URL::site($value, TRUE); + } + return $value; + } + + + /** + * Another classic filter to deal with boolean + * @param boolean $value + * @return string $value, true or false + */ + public function normalize_bool($value) + { + return $value ? "true" : "false"; + } + + + /** + * Returns this drivers XML metadata + * @return XML_Meta + */ + public function meta() + { + return XML::$_metas[strtolower(get_class($this))]; + } + + + /** + * Outputs nicely formatted XML when converting as string + * @return string + */ + public function __toString() + { + return $this->render(TRUE); + } + + + /** + * Render the XML. + * @param boolean $formatted [optional] Should the output be formatted and indented ? + * @return string + */ + public function render($formatted = FALSE) + { + $this->dom_doc->formatOutput = $formatted; + return $this->dom_doc->saveXML(); + } + + + /** + * Outputs the XML in a file + * @param string filename + * @return + */ + public function export($file) + { + return $this->dom_doc->save($file); + } + + + /** + * Returns this instance node value, if the dom_node is a text node + * + * @return string + */ + public function value() + { + if ($this->dom_node->hasChildNodes() AND $this->dom_node->firstChild->nodeType === XML_TEXT_NODE) + { + return $this->dom_node->nodeValue; + } + return NULL; + } + + + /** + * Returns this instance node value + * + * @return string|array attributes as array of attribute value if a name is specified + */ + public function attributes($attribute_name = NULL) + { + if ($attribute_name === NULL) + { + // Return an array of attributes + $attributes = array(); + + if ($this->dom_node->hasAttributes()) + { + foreach ($this->dom_node->attributes as $attribute) + { + $attributes[$attribute->name] = $attribute->value; + } + } + return $attributes; + } + + // Simply return the attribute value + return $this->dom_node->getAttribute($attribute_name); + } +} // End XML_Core diff --git a/includes/kohana/modules/xml/classes/xml/driver/atom.php b/includes/kohana/modules/xml/classes/xml/driver/atom.php new file mode 100644 index 0000000..4098964 --- /dev/null +++ b/includes/kohana/modules/xml/classes/xml/driver/atom.php @@ -0,0 +1,101 @@ + + * + * Description: + * Atom driver + */ + +class XML_Driver_Atom extends XML +{ + public $root_node = 'feed'; + + + protected static function initialize(XML_Meta $meta) + { + $meta ->content_type("application/atom+xml") + ->nodes ( + array( + "feed" => array("namespace" => "http://www.w3.org/2005/Atom"), + // "entry" => array("namespace" => "http://www.w3.org/2005/Atom"), + "href" => array("filter" => "normalize_uri"), + "link" => array("filter" => "normalize_uri"), + "logo" => array("filter" => "normalize_uri"), + "icon" => array("filter" => "normalize_uri"), + "id" => array("filter" => "normalize_uri"), + "updated" => array("filter" => "normalize_datetime"), + "published" => array("filter" => "normalize_datetime"), + "startDate" => array("filter" => "normalize_date"), + 'endDate' => array("filter" => "normalize_date"), + "summary" => array("filter" => "normalize_text"), + "subtitle" => array("filter" => "normalize_text"), + "title" => array("filter" => "normalize_text"), + "content" => array("filter" => "normalize_text") + ) + ); + } + + + public function add_person($type, $name, $email = NULL, $uri = NULL) + { + $author = $this->add_node($type); + $author->add_node("name", $name); + if ($email) + { + $author->add_node("email", $email); + } + if ($uri) + { + $author->add_node("uri", $uri); + } + return $this; + } + + + public function add_content(XML $xml_document) + { + $this->add_node("content", NULL, array("type" => $xml_document->meta()->content_type()))->import($xml_document); + return $this; + } + + + public function normalize_text($value, $node) + { + if (strpos($value, "<") >= 0 AND strpos($value, ">") > 0) + { + // Assume type = html + $node->setAttribute("type", "html"); + } + else + { + $node->setAttribute("type", "text"); + } + return $value; + } + + + public function normalize_datetime($value) + { + if ( ! is_numeric($value)) + { + $value = strtotime($value); + } + + // Convert timestamps to RFC 3339 formatted datetime + return date(DATE_RFC3339, $value); + } + + + public function normalize_date($value) + { + if ( ! is_numeric($value)) + { + $value = strtotime($value); + } + + // Convert timestamps to RFC 3339 formatted dates + return date("Y-m-d", $value); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/xml/classes/xml/driver/rss2.php b/includes/kohana/modules/xml/classes/xml/driver/rss2.php new file mode 100644 index 0000000..b521763 --- /dev/null +++ b/includes/kohana/modules/xml/classes/xml/driver/rss2.php @@ -0,0 +1,57 @@ + + * + * Description: + * RSS2 driver + */ + +class XML_Driver_Rss2 extends XML +{ + public $root_node = 'rss'; + + protected static function initialize(XML_Meta $meta) + { + $meta ->content_type("application/rss+xml") + ->nodes ( + array( + "rss" => array("attributes" => array("version" => "2.0")), + "title" => array("filter" => "normalize_text"), + "description" => array("filter" => "normalize_text"), + "link" => array("filter" => "normalize_uri"), + "atom:link" => array("attributes" => array( + "rel" => "self", + "type" => "application/rss+xml", + // "href" => URL::site(Request::initial()->uri(), TRUE) + ), + "namespace" => "http://www.w3.org/2005/Atom"), + "href" => array("filter" => "normalize_uri"), + "docs" => array("filter" => "normalize_uri"), + "guid" => array("filter" => "normalize_uri"), + "pubDate" => array("filter" => "normalize_date"), + "lastBuildDate" => array("filter" => "normalize_date") + ) + ); + } + + + public function normalize_date($value) + { + if ( ! is_numeric($value)) + { + $value = strtotime($value); + } + + // Convert timestamps to RFC 822 formatted dates, with 4 digits year + return date(DATE_RSS, $value); + } + + + public function normalize_text($value) + { + // Strip HTML tags + return strip_tags($value); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/xml/classes/xml/driver/xrds.php b/includes/kohana/modules/xml/classes/xml/driver/xrds.php new file mode 100644 index 0000000..2f536d2 --- /dev/null +++ b/includes/kohana/modules/xml/classes/xml/driver/xrds.php @@ -0,0 +1,55 @@ + + * + * Description: + * XRDS driver. For Service Discovery. + */ + +class XML_Driver_XRDS extends XML +{ + public $root_node = 'xrds:XRDS'; + + protected static function initialize(XML_Meta $meta) + { + $meta ->content_type("application/xrds+xml") + ->nodes ( + array( + "xrds:XRDS" => array("namespace" => 'xri://$xrds', "attributes" => array("xmlns" => 'xri://$xrd*($v*2.0)')), + "LocalID" => array("filter" => "normalize_uri"), + "openid:Delegate" => array("filter" => "normalize_uri", "namespace" => "http://openid.net/xmlns/1.0"), + "URI" => array("filter" => "normalize_uri"), + ) + ); + } + + + public function add_service($type, $uri, $priority = NULL) + { + if (! is_null($priority)) + { + $priority = array("priority" => $priority); + } + else + { + $priority = array(); + } + + $service_node = $this->add_node("Service", NULL, $priority); + + if (! is_array($type)) + { + $type = array($type); + } + + foreach ($type as $t) + { + $service_node->add_node("Type", $t); + } + $service_node->add_node("URI", $uri); + + return $service_node; + } +} \ No newline at end of file diff --git a/includes/kohana/modules/xml/classes/xml/meta.php b/includes/kohana/modules/xml/classes/xml/meta.php new file mode 100644 index 0000000..35df69d --- /dev/null +++ b/includes/kohana/modules/xml/classes/xml/meta.php @@ -0,0 +1,12 @@ + + * + * Description: + * XML_Meta class. Use this to override XML_Meta_Core + */ + + class XML_Meta extends XML_Meta_Core + {} diff --git a/includes/kohana/modules/xml/classes/xml/meta/core.php b/includes/kohana/modules/xml/classes/xml/meta/core.php new file mode 100644 index 0000000..93f699a --- /dev/null +++ b/includes/kohana/modules/xml/classes/xml/meta/core.php @@ -0,0 +1,187 @@ + + * + * Description: + * XML_Meta_Core class. This class contains XML drivers metadata + */ + + class XML_Meta_Core + { + /** + * @var array assoc array alias => $node_name + * This is used to abstract the node names + */ + protected $nodes = array(); + + /** + * @var array whole node configuration array + * array("node_name" => array( + * // Effective node name in the XML document. + * // This name is abstracted and "node_name" is always used when dealing with the object. + * "node" => "effective_node_name", + * // Defines a namespace URI for this node + * "namespace" => "http://www.namespace.uri", + * // Defines a prefix for the namespace above. If not defined, namespace is interpreted as a default namespace + * "prefix" => "ns", + * // Defines a callback function to filter/normalize the value + * "filter" => "filter_function_name", + * // Array of attributes + * "attributes" => array("default_attribute1" => "value") + * ), + * "alias" => "node_name", + * ) + */ + protected $nodes_config = array(); + + /** + * @var string content type for HTML headers + */ + protected $content_type; + + /** + * @var boolean whether the object is initialized + */ + protected $_initialized = FALSE; + + + /** + * Returns the name of a node, sort out aliases + * @param string $name + * @return string $node_name + */ + public function alias($name) + { + if (isset($this->nodes_config[$name])) + { + if ( ! is_array($this->nodes_config[$name])) + { + $name = $this->nodes_config[$name]; + } + } + + return Arr::get($this->nodes, $name, $name); + } + + /** + * Returns the value of a meta key for a given node name + * exemple $this->get('attributes', 'feed') will return all the attributes set up in the meta + * for the node feed. + * @param object $key meta key + * @param object $name node name + * @return meta value or NULL if not set + */ + public function get($key, $name) + { + $name = $this->alias($name); + + if (isset($this->nodes_config[$name]) AND is_array($this->nodes_config[$name]) AND array_key_exists($key, $this->nodes_config[$name])) + { + return $this->nodes_config[$name][$key]; + } + return NULL; + } + + + /** + * Set nodes config attribute + * Use it this way : + * nodes(array("node_name" => array("namespace" => "http://www.namespace.uri", "prefix" => "ns", "filter" => "filter_function_name", "attributes" => array("default_attribute1" => "value")))), + * OR to set up node alias names : + * nodes(array("alias" => "node_name")); + * + * @param array $nodes array formatted as mentionned above + * @param bool $overwrite [optional] Overwrite current values if they are set ? + * @return object $this + */ + public function nodes(Array $nodes) + { + $this->nodes_config = $this->_initialized ? + array_merge($nodes, $this->nodes_config) : + array_merge($this->nodes_config, $nodes); + + $this->generate_nodes_map(); + + return $this; + } + + + /** + * Sets the content type for headers + * @param string $type + * @return object $this + */ + public function content_type($type = NULL) + { + if ($type) + { + $this->content_type = $this->_initialized ? + $type : + $this->content_type ? + $this->content_type : + $type; + } + else + { + return $this->content_type; + } + + return $this; + } + + + /** + * Returns the key name corresponding to a node name + * This is used when using as_array(), to return array keys corresponding to the node names + * @param object $node_name + * @return + */ + public function key($node_name) + { + // Extract the name if it is prefixed + $expl = explode(":", $node_name); + $node_name = count($expl) > 1 ? end($expl) : current($expl); + + if (in_array($node_name, $this->nodes)) + { + return current(array_keys($this->nodes, $node_name)); + } + return $node_name; + } + + + /** + * Generates - or re-generates the node map + * @return object $this + */ + public function generate_nodes_map() + { + $map = array(); + foreach ($this->nodes_config as $key => $config) + { + if (is_array($config)) + { + if (isset ($config["node"])) + { + $map[$key] = $config["node"]; + } + } + } + $this->nodes = $map; + return $this; + } + + /** + * Reports the Meta as initialized. + * This basically allows Meta methods to overwrite existing value, if they are called explicitely + * @return object $this + */ + public function set_initialized() + { + $this->_initialized = TRUE; + return $this; + } + +} \ No newline at end of file