From 41e52c91b804637db5a6eb8722c42844c1e1dfcf Mon Sep 17 00:00:00 2001
From: AutumnsWind <kalone.cool@gmail.com>
Date: Wed, 25 Nov 2015 10:04:05 +0800
Subject: [PATCH] Update README.md

---
 README/Cache/Cache.php                             |  255 +++
 README/Cache/drivers/Cache_apc.php                 |  210 ++
 README/Cache/drivers/Cache_dummy.php               |  172 ++
 README/Cache/drivers/Cache_file.php                |  286 +++
 README/Cache/drivers/Cache_memcached.php           |  289 +++
 README/Cache/drivers/Cache_redis.php               |  320 +++
 README/Cache/drivers/Cache_wincache.php            |  206 ++
 README/Cache/drivers/index.html                    |   11 +
 README/Cache/index.html                            |   11 +
 README/Calendar.php                                |  546 +++++
 README/Cart.php                                    |  567 +++++
 README/Driver.php                                  |  342 +++
 README/Email.php                                   | 2314 ++++++++++++++++++++
 README/Encrypt.php                                 |  480 ++++
 README/Encryption.php                              |  926 ++++++++
 README/Form_validation.php                         | 1595 ++++++++++++++
 README/Ftp.php                                     |  667 ++++++
 README/Image_lib.php                               | 1825 +++++++++++++++
 README/Javascript.php                              |  856 ++++++++
 README/Javascript/Jquery.php                       | 1076 +++++++++
 README/Javascript/index.html                       |   11 +
 README/Migration.php                               |  446 ++++
 README/Pagination.php                              |  697 ++++++
 README/Parser.php                                  |  248 +++
 README/Profiler.php                                |  570 +++++
 README/README.php                                  | 1026 +++++++++
 README/Session/Session.php                         |  890 ++++++++
 README/Session/SessionHandlerInterface.php         |   59 +
 README/Session/Session_driver.php                  |  148 ++
 README/Session/drivers/Session_database_driver.php |  389 ++++
 README/Session/drivers/Session_files_driver.php    |  362 +++
 .../Session/drivers/Session_memcached_driver.php   |  363 +++
 README/Session/drivers/Session_redis_driver.php    |  381 ++++
 README/Session/drivers/index.html                  |   11 +
 README/Session/index.html                          |   11 +
 README/Table.php                                   |  537 +++++
 README/Trackback.php                               |  556 +++++
 README/Typography.php                              |  424 ++++
 README/Unit_test.php                               |  407 ++++
 README/Upload.php                                  | 1308 +++++++++++
 README/User_agent.php                              |  683 ++++++
 README/Xmlrpc.php                                  | 1923 ++++++++++++++++
 README/Xmlrpcs.php                                 |  625 ++++++
 README/Zip.php                                     |  484 ++++
 README/index.html                                  |   11 +
 45 files changed, 25524 insertions(+)
 create mode 100644 README/Cache/Cache.php
 create mode 100644 README/Cache/drivers/Cache_apc.php
 create mode 100644 README/Cache/drivers/Cache_dummy.php
 create mode 100644 README/Cache/drivers/Cache_file.php
 create mode 100644 README/Cache/drivers/Cache_memcached.php
 create mode 100644 README/Cache/drivers/Cache_redis.php
 create mode 100644 README/Cache/drivers/Cache_wincache.php
 create mode 100644 README/Cache/drivers/index.html
 create mode 100644 README/Cache/index.html
 create mode 100644 README/Calendar.php
 create mode 100644 README/Cart.php
 create mode 100644 README/Driver.php
 create mode 100644 README/Email.php
 create mode 100644 README/Encrypt.php
 create mode 100644 README/Encryption.php
 create mode 100644 README/Form_validation.php
 create mode 100644 README/Ftp.php
 create mode 100644 README/Image_lib.php
 create mode 100644 README/Javascript.php
 create mode 100644 README/Javascript/Jquery.php
 create mode 100644 README/Javascript/index.html
 create mode 100644 README/Migration.php
 create mode 100644 README/Pagination.php
 create mode 100644 README/Parser.php
 create mode 100644 README/Profiler.php
 create mode 100644 README/README.php
 create mode 100644 README/Session/Session.php
 create mode 100644 README/Session/SessionHandlerInterface.php
 create mode 100644 README/Session/Session_driver.php
 create mode 100644 README/Session/drivers/Session_database_driver.php
 create mode 100644 README/Session/drivers/Session_files_driver.php
 create mode 100644 README/Session/drivers/Session_memcached_driver.php
 create mode 100644 README/Session/drivers/Session_redis_driver.php
 create mode 100644 README/Session/drivers/index.html
 create mode 100644 README/Session/index.html
 create mode 100644 README/Table.php
 create mode 100644 README/Trackback.php
 create mode 100644 README/Typography.php
 create mode 100644 README/Unit_test.php
 create mode 100644 README/Upload.php
 create mode 100644 README/User_agent.php
 create mode 100644 README/Xmlrpc.php
 create mode 100644 README/Xmlrpcs.php
 create mode 100644 README/Zip.php
 create mode 100644 README/index.html

diff --git a/README/Cache/Cache.php b/README/Cache/Cache.php
new file mode 100644
index 0000000..0c87a56
--- /dev/null
+++ b/README/Cache/Cache.php
@@ -0,0 +1,255 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 2.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Caching Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Core
+ * @author		EllisLab Dev Team
+ * @link
+ */
+class CI_Cache extends CI_Driver_Library {
+
+	/**
+	 * Valid cache drivers
+	 *
+	 * @var array
+	 */
+	protected $valid_drivers = array(
+		'apc',
+		'dummy',
+		'file',
+		'memcached',
+		'redis',
+		'wincache'
+	);
+
+	/**
+	 * Path of cache files (if file-based cache)
+	 *
+	 * @var string
+	 */
+	protected $_cache_path = NULL;
+
+	/**
+	 * Reference to the driver
+	 *
+	 * @var mixed
+	 */
+	protected $_adapter = 'dummy';
+
+	/**
+	 * Fallback driver
+	 *
+	 * @var string
+	 */
+	protected $_backup_driver = 'dummy';
+
+	/**
+	 * Cache key prefix
+	 *
+	 * @var	string
+	 */
+	public $key_prefix = '';
+
+	/**
+	 * Constructor
+	 *
+	 * Initialize class properties based on the configuration array.
+	 *
+	 * @param	array	$config = array()
+	 * @return	void
+	 */
+	public function __construct($config = array())
+	{
+		isset($config['adapter']) && $this->_adapter = $config['adapter'];
+		isset($config['backup']) && $this->_backup_driver = $config['backup'];
+		isset($config['key_prefix']) && $this->key_prefix = $config['key_prefix'];
+
+		// If the specified adapter isn't available, check the backup.
+		if ( ! $this->is_supported($this->_adapter))
+		{
+			if ( ! $this->is_supported($this->_backup_driver))
+			{
+				// Backup isn't supported either. Default to 'Dummy' driver.
+				log_message('error', 'Cache adapter "'.$this->_adapter.'" and backup "'.$this->_backup_driver.'" are both unavailable. Cache is now using "Dummy" adapter.');
+				$this->_adapter = 'dummy';
+			}
+			else
+			{
+				// Backup is supported. Set it to primary.
+				log_message('debug', 'Cache adapter "'.$this->_adapter.'" is unavailable. Falling back to "'.$this->_backup_driver.'" backup adapter.');
+				$this->_adapter = $this->_backup_driver;
+			}
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get
+	 *
+	 * Look for a value in the cache. If it exists, return the data
+	 * if not, return FALSE
+	 *
+	 * @param	string	$id
+	 * @return	mixed	value matching $id or FALSE on failure
+	 */
+	public function get($id)
+	{
+		return $this->{$this->_adapter}->get($this->key_prefix.$id);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Save
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	mixed	$data	Data to store
+	 * @param	int	$ttl	Cache TTL (in seconds)
+	 * @param	bool	$raw	Whether to store the raw value
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function save($id, $data, $ttl = 60, $raw = FALSE)
+	{
+		return $this->{$this->_adapter}->save($this->key_prefix.$id, $data, $ttl, $raw);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Delete from Cache
+	 *
+	 * @param	string	$id	Cache ID
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function delete($id)
+	{
+		return $this->{$this->_adapter}->delete($this->key_prefix.$id);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Increment a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to add
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function increment($id, $offset = 1)
+	{
+		return $this->{$this->_adapter}->increment($this->key_prefix.$id, $offset);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Decrement a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to reduce by
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function decrement($id, $offset = 1)
+	{
+		return $this->{$this->_adapter}->decrement($this->key_prefix.$id, $offset);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Clean the cache
+	 *
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function clean()
+	{
+		return $this->{$this->_adapter}->clean();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Info
+	 *
+	 * @param	string	$type = 'user'	user/filehits
+	 * @return	mixed	array containing cache info on success OR FALSE on failure
+	 */
+	public function cache_info($type = 'user')
+	{
+		return $this->{$this->_adapter}->cache_info($type);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get Cache Metadata
+	 *
+	 * @param	string	$id	key to get cache metadata on
+	 * @return	mixed	cache item metadata
+	 */
+	public function get_metadata($id)
+	{
+		return $this->{$this->_adapter}->get_metadata($this->key_prefix.$id);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Is the requested driver supported in this environment?
+	 *
+	 * @param	string	$driver	The driver to test
+	 * @return	array
+	 */
+	public function is_supported($driver)
+	{
+		static $support;
+
+		if ( ! isset($support, $support[$driver]))
+		{
+			$support[$driver] = $this->{$driver}->is_supported();
+		}
+
+		return $support[$driver];
+	}
+}
diff --git a/README/Cache/drivers/Cache_apc.php b/README/Cache/drivers/Cache_apc.php
new file mode 100644
index 0000000..e0d2ffb
--- /dev/null
+++ b/README/Cache/drivers/Cache_apc.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 2.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter APC Caching Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Core
+ * @author		EllisLab Dev Team
+ * @link
+ */
+class CI_Cache_apc extends CI_Driver {
+
+	/**
+	 * Get
+	 *
+	 * Look for a value in the cache. If it exists, return the data
+	 * if not, return FALSE
+	 *
+	 * @param	string
+	 * @return	mixed	value that is stored/FALSE on failure
+	 */
+	public function get($id)
+	{
+		$success = FALSE;
+		$data = apc_fetch($id, $success);
+
+		if ($success === TRUE)
+		{
+			return is_array($data)
+				? unserialize($data[0])
+				: $data;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Save
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	mixed	$data	Data to store
+	 * @param	int	$ttol	Length of time (in seconds) to cache the data
+	 * @param	bool	$raw	Whether to store the raw value
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function save($id, $data, $ttl = 60, $raw = FALSE)
+	{
+		$ttl = (int) $ttl;
+
+		return apc_store(
+			$id,
+			($raw === TRUE ? $data : array(serialize($data), time(), $ttl)),
+			$ttl
+		);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Delete from Cache
+	 *
+	 * @param	mixed	unique identifier of the item in the cache
+	 * @return	bool	true on success/false on failure
+	 */
+	public function delete($id)
+	{
+		return apc_delete($id);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Increment a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to add
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function increment($id, $offset = 1)
+	{
+		return apc_inc($id, $offset);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Decrement a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to reduce by
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function decrement($id, $offset = 1)
+	{
+		return apc_dec($id, $offset);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Clean the cache
+	 *
+	 * @return	bool	false on failure/true on success
+	 */
+	public function clean()
+	{
+		return apc_clear_cache('user');
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Info
+	 *
+	 * @param	string	user/filehits
+	 * @return	mixed	array on success, false on failure
+	 */
+	 public function cache_info($type = NULL)
+	 {
+		 return apc_cache_info($type);
+	 }
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get Cache Metadata
+	 *
+	 * @param	mixed	key to get cache metadata on
+	 * @return	mixed	array on success/false on failure
+	 */
+	public function get_metadata($id)
+	{
+		$success = FALSE;
+		$stored = apc_fetch($id, $success);
+
+		if ($success === FALSE OR count($stored) !== 3)
+		{
+			return FALSE;
+		}
+
+		list($data, $time, $ttl) = $stored;
+
+		return array(
+			'expire'	=> $time + $ttl,
+			'mtime'		=> $time,
+			'data'		=> unserialize($data)
+		);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * is_supported()
+	 *
+	 * Check to see if APC is available on this system, bail if it isn't.
+	 *
+	 * @return	bool
+	 */
+	public function is_supported()
+	{
+		if ( ! extension_loaded('apc') OR ! ini_get('apc.enabled'))
+		{
+			log_message('debug', 'The APC PHP extension must be loaded to use APC Cache.');
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+}
diff --git a/README/Cache/drivers/Cache_dummy.php b/README/Cache/drivers/Cache_dummy.php
new file mode 100644
index 0000000..bf80945
--- /dev/null
+++ b/README/Cache/drivers/Cache_dummy.php
@@ -0,0 +1,172 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 2.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Dummy Caching Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Core
+ * @author		EllisLab Dev Team
+ * @link
+ */
+class CI_Cache_dummy extends CI_Driver {
+
+	/**
+	 * Get
+	 *
+	 * Since this is the dummy class, it's always going to return FALSE.
+	 *
+	 * @param	string
+	 * @return	bool	FALSE
+	 */
+	public function get($id)
+	{
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Save
+	 *
+	 * @param	string	Unique Key
+	 * @param	mixed	Data to store
+	 * @param	int	Length of time (in seconds) to cache the data
+	 * @param	bool	Whether to store the raw value
+	 * @return	bool	TRUE, Simulating success
+	 */
+	public function save($id, $data, $ttl = 60, $raw = FALSE)
+	{
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Delete from Cache
+	 *
+	 * @param	mixed	unique identifier of the item in the cache
+	 * @return	bool	TRUE, simulating success
+	 */
+	public function delete($id)
+	{
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Increment a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to add
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function increment($id, $offset = 1)
+	{
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Decrement a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to reduce by
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function decrement($id, $offset = 1)
+	{
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Clean the cache
+	 *
+	 * @return	bool	TRUE, simulating success
+	 */
+	public function clean()
+	{
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Info
+	 *
+	 * @param	string	user/filehits
+	 * @return	bool	FALSE
+	 */
+	 public function cache_info($type = NULL)
+	 {
+		 return FALSE;
+	 }
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get Cache Metadata
+	 *
+	 * @param	mixed	key to get cache metadata on
+	 * @return	bool	FALSE
+	 */
+	public function get_metadata($id)
+	{
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Is this caching driver supported on the system?
+	 * Of course this one is.
+	 *
+	 * @return	bool	TRUE
+	 */
+	public function is_supported()
+	{
+		return TRUE;
+	}
+
+}
diff --git a/README/Cache/drivers/Cache_file.php b/README/Cache/drivers/Cache_file.php
new file mode 100644
index 0000000..c046f3b
--- /dev/null
+++ b/README/Cache/drivers/Cache_file.php
@@ -0,0 +1,286 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 2.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter File Caching Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Core
+ * @author		EllisLab Dev Team
+ * @link
+ */
+class CI_Cache_file extends CI_Driver {
+
+	/**
+	 * Directory in which to save cache files
+	 *
+	 * @var string
+	 */
+	protected $_cache_path;
+
+	/**
+	 * Initialize file-based cache
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$CI =& get_instance();
+		$CI->load->helper('file');
+		$path = $CI->config->item('cache_path');
+		$this->_cache_path = ($path === '') ? APPPATH.'cache/' : $path;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Fetch from cache
+	 *
+	 * @param	string	$id	Cache ID
+	 * @return	mixed	Data on success, FALSE on failure
+	 */
+	public function get($id)
+	{
+		$data = $this->_get($id);
+		return is_array($data) ? $data['data'] : FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Save into cache
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	mixed	$data	Data to store
+	 * @param	int	$ttl	Time to live in seconds
+	 * @param	bool	$raw	Whether to store the raw value (unused)
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function save($id, $data, $ttl = 60, $raw = FALSE)
+	{
+		$contents = array(
+			'time'		=> time(),
+			'ttl'		=> $ttl,
+			'data'		=> $data
+		);
+
+		if (write_file($this->_cache_path.$id, serialize($contents)))
+		{
+			chmod($this->_cache_path.$id, 0640);
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Delete from Cache
+	 *
+	 * @param	mixed	unique identifier of item in cache
+	 * @return	bool	true on success/false on failure
+	 */
+	public function delete($id)
+	{
+		return file_exists($this->_cache_path.$id) ? unlink($this->_cache_path.$id) : FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Increment a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to add
+	 * @return	New value on success, FALSE on failure
+	 */
+	public function increment($id, $offset = 1)
+	{
+		$data = $this->_get($id);
+
+		if ($data === FALSE)
+		{
+			$data = array('data' => 0, 'ttl' => 60);
+		}
+		elseif ( ! is_int($data['data']))
+		{
+			return FALSE;
+		}
+
+		$new_value = $data['data'] + $offset;
+		return $this->save($id, $new_value, $data['ttl'])
+			? $new_value
+			: FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Decrement a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to reduce by
+	 * @return	New value on success, FALSE on failure
+	 */
+	public function decrement($id, $offset = 1)
+	{
+		$data = $this->_get($id);
+
+		if ($data === FALSE)
+		{
+			$data = array('data' => 0, 'ttl' => 60);
+		}
+		elseif ( ! is_int($data['data']))
+		{
+			return FALSE;
+		}
+
+		$new_value = $data['data'] - $offset;
+		return $this->save($id, $new_value, $data['ttl'])
+			? $new_value
+			: FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Clean the Cache
+	 *
+	 * @return	bool	false on failure/true on success
+	 */
+	public function clean()
+	{
+		return delete_files($this->_cache_path, FALSE, TRUE);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Info
+	 *
+	 * Not supported by file-based caching
+	 *
+	 * @param	string	user/filehits
+	 * @return	mixed	FALSE
+	 */
+	public function cache_info($type = NULL)
+	{
+		return get_dir_file_info($this->_cache_path);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get Cache Metadata
+	 *
+	 * @param	mixed	key to get cache metadata on
+	 * @return	mixed	FALSE on failure, array on success.
+	 */
+	public function get_metadata($id)
+	{
+		if ( ! file_exists($this->_cache_path.$id))
+		{
+			return FALSE;
+		}
+
+		$data = unserialize(file_get_contents($this->_cache_path.$id));
+
+		if (is_array($data))
+		{
+			$mtime = filemtime($this->_cache_path.$id);
+
+			if ( ! isset($data['ttl']))
+			{
+				return FALSE;
+			}
+
+			return array(
+				'expire' => $mtime + $data['ttl'],
+				'mtime'	 => $mtime
+			);
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Is supported
+	 *
+	 * In the file driver, check to see that the cache directory is indeed writable
+	 *
+	 * @return	bool
+	 */
+	public function is_supported()
+	{
+		return is_really_writable($this->_cache_path);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get all data
+	 *
+	 * Internal method to get all the relevant data about a cache item
+	 *
+	 * @param	string	$id	Cache ID
+	 * @return	mixed	Data array on success or FALSE on failure
+	 */
+	protected function _get($id)
+	{
+		if ( ! is_file($this->_cache_path.$id))
+		{
+			return FALSE;
+		}
+
+		$data = unserialize(file_get_contents($this->_cache_path.$id));
+
+		if ($data['ttl'] > 0 && time() > $data['time'] + $data['ttl'])
+		{
+			unlink($this->_cache_path.$id);
+			return FALSE;
+		}
+
+		return $data;
+	}
+
+}
diff --git a/README/Cache/drivers/Cache_memcached.php b/README/Cache/drivers/Cache_memcached.php
new file mode 100644
index 0000000..59cf468
--- /dev/null
+++ b/README/Cache/drivers/Cache_memcached.php
@@ -0,0 +1,289 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 2.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Memcached Caching Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Core
+ * @author		EllisLab Dev Team
+ * @link
+ */
+class CI_Cache_memcached extends CI_Driver {
+
+	/**
+	 * Holds the memcached object
+	 *
+	 * @var object
+	 */
+	protected $_memcached;
+
+	/**
+	 * Memcached configuration
+	 *
+	 * @var array
+	 */
+	protected $_memcache_conf = array(
+		'default' => array(
+			'host'		=> '127.0.0.1',
+			'port'		=> 11211,
+			'weight'	=> 1
+		)
+	);
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Setup Memcache(d)
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		// Try to load memcached server info from the config file.
+		$CI =& get_instance();
+		$defaults = $this->_memcache_conf['default'];
+
+		if ($CI->config->load('memcached', TRUE, TRUE))
+		{
+			if (is_array($CI->config->config['memcached']))
+			{
+				$this->_memcache_conf = array();
+
+				foreach ($CI->config->config['memcached'] as $name => $conf)
+				{
+					$this->_memcache_conf[$name] = $conf;
+				}
+			}
+		}
+
+		if (class_exists('Memcached', FALSE))
+		{
+			$this->_memcached = new Memcached();
+		}
+		elseif (class_exists('Memcache', FALSE))
+		{
+			$this->_memcached = new Memcache();
+		}
+		else
+		{
+			log_message('error', 'Cache: Failed to create Memcache(d) object; extension not loaded?');
+		}
+
+		foreach ($this->_memcache_conf as $cache_server)
+		{
+			isset($cache_server['hostname']) OR $cache_server['hostname'] = $defaults['host'];
+			isset($cache_server['port']) OR $cache_server['port'] = $defaults['port'];
+			isset($cache_server['weight']) OR $cache_server['weight'] = $defaults['weight'];
+
+			if (get_class($this->_memcached) === 'Memcache')
+			{
+				// Third parameter is persistance and defaults to TRUE.
+				$this->_memcached->addServer(
+					$cache_server['hostname'],
+					$cache_server['port'],
+					TRUE,
+					$cache_server['weight']
+				);
+			}
+			else
+			{
+				$this->_memcached->addServer(
+					$cache_server['hostname'],
+					$cache_server['port'],
+					$cache_server['weight']
+				);
+			}
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Fetch from cache
+	 *
+	 * @param	string	$id	Cache ID
+	 * @return	mixed	Data on success, FALSE on failure
+	 */
+	public function get($id)
+	{
+		$data = $this->_memcached->get($id);
+
+		return is_array($data) ? $data[0] : $data;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Save
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	mixed	$data	Data being cached
+	 * @param	int	$ttl	Time to live
+	 * @param	bool	$raw	Whether to store the raw value
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function save($id, $data, $ttl = 60, $raw = FALSE)
+	{
+		if ($raw !== TRUE)
+		{
+			$data = array($data, time(), $ttl);
+		}
+
+		if (get_class($this->_memcached) === 'Memcached')
+		{
+			return $this->_memcached->set($id, $data, $ttl);
+		}
+		elseif (get_class($this->_memcached) === 'Memcache')
+		{
+			return $this->_memcached->set($id, $data, 0, $ttl);
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Delete from Cache
+	 *
+	 * @param	mixed	key to be deleted.
+	 * @return	bool	true on success, false on failure
+	 */
+	public function delete($id)
+	{
+		return $this->_memcached->delete($id);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Increment a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to add
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function increment($id, $offset = 1)
+	{
+		return $this->_memcached->increment($id, $offset);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Decrement a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to reduce by
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function decrement($id, $offset = 1)
+	{
+		return $this->_memcached->decrement($id, $offset);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Clean the Cache
+	 *
+	 * @return	bool	false on failure/true on success
+	 */
+	public function clean()
+	{
+		return $this->_memcached->flush();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Info
+	 *
+	 * @return	mixed	array on success, false on failure
+	 */
+	public function cache_info()
+	{
+		return $this->_memcached->getStats();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get Cache Metadata
+	 *
+	 * @param	mixed	key to get cache metadata on
+	 * @return	mixed	FALSE on failure, array on success.
+	 */
+	public function get_metadata($id)
+	{
+		$stored = $this->_memcached->get($id);
+
+		if (count($stored) !== 3)
+		{
+			return FALSE;
+		}
+
+		list($data, $time, $ttl) = $stored;
+
+		return array(
+			'expire'	=> $time + $ttl,
+			'mtime'		=> $time,
+			'data'		=> $data
+		);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Is supported
+	 *
+	 * Returns FALSE if memcached is not supported on the system.
+	 * If it is, we setup the memcached object & return TRUE
+	 *
+	 * @return	bool
+	 */
+	public function is_supported()
+	{
+		return (extension_loaded('memcached') OR extension_loaded('memcache'));
+	}
+}
diff --git a/README/Cache/drivers/Cache_redis.php b/README/Cache/drivers/Cache_redis.php
new file mode 100644
index 0000000..ea0059f
--- /dev/null
+++ b/README/Cache/drivers/Cache_redis.php
@@ -0,0 +1,320 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Redis Caching Class
+ *
+ * @package	   CodeIgniter
+ * @subpackage Libraries
+ * @category   Core
+ * @author	   Anton Lindqvist <anton@qvister.se>
+ * @link
+ */
+class CI_Cache_redis extends CI_Driver
+{
+	/**
+	 * Default config
+	 *
+	 * @static
+	 * @var	array
+	 */
+	protected static $_default_config = array(
+		'socket_type' => 'tcp',
+		'host' => '127.0.0.1',
+		'password' => NULL,
+		'port' => 6379,
+		'timeout' => 0
+	);
+
+	/**
+	 * Redis connection
+	 *
+	 * @var	Redis
+	 */
+	protected $_redis;
+
+	/**
+	 * An internal cache for storing keys of serialized values.
+	 *
+	 * @var	array
+	 */
+	protected $_serialized = array();
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Setup Redis
+	 *
+	 * Loads Redis config file if present. Will halt execution
+	 * if a Redis connection can't be established.
+	 *
+	 * @return	void
+	 * @see		Redis::connect()
+	 */
+	public function __construct()
+	{
+		$config = array();
+		$CI =& get_instance();
+
+		if ($CI->config->load('redis', TRUE, TRUE))
+		{
+			$config = $CI->config->item('redis');
+		}
+
+		$config = array_merge(self::$_default_config, $config);
+		$this->_redis = new Redis();
+
+		try
+		{
+			if ($config['socket_type'] === 'unix')
+			{
+				$success = $this->_redis->connect($config['socket']);
+			}
+			else // tcp socket
+			{
+				$success = $this->_redis->connect($config['host'], $config['port'], $config['timeout']);
+			}
+
+			if ( ! $success)
+			{
+				log_message('error', 'Cache: Redis connection failed. Check your configuration.');
+			}
+
+			if (isset($config['password']) && ! $this->_redis->auth($config['password']))
+			{
+				log_message('error', 'Cache: Redis authentication failed.');
+			}
+		}
+		catch (RedisException $e)
+		{
+			log_message('error', 'Cache: Redis connection refused ('.$e->getMessage().')');
+		}
+
+		// Initialize the index of serialized values.
+		$serialized = $this->_redis->sMembers('_ci_redis_serialized');
+		empty($serialized) OR $this->_serialized = array_flip($serialized);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get cache
+	 *
+	 * @param	string	Cache ID
+	 * @return	mixed
+	 */
+	public function get($key)
+	{
+		$value = $this->_redis->get($key);
+
+		if ($value !== FALSE && isset($this->_serialized[$key]))
+		{
+			return unserialize($value);
+		}
+
+		return $value;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Save cache
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	mixed	$data	Data to save
+	 * @param	int	$ttl	Time to live in seconds
+	 * @param	bool	$raw	Whether to store the raw value (unused)
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function save($id, $data, $ttl = 60, $raw = FALSE)
+	{
+		if (is_array($data) OR is_object($data))
+		{
+			if ( ! $this->_redis->sIsMember('_ci_redis_serialized', $id) && ! $this->_redis->sAdd('_ci_redis_serialized', $id))
+			{
+				return FALSE;
+			}
+
+			isset($this->_serialized[$id]) OR $this->_serialized[$id] = TRUE;
+			$data = serialize($data);
+		}
+		elseif (isset($this->_serialized[$id]))
+		{
+			$this->_serialized[$id] = NULL;
+			$this->_redis->sRemove('_ci_redis_serialized', $id);
+		}
+
+		return $this->_redis->set($id, $data, $ttl);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Delete from cache
+	 *
+	 * @param	string	Cache key
+	 * @return	bool
+	 */
+	public function delete($key)
+	{
+		if ($this->_redis->delete($key) !== 1)
+		{
+			return FALSE;
+		}
+
+		if (isset($this->_serialized[$key]))
+		{
+			$this->_serialized[$key] = NULL;
+			$this->_redis->sRemove('_ci_redis_serialized', $key);
+		}
+
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Increment a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to add
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function increment($id, $offset = 1)
+	{
+		return $this->_redis->incr($id, $offset);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Decrement a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to reduce by
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function decrement($id, $offset = 1)
+	{
+		return $this->_redis->decr($id, $offset);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Clean cache
+	 *
+	 * @return	bool
+	 * @see		Redis::flushDB()
+	 */
+	public function clean()
+	{
+		return $this->_redis->flushDB();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get cache driver info
+	 *
+	 * @param	string	Not supported in Redis.
+	 *			Only included in order to offer a
+	 *			consistent cache API.
+	 * @return	array
+	 * @see		Redis::info()
+	 */
+	public function cache_info($type = NULL)
+	{
+		return $this->_redis->info();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get cache metadata
+	 *
+	 * @param	string	Cache key
+	 * @return	array
+	 */
+	public function get_metadata($key)
+	{
+		$value = $this->get($key);
+
+		if ($value !== FALSE)
+		{
+			return array(
+				'expire' => time() + $this->_redis->ttl($key),
+				'data' => $value
+			);
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Check if Redis driver is supported
+	 *
+	 * @return	bool
+	 */
+	public function is_supported()
+	{
+		return extension_loaded('redis');
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class destructor
+	 *
+	 * Closes the connection to Redis if present.
+	 *
+	 * @return	void
+	 */
+	public function __destruct()
+	{
+		if ($this->_redis)
+		{
+			$this->_redis->close();
+		}
+	}
+}
diff --git a/README/Cache/drivers/Cache_wincache.php b/README/Cache/drivers/Cache_wincache.php
new file mode 100644
index 0000000..9cc6ff0
--- /dev/null
+++ b/README/Cache/drivers/Cache_wincache.php
@@ -0,0 +1,206 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Wincache Caching Class
+ *
+ * Read more about Wincache functions here:
+ * http://www.php.net/manual/en/ref.wincache.php
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Core
+ * @author		Mike Murkovic
+ * @link
+ */
+class CI_Cache_wincache extends CI_Driver {
+
+	/**
+	 * Get
+	 *
+	 * Look for a value in the cache. If it exists, return the data,
+	 * if not, return FALSE
+	 *
+	 * @param	string	$id	Cache Ide
+	 * @return	mixed	Value that is stored/FALSE on failure
+	 */
+	public function get($id)
+	{
+		$success = FALSE;
+		$data = wincache_ucache_get($id, $success);
+
+		// Success returned by reference from wincache_ucache_get()
+		return ($success) ? $data : FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Save
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	mixed	$data	Data to store
+	 * @param	int	$ttl	Time to live (in seconds)
+	 * @param	bool	$raw	Whether to store the raw value (unused)
+	 * @return	bool	true on success/false on failure
+	 */
+	public function save($id, $data, $ttl = 60, $raw = FALSE)
+	{
+		return wincache_ucache_set($id, $data, $ttl);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Delete from Cache
+	 *
+	 * @param	mixed	unique identifier of the item in the cache
+	 * @return	bool	true on success/false on failure
+	 */
+	public function delete($id)
+	{
+		return wincache_ucache_delete($id);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Increment a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to add
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function increment($id, $offset = 1)
+	{
+		$success = FALSE;
+		$value = wincache_ucache_inc($id, $offset, $success);
+
+		return ($success === TRUE) ? $value : FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Decrement a raw value
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	int	$offset	Step/value to reduce by
+	 * @return	mixed	New value on success or FALSE on failure
+	 */
+	public function decrement($id, $offset = 1)
+	{
+		$success = FALSE;
+		$value = wincache_ucache_dec($id, $offset, $success);
+
+		return ($success === TRUE) ? $value : FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Clean the cache
+	 *
+	 * @return	bool	false on failure/true on success
+	 */
+	public function clean()
+	{
+		return wincache_ucache_clear();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Info
+	 *
+	 * @return	mixed	array on success, false on failure
+	 */
+	 public function cache_info()
+	 {
+		 return wincache_ucache_info(TRUE);
+	 }
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get Cache Metadata
+	 *
+	 * @param	mixed	key to get cache metadata on
+	 * @return	mixed	array on success/false on failure
+	 */
+	public function get_metadata($id)
+	{
+		if ($stored = wincache_ucache_info(FALSE, $id))
+		{
+			$age = $stored['ucache_entries'][1]['age_seconds'];
+			$ttl = $stored['ucache_entries'][1]['ttl_seconds'];
+			$hitcount = $stored['ucache_entries'][1]['hitcount'];
+
+			return array(
+				'expire'	=> $ttl - $age,
+				'hitcount'	=> $hitcount,
+				'age'		=> $age,
+				'ttl'		=> $ttl
+			);
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * is_supported()
+	 *
+	 * Check to see if WinCache is available on this system, bail if it isn't.
+	 *
+	 * @return	bool
+	 */
+	public function is_supported()
+	{
+		if ( ! extension_loaded('wincache') OR ! ini_get('wincache.ucenabled'))
+		{
+			log_message('debug', 'The Wincache PHP extension must be loaded to use Wincache Cache.');
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+}
diff --git a/README/Cache/drivers/index.html b/README/Cache/drivers/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/README/Cache/drivers/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>403 Forbidden</title>
+</head>
+<body>
+
+<p>Directory access is forbidden.</p>
+
+</body>
+</html>
diff --git a/README/Cache/index.html b/README/Cache/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/README/Cache/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>403 Forbidden</title>
+</head>
+<body>
+
+<p>Directory access is forbidden.</p>
+
+</body>
+</html>
diff --git a/README/Calendar.php b/README/Calendar.php
new file mode 100644
index 0000000..f6a0c39
--- /dev/null
+++ b/README/Calendar.php
@@ -0,0 +1,546 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Calendar Class
+ *
+ * This class enables the creation of calendars
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/calendar.html
+ */
+class CI_Calendar {
+
+	/**
+	 * Calendar layout template
+	 *
+	 * @var mixed
+	 */
+	public $template = '';
+
+	/**
+	 * Replacements array for template
+	 *
+	 * @var array
+	 */
+	public $replacements = array();
+
+	/**
+	 * Day of the week to start the calendar on
+	 *
+	 * @var string
+	 */
+	public $start_day = 'sunday';
+
+	/**
+	 * How to display months
+	 *
+	 * @var string
+	 */
+	public $month_type = 'long';
+
+	/**
+	 * How to display names of days
+	 *
+	 * @var string
+	 */
+	public $day_type = 'abr';
+
+	/**
+	 * Whether to show next/prev month links
+	 *
+	 * @var bool
+	 */
+	public $show_next_prev = FALSE;
+
+	/**
+	 * Url base to use for next/prev month links
+	 *
+	 * @var bool
+	 */
+	public $next_prev_url = '';
+
+	/**
+	 * Show days of other months
+	 *
+	 * @var bool
+	 */
+	public $show_other_days = FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CI Singleton
+	 *
+	 * @var object
+	 */
+	protected $CI;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Loads the calendar language file and sets the default time reference.
+	 *
+	 * @uses	CI_Lang::$is_loaded
+	 *
+	 * @param	array	$config	Calendar options
+	 * @return	void
+	 */
+	public function __construct($config = array())
+	{
+		$this->CI =& get_instance();
+		$this->CI->lang->load('calendar');
+
+		empty($config) OR $this->initialize($config);
+
+		log_message('info', 'Calendar Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize the user preferences
+	 *
+	 * Accepts an associative array as input, containing display preferences
+	 *
+	 * @param	array	config preferences
+	 * @return	CI_Calendar
+	 */
+	public function initialize($config = array())
+	{
+		foreach ($config as $key => $val)
+		{
+			if (isset($this->$key))
+			{
+				$this->$key = $val;
+			}
+		}
+
+		// Set the next_prev_url to the controller if required but not defined
+		if ($this->show_next_prev === TRUE && empty($this->next_prev_url))
+		{
+			$this->next_prev_url = $this->CI->config->site_url($this->CI->router->class.'/'.$this->CI->router->method);
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate the calendar
+	 *
+	 * @param	int	the year
+	 * @param	int	the month
+	 * @param	array	the data to be shown in the calendar cells
+	 * @return	string
+	 */
+	public function generate($year = '', $month = '', $data = array())
+	{
+		$local_time = time();
+
+		// Set and validate the supplied month/year
+		if (empty($year))
+		{
+			$year = date('Y', $local_time);
+		}
+		elseif (strlen($year) === 1)
+		{
+			$year = '200'.$year;
+		}
+		elseif (strlen($year) === 2)
+		{
+			$year = '20'.$year;
+		}
+
+		if (empty($month))
+		{
+			$month = date('m', $local_time);
+		}
+		elseif (strlen($month) === 1)
+		{
+			$month = '0'.$month;
+		}
+
+		$adjusted_date = $this->adjust_date($month, $year);
+
+		$month	= $adjusted_date['month'];
+		$year	= $adjusted_date['year'];
+
+		// Determine the total days in the month
+		$total_days = $this->get_total_days($month, $year);
+
+		// Set the starting day of the week
+		$start_days	= array('sunday' => 0, 'monday' => 1, 'tuesday' => 2, 'wednesday' => 3, 'thursday' => 4, 'friday' => 5, 'saturday' => 6);
+		$start_day	= isset($start_days[$this->start_day]) ? $start_days[$this->start_day] : 0;
+
+		// Set the starting day number
+		$local_date = mktime(12, 0, 0, $month, 1, $year);
+		$date = getdate($local_date);
+		$day  = $start_day + 1 - $date['wday'];
+
+		while ($day > 1)
+		{
+			$day -= 7;
+		}
+
+		// Set the current month/year/day
+		// We use this to determine the "today" date
+		$cur_year	= date('Y', $local_time);
+		$cur_month	= date('m', $local_time);
+		$cur_day	= date('j', $local_time);
+
+		$is_current_month = ($cur_year == $year && $cur_month == $month);
+
+		// Generate the template data array
+		$this->parse_template();
+
+		// Begin building the calendar output
+		$out = $this->replacements['table_open']."\n\n".$this->replacements['heading_row_start']."\n";
+
+		// "previous" month link
+		if ($this->show_next_prev === TRUE)
+		{
+			// Add a trailing slash to the URL if needed
+			$this->next_prev_url = preg_replace('/(.+?)\/*$/', '\\1/', $this->next_prev_url);
+
+			$adjusted_date = $this->adjust_date($month - 1, $year);
+			$out .= str_replace('{previous_url}', $this->next_prev_url.$adjusted_date['year'].'/'.$adjusted_date['month'], $this->replacements['heading_previous_cell'])."\n";
+		}
+
+		// Heading containing the month/year
+		$colspan = ($this->show_next_prev === TRUE) ? 5 : 7;
+
+		$this->replacements['heading_title_cell'] = str_replace('{colspan}', $colspan,
+								str_replace('{heading}', $this->get_month_name($month).'&nbsp;'.$year, $this->replacements['heading_title_cell']));
+
+		$out .= $this->replacements['heading_title_cell']."\n";
+
+		// "next" month link
+		if ($this->show_next_prev === TRUE)
+		{
+			$adjusted_date = $this->adjust_date($month + 1, $year);
+			$out .= str_replace('{next_url}', $this->next_prev_url.$adjusted_date['year'].'/'.$adjusted_date['month'], $this->replacements['heading_next_cell']);
+		}
+
+		$out .= "\n".$this->replacements['heading_row_end']."\n\n"
+			// Write the cells containing the days of the week
+			.$this->replacements['week_row_start']."\n";
+
+		$day_names = $this->get_day_names();
+
+		for ($i = 0; $i < 7; $i ++)
+		{
+			$out .= str_replace('{week_day}', $day_names[($start_day + $i) %7], $this->replacements['week_day_cell']);
+		}
+
+		$out .= "\n".$this->replacements['week_row_end']."\n";
+
+		// Build the main body of the calendar
+		while ($day <= $total_days)
+		{
+			$out .= "\n".$this->replacements['cal_row_start']."\n";
+
+			for ($i = 0; $i < 7; $i++)
+			{
+				if ($day > 0 && $day <= $total_days)
+				{
+					$out .= ($is_current_month === TRUE && $day == $cur_day) ? $this->replacements['cal_cell_start_today'] : $this->replacements['cal_cell_start'];
+
+					if (isset($data[$day]))
+					{
+						// Cells with content
+						$temp = ($is_current_month === TRUE && $day == $cur_day) ?
+								$this->replacements['cal_cell_content_today'] : $this->replacements['cal_cell_content'];
+						$out .= str_replace(array('{content}', '{day}'), array($data[$day], $day), $temp);
+					}
+					else
+					{
+						// Cells with no content
+						$temp = ($is_current_month === TRUE && $day == $cur_day) ?
+								$this->replacements['cal_cell_no_content_today'] : $this->replacements['cal_cell_no_content'];
+						$out .= str_replace('{day}', $day, $temp);
+					}
+
+					$out .= ($is_current_month === TRUE && $day == $cur_day) ? $this->replacements['cal_cell_end_today'] : $this->replacements['cal_cell_end'];
+				}
+				elseif ($this->show_other_days === TRUE)
+				{
+					$out .= $this->replacements['cal_cell_start_other'];
+
+					if ($day <= 0)
+					{
+						// Day of previous month
+						$prev_month = $this->adjust_date($month - 1, $year);
+						$prev_month_days = $this->get_total_days($prev_month['month'], $prev_month['year']);
+						$out .= str_replace('{day}', $prev_month_days + $day, $this->replacements['cal_cell_other']);
+					}
+					else
+					{
+						// Day of next month
+						$out .= str_replace('{day}', $day - $total_days, $this->replacements['cal_cell_other']);
+					}
+
+					$out .= $this->replacements['cal_cell_end_other'];
+				}
+				else
+				{
+					// Blank cells
+					$out .= $this->replacements['cal_cell_start'].$this->replacements['cal_cell_blank'].$this->replacements['cal_cell_end'];
+				}
+
+				$day++;
+			}
+
+			$out .= "\n".$this->replacements['cal_row_end']."\n";
+		}
+
+		return $out .= "\n".$this->replacements['table_close'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Month Name
+	 *
+	 * Generates a textual month name based on the numeric
+	 * month provided.
+	 *
+	 * @param	int	the month
+	 * @return	string
+	 */
+	public function get_month_name($month)
+	{
+		if ($this->month_type === 'short')
+		{
+			$month_names = array('01' => 'cal_jan', '02' => 'cal_feb', '03' => 'cal_mar', '04' => 'cal_apr', '05' => 'cal_may', '06' => 'cal_jun', '07' => 'cal_jul', '08' => 'cal_aug', '09' => 'cal_sep', '10' => 'cal_oct', '11' => 'cal_nov', '12' => 'cal_dec');
+		}
+		else
+		{
+			$month_names = array('01' => 'cal_january', '02' => 'cal_february', '03' => 'cal_march', '04' => 'cal_april', '05' => 'cal_mayl', '06' => 'cal_june', '07' => 'cal_july', '08' => 'cal_august', '09' => 'cal_september', '10' => 'cal_october', '11' => 'cal_november', '12' => 'cal_december');
+		}
+
+		return ($this->CI->lang->line($month_names[$month]) === FALSE)
+			? ucfirst(substr($month_names[$month], 4))
+			: $this->CI->lang->line($month_names[$month]);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Day Names
+	 *
+	 * Returns an array of day names (Sunday, Monday, etc.) based
+	 * on the type. Options: long, short, abr
+	 *
+	 * @param	string
+	 * @return	array
+	 */
+	public function get_day_names($day_type = '')
+	{
+		if ($day_type !== '')
+		{
+			$this->day_type = $day_type;
+		}
+
+		if ($this->day_type === 'long')
+		{
+			$day_names = array('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday');
+		}
+		elseif ($this->day_type === 'short')
+		{
+			$day_names = array('sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat');
+		}
+		else
+		{
+			$day_names = array('su', 'mo', 'tu', 'we', 'th', 'fr', 'sa');
+		}
+
+		$days = array();
+		for ($i = 0, $c = count($day_names); $i < $c; $i++)
+		{
+			$days[] = ($this->CI->lang->line('cal_'.$day_names[$i]) === FALSE) ? ucfirst($day_names[$i]) : $this->CI->lang->line('cal_'.$day_names[$i]);
+		}
+
+		return $days;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Adjust Date
+	 *
+	 * This function makes sure that we have a valid month/year.
+	 * For example, if you submit 13 as the month, the year will
+	 * increment and the month will become January.
+	 *
+	 * @param	int	the month
+	 * @param	int	the year
+	 * @return	array
+	 */
+	public function adjust_date($month, $year)
+	{
+		$date = array();
+
+		$date['month']	= $month;
+		$date['year']	= $year;
+
+		while ($date['month'] > 12)
+		{
+			$date['month'] -= 12;
+			$date['year']++;
+		}
+
+		while ($date['month'] <= 0)
+		{
+			$date['month'] += 12;
+			$date['year']--;
+		}
+
+		if (strlen($date['month']) === 1)
+		{
+			$date['month'] = '0'.$date['month'];
+		}
+
+		return $date;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Total days in a given month
+	 *
+	 * @param	int	the month
+	 * @param	int	the year
+	 * @return	int
+	 */
+	public function get_total_days($month, $year)
+	{
+		$this->CI->load->helper('date');
+		return days_in_month($month, $year);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Default Template Data
+	 *
+	 * This is used in the event that the user has not created their own template
+	 *
+	 * @return	array
+	 */
+	public function default_template()
+	{
+		return array(
+			'table_open'				=> '<table border="0" cellpadding="4" cellspacing="0">',
+			'heading_row_start'			=> '<tr>',
+			'heading_previous_cell'		=> '<th><a href="{previous_url}">&lt;&lt;</a></th>',
+			'heading_title_cell'		=> '<th colspan="{colspan}">{heading}</th>',
+			'heading_next_cell'			=> '<th><a href="{next_url}">&gt;&gt;</a></th>',
+			'heading_row_end'			=> '</tr>',
+			'week_row_start'			=> '<tr>',
+			'week_day_cell'				=> '<td>{week_day}</td>',
+			'week_row_end'				=> '</tr>',
+			'cal_row_start'				=> '<tr>',
+			'cal_cell_start'			=> '<td>',
+			'cal_cell_start_today'		=> '<td>',
+			'cal_cell_start_other'		=> '<td style="color: #666;">',
+			'cal_cell_content'			=> '<a href="{content}">{day}</a>',
+			'cal_cell_content_today'	=> '<a href="{content}"><strong>{day}</strong></a>',
+			'cal_cell_no_content'		=> '{day}',
+			'cal_cell_no_content_today'	=> '<strong>{day}</strong>',
+			'cal_cell_blank'			=> '&nbsp;',
+			'cal_cell_other'			=> '{day}',
+			'cal_cell_end'				=> '</td>',
+			'cal_cell_end_today'		=> '</td>',
+			'cal_cell_end_other'		=> '</td>',
+			'cal_row_end'				=> '</tr>',
+			'table_close'				=> '</table>'
+		);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse Template
+	 *
+	 * Harvests the data within the template {pseudo-variables}
+	 * used to display the calendar
+	 *
+	 * @return	CI_Calendar
+	 */
+	public function parse_template()
+	{
+		$this->replacements = $this->default_template();
+
+		if (empty($this->template))
+		{
+			return $this;
+		}
+
+		if (is_string($this->template))
+		{
+			$today = array('cal_cell_start_today', 'cal_cell_content_today', 'cal_cell_no_content_today', 'cal_cell_end_today');
+
+			foreach (array('table_open', 'table_close', 'heading_row_start', 'heading_previous_cell', 'heading_title_cell', 'heading_next_cell', 'heading_row_end', 'week_row_start', 'week_day_cell', 'week_row_end', 'cal_row_start', 'cal_cell_start', 'cal_cell_content', 'cal_cell_no_content', 'cal_cell_blank', 'cal_cell_end', 'cal_row_end', 'cal_cell_start_today', 'cal_cell_content_today', 'cal_cell_no_content_today', 'cal_cell_end_today', 'cal_cell_start_other', 'cal_cell_other', 'cal_cell_end_other') as $val)
+			{
+				if (preg_match('/\{'.$val.'\}(.*?)\{\/'.$val.'\}/si', $this->template, $match))
+				{
+					$this->replacements[$val] = $match[1];
+				}
+				elseif (in_array($val, $today, TRUE))
+				{
+					$this->replacements[$val] = $this->replacements[substr($val, 0, -6)];
+				}
+			}
+		}
+		elseif (is_array($this->template))
+		{
+			$this->replacements = array_merge($this->replacements, $this->template);
+		}
+
+		return $this;
+	}
+
+}
diff --git a/README/Cart.php b/README/Cart.php
new file mode 100644
index 0000000..bf27c63
--- /dev/null
+++ b/README/Cart.php
@@ -0,0 +1,567 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Shopping Cart Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Shopping Cart
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/cart.html
+ * @deprecated	3.0.0	This class is too specific for CI.
+ */
+class CI_Cart {
+
+	/**
+	 * These are the regular expression rules that we use to validate the product ID and product name
+	 * alpha-numeric, dashes, underscores, or periods
+	 *
+	 * @var string
+	 */
+	public $product_id_rules = '\.a-z0-9_-';
+
+	/**
+	 * These are the regular expression rules that we use to validate the product ID and product name
+	 * alpha-numeric, dashes, underscores, colons or periods
+	 *
+	 * @var string
+	 */
+	public $product_name_rules = '\w \-\.\:';
+
+	/**
+	 * only allow safe product names
+	 *
+	 * @var bool
+	 */
+	public $product_name_safe = TRUE;
+
+	// --------------------------------------------------------------------------
+
+	/**
+	 * Reference to CodeIgniter instance
+	 *
+	 * @var object
+	 */
+	protected $CI;
+
+	/**
+	 * Contents of the cart
+	 *
+	 * @var array
+	 */
+	protected $_cart_contents = array();
+
+	/**
+	 * Shopping Class Constructor
+	 *
+	 * The constructor loads the Session class, used to store the shopping cart contents.
+	 *
+	 * @param	array
+	 * @return	void
+	 */
+	public function __construct($params = array())
+	{
+		// Set the super object to a local variable for use later
+		$this->CI =& get_instance();
+
+		// Are any config settings being passed manually?  If so, set them
+		$config = is_array($params) ? $params : array();
+
+		// Load the Sessions class
+		$this->CI->load->driver('session', $config);
+
+		// Grab the shopping cart array from the session table
+		$this->_cart_contents = $this->CI->session->userdata('cart_contents');
+		if ($this->_cart_contents === NULL)
+		{
+			// No cart exists so we'll set some base values
+			$this->_cart_contents = array('cart_total' => 0, 'total_items' => 0);
+		}
+
+		log_message('info', 'Cart Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert items into the cart and save it to the session table
+	 *
+	 * @param	array
+	 * @return	bool
+	 */
+	public function insert($items = array())
+	{
+		// Was any cart data passed? No? Bah...
+		if ( ! is_array($items) OR count($items) === 0)
+		{
+			log_message('error', 'The insert method must be passed an array containing data.');
+			return FALSE;
+		}
+
+		// You can either insert a single product using a one-dimensional array,
+		// or multiple products using a multi-dimensional one. The way we
+		// determine the array type is by looking for a required array key named "id"
+		// at the top level. If it's not found, we will assume it's a multi-dimensional array.
+
+		$save_cart = FALSE;
+		if (isset($items['id']))
+		{
+			if (($rowid = $this->_insert($items)))
+			{
+				$save_cart = TRUE;
+			}
+		}
+		else
+		{
+			foreach ($items as $val)
+			{
+				if (is_array($val) && isset($val['id']))
+				{
+					if ($this->_insert($val))
+					{
+						$save_cart = TRUE;
+					}
+				}
+			}
+		}
+
+		// Save the cart data if the insert was successful
+		if ($save_cart === TRUE)
+		{
+			$this->_save_cart();
+			return isset($rowid) ? $rowid : TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert
+	 *
+	 * @param	array
+	 * @return	bool
+	 */
+	protected function _insert($items = array())
+	{
+		// Was any cart data passed? No? Bah...
+		if ( ! is_array($items) OR count($items) === 0)
+		{
+			log_message('error', 'The insert method must be passed an array containing data.');
+			return FALSE;
+		}
+
+		// --------------------------------------------------------------------
+
+		// Does the $items array contain an id, quantity, price, and name?  These are required
+		if ( ! isset($items['id'], $items['qty'], $items['price'], $items['name']))
+		{
+			log_message('error', 'The cart array must contain a product ID, quantity, price, and name.');
+			return FALSE;
+		}
+
+		// --------------------------------------------------------------------
+
+		// Prep the quantity. It can only be a number.  Duh... also trim any leading zeros
+		$items['qty'] = (float) $items['qty'];
+
+		// If the quantity is zero or blank there's nothing for us to do
+		if ($items['qty'] == 0)
+		{
+			return FALSE;
+		}
+
+		// --------------------------------------------------------------------
+
+		// Validate the product ID. It can only be alpha-numeric, dashes, underscores or periods
+		// Not totally sure we should impose this rule, but it seems prudent to standardize IDs.
+		// Note: These can be user-specified by setting the $this->product_id_rules variable.
+		if ( ! preg_match('/^['.$this->product_id_rules.']+$/i', $items['id']))
+		{
+			log_message('error', 'Invalid product ID.  The product ID can only contain alpha-numeric characters, dashes, and underscores');
+			return FALSE;
+		}
+
+		// --------------------------------------------------------------------
+
+		// Validate the product name. It can only be alpha-numeric, dashes, underscores, colons or periods.
+		// Note: These can be user-specified by setting the $this->product_name_rules variable.
+		if ($this->product_name_safe && ! preg_match('/^['.$this->product_name_rules.']+$/i'.(UTF8_ENABLED ? 'u' : ''), $items['name']))
+		{
+			log_message('error', 'An invalid name was submitted as the product name: '.$items['name'].' The name can only contain alpha-numeric characters, dashes, underscores, colons, and spaces');
+			return FALSE;
+		}
+
+		// --------------------------------------------------------------------
+
+		// Prep the price. Remove leading zeros and anything that isn't a number or decimal point.
+		$items['price'] = (float) $items['price'];
+
+		// We now need to create a unique identifier for the item being inserted into the cart.
+		// Every time something is added to the cart it is stored in the master cart array.
+		// Each row in the cart array, however, must have a unique index that identifies not only
+		// a particular product, but makes it possible to store identical products with different options.
+		// For example, what if someone buys two identical t-shirts (same product ID), but in
+		// different sizes?  The product ID (and other attributes, like the name) will be identical for
+		// both sizes because it's the same shirt. The only difference will be the size.
+		// Internally, we need to treat identical submissions, but with different options, as a unique product.
+		// Our solution is to convert the options array to a string and MD5 it along with the product ID.
+		// This becomes the unique "row ID"
+		if (isset($items['options']) && count($items['options']) > 0)
+		{
+			$rowid = md5($items['id'].serialize($items['options']));
+		}
+		else
+		{
+			// No options were submitted so we simply MD5 the product ID.
+			// Technically, we don't need to MD5 the ID in this case, but it makes
+			// sense to standardize the format of array indexes for both conditions
+			$rowid = md5($items['id']);
+		}
+
+		// --------------------------------------------------------------------
+
+		// Now that we have our unique "row ID", we'll add our cart items to the master array
+		// grab quantity if it's already there and add it on
+		$old_quantity = isset($this->_cart_contents[$rowid]['qty']) ? (int) $this->_cart_contents[$rowid]['qty'] : 0;
+
+		// Re-create the entry, just to make sure our index contains only the data from this submission
+		$items['rowid'] = $rowid;
+		$items['qty'] += $old_quantity;
+		$this->_cart_contents[$rowid] = $items;
+
+		return $rowid;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update the cart
+	 *
+	 * This function permits the quantity of a given item to be changed.
+	 * Typically it is called from the "view cart" page if a user makes
+	 * changes to the quantity before checkout. That array must contain the
+	 * product ID and quantity for each item.
+	 *
+	 * @param	array
+	 * @return	bool
+	 */
+	public function update($items = array())
+	{
+		// Was any cart data passed?
+		if ( ! is_array($items) OR count($items) === 0)
+		{
+			return FALSE;
+		}
+
+		// You can either update a single product using a one-dimensional array,
+		// or multiple products using a multi-dimensional one.  The way we
+		// determine the array type is by looking for a required array key named "rowid".
+		// If it's not found we assume it's a multi-dimensional array
+		$save_cart = FALSE;
+		if (isset($items['rowid']))
+		{
+			if ($this->_update($items) === TRUE)
+			{
+				$save_cart = TRUE;
+			}
+		}
+		else
+		{
+			foreach ($items as $val)
+			{
+				if (is_array($val) && isset($val['rowid']))
+				{
+					if ($this->_update($val) === TRUE)
+					{
+						$save_cart = TRUE;
+					}
+				}
+			}
+		}
+
+		// Save the cart data if the insert was successful
+		if ($save_cart === TRUE)
+		{
+			$this->_save_cart();
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update the cart
+	 *
+	 * This function permits changing item properties.
+	 * Typically it is called from the "view cart" page if a user makes
+	 * changes to the quantity before checkout. That array must contain the
+	 * rowid and quantity for each item.
+	 *
+	 * @param	array
+	 * @return	bool
+	 */
+	protected function _update($items = array())
+	{
+		// Without these array indexes there is nothing we can do
+		if ( ! isset($items['rowid'], $this->_cart_contents[$items['rowid']]))
+		{
+			return FALSE;
+		}
+
+		// Prep the quantity
+		if (isset($items['qty']))
+		{
+			$items['qty'] = (float) $items['qty'];
+			// Is the quantity zero?  If so we will remove the item from the cart.
+			// If the quantity is greater than zero we are updating
+			if ($items['qty'] == 0)
+			{
+				unset($this->_cart_contents[$items['rowid']]);
+				return TRUE;
+			}
+		}
+
+		// find updatable keys
+		$keys = array_intersect(array_keys($this->_cart_contents[$items['rowid']]), array_keys($items));
+		// if a price was passed, make sure it contains valid data
+		if (isset($items['price']))
+		{
+			$items['price'] = (float) $items['price'];
+		}
+
+		// product id & name shouldn't be changed
+		foreach (array_diff($keys, array('id', 'name')) as $key)
+		{
+			$this->_cart_contents[$items['rowid']][$key] = $items[$key];
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Save the cart array to the session DB
+	 *
+	 * @return	bool
+	 */
+	protected function _save_cart()
+	{
+		// Let's add up the individual prices and set the cart sub-total
+		$this->_cart_contents['total_items'] = $this->_cart_contents['cart_total'] = 0;
+		foreach ($this->_cart_contents as $key => $val)
+		{
+			// We make sure the array contains the proper indexes
+			if ( ! is_array($val) OR ! isset($val['price'], $val['qty']))
+			{
+				continue;
+			}
+
+			$this->_cart_contents['cart_total'] += ($val['price'] * $val['qty']);
+			$this->_cart_contents['total_items'] += $val['qty'];
+			$this->_cart_contents[$key]['subtotal'] = ($this->_cart_contents[$key]['price'] * $this->_cart_contents[$key]['qty']);
+		}
+
+		// Is our cart empty? If so we delete it from the session
+		if (count($this->_cart_contents) <= 2)
+		{
+			$this->CI->session->unset_userdata('cart_contents');
+
+			// Nothing more to do... coffee time!
+			return FALSE;
+		}
+
+		// If we made it this far it means that our cart has data.
+		// Let's pass it to the Session class so it can be stored
+		$this->CI->session->set_userdata(array('cart_contents' => $this->_cart_contents));
+
+		// Woot!
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Cart Total
+	 *
+	 * @return	int
+	 */
+	public function total()
+	{
+		return $this->_cart_contents['cart_total'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Remove Item
+	 *
+	 * Removes an item from the cart
+	 *
+	 * @param	int
+	 * @return	bool
+	 */
+	 public function remove($rowid)
+	 {
+		// unset & save
+		unset($this->_cart_contents[$rowid]);
+		$this->_save_cart();
+		return TRUE;
+	 }
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Total Items
+	 *
+	 * Returns the total item count
+	 *
+	 * @return	int
+	 */
+	public function total_items()
+	{
+		return $this->_cart_contents['total_items'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Cart Contents
+	 *
+	 * Returns the entire cart array
+	 *
+	 * @param	bool
+	 * @return	array
+	 */
+	public function contents($newest_first = FALSE)
+	{
+		// do we want the newest first?
+		$cart = ($newest_first) ? array_reverse($this->_cart_contents) : $this->_cart_contents;
+
+		// Remove these so they don't create a problem when showing the cart table
+		unset($cart['total_items']);
+		unset($cart['cart_total']);
+
+		return $cart;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get cart item
+	 *
+	 * Returns the details of a specific item in the cart
+	 *
+	 * @param	string	$row_id
+	 * @return	array
+	 */
+	public function get_item($row_id)
+	{
+		return (in_array($row_id, array('total_items', 'cart_total'), TRUE) OR ! isset($this->_cart_contents[$row_id]))
+			? FALSE
+			: $this->_cart_contents[$row_id];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Has options
+	 *
+	 * Returns TRUE if the rowid passed to this function correlates to an item
+	 * that has options associated with it.
+	 *
+	 * @param	string	$row_id = ''
+	 * @return	bool
+	 */
+	public function has_options($row_id = '')
+	{
+		return (isset($this->_cart_contents[$row_id]['options']) && count($this->_cart_contents[$row_id]['options']) !== 0);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Product options
+	 *
+	 * Returns the an array of options, for a particular product row ID
+	 *
+	 * @param	string	$row_id = ''
+	 * @return	array
+	 */
+	public function product_options($row_id = '')
+	{
+		return isset($this->_cart_contents[$row_id]['options']) ? $this->_cart_contents[$row_id]['options'] : array();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Format Number
+	 *
+	 * Returns the supplied number with commas and a decimal point.
+	 *
+	 * @param	float
+	 * @return	string
+	 */
+	public function format_number($n = '')
+	{
+		return ($n === '') ? '' : number_format( (float) $n, 2, '.', ',');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Destroy the cart
+	 *
+	 * Empties the cart and kills the session
+	 *
+	 * @return	void
+	 */
+	public function destroy()
+	{
+		$this->_cart_contents = array('cart_total' => 0, 'total_items' => 0);
+		$this->CI->session->unset_userdata('cart_contents');
+	}
+
+}
diff --git a/README/Driver.php b/README/Driver.php
new file mode 100644
index 0000000..da4c548
--- /dev/null
+++ b/README/Driver.php
@@ -0,0 +1,342 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Driver Library Class
+ *
+ * This class enables you to create "Driver" libraries that add runtime ability
+ * to extend the capabilities of a class via additional driver objects
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link
+ */
+class CI_Driver_Library {
+
+	/**
+	 * Array of drivers that are available to use with the driver class
+	 *
+	 * @var array
+	 */
+	protected $valid_drivers = array();
+
+	/**
+	 * Name of the current class - usually the driver class
+	 *
+	 * @var string
+	 */
+	protected $lib_name;
+
+	/**
+	 * Get magic method
+	 *
+	 * The first time a child is used it won't exist, so we instantiate it
+	 * subsequents calls will go straight to the proper child.
+	 *
+	 * @param	string	Child class name
+	 * @return	object	Child class
+	 */
+	public function __get($child)
+	{
+		// Try to load the driver
+		return $this->load_driver($child);
+	}
+
+	/**
+	 * Load driver
+	 *
+	 * Separate load_driver call to support explicit driver load by library or user
+	 *
+	 * @param	string	Driver name (w/o parent prefix)
+	 * @return	object	Child class
+	 */
+	public function load_driver($child)
+	{
+		// Get CodeIgniter instance and subclass prefix
+		$prefix = config_item('subclass_prefix');
+
+		if ( ! isset($this->lib_name))
+		{
+			// Get library name without any prefix
+			$this->lib_name = str_replace(array('CI_', $prefix), '', get_class($this));
+		}
+
+		// The child will be prefixed with the parent lib
+		$child_name = $this->lib_name.'_'.$child;
+
+		// See if requested child is a valid driver
+		if ( ! in_array($child, $this->valid_drivers))
+		{
+			// The requested driver isn't valid!
+			$msg = 'Invalid driver requested: '.$child_name;
+			log_message('error', $msg);
+			show_error($msg);
+		}
+
+		// Get package paths and filename case variations to search
+		$CI = get_instance();
+		$paths = $CI->load->get_package_paths(TRUE);
+
+		// Is there an extension?
+		$class_name = $prefix.$child_name;
+		$found = class_exists($class_name, FALSE);
+		if ( ! $found)
+		{
+			// Check for subclass file
+			foreach ($paths as $path)
+			{
+				// Does the file exist?
+				$file = $path.'libraries/'.$this->lib_name.'/drivers/'.$prefix.$child_name.'.php';
+				if (file_exists($file))
+				{
+					// Yes - require base class from BASEPATH
+					$basepath = BASEPATH.'libraries/'.$this->lib_name.'/drivers/'.$child_name.'.php';
+					if ( ! file_exists($basepath))
+					{
+						$msg = 'Unable to load the requested class: CI_'.$child_name;
+						log_message('error', $msg);
+						show_error($msg);
+					}
+
+					// Include both sources and mark found
+					include_once($basepath);
+					include_once($file);
+					$found = TRUE;
+					break;
+				}
+			}
+		}
+
+		// Do we need to search for the class?
+		if ( ! $found)
+		{
+			// Use standard class name
+			$class_name = 'CI_'.$child_name;
+			if ( ! class_exists($class_name, FALSE))
+			{
+				// Check package paths
+				foreach ($paths as $path)
+				{
+					// Does the file exist?
+					$file = $path.'libraries/'.$this->lib_name.'/drivers/'.$child_name.'.php';
+					if (file_exists($file))
+					{
+						// Include source
+						include_once($file);
+						break;
+					}
+				}
+			}
+		}
+
+		// Did we finally find the class?
+		if ( ! class_exists($class_name, FALSE))
+		{
+			if (class_exists($child_name, FALSE))
+			{
+				$class_name = $child_name;
+			}
+			else
+			{
+				$msg = 'Unable to load the requested driver: '.$class_name;
+				log_message('error', $msg);
+				show_error($msg);
+			}
+		}
+
+		// Instantiate, decorate and add child
+		$obj = new $class_name();
+		$obj->decorate($this);
+		$this->$child = $obj;
+		return $this->$child;
+	}
+
+}
+
+// --------------------------------------------------------------------------
+
+/**
+ * CodeIgniter Driver Class
+ *
+ * This class enables you to create drivers for a Library based on the Driver Library.
+ * It handles the drivers' access to the parent library
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link
+ */
+class CI_Driver {
+
+	/**
+	 * Instance of the parent class
+	 *
+	 * @var object
+	 */
+	protected $_parent;
+
+	/**
+	 * List of methods in the parent class
+	 *
+	 * @var array
+	 */
+	protected $_methods = array();
+
+	/**
+	 * List of properties in the parent class
+	 *
+	 * @var array
+	 */
+	protected $_properties = array();
+
+	/**
+	 * Array of methods and properties for the parent class(es)
+	 *
+	 * @static
+	 * @var	array
+	 */
+	protected static $_reflections = array();
+
+	/**
+	 * Decorate
+	 *
+	 * Decorates the child with the parent driver lib's methods and properties
+	 *
+	 * @param	object
+	 * @return	void
+	 */
+	public function decorate($parent)
+	{
+		$this->_parent = $parent;
+
+		// Lock down attributes to what is defined in the class
+		// and speed up references in magic methods
+
+		$class_name = get_class($parent);
+
+		if ( ! isset(self::$_reflections[$class_name]))
+		{
+			$r = new ReflectionObject($parent);
+
+			foreach ($r->getMethods() as $method)
+			{
+				if ($method->isPublic())
+				{
+					$this->_methods[] = $method->getName();
+				}
+			}
+
+			foreach ($r->getProperties() as $prop)
+			{
+				if ($prop->isPublic())
+				{
+					$this->_properties[] = $prop->getName();
+				}
+			}
+
+			self::$_reflections[$class_name] = array($this->_methods, $this->_properties);
+		}
+		else
+		{
+			list($this->_methods, $this->_properties) = self::$_reflections[$class_name];
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * __call magic method
+	 *
+	 * Handles access to the parent driver library's methods
+	 *
+	 * @param	string
+	 * @param	array
+	 * @return	mixed
+	 */
+	public function __call($method, $args = array())
+	{
+		if (in_array($method, $this->_methods))
+		{
+			return call_user_func_array(array($this->_parent, $method), $args);
+		}
+
+		throw new BadMethodCallException('No such method: '.$method.'()');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * __get magic method
+	 *
+	 * Handles reading of the parent driver library's properties
+	 *
+	 * @param	string
+	 * @return	mixed
+	 */
+	public function __get($var)
+	{
+		if (in_array($var, $this->_properties))
+		{
+			return $this->_parent->$var;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * __set magic method
+	 *
+	 * Handles writing to the parent driver library's properties
+	 *
+	 * @param	string
+	 * @param	array
+	 * @return	mixed
+	 */
+	public function __set($var, $val)
+	{
+		if (in_array($var, $this->_properties))
+		{
+			$this->_parent->$var = $val;
+		}
+	}
+
+}
diff --git a/README/Email.php b/README/Email.php
new file mode 100644
index 0000000..acf3629
--- /dev/null
+++ b/README/Email.php
@@ -0,0 +1,2314 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Email Class
+ *
+ * Permits email to be sent using Mail, Sendmail, or SMTP.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/email.html
+ */
+class CI_Email {
+
+	/**
+	 * Used as the User-Agent and X-Mailer headers' value.
+	 *
+	 * @var	string
+	 */
+	public $useragent	= 'CodeIgniter';
+
+	/**
+	 * Path to the Sendmail binary.
+	 *
+	 * @var	string
+	 */
+	public $mailpath	= '/usr/sbin/sendmail';	// Sendmail path
+
+	/**
+	 * Which method to use for sending e-mails.
+	 *
+	 * @var	string	'mail', 'sendmail' or 'smtp'
+	 */
+	public $protocol	= 'mail';		// mail/sendmail/smtp
+
+	/**
+	 * STMP Server host
+	 *
+	 * @var	string
+	 */
+	public $smtp_host	= '';
+
+	/**
+	 * SMTP Username
+	 *
+	 * @var	string
+	 */
+	public $smtp_user	= '';
+
+	/**
+	 * SMTP Password
+	 *
+	 * @var	string
+	 */
+	public $smtp_pass	= '';
+
+	/**
+	 * SMTP Server port
+	 *
+	 * @var	int
+	 */
+	public $smtp_port	= 25;
+
+	/**
+	 * SMTP connection timeout in seconds
+	 *
+	 * @var	int
+	 */
+	public $smtp_timeout	= 5;
+
+	/**
+	 * SMTP persistent connection
+	 *
+	 * @var	bool
+	 */
+	public $smtp_keepalive	= FALSE;
+
+	/**
+	 * SMTP Encryption
+	 *
+	 * @var	string	empty, 'tls' or 'ssl'
+	 */
+	public $smtp_crypto	= '';
+
+	/**
+	 * Whether to apply word-wrapping to the message body.
+	 *
+	 * @var	bool
+	 */
+	public $wordwrap	= TRUE;
+
+	/**
+	 * Number of characters to wrap at.
+	 *
+	 * @see	CI_Email::$wordwrap
+	 * @var	int
+	 */
+	public $wrapchars	= 76;
+
+	/**
+	 * Message format.
+	 *
+	 * @var	string	'text' or 'html'
+	 */
+	public $mailtype	= 'text';
+
+	/**
+	 * Character set (default: utf-8)
+	 *
+	 * @var	string
+	 */
+	public $charset		= 'utf-8';
+
+	/**
+	 * Multipart message
+	 *
+	 * @var	string	'mixed' (in the body) or 'related' (separate)
+	 */
+	public $multipart	= 'mixed';		// "mixed" (in the body) or "related" (separate)
+
+	/**
+	 * Alternative message (for HTML messages only)
+	 *
+	 * @var	string
+	 */
+	public $alt_message	= '';
+
+	/**
+	 * Whether to validate e-mail addresses.
+	 *
+	 * @var	bool
+	 */
+	public $validate	= FALSE;
+
+	/**
+	 * X-Priority header value.
+	 *
+	 * @var	int	1-5
+	 */
+	public $priority	= 3;			// Default priority (1 - 5)
+
+	/**
+	 * Newline character sequence.
+	 * Use "\r\n" to comply with RFC 822.
+	 *
+	 * @link	http://www.ietf.org/rfc/rfc822.txt
+	 * @var	string	"\r\n" or "\n"
+	 */
+	public $newline		= "\n";			// Default newline. "\r\n" or "\n" (Use "\r\n" to comply with RFC 822)
+
+	/**
+	 * CRLF character sequence
+	 *
+	 * RFC 2045 specifies that for 'quoted-printable' encoding,
+	 * "\r\n" must be used. However, it appears that some servers
+	 * (even on the receiving end) don't handle it properly and
+	 * switching to "\n", while improper, is the only solution
+	 * that seems to work for all environments.
+	 *
+	 * @link	http://www.ietf.org/rfc/rfc822.txt
+	 * @var	string
+	 */
+	public $crlf		= "\n";
+
+	/**
+	 * Whether to use Delivery Status Notification.
+	 *
+	 * @var	bool
+	 */
+	public $dsn		= FALSE;
+
+	/**
+	 * Whether to send multipart alternatives.
+	 * Yahoo! doesn't seem to like these.
+	 *
+	 * @var	bool
+	 */
+	public $send_multipart	= TRUE;
+
+	/**
+	 * Whether to send messages to BCC recipients in batches.
+	 *
+	 * @var	bool
+	 */
+	public $bcc_batch_mode	= FALSE;
+
+	/**
+	 * BCC Batch max number size.
+	 *
+	 * @see	CI_Email::$bcc_batch_mode
+	 * @var	int
+	 */
+	public $bcc_batch_size	= 200;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Whether PHP is running in safe mode. Initialized by the class constructor.
+	 *
+	 * @var	bool
+	 */
+	protected $_safe_mode		= FALSE;
+
+	/**
+	 * Subject header
+	 *
+	 * @var	string
+	 */
+	protected $_subject		= '';
+
+	/**
+	 * Message body
+	 *
+	 * @var	string
+	 */
+	protected $_body		= '';
+
+	/**
+	 * Final message body to be sent.
+	 *
+	 * @var	string
+	 */
+	protected $_finalbody		= '';
+
+	/**
+	 * multipart/alternative boundary
+	 *
+	 * @var	string
+	 */
+	protected $_alt_boundary	= '';
+
+	/**
+	 * Attachment boundary
+	 *
+	 * @var	string
+	 */
+	protected $_atc_boundary	= '';
+
+	/**
+	 * Final headers to send
+	 *
+	 * @var	string
+	 */
+	protected $_header_str		= '';
+
+	/**
+	 * SMTP Connection socket placeholder
+	 *
+	 * @var	resource
+	 */
+	protected $_smtp_connect	= '';
+
+	/**
+	 * Mail encoding
+	 *
+	 * @var	string	'8bit' or '7bit'
+	 */
+	protected $_encoding		= '8bit';
+
+	/**
+	 * Whether to perform SMTP authentication
+	 *
+	 * @var	bool
+	 */
+	protected $_smtp_auth		= FALSE;
+
+	/**
+	 * Whether to send a Reply-To header
+	 *
+	 * @var	bool
+	 */
+	protected $_replyto_flag	= FALSE;
+
+	/**
+	 * Debug messages
+	 *
+	 * @see	CI_Email::print_debugger()
+	 * @var	string
+	 */
+	protected $_debug_msg		= array();
+
+	/**
+	 * Recipients
+	 *
+	 * @var	string[]
+	 */
+	protected $_recipients		= array();
+
+	/**
+	 * CC Recipients
+	 *
+	 * @var	string[]
+	 */
+	protected $_cc_array		= array();
+
+	/**
+	 * BCC Recipients
+	 *
+	 * @var	string[]
+	 */
+	protected $_bcc_array		= array();
+
+	/**
+	 * Message headers
+	 *
+	 * @var	string[]
+	 */
+	protected $_headers		= array();
+
+	/**
+	 * Attachment data
+	 *
+	 * @var	array
+	 */
+	protected $_attachments		= array();
+
+	/**
+	 * Valid $protocol values
+	 *
+	 * @see	CI_Email::$protocol
+	 * @var	string[]
+	 */
+	protected $_protocols		= array('mail', 'sendmail', 'smtp');
+
+	/**
+	 * Base charsets
+	 *
+	 * Character sets valid for 7-bit encoding,
+	 * excluding language suffix.
+	 *
+	 * @var	string[]
+	 */
+	protected $_base_charsets	= array('us-ascii', 'iso-2022-');
+
+	/**
+	 * Bit depths
+	 *
+	 * Valid mail encodings
+	 *
+	 * @see	CI_Email::$_encoding
+	 * @var	string[]
+	 */
+	protected $_bit_depths		= array('7bit', '8bit');
+
+	/**
+	 * $priority translations
+	 *
+	 * Actual values to send with the X-Priority header
+	 *
+	 * @var	string[]
+	 */
+	protected $_priorities = array(
+		1 => '1 (Highest)',
+		2 => '2 (High)',
+		3 => '3 (Normal)',
+		4 => '4 (Low)',
+		5 => '5 (Lowest)'
+	);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor - Sets Email Preferences
+	 *
+	 * The constructor can be passed an array of config values
+	 *
+	 * @param	array	$config = array()
+	 * @return	void
+	 */
+	public function __construct(array $config = array())
+	{
+		$this->charset = config_item('charset');
+
+		if (count($config) > 0)
+		{
+			$this->initialize($config);
+		}
+		else
+		{
+			$this->_smtp_auth = ! ($this->smtp_user === '' && $this->smtp_pass === '');
+		}
+
+		$this->_safe_mode = ( ! is_php('5.4') && ini_get('safe_mode'));
+		$this->charset = strtoupper($this->charset);
+
+		log_message('info', 'Email Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Destructor - Releases Resources
+	 *
+	 * @return	void
+	 */
+	public function __destruct()
+	{
+		if (is_resource($this->_smtp_connect))
+		{
+			$this->_send_command('quit');
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize preferences
+	 *
+	 * @param	array
+	 * @return	CI_Email
+	 */
+	public function initialize($config = array())
+	{
+		foreach ($config as $key => $val)
+		{
+			if (isset($this->$key))
+			{
+				$method = 'set_'.$key;
+
+				if (method_exists($this, $method))
+				{
+					$this->$method($val);
+				}
+				else
+				{
+					$this->$key = $val;
+				}
+			}
+		}
+		$this->clear();
+
+		$this->_smtp_auth = ! ($this->smtp_user === '' && $this->smtp_pass === '');
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize the Email Data
+	 *
+	 * @param	bool
+	 * @return	CI_Email
+	 */
+	public function clear($clear_attachments = FALSE)
+	{
+		$this->_subject		= '';
+		$this->_body		= '';
+		$this->_finalbody	= '';
+		$this->_header_str	= '';
+		$this->_replyto_flag	= FALSE;
+		$this->_recipients	= array();
+		$this->_cc_array	= array();
+		$this->_bcc_array	= array();
+		$this->_headers		= array();
+		$this->_debug_msg	= array();
+
+		$this->set_header('User-Agent', $this->useragent);
+		$this->set_header('Date', $this->_set_date());
+
+		if ($clear_attachments !== FALSE)
+		{
+			$this->_attachments = array();
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set FROM
+	 *
+	 * @param	string	$from
+	 * @param	string	$name
+	 * @param	string	$return_path = NULL	Return-Path
+	 * @return	CI_Email
+	 */
+	public function from($from, $name = '', $return_path = NULL)
+	{
+		if (preg_match('/\<(.*)\>/', $from, $match))
+		{
+			$from = $match[1];
+		}
+
+		if ($this->validate)
+		{
+			$this->validate_email($this->_str_to_array($from));
+			if ($return_path)
+			{
+				$this->validate_email($this->_str_to_array($return_path));
+			}
+		}
+
+		// prepare the display name
+		if ($name !== '')
+		{
+			// only use Q encoding if there are characters that would require it
+			if ( ! preg_match('/[\200-\377]/', $name))
+			{
+				// add slashes for non-printing characters, slashes, and double quotes, and surround it in double quotes
+				$name = '"'.addcslashes($name, "\0..\37\177'\"\\").'"';
+			}
+			else
+			{
+				$name = $this->_prep_q_encoding($name);
+			}
+		}
+
+		$this->set_header('From', $name.' <'.$from.'>');
+
+		isset($return_path) OR $return_path = $from;
+		$this->set_header('Return-Path', '<'.$return_path.'>');
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Reply-to
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function reply_to($replyto, $name = '')
+	{
+		if (preg_match('/\<(.*)\>/', $replyto, $match))
+		{
+			$replyto = $match[1];
+		}
+
+		if ($this->validate)
+		{
+			$this->validate_email($this->_str_to_array($replyto));
+		}
+
+		if ($name === '')
+		{
+			$name = $replyto;
+		}
+
+		if (strpos($name, '"') !== 0)
+		{
+			$name = '"'.$name.'"';
+		}
+
+		$this->set_header('Reply-To', $name.' <'.$replyto.'>');
+		$this->_replyto_flag = TRUE;
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Recipients
+	 *
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function to($to)
+	{
+		$to = $this->_str_to_array($to);
+		$to = $this->clean_email($to);
+
+		if ($this->validate)
+		{
+			$this->validate_email($to);
+		}
+
+		if ($this->_get_protocol() !== 'mail')
+		{
+			$this->set_header('To', implode(', ', $to));
+		}
+
+		$this->_recipients = $to;
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set CC
+	 *
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function cc($cc)
+	{
+		$cc = $this->clean_email($this->_str_to_array($cc));
+
+		if ($this->validate)
+		{
+			$this->validate_email($cc);
+		}
+
+		$this->set_header('Cc', implode(', ', $cc));
+
+		if ($this->_get_protocol() === 'smtp')
+		{
+			$this->_cc_array = $cc;
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set BCC
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function bcc($bcc, $limit = '')
+	{
+		if ($limit !== '' && is_numeric($limit))
+		{
+			$this->bcc_batch_mode = TRUE;
+			$this->bcc_batch_size = $limit;
+		}
+
+		$bcc = $this->clean_email($this->_str_to_array($bcc));
+
+		if ($this->validate)
+		{
+			$this->validate_email($bcc);
+		}
+
+		if ($this->_get_protocol() === 'smtp' OR ($this->bcc_batch_mode && count($bcc) > $this->bcc_batch_size))
+		{
+			$this->_bcc_array = $bcc;
+		}
+		else
+		{
+			$this->set_header('Bcc', implode(', ', $bcc));
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Email Subject
+	 *
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function subject($subject)
+	{
+		$subject = $this->_prep_q_encoding($subject);
+		$this->set_header('Subject', $subject);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Body
+	 *
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function message($body)
+	{
+		$this->_body = rtrim(str_replace("\r", '', $body));
+
+		/* strip slashes only if magic quotes is ON
+		   if we do it with magic quotes OFF, it strips real, user-inputted chars.
+
+		   NOTE: In PHP 5.4 get_magic_quotes_gpc() will always return 0 and
+			 it will probably not exist in future versions at all.
+		*/
+		if ( ! is_php('5.4') && get_magic_quotes_gpc())
+		{
+			$this->_body = stripslashes($this->_body);
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Assign file attachments
+	 *
+	 * @param	string	$file	Can be local path, URL or buffered content
+	 * @param	string	$disposition = 'attachment'
+	 * @param	string	$newname = NULL
+	 * @param	string	$mime = ''
+	 * @return	CI_Email
+	 */
+	public function attach($file, $disposition = '', $newname = NULL, $mime = '')
+	{
+		if ($mime === '')
+		{
+			if (strpos($file, '://') === FALSE && ! file_exists($file))
+			{
+				$this->_set_error_message('lang:email_attachment_missing', $file);
+				return FALSE;
+			}
+
+			if ( ! $fp = @fopen($file, 'rb'))
+			{
+				$this->_set_error_message('lang:email_attachment_unreadable', $file);
+				return FALSE;
+			}
+
+			$file_content = stream_get_contents($fp);
+			$mime = $this->_mime_types(pathinfo($file, PATHINFO_EXTENSION));
+			fclose($fp);
+		}
+		else
+		{
+			$file_content =& $file; // buffered file
+		}
+
+		$this->_attachments[] = array(
+			'name'		=> array($file, $newname),
+			'disposition'	=> empty($disposition) ? 'attachment' : $disposition,  // Can also be 'inline'  Not sure if it matters
+			'type'		=> $mime,
+			'content'	=> chunk_split(base64_encode($file_content))
+		);
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set and return attachment Content-ID
+	 *
+	 * Useful for attached inline pictures
+	 *
+	 * @param	string	$filename
+	 * @return	string
+	 */
+	public function attachment_cid($filename)
+	{
+		if ($this->multipart !== 'related')
+		{
+			$this->multipart = 'related'; // Thunderbird need this for inline images
+		}
+
+		for ($i = 0, $c = count($this->_attachments); $i < $c; $i++)
+		{
+			if ($this->_attachments[$i]['name'][0] === $filename)
+			{
+				$this->_attachments[$i]['cid'] = uniqid(basename($this->_attachments[$i]['name'][0]).'@');
+				return $this->_attachments[$i]['cid'];
+			}
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add a Header Item
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function set_header($header, $value)
+	{
+		$this->_headers[$header] = str_replace(array("\n", "\r"), '', $value);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Convert a String to an Array
+	 *
+	 * @param	string
+	 * @return	array
+	 */
+	protected function _str_to_array($email)
+	{
+		if ( ! is_array($email))
+		{
+			return (strpos($email, ',') !== FALSE)
+				? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY)
+				: (array) trim($email);
+		}
+
+		return $email;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Multipart Value
+	 *
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function set_alt_message($str)
+	{
+		$this->alt_message = (string) $str;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Mailtype
+	 *
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function set_mailtype($type = 'text')
+	{
+		$this->mailtype = ($type === 'html') ? 'html' : 'text';
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Wordwrap
+	 *
+	 * @param	bool
+	 * @return	CI_Email
+	 */
+	public function set_wordwrap($wordwrap = TRUE)
+	{
+		$this->wordwrap = (bool) $wordwrap;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Protocol
+	 *
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function set_protocol($protocol = 'mail')
+	{
+		$this->protocol = in_array($protocol, $this->_protocols, TRUE) ? strtolower($protocol) : 'mail';
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Priority
+	 *
+	 * @param	int
+	 * @return	CI_Email
+	 */
+	public function set_priority($n = 3)
+	{
+		$this->priority = preg_match('/^[1-5]$/', $n) ? (int) $n : 3;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Newline Character
+	 *
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function set_newline($newline = "\n")
+	{
+		$this->newline = in_array($newline, array("\n", "\r\n", "\r")) ? $newline : "\n";
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set CRLF
+	 *
+	 * @param	string
+	 * @return	CI_Email
+	 */
+	public function set_crlf($crlf = "\n")
+	{
+		$this->crlf = ($crlf !== "\n" && $crlf !== "\r\n" && $crlf !== "\r") ? "\n" : $crlf;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Message Boundary
+	 *
+	 * @return	void
+	 */
+	protected function _set_boundaries()
+	{
+		$this->_alt_boundary = 'B_ALT_'.uniqid(''); // multipart/alternative
+		$this->_atc_boundary = 'B_ATC_'.uniqid(''); // attachment boundary
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the Message ID
+	 *
+	 * @return	string
+	 */
+	protected function _get_message_id()
+	{
+		$from = str_replace(array('>', '<'), '', $this->_headers['Return-Path']);
+		return '<'.uniqid('').strstr($from, '@').'>';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Mail Protocol
+	 *
+	 * @param	bool
+	 * @return	mixed
+	 */
+	protected function _get_protocol($return = TRUE)
+	{
+		$this->protocol = strtolower($this->protocol);
+		in_array($this->protocol, $this->_protocols, TRUE) OR $this->protocol = 'mail';
+
+		if ($return === TRUE)
+		{
+			return $this->protocol;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Mail Encoding
+	 *
+	 * @param	bool
+	 * @return	string
+	 */
+	protected function _get_encoding($return = TRUE)
+	{
+		in_array($this->_encoding, $this->_bit_depths) OR $this->_encoding = '8bit';
+
+		foreach ($this->_base_charsets as $charset)
+		{
+			if (strpos($charset, $this->charset) === 0)
+			{
+				$this->_encoding = '7bit';
+			}
+		}
+
+		if ($return === TRUE)
+		{
+			return $this->_encoding;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get content type (text/html/attachment)
+	 *
+	 * @return	string
+	 */
+	protected function _get_content_type()
+	{
+		if ($this->mailtype === 'html')
+		{
+			return (count($this->_attachments) === 0) ? 'html' : 'html-attach';
+		}
+		elseif	($this->mailtype === 'text' && count($this->_attachments) > 0)
+		{
+			return 'plain-attach';
+		}
+		else
+		{
+			return 'plain';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set RFC 822 Date
+	 *
+	 * @return	string
+	 */
+	protected function _set_date()
+	{
+		$timezone = date('Z');
+		$operator = ($timezone[0] === '-') ? '-' : '+';
+		$timezone = abs($timezone);
+		$timezone = floor($timezone/3600) * 100 + ($timezone % 3600) / 60;
+
+		return sprintf('%s %s%04d', date('D, j M Y H:i:s'), $operator, $timezone);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mime message
+	 *
+	 * @return	string
+	 */
+	protected function _get_mime_message()
+	{
+		return 'This is a multi-part message in MIME format.'.$this->newline.'Your email application may not support this format.';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate Email Address
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function validate_email($email)
+	{
+		if ( ! is_array($email))
+		{
+			$this->_set_error_message('lang:email_must_be_array');
+			return FALSE;
+		}
+
+		foreach ($email as $val)
+		{
+			if ( ! $this->valid_email($val))
+			{
+				$this->_set_error_message('lang:email_invalid_address', $val);
+				return FALSE;
+			}
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Email Validation
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function valid_email($email)
+	{
+		if (function_exists('idn_to_ascii') && $atpos = strpos($email, '@'))
+		{
+			$email = substr($email, 0, ++$atpos).idn_to_ascii(substr($email, $atpos));
+		}
+
+		return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Clean Extended Email Address: Joe Smith <joe@smith.com>
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function clean_email($email)
+	{
+		if ( ! is_array($email))
+		{
+			return preg_match('/\<(.*)\>/', $email, $match) ? $match[1] : $email;
+		}
+
+		$clean_email = array();
+
+		foreach ($email as $addy)
+		{
+			$clean_email[] = preg_match('/\<(.*)\>/', $addy, $match) ? $match[1] : $addy;
+		}
+
+		return $clean_email;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Build alternative plain text message
+	 *
+	 * Provides the raw message for use in plain-text headers of
+	 * HTML-formatted emails.
+	 * If the user hasn't specified his own alternative message
+	 * it creates one by stripping the HTML
+	 *
+	 * @return	string
+	 */
+	protected function _get_alt_message()
+	{
+		if ( ! empty($this->alt_message))
+		{
+			return ($this->wordwrap)
+				? $this->word_wrap($this->alt_message, 76)
+				: $this->alt_message;
+		}
+
+		$body = preg_match('/\<body.*?\>(.*)\<\/body\>/si', $this->_body, $match) ? $match[1] : $this->_body;
+		$body = str_replace("\t", '', preg_replace('#<!--(.*)--\>#', '', trim(strip_tags($body))));
+
+		for ($i = 20; $i >= 3; $i--)
+		{
+			$body = str_replace(str_repeat("\n", $i), "\n\n", $body);
+		}
+
+		// Reduce multiple spaces
+		$body = preg_replace('| +|', ' ', $body);
+
+		return ($this->wordwrap)
+			? $this->word_wrap($body, 76)
+			: $body;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Word Wrap
+	 *
+	 * @param	string
+	 * @param	int	line-length limit
+	 * @return	string
+	 */
+	public function word_wrap($str, $charlim = NULL)
+	{
+		// Set the character limit, if not already present
+		if (empty($charlim))
+		{
+			$charlim = empty($this->wrapchars) ? 76 : $this->wrapchars;
+		}
+
+		// Standardize newlines
+		if (strpos($str, "\r") !== FALSE)
+		{
+			$str = str_replace(array("\r\n", "\r"), "\n", $str);
+		}
+
+		// Reduce multiple spaces at end of line
+		$str = preg_replace('| +\n|', "\n", $str);
+
+		// If the current word is surrounded by {unwrap} tags we'll
+		// strip the entire chunk and replace it with a marker.
+		$unwrap = array();
+		if (preg_match_all('|\{unwrap\}(.+?)\{/unwrap\}|s', $str, $matches))
+		{
+			for ($i = 0, $c = count($matches[0]); $i < $c; $i++)
+			{
+				$unwrap[] = $matches[1][$i];
+				$str = str_replace($matches[0][$i], '{{unwrapped'.$i.'}}', $str);
+			}
+		}
+
+		// Use PHP's native function to do the initial wordwrap.
+		// We set the cut flag to FALSE so that any individual words that are
+		// too long get left alone. In the next step we'll deal with them.
+		$str = wordwrap($str, $charlim, "\n", FALSE);
+
+		// Split the string into individual lines of text and cycle through them
+		$output = '';
+		foreach (explode("\n", $str) as $line)
+		{
+			// Is the line within the allowed character count?
+			// If so we'll join it to the output and continue
+			if (mb_strlen($line) <= $charlim)
+			{
+				$output .= $line.$this->newline;
+				continue;
+			}
+
+			$temp = '';
+			do
+			{
+				// If the over-length word is a URL we won't wrap it
+				if (preg_match('!\[url.+\]|://|www\.!', $line))
+				{
+					break;
+				}
+
+				// Trim the word down
+				$temp .= mb_substr($line, 0, $charlim - 1);
+				$line = mb_substr($line, $charlim - 1);
+			}
+			while (mb_strlen($line) > $charlim);
+
+			// If $temp contains data it means we had to split up an over-length
+			// word into smaller chunks so we'll add it back to our current line
+			if ($temp !== '')
+			{
+				$output .= $temp.$this->newline;
+			}
+
+			$output .= $line.$this->newline;
+		}
+
+		// Put our markers back
+		if (count($unwrap) > 0)
+		{
+			foreach ($unwrap as $key => $val)
+			{
+				$output = str_replace('{{unwrapped'.$key.'}}', $val, $output);
+			}
+		}
+
+		return $output;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Build final headers
+	 *
+	 * @return	string
+	 */
+	protected function _build_headers()
+	{
+		$this->set_header('X-Sender', $this->clean_email($this->_headers['From']));
+		$this->set_header('X-Mailer', $this->useragent);
+		$this->set_header('X-Priority', $this->_priorities[$this->priority]);
+		$this->set_header('Message-ID', $this->_get_message_id());
+		$this->set_header('Mime-Version', '1.0');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Write Headers as a string
+	 *
+	 * @return	void
+	 */
+	protected function _write_headers()
+	{
+		if ($this->protocol === 'mail')
+		{
+			if (isset($this->_headers['Subject']))
+			{
+				$this->_subject = $this->_headers['Subject'];
+				unset($this->_headers['Subject']);
+			}
+		}
+
+		reset($this->_headers);
+		$this->_header_str = '';
+
+		foreach ($this->_headers as $key => $val)
+		{
+			$val = trim($val);
+
+			if ($val !== '')
+			{
+				$this->_header_str .= $key.': '.$val.$this->newline;
+			}
+		}
+
+		if ($this->_get_protocol() === 'mail')
+		{
+			$this->_header_str = rtrim($this->_header_str);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Build Final Body and attachments
+	 *
+	 * @return	bool
+	 */
+	protected function _build_message()
+	{
+		if ($this->wordwrap === TRUE && $this->mailtype !== 'html')
+		{
+			$this->_body = $this->word_wrap($this->_body);
+		}
+
+		$this->_set_boundaries();
+		$this->_write_headers();
+
+		$hdr = ($this->_get_protocol() === 'mail') ? $this->newline : '';
+		$body = '';
+
+		switch ($this->_get_content_type())
+		{
+			case 'plain' :
+
+				$hdr .= 'Content-Type: text/plain; charset='.$this->charset.$this->newline
+					.'Content-Transfer-Encoding: '.$this->_get_encoding();
+
+				if ($this->_get_protocol() === 'mail')
+				{
+					$this->_header_str .= $hdr;
+					$this->_finalbody = $this->_body;
+				}
+				else
+				{
+					$this->_finalbody = $hdr.$this->newline.$this->newline.$this->_body;
+				}
+
+				return;
+
+			case 'html' :
+
+				if ($this->send_multipart === FALSE)
+				{
+					$hdr .= 'Content-Type: text/html; charset='.$this->charset.$this->newline
+						.'Content-Transfer-Encoding: quoted-printable';
+				}
+				else
+				{
+					$hdr .= 'Content-Type: multipart/alternative; boundary="'.$this->_alt_boundary.'"';
+
+					$body .= $this->_get_mime_message().$this->newline.$this->newline
+						.'--'.$this->_alt_boundary.$this->newline
+
+						.'Content-Type: text/plain; charset='.$this->charset.$this->newline
+						.'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline.$this->newline
+						.$this->_get_alt_message().$this->newline.$this->newline.'--'.$this->_alt_boundary.$this->newline
+
+						.'Content-Type: text/html; charset='.$this->charset.$this->newline
+						.'Content-Transfer-Encoding: quoted-printable'.$this->newline.$this->newline;
+				}
+
+				$this->_finalbody = $body.$this->_prep_quoted_printable($this->_body).$this->newline.$this->newline;
+
+				if ($this->_get_protocol() === 'mail')
+				{
+					$this->_header_str .= $hdr;
+				}
+				else
+				{
+					$this->_finalbody = $hdr.$this->newline.$this->newline.$this->_finalbody;
+				}
+
+				if ($this->send_multipart !== FALSE)
+				{
+					$this->_finalbody .= '--'.$this->_alt_boundary.'--';
+				}
+
+				return;
+
+			case 'plain-attach' :
+
+				$hdr .= 'Content-Type: multipart/'.$this->multipart.'; boundary="'.$this->_atc_boundary.'"';
+
+				if ($this->_get_protocol() === 'mail')
+				{
+					$this->_header_str .= $hdr;
+				}
+
+				$body .= $this->_get_mime_message().$this->newline
+					.$this->newline
+					.'--'.$this->_atc_boundary.$this->newline
+					.'Content-Type: text/plain; charset='.$this->charset.$this->newline
+					.'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline
+					.$this->newline
+					.$this->_body.$this->newline.$this->newline;
+
+			break;
+			case 'html-attach' :
+
+				$hdr .= 'Content-Type: multipart/'.$this->multipart.'; boundary="'.$this->_atc_boundary.'"';
+
+				if ($this->_get_protocol() === 'mail')
+				{
+					$this->_header_str .= $hdr;
+				}
+
+				$body .= $this->_get_mime_message().$this->newline.$this->newline
+					.'--'.$this->_atc_boundary.$this->newline
+
+					.'Content-Type: multipart/alternative; boundary="'.$this->_alt_boundary.'"'.$this->newline.$this->newline
+					.'--'.$this->_alt_boundary.$this->newline
+
+					.'Content-Type: text/plain; charset='.$this->charset.$this->newline
+					.'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline.$this->newline
+					.$this->_get_alt_message().$this->newline.$this->newline.'--'.$this->_alt_boundary.$this->newline
+
+					.'Content-Type: text/html; charset='.$this->charset.$this->newline
+					.'Content-Transfer-Encoding: quoted-printable'.$this->newline.$this->newline
+
+					.$this->_prep_quoted_printable($this->_body).$this->newline.$this->newline
+					.'--'.$this->_alt_boundary.'--'.$this->newline.$this->newline;
+
+			break;
+		}
+
+		$attachment = array();
+		for ($i = 0, $c = count($this->_attachments), $z = 0; $i < $c; $i++)
+		{
+			$filename = $this->_attachments[$i]['name'][0];
+			$basename = ($this->_attachments[$i]['name'][1] === NULL)
+				? basename($filename) : $this->_attachments[$i]['name'][1];
+
+			$attachment[$z++] = '--'.$this->_atc_boundary.$this->newline
+				.'Content-type: '.$this->_attachments[$i]['type'].'; '
+				.'name="'.$basename.'"'.$this->newline
+				.'Content-Disposition: '.$this->_attachments[$i]['disposition'].';'.$this->newline
+				.'Content-Transfer-Encoding: base64'.$this->newline
+				.(empty($this->_attachments[$i]['cid']) ? '' : 'Content-ID: <'.$this->_attachments[$i]['cid'].'>'.$this->newline);
+
+			$attachment[$z++] = $this->_attachments[$i]['content'];
+		}
+
+		$body .= implode($this->newline, $attachment).$this->newline.'--'.$this->_atc_boundary.'--';
+		$this->_finalbody = ($this->_get_protocol() === 'mail')
+			? $body
+			: $hdr.$this->newline.$this->newline.$body;
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep Quoted Printable
+	 *
+	 * Prepares string for Quoted-Printable Content-Transfer-Encoding
+	 * Refer to RFC 2045 http://www.ietf.org/rfc/rfc2045.txt
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _prep_quoted_printable($str)
+	{
+		// We are intentionally wrapping so mail servers will encode characters
+		// properly and MUAs will behave, so {unwrap} must go!
+		$str = str_replace(array('{unwrap}', '{/unwrap}'), '', $str);
+
+		// RFC 2045 specifies CRLF as "\r\n".
+		// However, many developers choose to override that and violate
+		// the RFC rules due to (apparently) a bug in MS Exchange,
+		// which only works with "\n".
+		if ($this->crlf === "\r\n")
+		{
+			if (is_php('5.3'))
+			{
+				return quoted_printable_encode($str);
+			}
+			elseif (function_exists('imap_8bit'))
+			{
+				return imap_8bit($str);
+			}
+		}
+
+		// Reduce multiple spaces & remove nulls
+		$str = preg_replace(array('| +|', '/\x00+/'), array(' ', ''), $str);
+
+		// Standardize newlines
+		if (strpos($str, "\r") !== FALSE)
+		{
+			$str = str_replace(array("\r\n", "\r"), "\n", $str);
+		}
+
+		$escape = '=';
+		$output = '';
+
+		foreach (explode("\n", $str) as $line)
+		{
+			$length = strlen($line);
+			$temp = '';
+
+			// Loop through each character in the line to add soft-wrap
+			// characters at the end of a line " =\r\n" and add the newly
+			// processed line(s) to the output (see comment on $crlf class property)
+			for ($i = 0; $i < $length; $i++)
+			{
+				// Grab the next character
+				$char = $line[$i];
+				$ascii = ord($char);
+
+				// Convert spaces and tabs but only if it's the end of the line
+				if ($i === ($length - 1) && ($ascii === 32 OR $ascii === 9))
+				{
+					$char = $escape.sprintf('%02s', dechex($ascii));
+				}
+				elseif ($ascii === 61) // encode = signs
+				{
+					$char = $escape.strtoupper(sprintf('%02s', dechex($ascii)));  // =3D
+				}
+
+				// If we're at the character limit, add the line to the output,
+				// reset our temp variable, and keep on chuggin'
+				if ((strlen($temp) + strlen($char)) >= 76)
+				{
+					$output .= $temp.$escape.$this->crlf;
+					$temp = '';
+				}
+
+				// Add the character to our temporary line
+				$temp .= $char;
+			}
+
+			// Add our completed line to the output
+			$output .= $temp.$this->crlf;
+		}
+
+		// get rid of extra CRLF tacked onto the end
+		return substr($output, 0, strlen($this->crlf) * -1);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep Q Encoding
+	 *
+	 * Performs "Q Encoding" on a string for use in email headers.
+	 * It's related but not identical to quoted-printable, so it has its
+	 * own method.
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _prep_q_encoding($str)
+	{
+		$str = str_replace(array("\r", "\n"), '', $str);
+
+		if ($this->charset === 'UTF-8')
+		{
+			if (MB_ENABLED === TRUE)
+			{
+				return mb_encode_mimeheader($str, $this->charset, 'Q', $this->crlf);
+			}
+			elseif (ICONV_ENABLED === TRUE)
+			{
+				$output = @iconv_mime_encode('', $str,
+					array(
+						'scheme' => 'Q',
+						'line-length' => 76,
+						'input-charset' => $this->charset,
+						'output-charset' => $this->charset,
+						'line-break-chars' => $this->crlf
+					)
+				);
+
+				// There are reports that iconv_mime_encode() might fail and return FALSE
+				if ($output !== FALSE)
+				{
+					// iconv_mime_encode() will always put a header field name.
+					// We've passed it an empty one, but it still prepends our
+					// encoded string with ': ', so we need to strip it.
+					return substr($output, 2);
+				}
+
+				$chars = iconv_strlen($str, 'UTF-8');
+			}
+		}
+
+		// We might already have this set for UTF-8
+		isset($chars) OR $chars = strlen($str);
+
+		$output = '=?'.$this->charset.'?Q?';
+		for ($i = 0, $length = strlen($output); $i < $chars; $i++)
+		{
+			$chr = ($this->charset === 'UTF-8' && ICONV_ENABLED === TRUE)
+				? '='.implode('=', str_split(strtoupper(bin2hex(iconv_substr($str, $i, 1, $this->charset))), 2))
+				: '='.strtoupper(bin2hex($str[$i]));
+
+			// RFC 2045 sets a limit of 76 characters per line.
+			// We'll append ?= to the end of each line though.
+			if ($length + ($l = strlen($chr)) > 74)
+			{
+				$output .= '?='.$this->crlf // EOL
+					.' =?'.$this->charset.'?Q?'.$chr; // New line
+				$length = 6 + strlen($this->charset) + $l; // Reset the length for the new line
+			}
+			else
+			{
+				$output .= $chr;
+				$length += $l;
+			}
+		}
+
+		// End the header
+		return $output.'?=';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send Email
+	 *
+	 * @param	bool	$auto_clear = TRUE
+	 * @return	bool
+	 */
+	public function send($auto_clear = TRUE)
+	{
+		if ( ! isset($this->_headers['From']))
+		{
+			$this->_set_error_message('lang:email_no_from');
+			return FALSE;
+		}
+
+		if ($this->_replyto_flag === FALSE)
+		{
+			$this->reply_to($this->_headers['From']);
+		}
+
+		if ( ! isset($this->_recipients) && ! isset($this->_headers['To'])
+			&& ! isset($this->_bcc_array) && ! isset($this->_headers['Bcc'])
+			&& ! isset($this->_headers['Cc']))
+		{
+			$this->_set_error_message('lang:email_no_recipients');
+			return FALSE;
+		}
+
+		$this->_build_headers();
+
+		if ($this->bcc_batch_mode && count($this->_bcc_array) > $this->bcc_batch_size)
+		{
+			$result = $this->batch_bcc_send();
+
+			if ($result && $auto_clear)
+			{
+				$this->clear();
+			}
+
+			return $result;
+		}
+
+		if ($this->_build_message() === FALSE)
+		{
+			return FALSE;
+		}
+
+		$result = $this->_spool_email();
+
+		if ($result && $auto_clear)
+		{
+			$this->clear();
+		}
+
+		return $result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Batch Bcc Send. Sends groups of BCCs in batches
+	 *
+	 * @return	void
+	 */
+	public function batch_bcc_send()
+	{
+		$float = $this->bcc_batch_size - 1;
+		$set = '';
+		$chunk = array();
+
+		for ($i = 0, $c = count($this->_bcc_array); $i < $c; $i++)
+		{
+			if (isset($this->_bcc_array[$i]))
+			{
+				$set .= ', '.$this->_bcc_array[$i];
+			}
+
+			if ($i === $float)
+			{
+				$chunk[] = substr($set, 1);
+				$float += $this->bcc_batch_size;
+				$set = '';
+			}
+
+			if ($i === $c-1)
+			{
+				$chunk[] = substr($set, 1);
+			}
+		}
+
+		for ($i = 0, $c = count($chunk); $i < $c; $i++)
+		{
+			unset($this->_headers['Bcc']);
+
+			$bcc = $this->clean_email($this->_str_to_array($chunk[$i]));
+
+			if ($this->protocol !== 'smtp')
+			{
+				$this->set_header('Bcc', implode(', ', $bcc));
+			}
+			else
+			{
+				$this->_bcc_array = $bcc;
+			}
+
+			if ($this->_build_message() === FALSE)
+			{
+				return FALSE;
+			}
+
+			$this->_spool_email();
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Unwrap special elements
+	 *
+	 * @return	void
+	 */
+	protected function _unwrap_specials()
+	{
+		$this->_finalbody = preg_replace_callback('/\{unwrap\}(.*?)\{\/unwrap\}/si', array($this, '_remove_nl_callback'), $this->_finalbody);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Strip line-breaks via callback
+	 *
+	 * @param	string	$matches
+	 * @return	string
+	 */
+	protected function _remove_nl_callback($matches)
+	{
+		if (strpos($matches[1], "\r") !== FALSE OR strpos($matches[1], "\n") !== FALSE)
+		{
+			$matches[1] = str_replace(array("\r\n", "\r", "\n"), '', $matches[1]);
+		}
+
+		return $matches[1];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Spool mail to the mail server
+	 *
+	 * @return	bool
+	 */
+	protected function _spool_email()
+	{
+		$this->_unwrap_specials();
+
+		$method = '_send_with_'.$this->_get_protocol();
+		if ( ! $this->$method())
+		{
+			$this->_set_error_message('lang:email_send_failure_'.($this->_get_protocol() === 'mail' ? 'phpmail' : $this->_get_protocol()));
+			return FALSE;
+		}
+
+		$this->_set_error_message('lang:email_sent', $this->_get_protocol());
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send using mail()
+	 *
+	 * @return	bool
+	 */
+	protected function _send_with_mail()
+	{
+		if (is_array($this->_recipients))
+		{
+			$this->_recipients = implode(', ', $this->_recipients);
+		}
+
+		if ($this->_safe_mode === TRUE)
+		{
+			return mail($this->_recipients, $this->_subject, $this->_finalbody, $this->_header_str);
+		}
+		else
+		{
+			// most documentation of sendmail using the "-f" flag lacks a space after it, however
+			// we've encountered servers that seem to require it to be in place.
+			return mail($this->_recipients, $this->_subject, $this->_finalbody, $this->_header_str, '-f '.$this->clean_email($this->_headers['Return-Path']));
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send using Sendmail
+	 *
+	 * @return	bool
+	 */
+	protected function _send_with_sendmail()
+	{
+		// is popen() enabled?
+		if ( ! function_usable('popen')
+			OR FALSE === ($fp = @popen(
+						$this->mailpath.' -oi -f '.$this->clean_email($this->_headers['From'])
+							.' -t -r '.$this->clean_email($this->_headers['Return-Path'])
+						, 'w'))
+		) // server probably has popen disabled, so nothing we can do to get a verbose error.
+		{
+			return FALSE;
+		}
+
+		fputs($fp, $this->_header_str);
+		fputs($fp, $this->_finalbody);
+
+		$status = pclose($fp);
+
+		if ($status !== 0)
+		{
+			$this->_set_error_message('lang:email_exit_status', $status);
+			$this->_set_error_message('lang:email_no_socket');
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send using SMTP
+	 *
+	 * @return	bool
+	 */
+	protected function _send_with_smtp()
+	{
+		if ($this->smtp_host === '')
+		{
+			$this->_set_error_message('lang:email_no_hostname');
+			return FALSE;
+		}
+
+		if ( ! $this->_smtp_connect() OR ! $this->_smtp_authenticate())
+		{
+			return FALSE;
+		}
+
+		if ( ! $this->_send_command('from', $this->clean_email($this->_headers['From'])))
+		{
+			return FALSE;
+		}
+
+		foreach ($this->_recipients as $val)
+		{
+			if ( ! $this->_send_command('to', $val))
+			{
+				return FALSE;
+			}
+		}
+
+		if (count($this->_cc_array) > 0)
+		{
+			foreach ($this->_cc_array as $val)
+			{
+				if ($val !== '' && ! $this->_send_command('to', $val))
+				{
+					return FALSE;
+				}
+			}
+		}
+
+		if (count($this->_bcc_array) > 0)
+		{
+			foreach ($this->_bcc_array as $val)
+			{
+				if ($val !== '' && ! $this->_send_command('to', $val))
+				{
+					return FALSE;
+				}
+			}
+		}
+
+		if ( ! $this->_send_command('data'))
+		{
+			return FALSE;
+		}
+
+		// perform dot transformation on any lines that begin with a dot
+		$this->_send_data($this->_header_str.preg_replace('/^\./m', '..$1', $this->_finalbody));
+
+		$this->_send_data('.');
+
+		$reply = $this->_get_smtp_data();
+
+		$this->_set_error_message($reply);
+
+		if (strpos($reply, '250') !== 0)
+		{
+			$this->_set_error_message('lang:email_smtp_error', $reply);
+			return FALSE;
+		}
+
+		if ($this->smtp_keepalive)
+		{
+			$this->_send_command('reset');
+		}
+		else
+		{
+			$this->_send_command('quit');
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * SMTP Connect
+	 *
+	 * @return	string
+	 */
+	protected function _smtp_connect()
+	{
+		if (is_resource($this->_smtp_connect))
+		{
+			return TRUE;
+		}
+
+		$ssl = ($this->smtp_crypto === 'ssl') ? 'ssl://' : '';
+
+		$this->_smtp_connect = fsockopen($ssl.$this->smtp_host,
+							$this->smtp_port,
+							$errno,
+							$errstr,
+							$this->smtp_timeout);
+
+		if ( ! is_resource($this->_smtp_connect))
+		{
+			$this->_set_error_message('lang:email_smtp_error', $errno.' '.$errstr);
+			return FALSE;
+		}
+
+		stream_set_timeout($this->_smtp_connect, $this->smtp_timeout);
+		$this->_set_error_message($this->_get_smtp_data());
+
+		if ($this->smtp_crypto === 'tls')
+		{
+			$this->_send_command('hello');
+			$this->_send_command('starttls');
+
+			$crypto = stream_socket_enable_crypto($this->_smtp_connect, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT);
+
+			if ($crypto !== TRUE)
+			{
+				$this->_set_error_message('lang:email_smtp_error', $this->_get_smtp_data());
+				return FALSE;
+			}
+		}
+
+		return $this->_send_command('hello');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send SMTP command
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _send_command($cmd, $data = '')
+	{
+		switch ($cmd)
+		{
+			case 'hello' :
+
+						if ($this->_smtp_auth OR $this->_get_encoding() === '8bit')
+						{
+							$this->_send_data('EHLO '.$this->_get_hostname());
+						}
+						else
+						{
+							$this->_send_data('HELO '.$this->_get_hostname());
+						}
+
+						$resp = 250;
+			break;
+			case 'starttls'	:
+
+						$this->_send_data('STARTTLS');
+						$resp = 220;
+			break;
+			case 'from' :
+
+						$this->_send_data('MAIL FROM:<'.$data.'>');
+						$resp = 250;
+			break;
+			case 'to' :
+
+						if ($this->dsn)
+						{
+							$this->_send_data('RCPT TO:<'.$data.'> NOTIFY=SUCCESS,DELAY,FAILURE ORCPT=rfc822;'.$data);
+						}
+						else
+						{
+							$this->_send_data('RCPT TO:<'.$data.'>');
+						}
+
+						$resp = 250;
+			break;
+			case 'data'	:
+
+						$this->_send_data('DATA');
+						$resp = 354;
+			break;
+			case 'reset':
+
+						$this->_send_data('RSET');
+						$resp = 250;
+			break;
+			case 'quit'	:
+
+						$this->_send_data('QUIT');
+						$resp = 221;
+			break;
+		}
+
+		$reply = $this->_get_smtp_data();
+
+		$this->_debug_msg[] = '<pre>'.$cmd.': '.$reply.'</pre>';
+
+		if ((int) substr($reply, 0, 3) !== $resp)
+		{
+			$this->_set_error_message('lang:email_smtp_error', $reply);
+			return FALSE;
+		}
+
+		if ($cmd === 'quit')
+		{
+			fclose($this->_smtp_connect);
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * SMTP Authenticate
+	 *
+	 * @return	bool
+	 */
+	protected function _smtp_authenticate()
+	{
+		if ( ! $this->_smtp_auth)
+		{
+			return TRUE;
+		}
+
+		if ($this->smtp_user === '' && $this->smtp_pass === '')
+		{
+			$this->_set_error_message('lang:email_no_smtp_unpw');
+			return FALSE;
+		}
+
+		$this->_send_data('AUTH LOGIN');
+
+		$reply = $this->_get_smtp_data();
+
+		if (strpos($reply, '503') === 0)	// Already authenticated
+		{
+			return TRUE;
+		}
+		elseif (strpos($reply, '334') !== 0)
+		{
+			$this->_set_error_message('lang:email_failed_smtp_login', $reply);
+			return FALSE;
+		}
+
+		$this->_send_data(base64_encode($this->smtp_user));
+
+		$reply = $this->_get_smtp_data();
+
+		if (strpos($reply, '334') !== 0)
+		{
+			$this->_set_error_message('lang:email_smtp_auth_un', $reply);
+			return FALSE;
+		}
+
+		$this->_send_data(base64_encode($this->smtp_pass));
+
+		$reply = $this->_get_smtp_data();
+
+		if (strpos($reply, '235') !== 0)
+		{
+			$this->_set_error_message('lang:email_smtp_auth_pw', $reply);
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send SMTP data
+	 *
+	 * @param	string	$data
+	 * @return	bool
+	 */
+	protected function _send_data($data)
+	{
+		$data .= $this->newline;
+		for ($written = $timestamp = 0, $length = strlen($data); $written < $length; $written += $result)
+		{
+			if (($result = fwrite($this->_smtp_connect, substr($data, $written))) === FALSE)
+			{
+				break;
+			}
+			// See https://bugs.php.net/bug.php?id=39598 and http://php.net/manual/en/function.fwrite.php#96951
+			elseif ($result === 0)
+			{
+				if ($timestamp === 0)
+				{
+					$timestamp = time();
+				}
+				elseif ($timestamp < (time() - $this->smtp_timeout))
+				{
+					$result = FALSE;
+					break;
+				}
+
+				usleep(250000);
+				continue;
+			}
+			else
+			{
+				$timestamp = 0;
+			}
+		}
+
+		if ($result === FALSE)
+		{
+			$this->_set_error_message('lang:email_smtp_data_failure', $data);
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get SMTP data
+	 *
+	 * @return	string
+	 */
+	protected function _get_smtp_data()
+	{
+		$data = '';
+
+		while ($str = fgets($this->_smtp_connect, 512))
+		{
+			$data .= $str;
+
+			if ($str[3] === ' ')
+			{
+				break;
+			}
+		}
+
+		return $data;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Hostname
+	 *
+	 * There are only two legal types of hostname - either a fully
+	 * qualified domain name (eg: "mail.example.com") or an IP literal
+	 * (eg: "[1.2.3.4]").
+	 *
+	 * @link	https://tools.ietf.org/html/rfc5321#section-2.3.5
+	 * @link	http://cbl.abuseat.org/namingproblems.html
+	 * @return	string
+	 */
+	protected function _get_hostname()
+	{
+		if (isset($_SERVER['SERVER_NAME']))
+		{
+			return $_SERVER['SERVER_NAME'];
+		}
+
+		return isset($_SERVER['SERVER_ADDR']) ? '['.$_SERVER['SERVER_ADDR'].']' : '[127.0.0.1]';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Debug Message
+	 *
+	 * @param	array	$include	List of raw data chunks to include in the output
+	 *					Valid options are: 'headers', 'subject', 'body'
+	 * @return	string
+	 */
+	public function print_debugger($include = array('headers', 'subject', 'body'))
+	{
+		$msg = '';
+
+		if (count($this->_debug_msg) > 0)
+		{
+			foreach ($this->_debug_msg as $val)
+			{
+				$msg .= $val;
+			}
+		}
+
+		// Determine which parts of our raw data needs to be printed
+		$raw_data = '';
+		is_array($include) OR $include = array($include);
+
+		if (in_array('headers', $include, TRUE))
+		{
+			$raw_data = htmlspecialchars($this->_header_str)."\n";
+		}
+
+		if (in_array('subject', $include, TRUE))
+		{
+			$raw_data .= htmlspecialchars($this->_subject)."\n";
+		}
+
+		if (in_array('body', $include, TRUE))
+		{
+			$raw_data .= htmlspecialchars($this->_finalbody);
+		}
+
+		return $msg.($raw_data === '' ? '' : '<pre>'.$raw_data.'</pre>');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Message
+	 *
+	 * @param	string	$msg
+	 * @param	string	$val = ''
+	 * @return	void
+	 */
+	protected function _set_error_message($msg, $val = '')
+	{
+		$CI =& get_instance();
+		$CI->lang->load('email');
+
+		if (sscanf($msg, 'lang:%s', $line) !== 1 OR FALSE === ($line = $CI->lang->line($line)))
+		{
+			$this->_debug_msg[] = str_replace('%s', $val, $msg).'<br />';
+		}
+		else
+		{
+			$this->_debug_msg[] = str_replace('%s', $val, $line).'<br />';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mime Types
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _mime_types($ext = '')
+	{
+		$ext = strtolower($ext);
+
+		$mimes =& get_mimes();
+
+		if (isset($mimes[$ext]))
+		{
+			return is_array($mimes[$ext])
+				? current($mimes[$ext])
+				: $mimes[$ext];
+		}
+
+		return 'application/x-unknown-content-type';
+	}
+
+}
diff --git a/README/Encrypt.php b/README/Encrypt.php
new file mode 100644
index 0000000..a46d4f4
--- /dev/null
+++ b/README/Encrypt.php
@@ -0,0 +1,480 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Encryption Class
+ *
+ * Provides two-way keyed encoding using Mcrypt
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/encryption.html
+ */
+class CI_Encrypt {
+
+	/**
+	 * Reference to the user's encryption key
+	 *
+	 * @var string
+	 */
+	public $encryption_key		= '';
+
+	/**
+	 * Type of hash operation
+	 *
+	 * @var string
+	 */
+	protected $_hash_type		= 'sha1';
+
+	/**
+	 * Flag for the existence of mcrypt
+	 *
+	 * @var bool
+	 */
+	protected $_mcrypt_exists	= FALSE;
+
+	/**
+	 * Current cipher to be used with mcrypt
+	 *
+	 * @var string
+	 */
+	protected $_mcrypt_cipher;
+
+	/**
+	 * Method for encrypting/decrypting data
+	 *
+	 * @var int
+	 */
+	protected $_mcrypt_mode;
+
+	/**
+	 * Initialize Encryption class
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		if (($this->_mcrypt_exists = function_exists('mcrypt_encrypt')) === FALSE)
+		{
+			show_error('The Encrypt library requires the Mcrypt extension.');
+		}
+
+		log_message('info', 'Encrypt Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch the encryption key
+	 *
+	 * Returns it as MD5 in order to have an exact-length 128 bit key.
+	 * Mcrypt is sensitive to keys that are not the correct length
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function get_key($key = '')
+	{
+		if ($key === '')
+		{
+			if ($this->encryption_key !== '')
+			{
+				return $this->encryption_key;
+			}
+
+			$key = config_item('encryption_key');
+
+			if ( ! strlen($key))
+			{
+				show_error('In order to use the encryption class requires that you set an encryption key in your config file.');
+			}
+		}
+
+		return md5($key);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the encryption key
+	 *
+	 * @param	string
+	 * @return	CI_Encrypt
+	 */
+	public function set_key($key = '')
+	{
+		$this->encryption_key = $key;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Encode
+	 *
+	 * Encodes the message string using bitwise XOR encoding.
+	 * The key is combined with a random hash, and then it
+	 * too gets converted using XOR. The whole thing is then run
+	 * through mcrypt using the randomized key. The end result
+	 * is a double-encrypted message string that is randomized
+	 * with each call to this function, even if the supplied
+	 * message and key are the same.
+	 *
+	 * @param	string	the string to encode
+	 * @param	string	the key
+	 * @return	string
+	 */
+	public function encode($string, $key = '')
+	{
+		return base64_encode($this->mcrypt_encode($string, $this->get_key($key)));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Decode
+	 *
+	 * Reverses the above process
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	public function decode($string, $key = '')
+	{
+		if (preg_match('/[^a-zA-Z0-9\/\+=]/', $string) OR base64_encode(base64_decode($string)) !== $string)
+		{
+			return FALSE;
+		}
+
+		return $this->mcrypt_decode(base64_decode($string), $this->get_key($key));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Encode from Legacy
+	 *
+	 * Takes an encoded string from the original Encryption class algorithms and
+	 * returns a newly encoded string using the improved method added in 2.0.0
+	 * This allows for backwards compatibility and a method to transition to the
+	 * new encryption algorithms.
+	 *
+	 * For more details, see http://codeigniter.com/user_guide/installation/upgrade_200.html#encryption
+	 *
+	 * @param	string
+	 * @param	int		(mcrypt mode constant)
+	 * @param	string
+	 * @return	string
+	 */
+	public function encode_from_legacy($string, $legacy_mode = MCRYPT_MODE_ECB, $key = '')
+	{
+		if (preg_match('/[^a-zA-Z0-9\/\+=]/', $string))
+		{
+			return FALSE;
+		}
+
+		// decode it first
+		// set mode temporarily to what it was when string was encoded with the legacy
+		// algorithm - typically MCRYPT_MODE_ECB
+		$current_mode = $this->_get_mode();
+		$this->set_mode($legacy_mode);
+
+		$key = $this->get_key($key);
+		$dec = base64_decode($string);
+		if (($dec = $this->mcrypt_decode($dec, $key)) === FALSE)
+		{
+			$this->set_mode($current_mode);
+			return FALSE;
+		}
+
+		$dec = $this->_xor_decode($dec, $key);
+
+		// set the mcrypt mode back to what it should be, typically MCRYPT_MODE_CBC
+		$this->set_mode($current_mode);
+
+		// and re-encode
+		return base64_encode($this->mcrypt_encode($dec, $key));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * XOR Decode
+	 *
+	 * Takes an encoded string and key as input and generates the
+	 * plain-text original message
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _xor_decode($string, $key)
+	{
+		$string = $this->_xor_merge($string, $key);
+
+		$dec = '';
+		for ($i = 0, $l = strlen($string); $i < $l; $i++)
+		{
+			$dec .= ($string[$i++] ^ $string[$i]);
+		}
+
+		return $dec;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * XOR key + string Combiner
+	 *
+	 * Takes a string and key as input and computes the difference using XOR
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _xor_merge($string, $key)
+	{
+		$hash = $this->hash($key);
+		$str = '';
+		for ($i = 0, $ls = strlen($string), $lh = strlen($hash); $i < $ls; $i++)
+		{
+			$str .= $string[$i] ^ $hash[($i % $lh)];
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Encrypt using Mcrypt
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	public function mcrypt_encode($data, $key)
+	{
+		$init_size = mcrypt_get_iv_size($this->_get_cipher(), $this->_get_mode());
+		$init_vect = mcrypt_create_iv($init_size, MCRYPT_RAND);
+		return $this->_add_cipher_noise($init_vect.mcrypt_encrypt($this->_get_cipher(), $key, $data, $this->_get_mode(), $init_vect), $key);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Decrypt using Mcrypt
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	public function mcrypt_decode($data, $key)
+	{
+		$data = $this->_remove_cipher_noise($data, $key);
+		$init_size = mcrypt_get_iv_size($this->_get_cipher(), $this->_get_mode());
+
+		if ($init_size > strlen($data))
+		{
+			return FALSE;
+		}
+
+		$init_vect = substr($data, 0, $init_size);
+		$data = substr($data, $init_size);
+		return rtrim(mcrypt_decrypt($this->_get_cipher(), $key, $data, $this->_get_mode(), $init_vect), "\0");
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Adds permuted noise to the IV + encrypted data to protect
+	 * against Man-in-the-middle attacks on CBC mode ciphers
+	 * http://www.ciphersbyritter.com/GLOSSARY.HTM#IV
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _add_cipher_noise($data, $key)
+	{
+		$key = $this->hash($key);
+		$str = '';
+
+		for ($i = 0, $j = 0, $ld = strlen($data), $lk = strlen($key); $i < $ld; ++$i, ++$j)
+		{
+			if ($j >= $lk)
+			{
+				$j = 0;
+			}
+
+			$str .= chr((ord($data[$i]) + ord($key[$j])) % 256);
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Removes permuted noise from the IV + encrypted data, reversing
+	 * _add_cipher_noise()
+	 *
+	 * Function description
+	 *
+	 * @param	string	$data
+	 * @param	string	$key
+	 * @return	string
+	 */
+	protected function _remove_cipher_noise($data, $key)
+	{
+		$key = $this->hash($key);
+		$str = '';
+
+		for ($i = 0, $j = 0, $ld = strlen($data), $lk = strlen($key); $i < $ld; ++$i, ++$j)
+		{
+			if ($j >= $lk)
+			{
+				$j = 0;
+			}
+
+			$temp = ord($data[$i]) - ord($key[$j]);
+
+			if ($temp < 0)
+			{
+				$temp += 256;
+			}
+
+			$str .= chr($temp);
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the Mcrypt Cipher
+	 *
+	 * @param	int
+	 * @return	CI_Encrypt
+	 */
+	public function set_cipher($cipher)
+	{
+		$this->_mcrypt_cipher = $cipher;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the Mcrypt Mode
+	 *
+	 * @param	int
+	 * @return	CI_Encrypt
+	 */
+	public function set_mode($mode)
+	{
+		$this->_mcrypt_mode = $mode;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Mcrypt cipher Value
+	 *
+	 * @return	int
+	 */
+	protected function _get_cipher()
+	{
+		if ($this->_mcrypt_cipher === NULL)
+		{
+			return $this->_mcrypt_cipher = MCRYPT_RIJNDAEL_256;
+		}
+
+		return $this->_mcrypt_cipher;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Mcrypt Mode Value
+	 *
+	 * @return	int
+	 */
+	protected function _get_mode()
+	{
+		if ($this->_mcrypt_mode === NULL)
+		{
+			return $this->_mcrypt_mode = MCRYPT_MODE_CBC;
+		}
+
+		return $this->_mcrypt_mode;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the Hash type
+	 *
+	 * @param	string
+	 * @return	void
+	 */
+	public function set_hash($type = 'sha1')
+	{
+		$this->_hash_type = in_array($type, hash_algos()) ? $type : 'sha1';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Hash encode a string
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function hash($str)
+	{
+		return hash($this->_hash_type, $str);
+	}
+
+}
diff --git a/README/Encryption.php b/README/Encryption.php
new file mode 100644
index 0000000..f3e0398
--- /dev/null
+++ b/README/Encryption.php
@@ -0,0 +1,926 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Encryption Class
+ *
+ * Provides two-way keyed encryption via PHP's MCrypt and/or OpenSSL extensions.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		Andrey Andreev
+ * @link		http://codeigniter.com/user_guide/libraries/encryption.html
+ */
+class CI_Encryption {
+
+	/**
+	 * Encryption cipher
+	 *
+	 * @var	string
+	 */
+	protected $_cipher = 'aes-128';
+
+	/**
+	 * Cipher mode
+	 *
+	 * @var	string
+	 */
+	protected $_mode = 'cbc';
+
+	/**
+	 * Cipher handle
+	 *
+	 * @var	mixed
+	 */
+	protected $_handle;
+
+	/**
+	 * Encryption key
+	 *
+	 * @var	string
+	 */
+	protected $_key;
+
+	/**
+	 * PHP extension to be used
+	 *
+	 * @var	string
+	 */
+	protected $_driver;
+
+	/**
+	 * List of usable drivers (PHP extensions)
+	 *
+	 * @var	array
+	 */
+	protected $_drivers = array();
+
+	/**
+	 * List of available modes
+	 *
+	 * @var	array
+	 */
+	protected $_modes = array(
+		'mcrypt' => array(
+			'cbc' => 'cbc',
+			'ecb' => 'ecb',
+			'ofb' => 'nofb',
+			'ofb8' => 'ofb',
+			'cfb' => 'ncfb',
+			'cfb8' => 'cfb',
+			'ctr' => 'ctr',
+			'stream' => 'stream'
+		),
+		'openssl' => array(
+			'cbc' => 'cbc',
+			'ecb' => 'ecb',
+			'ofb' => 'ofb',
+			'cfb' => 'cfb',
+			'cfb8' => 'cfb8',
+			'ctr' => 'ctr',
+			'stream' => '',
+			'xts' => 'xts'
+		)
+	);
+
+	/**
+	 * List of supported HMAC algorithms
+	 *
+	 * name => digest size pairs
+	 *
+	 * @var	array
+	 */
+	protected $_digests = array(
+		'sha224' => 28,
+		'sha256' => 32,
+		'sha384' => 48,
+		'sha512' => 64
+	);
+
+	/**
+	 * mbstring.func_override flag
+	 *
+	 * @var	bool
+	 */
+	protected static $func_override;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(array $params = array())
+	{
+		$this->_drivers = array(
+			'mcrypt' => defined('MCRYPT_DEV_URANDOM'),
+			// While OpenSSL is available for PHP 5.3.0, an IV parameter
+			// for the encrypt/decrypt functions is only available since 5.3.3
+			'openssl' => (is_php('5.3.3') && extension_loaded('openssl'))
+		);
+
+		if ( ! $this->_drivers['mcrypt'] && ! $this->_drivers['openssl'])
+		{
+			show_error('Encryption: Unable to find an available encryption driver.');
+		}
+
+		isset(self::$func_override) OR self::$func_override = (extension_loaded('mbstring') && ini_get('mbstring.func_override'));
+		$this->initialize($params);
+
+		if ( ! isset($this->_key) && self::strlen($key = config_item('encryption_key')) > 0)
+		{
+			$this->_key = $key;
+		}
+
+		log_message('info', 'Encryption Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	CI_Encryption
+	 */
+	public function initialize(array $params)
+	{
+		if ( ! empty($params['driver']))
+		{
+			if (isset($this->_drivers[$params['driver']]))
+			{
+				if ($this->_drivers[$params['driver']])
+				{
+					$this->_driver = $params['driver'];
+				}
+				else
+				{
+					log_message('error', "Encryption: Driver '".$params['driver']."' is not available.");
+				}
+			}
+			else
+			{
+				log_message('error', "Encryption: Unknown driver '".$params['driver']."' cannot be configured.");
+			}
+		}
+
+		if (empty($this->_driver))
+		{
+			$this->_driver = ($this->_drivers['openssl'] === TRUE)
+				? 'openssl'
+				: 'mcrypt';
+
+			log_message('debug', "Encryption: Auto-configured driver '".$this->_driver."'.");
+		}
+
+		empty($params['cipher']) && $params['cipher'] = $this->_cipher;
+		empty($params['key']) OR $this->_key = $params['key'];
+		$this->{'_'.$this->_driver.'_initialize'}($params);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize MCrypt
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	protected function _mcrypt_initialize($params)
+	{
+		if ( ! empty($params['cipher']))
+		{
+			$params['cipher'] = strtolower($params['cipher']);
+			$this->_cipher_alias($params['cipher']);
+
+			if ( ! in_array($params['cipher'], mcrypt_list_algorithms(), TRUE))
+			{
+				log_message('error', 'Encryption: MCrypt cipher '.strtoupper($params['cipher']).' is not available.');
+			}
+			else
+			{
+				$this->_cipher = $params['cipher'];
+			}
+		}
+
+		if ( ! empty($params['mode']))
+		{
+			$params['mode'] = strtolower($params['mode']);
+			if ( ! isset($this->_modes['mcrypt'][$params['mode']]))
+			{
+				log_message('error', 'Encryption: MCrypt mode '.strtoupper($params['mode']).' is not available.');
+			}
+			else
+			{
+				$this->_mode = $this->_modes['mcrypt'][$params['mode']];
+			}
+		}
+
+		if (isset($this->_cipher, $this->_mode))
+		{
+			if (is_resource($this->_handle)
+				&& (strtolower(mcrypt_enc_get_algorithms_name($this->_handle)) !== $this->_cipher
+					OR strtolower(mcrypt_enc_get_modes_name($this->_handle)) !== $this->_mode)
+			)
+			{
+				mcrypt_module_close($this->_handle);
+			}
+
+			if ($this->_handle = mcrypt_module_open($this->_cipher, '', $this->_mode, ''))
+			{
+				log_message('info', 'Encryption: MCrypt cipher '.strtoupper($this->_cipher).' initialized in '.strtoupper($this->_mode).' mode.');
+			}
+			else
+			{
+				log_message('error', 'Encryption: Unable to initialize MCrypt with cipher '.strtoupper($this->_cipher).' in '.strtoupper($this->_mode).' mode.');
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize OpenSSL
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	protected function _openssl_initialize($params)
+	{
+		if ( ! empty($params['cipher']))
+		{
+			$params['cipher'] = strtolower($params['cipher']);
+			$this->_cipher_alias($params['cipher']);
+			$this->_cipher = $params['cipher'];
+		}
+
+		if ( ! empty($params['mode']))
+		{
+			$params['mode'] = strtolower($params['mode']);
+			if ( ! isset($this->_modes['openssl'][$params['mode']]))
+			{
+				log_message('error', 'Encryption: OpenSSL mode '.strtoupper($params['mode']).' is not available.');
+			}
+			else
+			{
+				$this->_mode = $this->_modes['openssl'][$params['mode']];
+			}
+		}
+
+		if (isset($this->_cipher, $this->_mode))
+		{
+			// This is mostly for the stream mode, which doesn't get suffixed in OpenSSL
+			$handle = empty($this->_mode)
+				? $this->_cipher
+				: $this->_cipher.'-'.$this->_mode;
+
+			if ( ! in_array($handle, openssl_get_cipher_methods(), TRUE))
+			{
+				$this->_handle = NULL;
+				log_message('error', 'Encryption: Unable to initialize OpenSSL with method '.strtoupper($handle).'.');
+			}
+			else
+			{
+				$this->_handle = $handle;
+				log_message('info', 'Encryption: OpenSSL initialized with method '.strtoupper($handle).'.');
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create a random key
+	 *
+	 * @param	int	$length	Output length
+	 * @return	string
+	 */
+	public function create_key($length)
+	{
+		return ($this->_driver === 'mcrypt')
+			? mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)
+			: openssl_random_pseudo_bytes($length);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Encrypt
+	 *
+	 * @param	string	$data	Input data
+	 * @param	array	$params	Input parameters
+	 * @return	string
+	 */
+	public function encrypt($data, array $params = NULL)
+	{
+		if (($params = $this->_get_params($params)) === FALSE)
+		{
+			return FALSE;
+		}
+
+		isset($params['key']) OR $params['key'] = $this->hkdf($this->_key, 'sha512', NULL, self::strlen($this->_key), 'encryption');
+
+		if (($data = $this->{'_'.$this->_driver.'_encrypt'}($data, $params)) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$params['base64'] && $data = base64_encode($data);
+
+		if (isset($params['hmac_digest']))
+		{
+			isset($params['hmac_key']) OR $params['hmac_key'] = $this->hkdf($this->_key, 'sha512', NULL, NULL, 'authentication');
+			return hash_hmac($params['hmac_digest'], $data, $params['hmac_key'], ! $params['base64']).$data;
+		}
+
+		return $data;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Encrypt via MCrypt
+	 *
+	 * @param	string	$data	Input data
+	 * @param	array	$params	Input parameters
+	 * @return	string
+	 */
+	protected function _mcrypt_encrypt($data, $params)
+	{
+		if ( ! is_resource($params['handle']))
+		{
+			return FALSE;
+		}
+
+		// The greater-than-1 comparison is mostly a work-around for a bug,
+		// where 1 is returned for ARCFour instead of 0.
+		$iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1)
+			? mcrypt_create_iv($iv_size, MCRYPT_DEV_URANDOM)
+			: NULL;
+
+		if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0)
+		{
+			if ($params['handle'] !== $this->_handle)
+			{
+				mcrypt_module_close($params['handle']);
+			}
+
+			return FALSE;
+		}
+
+		// Use PKCS#7 padding in order to ensure compatibility with OpenSSL
+		// and other implementations outside of PHP.
+		if (in_array(strtolower(mcrypt_enc_get_modes_name($params['handle'])), array('cbc', 'ecb'), TRUE))
+		{
+			$block_size = mcrypt_enc_get_block_size($params['handle']);
+			$pad = $block_size - (self::strlen($data) % $block_size);
+			$data .= str_repeat(chr($pad), $pad);
+		}
+
+		// Work-around for yet another strange behavior in MCrypt.
+		//
+		// When encrypting in ECB mode, the IV is ignored. Yet
+		// mcrypt_enc_get_iv_size() returns a value larger than 0
+		// even if ECB is used AND mcrypt_generic_init() complains
+		// if you don't pass an IV with length equal to the said
+		// return value.
+		//
+		// This probably would've been fine (even though still wasteful),
+		// but OpenSSL isn't that dumb and we need to make the process
+		// portable, so ...
+		$data = (mcrypt_enc_get_modes_name($params['handle']) !== 'ECB')
+			? $iv.mcrypt_generic($params['handle'], $data)
+			: mcrypt_generic($params['handle'], $data);
+
+		mcrypt_generic_deinit($params['handle']);
+		if ($params['handle'] !== $this->_handle)
+		{
+			mcrypt_module_close($params['handle']);
+		}
+
+		return $data;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Encrypt via OpenSSL
+	 *
+	 * @param	string	$data	Input data
+	 * @param	array	$params	Input parameters
+	 * @return	string
+	 */
+	protected function _openssl_encrypt($data, $params)
+	{
+		if (empty($params['handle']))
+		{
+			return FALSE;
+		}
+
+		$iv = ($iv_size = openssl_cipher_iv_length($params['handle']))
+			? openssl_random_pseudo_bytes($iv_size)
+			: NULL;
+
+		$data = openssl_encrypt(
+			$data,
+			$params['handle'],
+			$params['key'],
+			1, // DO NOT TOUCH!
+			$iv
+		);
+
+		if ($data === FALSE)
+		{
+			return FALSE;
+		}
+
+		return $iv.$data;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Decrypt
+	 *
+	 * @param	string	$data	Encrypted data
+	 * @param	array	$params	Input parameters
+	 * @return	string
+	 */
+	public function decrypt($data, array $params = NULL)
+	{
+		if (($params = $this->_get_params($params)) === FALSE)
+		{
+			return FALSE;
+		}
+
+		if (isset($params['hmac_digest']))
+		{
+			// This might look illogical, but it is done during encryption as well ...
+			// The 'base64' value is effectively an inverted "raw data" parameter
+			$digest_size = ($params['base64'])
+				? $this->_digests[$params['hmac_digest']] * 2
+				: $this->_digests[$params['hmac_digest']];
+
+			if (self::strlen($data) <= $digest_size)
+			{
+				return FALSE;
+			}
+
+			$hmac_input = self::substr($data, 0, $digest_size);
+			$data = self::substr($data, $digest_size);
+
+			isset($params['hmac_key']) OR $params['hmac_key'] = $this->hkdf($this->_key, 'sha512', NULL, NULL, 'authentication');
+			$hmac_check = hash_hmac($params['hmac_digest'], $data, $params['hmac_key'], ! $params['base64']);
+
+			// Time-attack-safe comparison
+			$diff = 0;
+			for ($i = 0; $i < $digest_size; $i++)
+			{
+				$diff |= ord($hmac_input[$i]) ^ ord($hmac_check[$i]);
+			}
+
+			if ($diff !== 0)
+			{
+				return FALSE;
+			}
+		}
+
+		if ($params['base64'])
+		{
+			$data = base64_decode($data);
+		}
+
+		isset($params['key']) OR $params['key'] = $this->hkdf($this->_key, 'sha512', NULL, self::strlen($this->_key), 'encryption');
+
+		return $this->{'_'.$this->_driver.'_decrypt'}($data, $params);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Decrypt via MCrypt
+	 *
+	 * @param	string	$data	Encrypted data
+	 * @param	array	$params	Input parameters
+	 * @return	string
+	 */
+	protected function _mcrypt_decrypt($data, $params)
+	{
+		if ( ! is_resource($params['handle']))
+		{
+			return FALSE;
+		}
+
+		// The greater-than-1 comparison is mostly a work-around for a bug,
+		// where 1 is returned for ARCFour instead of 0.
+		if (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1)
+		{
+			if (mcrypt_enc_get_modes_name($params['handle']) !== 'ECB')
+			{
+				$iv = self::substr($data, 0, $iv_size);
+				$data = self::substr($data, $iv_size);
+			}
+			else
+			{
+				// MCrypt is dumb and this is ignored, only size matters
+				$iv = str_repeat("\x0", $iv_size);
+			}
+		}
+		else
+		{
+			$iv = NULL;
+		}
+
+		if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0)
+		{
+			if ($params['handle'] !== $this->_handle)
+			{
+				mcrypt_module_close($params['handle']);
+			}
+
+			return FALSE;
+		}
+
+		$data = mdecrypt_generic($params['handle'], $data);
+		// Remove PKCS#7 padding, if necessary
+		if (in_array(strtolower(mcrypt_enc_get_modes_name($params['handle'])), array('cbc', 'ecb'), TRUE))
+		{
+			$data = self::substr($data, 0, -ord($data[self::strlen($data)-1]));
+		}
+
+		mcrypt_generic_deinit($params['handle']);
+		if ($params['handle'] !== $this->_handle)
+		{
+			mcrypt_module_close($params['handle']);
+		}
+
+		return $data;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Decrypt via OpenSSL
+	 *
+	 * @param	string	$data	Encrypted data
+	 * @param	array	$params	Input parameters
+	 * @return	string
+	 */
+	protected function _openssl_decrypt($data, $params)
+	{
+		if ($iv_size = openssl_cipher_iv_length($params['handle']))
+		{
+			$iv = self::substr($data, 0, $iv_size);
+			$data = self::substr($data, $iv_size);
+		}
+		else
+		{
+			$iv = NULL;
+		}
+
+		return empty($params['handle'])
+			? FALSE
+			: openssl_decrypt(
+				$data,
+				$params['handle'],
+				$params['key'],
+				1, // DO NOT TOUCH!
+				$iv
+			);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get params
+	 *
+	 * @param	array	$params	Input parameters
+	 * @return	array
+	 */
+	protected function _get_params($params)
+	{
+		if (empty($params))
+		{
+			return isset($this->_cipher, $this->_mode, $this->_key, $this->_handle)
+				? array(
+					'handle' => $this->_handle,
+					'cipher' => $this->_cipher,
+					'mode' => $this->_mode,
+					'key' => NULL,
+					'base64' => TRUE,
+					'hmac_digest' => 'sha512',
+					'hmac_key' => NULL
+				)
+				: FALSE;
+		}
+		elseif ( ! isset($params['cipher'], $params['mode'], $params['key']))
+		{
+			return FALSE;
+		}
+
+		if (isset($params['mode']))
+		{
+			$params['mode'] = strtolower($params['mode']);
+			if ( ! isset($this->_modes[$this->_driver][$params['mode']]))
+			{
+				return FALSE;
+			}
+			else
+			{
+				$params['mode'] = $this->_modes[$this->_driver][$params['mode']];
+			}
+		}
+
+		if (isset($params['hmac']) && $params['hmac'] === FALSE)
+		{
+			$params['hmac_digest'] = $params['hmac_key'] = NULL;
+		}
+		else
+		{
+			if ( ! isset($params['hmac_key']))
+			{
+				return FALSE;
+			}
+			elseif (isset($params['hmac_digest']))
+			{
+				$params['hmac_digest'] = strtolower($params['hmac_digest']);
+				if ( ! isset($this->_digests[$params['hmac_digest']]))
+				{
+					return FALSE;
+				}
+			}
+			else
+			{
+				$params['hmac_digest'] = 'sha512';
+			}
+		}
+
+		$params = array(
+			'handle' => NULL,
+			'cipher' => $params['cipher'],
+			'mode' => $params['mode'],
+			'key' => $params['key'],
+			'base64' => isset($params['raw_data']) ? ! $params['raw_data'] : FALSE,
+			'hmac_digest' => $params['hmac_digest'],
+			'hmac_key' => $params['hmac_key']
+		);
+
+		$this->_cipher_alias($params['cipher']);
+		$params['handle'] = ($params['cipher'] !== $this->_cipher OR $params['mode'] !== $this->_mode)
+			? $this->{'_'.$this->_driver.'_get_handle'}($params['cipher'], $params['mode'])
+			: $this->_handle;
+
+		return $params;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get MCrypt handle
+	 *
+	 * @param	string	$cipher	Cipher name
+	 * @param	string	$mode	Encryption mode
+	 * @return	resource
+	 */
+	protected function _mcrypt_get_handle($cipher, $mode)
+	{
+		return mcrypt_module_open($cipher, '', $mode, '');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get OpenSSL handle
+	 *
+	 * @param	string	$cipher	Cipher name
+	 * @param	string	$mode	Encryption mode
+	 * @return	string
+	 */
+	protected function _openssl_get_handle($cipher, $mode)
+	{
+		// OpenSSL methods aren't suffixed with '-stream' for this mode
+		return ($mode === 'stream')
+			? $cipher
+			: $cipher.'-'.$mode;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Cipher alias
+	 *
+	 * Tries to translate cipher names between MCrypt and OpenSSL's "dialects".
+	 *
+	 * @param	string	$cipher	Cipher name
+	 * @return	void
+	 */
+	protected function _cipher_alias(&$cipher)
+	{
+		static $dictionary;
+
+		if (empty($dictionary))
+		{
+			$dictionary = array(
+				'mcrypt' => array(
+					'aes-128' => 'rijndael-128',
+					'aes-192' => 'rijndael-128',
+					'aes-256' => 'rijndael-128',
+					'des3-ede3' => 'tripledes',
+					'bf' => 'blowfish',
+					'cast5' => 'cast-128',
+					'rc4' => 'arcfour',
+					'rc4-40' => 'arcfour'
+				),
+				'openssl' => array(
+					'rijndael-128' => 'aes-128',
+					'tripledes' => 'des-ede3',
+					'blowfish' => 'bf',
+					'cast-128' => 'cast5',
+					'arcfour' => 'rc4-40',
+					'rc4' => 'rc4-40'
+				)
+			);
+
+			// Notes:
+			//
+			// - Rijndael-128 is, at the same time all three of AES-128,
+			//   AES-192 and AES-256. The only difference between them is
+			//   the key size. Rijndael-192, Rijndael-256 on the other hand
+			//   also have different block sizes and are NOT AES-compatible.
+			//
+			// - Blowfish is said to be supporting key sizes between
+			//   4 and 56 bytes, but it appears that between MCrypt and
+			//   OpenSSL, only those of 16 and more bytes are compatible.
+			//   Also, don't know what MCrypt's 'blowfish-compat' is.
+			//
+			// - CAST-128/CAST5 produces a longer cipher when encrypted via
+			//   OpenSSL, but (strangely enough) can be decrypted by either
+			//   extension anyway.
+			//   Also, it appears that OpenSSL uses 16 rounds regardless of
+			//   the key size, while RFC2144 says that for key sizes lower
+			//   than 11 bytes, only 12 rounds should be used. This makes
+			//   it portable only with keys of between 11 and 16 bytes.
+			//
+			// - RC4 (ARCFour) has a strange implementation under OpenSSL.
+			//   Its 'rc4-40' cipher method seems to work flawlessly, yet
+			//   there's another one, 'rc4' that only works with a 16-byte key.
+			//
+			// - DES is compatible, but doesn't need an alias.
+			//
+			// Other seemingly matching ciphers between MCrypt, OpenSSL:
+			//
+			// - RC2 is NOT compatible and only an obscure forum post
+			//   confirms that it is MCrypt's fault.
+		}
+
+		if (isset($dictionary[$this->_driver][$cipher]))
+		{
+			$cipher = $dictionary[$this->_driver][$cipher];
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * HKDF
+	 *
+	 * @link	https://tools.ietf.org/rfc/rfc5869.txt
+	 * @param	$key	Input key
+	 * @param	$digest	A SHA-2 hashing algorithm
+	 * @param	$salt	Optional salt
+	 * @param	$length	Output length (defaults to the selected digest size)
+	 * @param	$info	Optional context/application-specific info
+	 * @return	string	A pseudo-random key
+	 */
+	public function hkdf($key, $digest = 'sha512', $salt = NULL, $length = NULL, $info = '')
+	{
+		if ( ! isset($this->_digests[$digest]))
+		{
+			return FALSE;
+		}
+
+		if (empty($length) OR ! is_int($length))
+		{
+			$length = $this->_digests[$digest];
+		}
+		elseif ($length > (255 * $this->_digests[$digest]))
+		{
+			return FALSE;
+		}
+
+		self::strlen($salt) OR $salt = str_repeat("\0", $this->_digests[$digest]);
+
+		$prk = hash_hmac($digest, $key, $salt, TRUE);
+		$key = '';
+		for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index++)
+		{
+			$key_block = hash_hmac($digest, $key_block.$info.chr($block_index), $prk, TRUE);
+			$key .= $key_block;
+		}
+
+		return self::substr($key, 0, $length);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * __get() magic
+	 *
+	 * @param	string	$key	Property name
+	 * @return	mixed
+	 */
+	public function __get($key)
+	{
+		// Because aliases
+		if ($key === 'mode')
+		{
+			return array_search($this->_mode, $this->_modes[$this->_driver], TRUE);
+		}
+		elseif (in_array($key, array('cipher', 'driver', 'drivers', 'digests'), TRUE))
+		{
+			return $this->{'_'.$key};
+		}
+
+		return NULL;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Byte-safe strlen()
+	 *
+	 * @param	string	$str
+	 * @return	integer
+	 */
+	protected static function strlen($str)
+	{
+		return (self::$func_override)
+			? mb_strlen($str, '8bit')
+			: strlen($str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Byte-safe substr()
+	 *
+	 * @param	string	$str
+	 * @param	int	$start
+	 * @param	int	$length
+	 * @return	string
+	 */
+	protected static function substr($str, $start, $length = NULL)
+	{
+		if (self::$func_override)
+		{
+			// mb_substr($str, $start, null, '8bit') returns an empty
+			// string on PHP 5.3
+			isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start);
+			return mb_substr($str, $start, $length, '8bit');
+		}
+
+		return isset($length)
+			? substr($str, $start, $length)
+			: substr($str, $start);
+	}
+}
diff --git a/README/Form_validation.php b/README/Form_validation.php
new file mode 100644
index 0000000..a158225
--- /dev/null
+++ b/README/Form_validation.php
@@ -0,0 +1,1595 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Form Validation Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Validation
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/form_validation.html
+ */
+class CI_Form_validation {
+
+	/**
+	 * Reference to the CodeIgniter instance
+	 *
+	 * @var object
+	 */
+	protected $CI;
+
+	/**
+	 * Validation data for the current form submission
+	 *
+	 * @var array
+	 */
+	protected $_field_data		= array();
+
+	/**
+	 * Validation rules for the current form
+	 *
+	 * @var array
+	 */
+	protected $_config_rules	= array();
+
+	/**
+	 * Array of validation errors
+	 *
+	 * @var array
+	 */
+	protected $_error_array		= array();
+
+	/**
+	 * Array of custom error messages
+	 *
+	 * @var array
+	 */
+	protected $_error_messages	= array();
+
+	/**
+	 * Start tag for error wrapping
+	 *
+	 * @var string
+	 */
+	protected $_error_prefix	= '<p>';
+
+	/**
+	 * End tag for error wrapping
+	 *
+	 * @var string
+	 */
+	protected $_error_suffix	= '</p>';
+
+	/**
+	 * Custom error message
+	 *
+	 * @var string
+	 */
+	protected $error_string		= '';
+
+	/**
+	 * Whether the form data has been validated as safe
+	 *
+	 * @var bool
+	 */
+	protected $_safe_form_data	= FALSE;
+
+	/**
+	 * Custom data to validate
+	 *
+	 * @var array
+	 */
+	public $validation_data	= array();
+
+	/**
+	 * Initialize Form_Validation class
+	 *
+	 * @param	array	$rules
+	 * @return	void
+	 */
+	public function __construct($rules = array())
+	{
+		$this->CI =& get_instance();
+
+		// applies delimiters set in config file.
+		if (isset($rules['error_prefix']))
+		{
+			$this->_error_prefix = $rules['error_prefix'];
+			unset($rules['error_prefix']);
+		}
+		if (isset($rules['error_suffix']))
+		{
+			$this->_error_suffix = $rules['error_suffix'];
+			unset($rules['error_suffix']);
+		}
+
+		// Validation rules can be stored in a config file.
+		$this->_config_rules = $rules;
+
+		// Automatically load the form helper
+		$this->CI->load->helper('form');
+
+		log_message('info', 'Form Validation Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Rules
+	 *
+	 * This function takes an array of field names and validation
+	 * rules as input, any custom error messages, validates the info,
+	 * and stores it
+	 *
+	 * @param	mixed	$field
+	 * @param	string	$label
+	 * @param	mixed	$rules
+	 * @param	array	$errors
+	 * @return	CI_Form_validation
+	 */
+	public function set_rules($field, $label = '', $rules = array(), $errors = array())
+	{
+		// No reason to set rules if we have no POST data
+		// or a validation array has not been specified
+		if ($this->CI->input->method() !== 'post' && empty($this->validation_data))
+		{
+			return $this;
+		}
+
+		// If an array was passed via the first parameter instead of individual string
+		// values we cycle through it and recursively call this function.
+		if (is_array($field))
+		{
+			foreach ($field as $row)
+			{
+				// Houston, we have a problem...
+				if ( ! isset($row['field'], $row['rules']))
+				{
+					continue;
+				}
+
+				// If the field label wasn't passed we use the field name
+				$label = isset($row['label']) ? $row['label'] : $row['field'];
+
+				// Add the custom error message array
+				$errors = (isset($row['errors']) && is_array($row['errors'])) ? $row['errors'] : array();
+
+				// Here we go!
+				$this->set_rules($row['field'], $label, $row['rules'], $errors);
+			}
+
+			return $this;
+		}
+
+		// No fields or no rules? Nothing to do...
+		if ( ! is_string($field) OR $field === '' OR empty($rules))
+		{
+			return $this;
+		}
+		elseif ( ! is_array($rules))
+		{
+			// BC: Convert pipe-separated rules string to an array
+			if ( ! is_string($rules))
+			{
+				return $this;
+			}
+
+			$rules = preg_split('/\|(?![^\[]*\])/', $rules);
+		}
+
+		// If the field label wasn't passed we use the field name
+		$label = ($label === '') ? $field : $label;
+
+		$indexes = array();
+
+		// Is the field name an array? If it is an array, we break it apart
+		// into its components so that we can fetch the corresponding POST data later
+		if (($is_array = (bool) preg_match_all('/\[(.*?)\]/', $field, $matches)) === TRUE)
+		{
+			sscanf($field, '%[^[][', $indexes[0]);
+
+			for ($i = 0, $c = count($matches[0]); $i < $c; $i++)
+			{
+				if ($matches[1][$i] !== '')
+				{
+					$indexes[] = $matches[1][$i];
+				}
+			}
+		}
+
+		// Build our master array
+		$this->_field_data[$field] = array(
+			'field'		=> $field,
+			'label'		=> $label,
+			'rules'		=> $rules,
+			'errors'	=> $errors,
+			'is_array'	=> $is_array,
+			'keys'		=> $indexes,
+			'postdata'	=> NULL,
+			'error'		=> ''
+		);
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * By default, form validation uses the $_POST array to validate
+	 *
+	 * If an array is set through this method, then this array will
+	 * be used instead of the $_POST array
+	 *
+	 * Note that if you are validating multiple arrays, then the
+	 * reset_validation() function should be called after validating
+	 * each array due to the limitations of CI's singleton
+	 *
+	 * @param	array	$data
+	 * @return	CI_Form_validation
+	 */
+	public function set_data(array $data)
+	{
+		if ( ! empty($data))
+		{
+			$this->validation_data = $data;
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Error Message
+	 *
+	 * Lets users set their own error messages on the fly. Note:
+	 * The key name has to match the function name that it corresponds to.
+	 *
+	 * @param	array
+	 * @param	string
+	 * @return	CI_Form_validation
+	 */
+	public function set_message($lang, $val = '')
+	{
+		if ( ! is_array($lang))
+		{
+			$lang = array($lang => $val);
+		}
+
+		$this->_error_messages = array_merge($this->_error_messages, $lang);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set The Error Delimiter
+	 *
+	 * Permits a prefix/suffix to be added to each error message
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	CI_Form_validation
+	 */
+	public function set_error_delimiters($prefix = '<p>', $suffix = '</p>')
+	{
+		$this->_error_prefix = $prefix;
+		$this->_error_suffix = $suffix;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Error Message
+	 *
+	 * Gets the error message associated with a particular field
+	 *
+	 * @param	string	$field	Field name
+	 * @param	string	$prefix	HTML start tag
+	 * @param 	string	$suffix	HTML end tag
+	 * @return	string
+	 */
+	public function error($field, $prefix = '', $suffix = '')
+	{
+		if (empty($this->_field_data[$field]['error']))
+		{
+			return '';
+		}
+
+		if ($prefix === '')
+		{
+			$prefix = $this->_error_prefix;
+		}
+
+		if ($suffix === '')
+		{
+			$suffix = $this->_error_suffix;
+		}
+
+		return $prefix.$this->_field_data[$field]['error'].$suffix;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Array of Error Messages
+	 *
+	 * Returns the error messages as an array
+	 *
+	 * @return	array
+	 */
+	public function error_array()
+	{
+		return $this->_error_array;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error String
+	 *
+	 * Returns the error messages as a string, wrapped in the error delimiters
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	public function error_string($prefix = '', $suffix = '')
+	{
+		// No errors, validation passes!
+		if (count($this->_error_array) === 0)
+		{
+			return '';
+		}
+
+		if ($prefix === '')
+		{
+			$prefix = $this->_error_prefix;
+		}
+
+		if ($suffix === '')
+		{
+			$suffix = $this->_error_suffix;
+		}
+
+		// Generate the error string
+		$str = '';
+		foreach ($this->_error_array as $val)
+		{
+			if ($val !== '')
+			{
+				$str .= $prefix.$val.$suffix."\n";
+			}
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Run the Validator
+	 *
+	 * This function does all the work.
+	 *
+	 * @param	string	$group
+	 * @return	bool
+	 */
+	public function run($group = '')
+	{
+		// Do we even have any data to process?  Mm?
+		$validation_array = empty($this->validation_data) ? $_POST : $this->validation_data;
+		if (count($validation_array) === 0)
+		{
+			return FALSE;
+		}
+
+		// Does the _field_data array containing the validation rules exist?
+		// If not, we look to see if they were assigned via a config file
+		if (count($this->_field_data) === 0)
+		{
+			// No validation rules?  We're done...
+			if (count($this->_config_rules) === 0)
+			{
+				return FALSE;
+			}
+
+			if (empty($group))
+			{
+				// Is there a validation rule for the particular URI being accessed?
+				$group = trim($this->CI->uri->ruri_string(), '/');
+				isset($this->_config_rules[$group]) OR $group = $this->CI->router->class.'/'.$this->CI->router->method;
+			}
+
+			$this->set_rules(isset($this->_config_rules[$group]) ? $this->_config_rules[$group] : $this->_config_rules);
+
+			// Were we able to set the rules correctly?
+			if (count($this->_field_data) === 0)
+			{
+				log_message('debug', 'Unable to find validation rules');
+				return FALSE;
+			}
+		}
+
+		// Load the language file containing error messages
+		$this->CI->lang->load('form_validation');
+
+		// Cycle through the rules for each field and match the corresponding $validation_data item
+		foreach ($this->_field_data as $field => $row)
+		{
+			// Fetch the data from the validation_data array item and cache it in the _field_data array.
+			// Depending on whether the field name is an array or a string will determine where we get it from.
+			if ($row['is_array'] === TRUE)
+			{
+				$this->_field_data[$field]['postdata'] = $this->_reduce_array($validation_array, $row['keys']);
+			}
+			elseif (isset($validation_array[$field]))
+			{
+				$this->_field_data[$field]['postdata'] = $validation_array[$field];
+			}
+		}
+
+		// Execute validation rules
+		// Note: A second foreach (for now) is required in order to avoid false-positives
+		//	 for rules like 'matches', which correlate to other validation fields.
+		foreach ($this->_field_data as $field => $row)
+		{
+			// Don't try to validate if we have no rules set
+			if (empty($row['rules']))
+			{
+				continue;
+			}
+
+			$this->_execute($row, $row['rules'], $this->_field_data[$field]['postdata']);
+		}
+
+		// Did we end up with any errors?
+		$total_errors = count($this->_error_array);
+		if ($total_errors > 0)
+		{
+			$this->_safe_form_data = TRUE;
+		}
+
+		// Now we need to re-set the POST data with the new, processed data
+		$this->_reset_post_array();
+
+		return ($total_errors === 0);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Traverse a multidimensional $_POST array index until the data is found
+	 *
+	 * @param	array
+	 * @param	array
+	 * @param	int
+	 * @return	mixed
+	 */
+	protected function _reduce_array($array, $keys, $i = 0)
+	{
+		if (is_array($array) && isset($keys[$i]))
+		{
+			return isset($array[$keys[$i]]) ? $this->_reduce_array($array[$keys[$i]], $keys, ($i+1)) : NULL;
+		}
+
+		// NULL must be returned for empty fields
+		return ($array === '') ? NULL : $array;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Re-populate the _POST array with our finalized and processed data
+	 *
+	 * @return	void
+	 */
+	protected function _reset_post_array()
+	{
+		foreach ($this->_field_data as $field => $row)
+		{
+			if ($row['postdata'] !== NULL)
+			{
+				if ($row['is_array'] === FALSE)
+				{
+					if (isset($_POST[$row['field']]))
+					{
+						$_POST[$row['field']] = $row['postdata'];
+					}
+				}
+				else
+				{
+					// start with a reference
+					$post_ref =& $_POST;
+
+					// before we assign values, make a reference to the right POST key
+					if (count($row['keys']) === 1)
+					{
+						$post_ref =& $post_ref[current($row['keys'])];
+					}
+					else
+					{
+						foreach ($row['keys'] as $val)
+						{
+							$post_ref =& $post_ref[$val];
+						}
+					}
+
+					if (is_array($row['postdata']))
+					{
+						$array = array();
+						foreach ($row['postdata'] as $k => $v)
+						{
+							$array[$k] = $v;
+						}
+
+						$post_ref = $array;
+					}
+					else
+					{
+						$post_ref = $row['postdata'];
+					}
+				}
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Executes the Validation routines
+	 *
+	 * @param	array
+	 * @param	array
+	 * @param	mixed
+	 * @param	int
+	 * @return	mixed
+	 */
+	protected function _execute($row, $rules, $postdata = NULL, $cycles = 0)
+	{
+		// If the $_POST data is an array we will run a recursive call
+		if (is_array($postdata))
+		{
+			foreach ($postdata as $key => $val)
+			{
+				$this->_execute($row, $rules, $val, $key);
+			}
+
+			return;
+		}
+
+		// If the field is blank, but NOT required, no further tests are necessary
+		$callback = FALSE;
+		if ( ! in_array('required', $rules) && ($postdata === NULL OR $postdata === ''))
+		{
+			// Before we bail out, does the rule contain a callback?
+			foreach ($rules as &$rule)
+			{
+				if (is_string($rule))
+				{
+					if (strncmp($rule, 'callback_', 9) === 0)
+					{
+						$callback = TRUE;
+						$rules = array(1 => $rule);
+						break;
+					}
+				}
+				elseif (is_callable($rule))
+				{
+					$callback = TRUE;
+					$rules = array(1 => $rule);
+					break;
+				}
+				elseif (is_array($rule) && isset($rule[0], $rule[1]) && is_callable($rule[1]))
+				{
+					$callback = TRUE;
+					$rules = array(array($rule[0], $rule[1]));
+					break;
+				}
+			}
+
+			if ( ! $callback)
+			{
+				return;
+			}
+		}
+
+		// Isset Test. Typically this rule will only apply to checkboxes.
+		if (($postdata === NULL OR $postdata === '') && ! $callback)
+		{
+			if (in_array('isset', $rules, TRUE) OR in_array('required', $rules))
+			{
+				// Set the message type
+				$type = in_array('required', $rules) ? 'required' : 'isset';
+
+				// Check if a custom message is defined
+				if (isset($this->_field_data[$row['field']]['errors'][$type]))
+				{
+					$line = $this->_field_data[$row['field']]['errors'][$type];
+				}
+				elseif (isset($this->_error_messages[$type]))
+				{
+					$line = $this->_error_messages[$type];
+				}
+				elseif (FALSE === ($line = $this->CI->lang->line('form_validation_'.$type))
+					// DEPRECATED support for non-prefixed keys
+					&& FALSE === ($line = $this->CI->lang->line($type, FALSE)))
+				{
+					$line = 'The field was not set';
+				}
+
+				// Build the error message
+				$message = $this->_build_error_msg($line, $this->_translate_fieldname($row['label']));
+
+				// Save the error message
+				$this->_field_data[$row['field']]['error'] = $message;
+
+				if ( ! isset($this->_error_array[$row['field']]))
+				{
+					$this->_error_array[$row['field']] = $message;
+				}
+			}
+
+			return;
+		}
+
+		// --------------------------------------------------------------------
+
+		// Cycle through each rule and run it
+		foreach ($rules as $rule)
+		{
+			$_in_array = FALSE;
+
+			// We set the $postdata variable with the current data in our master array so that
+			// each cycle of the loop is dealing with the processed data from the last cycle
+			if ($row['is_array'] === TRUE && is_array($this->_field_data[$row['field']]['postdata']))
+			{
+				// We shouldn't need this safety, but just in case there isn't an array index
+				// associated with this cycle we'll bail out
+				if ( ! isset($this->_field_data[$row['field']]['postdata'][$cycles]))
+				{
+					continue;
+				}
+
+				$postdata = $this->_field_data[$row['field']]['postdata'][$cycles];
+				$_in_array = TRUE;
+			}
+			else
+			{
+				// If we get an array field, but it's not expected - then it is most likely
+				// somebody messing with the form on the client side, so we'll just consider
+				// it an empty field
+				$postdata = is_array($this->_field_data[$row['field']]['postdata'])
+					? NULL
+					: $this->_field_data[$row['field']]['postdata'];
+			}
+
+			// Is the rule a callback?
+			$callback = $callable = FALSE;
+			if (is_string($rule))
+			{
+				if (strpos($rule, 'callback_') === 0)
+				{
+					$rule = substr($rule, 9);
+					$callback = TRUE;
+				}
+			}
+			elseif (is_callable($rule))
+			{
+				$callable = TRUE;
+			}
+			elseif (is_array($rule) && isset($rule[0], $rule[1]) && is_callable($rule[1]))
+			{
+				// We have a "named" callable, so save the name
+				$callable = $rule[0];
+				$rule = $rule[1];
+			}
+
+			// Strip the parameter (if exists) from the rule
+			// Rules can contain a parameter: max_length[5]
+			$param = FALSE;
+			if ( ! $callable && preg_match('/(.*?)\[(.*)\]/', $rule, $match))
+			{
+				$rule = $match[1];
+				$param = $match[2];
+			}
+
+			// Call the function that corresponds to the rule
+			if ($callback OR $callable !== FALSE)
+			{
+				if ($callback)
+				{
+					if ( ! method_exists($this->CI, $rule))
+					{
+						log_message('debug', 'Unable to find callback validation rule: '.$rule);
+						$result = FALSE;
+					}
+					else
+					{
+						// Run the function and grab the result
+						$result = $this->CI->$rule($postdata, $param);
+					}
+				}
+				else
+				{
+					$result = is_array($rule)
+						? $rule[0]->{$rule[1]}($postdata)
+						: $rule($postdata);
+
+					// Is $callable set to a rule name?
+					if ($callable !== FALSE)
+					{
+						$rule = $callable;
+					}
+				}
+
+				// Re-assign the result to the master data array
+				if ($_in_array === TRUE)
+				{
+					$this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
+				}
+				else
+				{
+					$this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
+				}
+
+				// If the field isn't required and we just processed a callback we'll move on...
+				if ( ! in_array('required', $rules, TRUE) && $result !== FALSE)
+				{
+					continue;
+				}
+			}
+			elseif ( ! method_exists($this, $rule))
+			{
+				// If our own wrapper function doesn't exist we see if a native PHP function does.
+				// Users can use any native PHP function call that has one param.
+				if (function_exists($rule))
+				{
+					// Native PHP functions issue warnings if you pass them more parameters than they use
+					$result = ($param !== FALSE) ? $rule($postdata, $param) : $rule($postdata);
+
+					if ($_in_array === TRUE)
+					{
+						$this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
+					}
+					else
+					{
+						$this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
+					}
+				}
+				else
+				{
+					log_message('debug', 'Unable to find validation rule: '.$rule);
+					$result = FALSE;
+				}
+			}
+			else
+			{
+				$result = $this->$rule($postdata, $param);
+
+				if ($_in_array === TRUE)
+				{
+					$this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
+				}
+				else
+				{
+					$this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
+				}
+			}
+
+			// Did the rule test negatively? If so, grab the error.
+			if ($result === FALSE)
+			{
+				// Callable rules might not have named error messages
+				if ( ! is_string($rule))
+				{
+					$line = $this->CI->lang->line('form_validation_error_message_not_set').'(Anonymous function)';
+				}
+				// Check if a custom message is defined
+				elseif (isset($this->_field_data[$row['field']]['errors'][$rule]))
+				{
+					$line = $this->_field_data[$row['field']]['errors'][$rule];
+				}
+				elseif ( ! isset($this->_error_messages[$rule]))
+				{
+					if (FALSE === ($line = $this->CI->lang->line('form_validation_'.$rule))
+						// DEPRECATED support for non-prefixed keys
+						&& FALSE === ($line = $this->CI->lang->line($rule, FALSE)))
+					{
+						$line = $this->CI->lang->line('form_validation_error_message_not_set').'('.$rule.')';
+					}
+				}
+				else
+				{
+					$line = $this->_error_messages[$rule];
+				}
+
+				// Is the parameter we are inserting into the error message the name
+				// of another field? If so we need to grab its "field label"
+				if (isset($this->_field_data[$param], $this->_field_data[$param]['label']))
+				{
+					$param = $this->_translate_fieldname($this->_field_data[$param]['label']);
+				}
+
+				// Build the error message
+				$message = $this->_build_error_msg($line, $this->_translate_fieldname($row['label']), $param);
+
+				// Save the error message
+				$this->_field_data[$row['field']]['error'] = $message;
+
+				if ( ! isset($this->_error_array[$row['field']]))
+				{
+					$this->_error_array[$row['field']] = $message;
+				}
+
+				return;
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Translate a field name
+	 *
+	 * @param	string	the field name
+	 * @return	string
+	 */
+	protected function _translate_fieldname($fieldname)
+	{
+		// Do we need to translate the field name? We look for the prefix 'lang:' to determine this
+		// If we find one, but there's no translation for the string - just return it
+		if (sscanf($fieldname, 'lang:%s', $line) === 1 && FALSE === ($fieldname = $this->CI->lang->line($line, FALSE)))
+		{
+			return $line;
+		}
+
+		return $fieldname;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Build an error message using the field and param.
+	 *
+	 * @param	string	The error message line
+	 * @param	string	A field's human name
+	 * @param	mixed	A rule's optional parameter
+	 * @return	string
+	 */
+	protected function _build_error_msg($line, $field = '', $param = '')
+	{
+		// Check for %s in the string for legacy support.
+		if (strpos($line, '%s') !== FALSE)
+		{
+			return sprintf($line, $field, $param);
+		}
+
+		return str_replace(array('{field}', '{param}'), array($field, $param), $line);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Checks if the rule is present within the validator
+	 *
+	 * Permits you to check if a rule is present within the validator
+	 *
+	 * @param	string	the field name
+	 * @return	bool
+	 */
+	public function has_rule($field)
+	{
+		return isset($this->_field_data[$field]);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the value from a form
+	 *
+	 * Permits you to repopulate a form field with the value it was submitted
+	 * with, or, if that value doesn't exist, with the default
+	 *
+	 * @param	string	the field name
+	 * @param	string
+	 * @return	string
+	 */
+	public function set_value($field = '', $default = '')
+	{
+		if ( ! isset($this->_field_data[$field], $this->_field_data[$field]['postdata']))
+		{
+			return $default;
+		}
+
+		// If the data is an array output them one at a time.
+		//	E.g: form_input('name[]', set_value('name[]');
+		if (is_array($this->_field_data[$field]['postdata']))
+		{
+			return array_shift($this->_field_data[$field]['postdata']);
+		}
+
+		return $this->_field_data[$field]['postdata'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Select
+	 *
+	 * Enables pull-down lists to be set to the value the user
+	 * selected in the event of an error
+	 *
+	 * @param	string
+	 * @param	string
+	 * @param	bool
+	 * @return	string
+	 */
+	public function set_select($field = '', $value = '', $default = FALSE)
+	{
+		if ( ! isset($this->_field_data[$field], $this->_field_data[$field]['postdata']))
+		{
+			return ($default === TRUE && count($this->_field_data) === 0) ? ' selected="selected"' : '';
+		}
+
+		$field = $this->_field_data[$field]['postdata'];
+		$value = (string) $value;
+		if (is_array($field))
+		{
+			// Note: in_array('', array(0)) returns TRUE, do not use it
+			foreach ($field as &$v)
+			{
+				if ($value === $v)
+				{
+					return ' selected="selected"';
+				}
+			}
+
+			return '';
+		}
+		elseif (($field === '' OR $value === '') OR ($field !== $value))
+		{
+			return '';
+		}
+
+		return ' selected="selected"';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Radio
+	 *
+	 * Enables radio buttons to be set to the value the user
+	 * selected in the event of an error
+	 *
+	 * @param	string
+	 * @param	string
+	 * @param	bool
+	 * @return	string
+	 */
+	public function set_radio($field = '', $value = '', $default = FALSE)
+	{
+		if ( ! isset($this->_field_data[$field], $this->_field_data[$field]['postdata']))
+		{
+			return ($default === TRUE && count($this->_field_data) === 0) ? ' checked="checked"' : '';
+		}
+
+		$field = $this->_field_data[$field]['postdata'];
+		$value = (string) $value;
+		if (is_array($field))
+		{
+			// Note: in_array('', array(0)) returns TRUE, do not use it
+			foreach ($field as &$v)
+			{
+				if ($value === $v)
+				{
+					return ' checked="checked"';
+				}
+			}
+
+			return '';
+		}
+		elseif (($field === '' OR $value === '') OR ($field !== $value))
+		{
+			return '';
+		}
+
+		return ' checked="checked"';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Checkbox
+	 *
+	 * Enables checkboxes to be set to the value the user
+	 * selected in the event of an error
+	 *
+	 * @param	string
+	 * @param	string
+	 * @param	bool
+	 * @return	string
+	 */
+	public function set_checkbox($field = '', $value = '', $default = FALSE)
+	{
+		// Logic is exactly the same as for radio fields
+		return $this->set_radio($field, $value, $default);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Required
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function required($str)
+	{
+		return is_array($str) ? (bool) count($str) : (trim($str) !== '');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Performs a Regular Expression match test.
+	 *
+	 * @param	string
+	 * @param	string	regex
+	 * @return	bool
+	 */
+	public function regex_match($str, $regex)
+	{
+		return (bool) preg_match($regex, $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Match one field to another
+	 *
+	 * @param	string	$str	string to compare against
+	 * @param	string	$field
+	 * @return	bool
+	 */
+	public function matches($str, $field)
+	{
+		return isset($this->_field_data[$field], $this->_field_data[$field]['postdata'])
+			? ($str === $this->_field_data[$field]['postdata'])
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Differs from another field
+	 *
+	 * @param	string
+	 * @param	string	field
+	 * @return	bool
+	 */
+	public function differs($str, $field)
+	{
+		return ! (isset($this->_field_data[$field]) && $this->_field_data[$field]['postdata'] === $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is Unique
+	 *
+	 * Check if the input value doesn't already exist
+	 * in the specified database field.
+	 *
+	 * @param	string	$str
+	 * @param	string	$field
+	 * @return	bool
+	 */
+	public function is_unique($str, $field)
+	{
+		sscanf($field, '%[^.].%[^.]', $table, $field);
+		return isset($this->CI->db)
+			? ($this->CI->db->limit(1)->get_where($table, array($field => $str))->num_rows() === 0)
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Minimum Length
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	bool
+	 */
+	public function min_length($str, $val)
+	{
+		if ( ! is_numeric($val))
+		{
+			return FALSE;
+		}
+
+		return ($val <= mb_strlen($str));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Max Length
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	bool
+	 */
+	public function max_length($str, $val)
+	{
+		if ( ! is_numeric($val))
+		{
+			return FALSE;
+		}
+
+		return ($val >= mb_strlen($str));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Exact Length
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	bool
+	 */
+	public function exact_length($str, $val)
+	{
+		if ( ! is_numeric($val))
+		{
+			return FALSE;
+		}
+
+		return (mb_strlen($str) === (int) $val);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Valid URL
+	 *
+	 * @param	string	$str
+	 * @return	bool
+	 */
+	public function valid_url($str)
+	{
+		if (empty($str))
+		{
+			return FALSE;
+		}
+		elseif (preg_match('/^(?:([^:]*)\:)?\/\/(.+)$/', $str, $matches))
+		{
+			if (empty($matches[2]))
+			{
+				return FALSE;
+			}
+			elseif ( ! in_array($matches[1], array('http', 'https'), TRUE))
+			{
+				return FALSE;
+			}
+
+			$str = $matches[2];
+		}
+
+		$str = 'http://'.$str;
+
+		// There's a bug affecting PHP 5.2.13, 5.3.2 that considers the
+		// underscore to be a valid hostname character instead of a dash.
+		// Reference: https://bugs.php.net/bug.php?id=51192
+		if (version_compare(PHP_VERSION, '5.2.13', '==') OR version_compare(PHP_VERSION, '5.3.2', '=='))
+		{
+			sscanf($str, 'http://%[^/]', $host);
+			$str = substr_replace($str, strtr($host, array('_' => '-', '-' => '_')), 7, strlen($host));
+		}
+
+		return (filter_var($str, FILTER_VALIDATE_URL) !== FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Valid Email
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function valid_email($str)
+	{
+		if (function_exists('idn_to_ascii') && $atpos = strpos($str, '@'))
+		{
+			$str = substr($str, 0, ++$atpos).idn_to_ascii(substr($str, $atpos));
+		}
+
+		return (bool) filter_var($str, FILTER_VALIDATE_EMAIL);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Valid Emails
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function valid_emails($str)
+	{
+		if (strpos($str, ',') === FALSE)
+		{
+			return $this->valid_email(trim($str));
+		}
+
+		foreach (explode(',', $str) as $email)
+		{
+			if (trim($email) !== '' && $this->valid_email(trim($email)) === FALSE)
+			{
+				return FALSE;
+			}
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate IP Address
+	 *
+	 * @param	string
+	 * @param	string	'ipv4' or 'ipv6' to validate a specific IP format
+	 * @return	bool
+	 */
+	public function valid_ip($ip, $which = '')
+	{
+		return $this->CI->input->valid_ip($ip, $which);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Alpha
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function alpha($str)
+	{
+		return ctype_alpha($str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Alpha-numeric
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function alpha_numeric($str)
+	{
+		return ctype_alnum((string) $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Alpha-numeric w/ spaces
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function alpha_numeric_spaces($str)
+	{
+		return (bool) preg_match('/^[A-Z0-9 ]+$/i', $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Alpha-numeric with underscores and dashes
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function alpha_dash($str)
+	{
+		return (bool) preg_match('/^[a-z0-9_-]+$/i', $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Numeric
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function numeric($str)
+	{
+		return (bool) preg_match('/^[\-+]?[0-9]*\.?[0-9]+$/', $str);
+
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Integer
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function integer($str)
+	{
+		return (bool) preg_match('/^[\-+]?[0-9]+$/', $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Decimal number
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function decimal($str)
+	{
+		return (bool) preg_match('/^[\-+]?[0-9]+\.[0-9]+$/', $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Greater than
+	 *
+	 * @param	string
+	 * @param	int
+	 * @return	bool
+	 */
+	public function greater_than($str, $min)
+	{
+		return is_numeric($str) ? ($str > $min) : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Equal to or Greater than
+	 *
+	 * @param	string
+	 * @param	int
+	 * @return	bool
+	 */
+	public function greater_than_equal_to($str, $min)
+	{
+		return is_numeric($str) ? ($str >= $min) : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Less than
+	 *
+	 * @param	string
+	 * @param	int
+	 * @return	bool
+	 */
+	public function less_than($str, $max)
+	{
+		return is_numeric($str) ? ($str < $max) : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Equal to or Less than
+	 *
+	 * @param	string
+	 * @param	int
+	 * @return	bool
+	 */
+	public function less_than_equal_to($str, $max)
+	{
+		return is_numeric($str) ? ($str <= $max) : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Value should be within an array of values
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	bool
+	 */
+	public function in_list($value, $list)
+	{
+		return in_array($value, explode(',', $list), TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is a Natural number  (0,1,2,3, etc.)
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function is_natural($str)
+	{
+		return ctype_digit((string) $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is a Natural number, but not a zero  (1,2,3, etc.)
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function is_natural_no_zero($str)
+	{
+		return ($str != 0 && ctype_digit((string) $str));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Valid Base64
+	 *
+	 * Tests a string for characters outside of the Base64 alphabet
+	 * as defined by RFC 2045 http://www.faqs.org/rfcs/rfc2045
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function valid_base64($str)
+	{
+		return (base64_encode(base64_decode($str)) === $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep data for form
+	 *
+	 * This function allows HTML to be safely shown in a form.
+	 * Special characters are converted.
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function prep_for_form($data = '')
+	{
+		if ($this->_safe_form_data === FALSE OR empty($data))
+		{
+			return $data;
+		}
+
+		if (is_array($data))
+		{
+			foreach ($data as $key => $val)
+			{
+				$data[$key] = $this->prep_for_form($val);
+			}
+
+			return $data;
+		}
+
+		return str_replace(array("'", '"', '<', '>'), array('&#39;', '&quot;', '&lt;', '&gt;'), stripslashes($data));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep URL
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function prep_url($str = '')
+	{
+		if ($str === 'http://' OR $str === '')
+		{
+			return '';
+		}
+
+		if (strpos($str, 'http://') !== 0 && strpos($str, 'https://') !== 0)
+		{
+			return 'http://'.$str;
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Strip Image Tags
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function strip_image_tags($str)
+	{
+		return $this->CI->security->strip_image_tags($str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Convert PHP tags to entities
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function encode_php_tags($str)
+	{
+		return str_replace(array('<?', '?>'), array('&lt;?', '?&gt;'), $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Reset validation vars
+	 *
+	 * Prevents subsequent validation routines from being affected by the
+	 * results of any previous validation routine due to the CI singleton.
+	 *
+	 * @return	CI_Form_validation
+	 */
+	public function reset_validation()
+	{
+		$this->_field_data = array();
+		$this->_error_array = array();
+		$this->_error_messages = array();
+		$this->error_string = '';
+		return $this;
+	}
+
+}
diff --git a/README/Ftp.php b/README/Ftp.php
new file mode 100644
index 0000000..2d345c2
--- /dev/null
+++ b/README/Ftp.php
@@ -0,0 +1,667 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * FTP Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/ftp.html
+ */
+class CI_FTP {
+
+	/**
+	 * FTP Server hostname
+	 *
+	 * @var	string
+	 */
+	public $hostname = '';
+
+	/**
+	 * FTP Username
+	 *
+	 * @var	string
+	 */
+	public $username = '';
+
+	/**
+	 * FTP Password
+	 *
+	 * @var	string
+	 */
+	public $password = '';
+
+	/**
+	 * FTP Server port
+	 *
+	 * @var	int
+	 */
+	public $port = 21;
+
+	/**
+	 * Passive mode flag
+	 *
+	 * @var	bool
+	 */
+	public $passive = TRUE;
+
+	/**
+	 * Debug flag
+	 *
+	 * Specifies whether to display error messages.
+	 *
+	 * @var	bool
+	 */
+	public $debug = FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Connection ID
+	 *
+	 * @var	resource
+	 */
+	protected $conn_id;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	array	$config
+	 * @return	void
+	 */
+	public function __construct($config = array())
+	{
+		empty($config) OR $this->initialize($config);
+		log_message('info', 'FTP Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize preferences
+	 *
+	 * @param	array	$config
+	 * @return	void
+	 */
+	public function initialize($config = array())
+	{
+		foreach ($config as $key => $val)
+		{
+			if (isset($this->$key))
+			{
+				$this->$key = $val;
+			}
+		}
+
+		// Prep the hostname
+		$this->hostname = preg_replace('|.+?://|', '', $this->hostname);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * FTP Connect
+	 *
+	 * @param	array	 $config	Connection values
+	 * @return	bool
+	 */
+	public function connect($config = array())
+	{
+		if (count($config) > 0)
+		{
+			$this->initialize($config);
+		}
+
+		if (FALSE === ($this->conn_id = @ftp_connect($this->hostname, $this->port)))
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_unable_to_connect');
+			}
+
+			return FALSE;
+		}
+
+		if ( ! $this->_login())
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_unable_to_login');
+			}
+
+			return FALSE;
+		}
+
+		// Set passive mode if needed
+		if ($this->passive === TRUE)
+		{
+			ftp_pasv($this->conn_id, TRUE);
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * FTP Login
+	 *
+	 * @return	bool
+	 */
+	protected function _login()
+	{
+		return @ftp_login($this->conn_id, $this->username, $this->password);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validates the connection ID
+	 *
+	 * @return	bool
+	 */
+	protected function _is_conn()
+	{
+		if ( ! is_resource($this->conn_id))
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_no_connection');
+			}
+
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Change directory
+	 *
+	 * The second parameter lets us momentarily turn off debugging so that
+	 * this function can be used to test for the existence of a folder
+	 * without throwing an error. There's no FTP equivalent to is_dir()
+	 * so we do it by trying to change to a particular directory.
+	 * Internally, this parameter is only used by the "mirror" function below.
+	 *
+	 * @param	string	$path
+	 * @param	bool	$suppress_debug
+	 * @return	bool
+	 */
+	public function changedir($path, $suppress_debug = FALSE)
+	{
+		if ( ! $this->_is_conn())
+		{
+			return FALSE;
+		}
+
+		$result = @ftp_chdir($this->conn_id, $path);
+
+		if ($result === FALSE)
+		{
+			if ($this->debug === TRUE && $suppress_debug === FALSE)
+			{
+				$this->_error('ftp_unable_to_changedir');
+			}
+
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create a directory
+	 *
+	 * @param	string	$path
+	 * @param	int	$permissions
+	 * @return	bool
+	 */
+	public function mkdir($path, $permissions = NULL)
+	{
+		if ($path === '' OR ! $this->_is_conn())
+		{
+			return FALSE;
+		}
+
+		$result = @ftp_mkdir($this->conn_id, $path);
+
+		if ($result === FALSE)
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_unable_to_mkdir');
+			}
+
+			return FALSE;
+		}
+
+		// Set file permissions if needed
+		if ($permissions !== NULL)
+		{
+			$this->chmod($path, (int) $permissions);
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Upload a file to the server
+	 *
+	 * @param	string	$locpath
+	 * @param	string	$rempath
+	 * @param	string	$mode
+	 * @param	int	$permissions
+	 * @return	bool
+	 */
+	public function upload($locpath, $rempath, $mode = 'auto', $permissions = NULL)
+	{
+		if ( ! $this->_is_conn())
+		{
+			return FALSE;
+		}
+
+		if ( ! file_exists($locpath))
+		{
+			$this->_error('ftp_no_source_file');
+			return FALSE;
+		}
+
+		// Set the mode if not specified
+		if ($mode === 'auto')
+		{
+			// Get the file extension so we can set the upload type
+			$ext = $this->_getext($locpath);
+			$mode = $this->_settype($ext);
+		}
+
+		$mode = ($mode === 'ascii') ? FTP_ASCII : FTP_BINARY;
+
+		$result = @ftp_put($this->conn_id, $rempath, $locpath, $mode);
+
+		if ($result === FALSE)
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_unable_to_upload');
+			}
+
+			return FALSE;
+		}
+
+		// Set file permissions if needed
+		if ($permissions !== NULL)
+		{
+			$this->chmod($rempath, (int) $permissions);
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Download a file from a remote server to the local server
+	 *
+	 * @param	string	$rempath
+	 * @param	string	$locpath
+	 * @param	string	$mode
+	 * @return	bool
+	 */
+	public function download($rempath, $locpath, $mode = 'auto')
+	{
+		if ( ! $this->_is_conn())
+		{
+			return FALSE;
+		}
+
+		// Set the mode if not specified
+		if ($mode === 'auto')
+		{
+			// Get the file extension so we can set the upload type
+			$ext = $this->_getext($rempath);
+			$mode = $this->_settype($ext);
+		}
+
+		$mode = ($mode === 'ascii') ? FTP_ASCII : FTP_BINARY;
+
+		$result = @ftp_get($this->conn_id, $locpath, $rempath, $mode);
+
+		if ($result === FALSE)
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_unable_to_download');
+			}
+
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rename (or move) a file
+	 *
+	 * @param	string	$old_file
+	 * @param	string	$new_file
+	 * @param	bool	$move
+	 * @return	bool
+	 */
+	public function rename($old_file, $new_file, $move = FALSE)
+	{
+		if ( ! $this->_is_conn())
+		{
+			return FALSE;
+		}
+
+		$result = @ftp_rename($this->conn_id, $old_file, $new_file);
+
+		if ($result === FALSE)
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_unable_to_'.($move === FALSE ? 'rename' : 'move'));
+			}
+
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Move a file
+	 *
+	 * @param	string	$old_file
+	 * @param	string	$new_file
+	 * @return	bool
+	 */
+	public function move($old_file, $new_file)
+	{
+		return $this->rename($old_file, $new_file, TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rename (or move) a file
+	 *
+	 * @param	string	$filepath
+	 * @return	bool
+	 */
+	public function delete_file($filepath)
+	{
+		if ( ! $this->_is_conn())
+		{
+			return FALSE;
+		}
+
+		$result = @ftp_delete($this->conn_id, $filepath);
+
+		if ($result === FALSE)
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_unable_to_delete');
+			}
+
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete a folder and recursively delete everything (including sub-folders)
+	 * contained within it.
+	 *
+	 * @param	string	$filepath
+	 * @return	bool
+	 */
+	public function delete_dir($filepath)
+	{
+		if ( ! $this->_is_conn())
+		{
+			return FALSE;
+		}
+
+		// Add a trailing slash to the file path if needed
+		$filepath = preg_replace('/(.+?)\/*$/', '\\1/', $filepath);
+
+		$list = $this->list_files($filepath);
+		if ( ! empty($list))
+		{
+			for ($i = 0, $c = count($list); $i < $c; $i++)
+			{
+				// If we can't delete the item it's probaly a directory,
+				// so we'll recursively call delete_dir()
+				if ( ! preg_match('#/\.\.?$#', $list[$i]) && ! @ftp_delete($this->conn_id, $list[$i]))
+				{
+					$this->delete_dir($filepath.$list[$i]);
+				}
+			}
+		}
+
+		if (@ftp_rmdir($this->conn_id, $filepath) === FALSE)
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_unable_to_delete');
+			}
+
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set file permissions
+	 *
+	 * @param	string	$path	File path
+	 * @param	int	$perm	Permissions
+	 * @return	bool
+	 */
+	public function chmod($path, $perm)
+	{
+		if ( ! $this->_is_conn())
+		{
+			return FALSE;
+		}
+
+		if (@ftp_chmod($this->conn_id, $perm, $path) === FALSE)
+		{
+			if ($this->debug === TRUE)
+			{
+				$this->_error('ftp_unable_to_chmod');
+			}
+
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * FTP List files in the specified directory
+	 *
+	 * @param	string	$path
+	 * @return	array
+	 */
+	public function list_files($path = '.')
+	{
+		return $this->_is_conn()
+			? ftp_nlist($this->conn_id, $path)
+			: FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Read a directory and recreate it remotely
+	 *
+	 * This function recursively reads a folder and everything it contains
+	 * (including sub-folders) and creates a mirror via FTP based on it.
+	 * Whatever the directory structure of the original file path will be
+	 * recreated on the server.
+	 *
+	 * @param	string	$locpath	Path to source with trailing slash
+	 * @param	string	$rempath	Path to destination - include the base folder with trailing slash
+	 * @return	bool
+	 */
+	public function mirror($locpath, $rempath)
+	{
+		if ( ! $this->_is_conn())
+		{
+			return FALSE;
+		}
+
+		// Open the local file path
+		if ($fp = @opendir($locpath))
+		{
+			// Attempt to open the remote file path and try to create it, if it doesn't exist
+			if ( ! $this->changedir($rempath, TRUE) && ( ! $this->mkdir($rempath) OR ! $this->changedir($rempath)))
+			{
+				return FALSE;
+			}
+
+			// Recursively read the local directory
+			while (FALSE !== ($file = readdir($fp)))
+			{
+				if (is_dir($locpath.$file) && $file[0] !== '.')
+				{
+					$this->mirror($locpath.$file.'/', $rempath.$file.'/');
+				}
+				elseif ($file[0] !== '.')
+				{
+					// Get the file extension so we can se the upload type
+					$ext = $this->_getext($file);
+					$mode = $this->_settype($ext);
+
+					$this->upload($locpath.$file, $rempath.$file, $mode);
+				}
+			}
+
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Extract the file extension
+	 *
+	 * @param	string	$filename
+	 * @return	string
+	 */
+	protected function _getext($filename)
+	{
+		return (($dot = strrpos($filename, '.')) === FALSE)
+			? 'txt'
+			: substr($filename, $dot + 1);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the upload type
+	 *
+	 * @param	string	$ext	Filename extension
+	 * @return	string
+	 */
+	protected function _settype($ext)
+	{
+		return in_array($ext, array('txt', 'text', 'php', 'phps', 'php4', 'js', 'css', 'htm', 'html', 'phtml', 'shtml', 'log', 'xml'), TRUE)
+			? 'ascii'
+			: 'binary';
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Close the connection
+	 *
+	 * @return	bool
+	 */
+	public function close()
+	{
+		return $this->_is_conn()
+			? @ftp_close($this->conn_id)
+			: FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Display error message
+	 *
+	 * @param	string	$line
+	 * @return	void
+	 */
+	protected function _error($line)
+	{
+		$CI =& get_instance();
+		$CI->lang->load('ftp');
+		show_error($CI->lang->line($line));
+	}
+
+}
diff --git a/README/Image_lib.php b/README/Image_lib.php
new file mode 100644
index 0000000..e813efd
--- /dev/null
+++ b/README/Image_lib.php
@@ -0,0 +1,1825 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Image Manipulation class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Image_lib
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/image_lib.html
+ */
+class CI_Image_lib {
+
+	/**
+	 * PHP extension/library to use for image manipulation
+	 * Can be: imagemagick, netpbm, gd, gd2
+	 *
+	 * @var string
+	 */
+	public $image_library		= 'gd2';
+
+	/**
+	 * Path to the graphic library (if applicable)
+	 *
+	 * @var string
+	 */
+	public $library_path		= '';
+
+	/**
+	 * Whether to send to browser or write to disk
+	 *
+	 * @var bool
+	 */
+	public $dynamic_output		= FALSE;
+
+	/**
+	 * Path to original image
+	 *
+	 * @var string
+	 */
+	public $source_image		= '';
+
+	/**
+	 * Path to the modified image
+	 *
+	 * @var string
+	 */
+	public $new_image		= '';
+
+	/**
+	 * Image width
+	 *
+	 * @var int
+	 */
+	public $width			= '';
+
+	/**
+	 * Image height
+	 *
+	 * @var int
+	 */
+	public $height			= '';
+
+	/**
+	 * Quality percentage of new image
+	 *
+	 * @var int
+	 */
+	public $quality			= 90;
+
+	/**
+	 * Whether to create a thumbnail
+	 *
+	 * @var bool
+	 */
+	public $create_thumb		= FALSE;
+
+	/**
+	 * String to add to thumbnail version of image
+	 *
+	 * @var string
+	 */
+	public $thumb_marker		= '_thumb';
+
+	/**
+	 * Whether to maintain aspect ratio when resizing or use hard values
+	 *
+	 * @var bool
+	 */
+	public $maintain_ratio		= TRUE;
+
+	/**
+	 * auto, height, or width.  Determines what to use as the master dimension
+	 *
+	 * @var string
+	 */
+	public $master_dim		= 'auto';
+
+	/**
+	 * Angle at to rotate image
+	 *
+	 * @var string
+	 */
+	public $rotation_angle		= '';
+
+	/**
+	 * X Coordinate for manipulation of the current image
+	 *
+	 * @var int
+	 */
+	public $x_axis			= '';
+
+	/**
+	 * Y Coordinate for manipulation of the current image
+	 *
+	 * @var int
+	 */
+	public $y_axis			= '';
+
+	// --------------------------------------------------------------------------
+	// Watermark Vars
+	// --------------------------------------------------------------------------
+
+	/**
+	 * Watermark text if graphic is not used
+	 *
+	 * @var string
+	 */
+	public $wm_text			= '';
+
+	/**
+	 * Type of watermarking.  Options:  text/overlay
+	 *
+	 * @var string
+	 */
+	public $wm_type			= 'text';
+
+	/**
+	 * Default transparency for watermark
+	 *
+	 * @var int
+	 */
+	public $wm_x_transp		= 4;
+
+	/**
+	 * Default transparency for watermark
+	 *
+	 * @var int
+	 */
+	public $wm_y_transp		= 4;
+
+	/**
+	 * Watermark image path
+	 *
+	 * @var string
+	 */
+	public $wm_overlay_path		= '';
+
+	/**
+	 * TT font
+	 *
+	 * @var string
+	 */
+	public $wm_font_path		= '';
+
+	/**
+	 * Font size (different versions of GD will either use points or pixels)
+	 *
+	 * @var int
+	 */
+	public $wm_font_size		= 17;
+
+	/**
+	 * Vertical alignment:   T M B
+	 *
+	 * @var string
+	 */
+	public $wm_vrt_alignment	= 'B';
+
+	/**
+	 * Horizontal alignment: L R C
+	 *
+	 * @var string
+	 */
+	public $wm_hor_alignment	= 'C';
+
+	/**
+	 * Padding around text
+	 *
+	 * @var int
+	 */
+	public $wm_padding			= 0;
+
+	/**
+	 * Lets you push text to the right
+	 *
+	 * @var int
+	 */
+	public $wm_hor_offset		= 0;
+
+	/**
+	 * Lets you push text down
+	 *
+	 * @var int
+	 */
+	public $wm_vrt_offset		= 0;
+
+	/**
+	 * Text color
+	 *
+	 * @var string
+	 */
+	protected $wm_font_color	= '#ffffff';
+
+	/**
+	 * Dropshadow color
+	 *
+	 * @var string
+	 */
+	protected $wm_shadow_color	= '';
+
+	/**
+	 * Dropshadow distance
+	 *
+	 * @var int
+	 */
+	public $wm_shadow_distance	= 2;
+
+	/**
+	 * Image opacity: 1 - 100  Only works with image
+	 *
+	 * @var int
+	 */
+	public $wm_opacity		= 50;
+
+	// --------------------------------------------------------------------------
+	// Private Vars
+	// --------------------------------------------------------------------------
+
+	/**
+	 * Source image folder
+	 *
+	 * @var string
+	 */
+	public $source_folder		= '';
+
+	/**
+	 * Destination image folder
+	 *
+	 * @var string
+	 */
+	public $dest_folder		= '';
+
+	/**
+	 * Image mime-type
+	 *
+	 * @var string
+	 */
+	public $mime_type		= '';
+
+	/**
+	 * Original image width
+	 *
+	 * @var int
+	 */
+	public $orig_width		= '';
+
+	/**
+	 * Original image height
+	 *
+	 * @var int
+	 */
+	public $orig_height		= '';
+
+	/**
+	 * Image format
+	 *
+	 * @var string
+	 */
+	public $image_type		= '';
+
+	/**
+	 * Size of current image
+	 *
+	 * @var string
+	 */
+	public $size_str		= '';
+
+	/**
+	 * Full path to source image
+	 *
+	 * @var string
+	 */
+	public $full_src_path		= '';
+
+	/**
+	 * Full path to destination image
+	 *
+	 * @var string
+	 */
+	public $full_dst_path		= '';
+
+	/**
+	 * File permissions
+	 *
+	 * @var	int
+	 */
+	public $file_permissions = 0644;
+
+	/**
+	 * Name of function to create image
+	 *
+	 * @var string
+	 */
+	public $create_fnc		= 'imagecreatetruecolor';
+
+	/**
+	 * Name of function to copy image
+	 *
+	 * @var string
+	 */
+	public $copy_fnc		= 'imagecopyresampled';
+
+	/**
+	 * Error messages
+	 *
+	 * @var array
+	 */
+	public $error_msg		= array();
+
+	/**
+	 * Whether to have a drop shadow on watermark
+	 *
+	 * @var bool
+	 */
+	protected $wm_use_drop_shadow	= FALSE;
+
+	/**
+	 * Whether to use truetype fonts
+	 *
+	 * @var bool
+	 */
+	public $wm_use_truetype	= FALSE;
+
+	/**
+	 * Initialize Image Library
+	 *
+	 * @param	array	$props
+	 * @return	void
+	 */
+	public function __construct($props = array())
+	{
+		if (count($props) > 0)
+		{
+			$this->initialize($props);
+		}
+
+		log_message('info', 'Image Lib Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize image properties
+	 *
+	 * Resets values in case this class is used in a loop
+	 *
+	 * @return	void
+	 */
+	public function clear()
+	{
+		$props = array('thumb_marker', 'library_path', 'source_image', 'new_image', 'width', 'height', 'rotation_angle', 'x_axis', 'y_axis', 'wm_text', 'wm_overlay_path', 'wm_font_path', 'wm_shadow_color', 'source_folder', 'dest_folder', 'mime_type', 'orig_width', 'orig_height', 'image_type', 'size_str', 'full_src_path', 'full_dst_path');
+
+		foreach ($props as $val)
+		{
+			$this->$val = '';
+		}
+
+		$this->image_library 		= 'gd2';
+		$this->dynamic_output 		= FALSE;
+		$this->quality 				= 90;
+		$this->create_thumb 		= FALSE;
+		$this->thumb_marker 		= '_thumb';
+		$this->maintain_ratio 		= TRUE;
+		$this->master_dim 			= 'auto';
+		$this->wm_type 				= 'text';
+		$this->wm_x_transp 			= 4;
+		$this->wm_y_transp 			= 4;
+		$this->wm_font_size 		= 17;
+		$this->wm_vrt_alignment 	= 'B';
+		$this->wm_hor_alignment 	= 'C';
+		$this->wm_padding 			= 0;
+		$this->wm_hor_offset 		= 0;
+		$this->wm_vrt_offset 		= 0;
+		$this->wm_font_color		= '#ffffff';
+		$this->wm_shadow_distance 	= 2;
+		$this->wm_opacity 			= 50;
+		$this->create_fnc 			= 'imagecreatetruecolor';
+		$this->copy_fnc 			= 'imagecopyresampled';
+		$this->error_msg 			= array();
+		$this->wm_use_drop_shadow 	= FALSE;
+		$this->wm_use_truetype 		= FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * initialize image preferences
+	 *
+	 * @param	array
+	 * @return	bool
+	 */
+	public function initialize($props = array())
+	{
+		// Convert array elements into class variables
+		if (count($props) > 0)
+		{
+			foreach ($props as $key => $val)
+			{
+				if (property_exists($this, $key))
+				{
+					if (in_array($key, array('wm_font_color', 'wm_shadow_color')))
+					{
+						if (preg_match('/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i', $val, $matches))
+						{
+							/* $matches[1] contains our hex color value, but it might be
+							 * both in the full 6-length format or the shortened 3-length
+							 * value.
+							 * We'll later need the full version, so we keep it if it's
+							 * already there and if not - we'll convert to it. We can
+							 * access string characters by their index as in an array,
+							 * so we'll do that and use concatenation to form the final
+							 * value:
+							 */
+							$val = (strlen($matches[1]) === 6)
+								? '#'.$matches[1]
+								: '#'.$matches[1][0].$matches[1][0].$matches[1][1].$matches[1][1].$matches[1][2].$matches[1][2];
+						}
+						else
+						{
+							continue;
+						}
+					}
+
+					$this->$key = $val;
+				}
+			}
+		}
+
+		// Is there a source image? If not, there's no reason to continue
+		if ($this->source_image === '')
+		{
+			$this->set_error('imglib_source_image_required');
+			return FALSE;
+		}
+
+		/* Is getimagesize() available?
+		 *
+		 * We use it to determine the image properties (width/height).
+		 * Note: We need to figure out how to determine image
+		 * properties using ImageMagick and NetPBM
+		 */
+		if ( ! function_exists('getimagesize'))
+		{
+			$this->set_error('imglib_gd_required_for_props');
+			return FALSE;
+		}
+
+		$this->image_library = strtolower($this->image_library);
+
+		/* Set the full server path
+		 *
+		 * The source image may or may not contain a path.
+		 * Either way, we'll try use realpath to generate the
+		 * full server path in order to more reliably read it.
+		 */
+		if (($full_source_path = realpath($this->source_image)) !== FALSE)
+		{
+			$full_source_path = str_replace('\\', '/', $full_source_path);
+		}
+		else
+		{
+			$full_source_path = $this->source_image;
+		}
+
+		$x = explode('/', $full_source_path);
+		$this->source_image = end($x);
+		$this->source_folder = str_replace($this->source_image, '', $full_source_path);
+
+		// Set the Image Properties
+		if ( ! $this->get_image_properties($this->source_folder.$this->source_image))
+		{
+			return FALSE;
+		}
+
+		/*
+		 * Assign the "new" image name/path
+		 *
+		 * If the user has set a "new_image" name it means
+		 * we are making a copy of the source image. If not
+		 * it means we are altering the original. We'll
+		 * set the destination filename and path accordingly.
+		 */
+		if ($this->new_image === '')
+		{
+			$this->dest_image = $this->source_image;
+			$this->dest_folder = $this->source_folder;
+		}
+		elseif (strpos($this->new_image, '/') === FALSE)
+		{
+			$this->dest_folder = $this->source_folder;
+			$this->dest_image = $this->new_image;
+		}
+		else
+		{
+			if (strpos($this->new_image, '/') === FALSE && strpos($this->new_image, '\\') === FALSE)
+			{
+				$full_dest_path = str_replace('\\', '/', realpath($this->new_image));
+			}
+			else
+			{
+				$full_dest_path = $this->new_image;
+			}
+
+			// Is there a file name?
+			if ( ! preg_match('#\.(jpg|jpeg|gif|png)$#i', $full_dest_path))
+			{
+				$this->dest_folder = $full_dest_path.'/';
+				$this->dest_image = $this->source_image;
+			}
+			else
+			{
+				$x = explode('/', $full_dest_path);
+				$this->dest_image = end($x);
+				$this->dest_folder = str_replace($this->dest_image, '', $full_dest_path);
+			}
+		}
+
+		/* Compile the finalized filenames/paths
+		 *
+		 * We'll create two master strings containing the
+		 * full server path to the source image and the
+		 * full server path to the destination image.
+		 * We'll also split the destination image name
+		 * so we can insert the thumbnail marker if needed.
+		 */
+		if ($this->create_thumb === FALSE OR $this->thumb_marker === '')
+		{
+			$this->thumb_marker = '';
+		}
+
+		$xp = $this->explode_name($this->dest_image);
+
+		$filename = $xp['name'];
+		$file_ext = $xp['ext'];
+
+		$this->full_src_path = $this->source_folder.$this->source_image;
+		$this->full_dst_path = $this->dest_folder.$filename.$this->thumb_marker.$file_ext;
+
+		/* Should we maintain image proportions?
+		 *
+		 * When creating thumbs or copies, the target width/height
+		 * might not be in correct proportion with the source
+		 * image's width/height. We'll recalculate it here.
+		 */
+		if ($this->maintain_ratio === TRUE && ($this->width !== 0 OR $this->height !== 0))
+		{
+			$this->image_reproportion();
+		}
+
+		/* Was a width and height specified?
+		 *
+		 * If the destination width/height was not submitted we
+		 * will use the values from the actual file
+		 */
+		if ($this->width === '')
+		{
+			$this->width = $this->orig_width;
+		}
+
+		if ($this->height === '')
+		{
+			$this->height = $this->orig_height;
+		}
+
+		// Set the quality
+		$this->quality = trim(str_replace('%', '', $this->quality));
+
+		if ($this->quality === '' OR $this->quality === 0 OR ! ctype_digit($this->quality))
+		{
+			$this->quality = 90;
+		}
+
+		// Set the x/y coordinates
+		is_numeric($this->x_axis) OR $this->x_axis = 0;
+		is_numeric($this->y_axis) OR $this->y_axis = 0;
+
+		// Watermark-related Stuff...
+		if ($this->wm_overlay_path !== '')
+		{
+			$this->wm_overlay_path = str_replace('\\', '/', realpath($this->wm_overlay_path));
+		}
+
+		if ($this->wm_shadow_color !== '')
+		{
+			$this->wm_use_drop_shadow = TRUE;
+		}
+		elseif ($this->wm_use_drop_shadow === TRUE && $this->wm_shadow_color === '')
+		{
+			$this->wm_use_drop_shadow = FALSE;
+		}
+
+		if ($this->wm_font_path !== '')
+		{
+			$this->wm_use_truetype = TRUE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Image Resize
+	 *
+	 * This is a wrapper function that chooses the proper
+	 * resize function based on the protocol specified
+	 *
+	 * @return	bool
+	 */
+	public function resize()
+	{
+		$protocol = ($this->image_library === 'gd2') ? 'image_process_gd' : 'image_process_'.$this->image_library;
+		return $this->$protocol('resize');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Image Crop
+	 *
+	 * This is a wrapper function that chooses the proper
+	 * cropping function based on the protocol specified
+	 *
+	 * @return	bool
+	 */
+	public function crop()
+	{
+		$protocol = ($this->image_library === 'gd2') ? 'image_process_gd' : 'image_process_'.$this->image_library;
+		return $this->$protocol('crop');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Image Rotate
+	 *
+	 * This is a wrapper function that chooses the proper
+	 * rotation function based on the protocol specified
+	 *
+	 * @return	bool
+	 */
+	public function rotate()
+	{
+		// Allowed rotation values
+		$degs = array(90, 180, 270, 'vrt', 'hor');
+
+		if ($this->rotation_angle === '' OR ! in_array($this->rotation_angle, $degs))
+		{
+			$this->set_error('imglib_rotation_angle_required');
+			return FALSE;
+		}
+
+		// Reassign the width and height
+		if ($this->rotation_angle === 90 OR $this->rotation_angle === 270)
+		{
+			$this->width	= $this->orig_height;
+			$this->height	= $this->orig_width;
+		}
+		else
+		{
+			$this->width	= $this->orig_width;
+			$this->height	= $this->orig_height;
+		}
+
+		// Choose resizing function
+		if ($this->image_library === 'imagemagick' OR $this->image_library === 'netpbm')
+		{
+			$protocol = 'image_process_'.$this->image_library;
+			return $this->$protocol('rotate');
+		}
+
+		return ($this->rotation_angle === 'hor' OR $this->rotation_angle === 'vrt')
+			? $this->image_mirror_gd()
+			: $this->image_rotate_gd();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Image Process Using GD/GD2
+	 *
+	 * This function will resize or crop
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function image_process_gd($action = 'resize')
+	{
+		$v2_override = FALSE;
+
+		// If the target width/height match the source, AND if the new file name is not equal to the old file name
+		// we'll simply make a copy of the original with the new name... assuming dynamic rendering is off.
+		if ($this->dynamic_output === FALSE && $this->orig_width === $this->width && $this->orig_height === $this->height)
+		{
+			if ($this->source_image !== $this->new_image && @copy($this->full_src_path, $this->full_dst_path))
+			{
+				chmod($this->full_dst_path, $this->file_permissions);
+			}
+
+			return TRUE;
+		}
+
+		// Let's set up our values based on the action
+		if ($action === 'crop')
+		{
+			// Reassign the source width/height if cropping
+			$this->orig_width  = $this->width;
+			$this->orig_height = $this->height;
+
+			// GD 2.0 has a cropping bug so we'll test for it
+			if ($this->gd_version() !== FALSE)
+			{
+				$gd_version = str_replace('0', '', $this->gd_version());
+				$v2_override = ($gd_version == 2);
+			}
+		}
+		else
+		{
+			// If resizing the x/y axis must be zero
+			$this->x_axis = 0;
+			$this->y_axis = 0;
+		}
+
+		// Create the image handle
+		if ( ! ($src_img = $this->image_create_gd()))
+		{
+			return FALSE;
+		}
+
+		/* Create the image
+		 *
+		 * Old conditional which users report cause problems with shared GD libs who report themselves as "2.0 or greater"
+		 * it appears that this is no longer the issue that it was in 2004, so we've removed it, retaining it in the comment
+		 * below should that ever prove inaccurate.
+		 *
+		 * if ($this->image_library === 'gd2' && function_exists('imagecreatetruecolor') && $v2_override === FALSE)
+		 */
+		if ($this->image_library === 'gd2' && function_exists('imagecreatetruecolor'))
+		{
+			$create	= 'imagecreatetruecolor';
+			$copy	= 'imagecopyresampled';
+		}
+		else
+		{
+			$create	= 'imagecreate';
+			$copy	= 'imagecopyresized';
+		}
+
+		$dst_img = $create($this->width, $this->height);
+
+		if ($this->image_type === 3) // png we can actually preserve transparency
+		{
+			imagealphablending($dst_img, FALSE);
+			imagesavealpha($dst_img, TRUE);
+		}
+
+		$copy($dst_img, $src_img, 0, 0, $this->x_axis, $this->y_axis, $this->width, $this->height, $this->orig_width, $this->orig_height);
+
+		// Show the image
+		if ($this->dynamic_output === TRUE)
+		{
+			$this->image_display_gd($dst_img);
+		}
+		elseif ( ! $this->image_save_gd($dst_img)) // Or save it
+		{
+			return FALSE;
+		}
+
+		// Kill the file handles
+		imagedestroy($dst_img);
+		imagedestroy($src_img);
+
+		chmod($this->full_dst_path, $this->file_permissions);
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Image Process Using ImageMagick
+	 *
+	 * This function will resize, crop or rotate
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function image_process_imagemagick($action = 'resize')
+	{
+		// Do we have a vaild library path?
+		if ($this->library_path === '')
+		{
+			$this->set_error('imglib_libpath_invalid');
+			return FALSE;
+		}
+
+		if ( ! preg_match('/convert$/i', $this->library_path))
+		{
+			$this->library_path = rtrim($this->library_path, '/').'/convert';
+		}
+
+		// Execute the command
+		$cmd = $this->library_path.' -quality '.$this->quality;
+
+		if ($action === 'crop')
+		{
+			$cmd .= ' -crop '.$this->width.'x'.$this->height.'+'.$this->x_axis.'+'.$this->y_axis.' "'.$this->full_src_path.'" "'.$this->full_dst_path .'" 2>&1';
+		}
+		elseif ($action === 'rotate')
+		{
+			$angle = ($this->rotation_angle === 'hor' OR $this->rotation_angle === 'vrt')
+					? '-flop' : '-rotate '.$this->rotation_angle;
+
+			$cmd .= ' '.$angle.' "'.$this->full_src_path.'" "'.$this->full_dst_path.'" 2>&1';
+		}
+		else // Resize
+		{
+			if($this->maintain_ratio === TRUE)
+			{
+				$cmd .= ' -resize '.$this->width.'x'.$this->height.' "'.$this->full_src_path.'" "'.$this->full_dst_path.'" 2>&1';
+			}
+			else
+			{
+				$cmd .= ' -resize '.$this->width.'x'.$this->height.'\! "'.$this->full_src_path.'" "'.$this->full_dst_path.'" 2>&1';
+			}
+		}
+
+		$retval = 1;
+		// exec() might be disabled
+		if (function_usable('exec'))
+		{
+			@exec($cmd, $output, $retval);
+		}
+
+		// Did it work?
+		if ($retval > 0)
+		{
+			$this->set_error('imglib_image_process_failed');
+			return FALSE;
+		}
+
+		chmod($this->full_dst_path, $this->file_permissions);
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Image Process Using NetPBM
+	 *
+	 * This function will resize, crop or rotate
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function image_process_netpbm($action = 'resize')
+	{
+		if ($this->library_path === '')
+		{
+			$this->set_error('imglib_libpath_invalid');
+			return FALSE;
+		}
+
+		// Build the resizing command
+		switch ($this->image_type)
+		{
+			case 1 :
+				$cmd_in		= 'giftopnm';
+				$cmd_out	= 'ppmtogif';
+				break;
+			case 2 :
+				$cmd_in		= 'jpegtopnm';
+				$cmd_out	= 'ppmtojpeg';
+				break;
+			case 3 :
+				$cmd_in		= 'pngtopnm';
+				$cmd_out	= 'ppmtopng';
+				break;
+		}
+
+		if ($action === 'crop')
+		{
+			$cmd_inner = 'pnmcut -left '.$this->x_axis.' -top '.$this->y_axis.' -width '.$this->width.' -height '.$this->height;
+		}
+		elseif ($action === 'rotate')
+		{
+			switch ($this->rotation_angle)
+			{
+				case 90:	$angle = 'r270';
+					break;
+				case 180:	$angle = 'r180';
+					break;
+				case 270:	$angle = 'r90';
+					break;
+				case 'vrt':	$angle = 'tb';
+					break;
+				case 'hor':	$angle = 'lr';
+					break;
+			}
+
+			$cmd_inner = 'pnmflip -'.$angle.' ';
+		}
+		else // Resize
+		{
+			$cmd_inner = 'pnmscale -xysize '.$this->width.' '.$this->height;
+		}
+
+		$cmd = $this->library_path.$cmd_in.' '.$this->full_src_path.' | '.$cmd_inner.' | '.$cmd_out.' > '.$this->dest_folder.'netpbm.tmp';
+
+		$retval = 1;
+		// exec() might be disabled
+		if (function_usable('exec'))
+		{
+			@exec($cmd, $output, $retval);
+		}
+
+		// Did it work?
+		if ($retval > 0)
+		{
+			$this->set_error('imglib_image_process_failed');
+			return FALSE;
+		}
+
+		// With NetPBM we have to create a temporary image.
+		// If you try manipulating the original it fails so
+		// we have to rename the temp file.
+		copy($this->dest_folder.'netpbm.tmp', $this->full_dst_path);
+		unlink($this->dest_folder.'netpbm.tmp');
+		chmod($this->full_dst_path, $this->file_permissions);
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Image Rotate Using GD
+	 *
+	 * @return	bool
+	 */
+	public function image_rotate_gd()
+	{
+		// Create the image handle
+		if ( ! ($src_img = $this->image_create_gd()))
+		{
+			return FALSE;
+		}
+
+		// Set the background color
+		// This won't work with transparent PNG files so we are
+		// going to have to figure out how to determine the color
+		// of the alpha channel in a future release.
+
+		$white = imagecolorallocate($src_img, 255, 255, 255);
+
+		// Rotate it!
+		$dst_img = imagerotate($src_img, $this->rotation_angle, $white);
+
+		// Show the image
+		if ($this->dynamic_output === TRUE)
+		{
+			$this->image_display_gd($dst_img);
+		}
+		elseif ( ! $this->image_save_gd($dst_img)) // ... or save it
+		{
+			return FALSE;
+		}
+
+		// Kill the file handles
+		imagedestroy($dst_img);
+		imagedestroy($src_img);
+
+		chmod($this->full_dst_path, $this->file_permissions);
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create Mirror Image using GD
+	 *
+	 * This function will flip horizontal or vertical
+	 *
+	 * @return	bool
+	 */
+	public function image_mirror_gd()
+	{
+		if ( ! $src_img = $this->image_create_gd())
+		{
+			return FALSE;
+		}
+
+		$width  = $this->orig_width;
+		$height = $this->orig_height;
+
+		if ($this->rotation_angle === 'hor')
+		{
+			for ($i = 0; $i < $height; $i++)
+			{
+				$left = 0;
+				$right = $width - 1;
+
+				while ($left < $right)
+				{
+					$cl = imagecolorat($src_img, $left, $i);
+					$cr = imagecolorat($src_img, $right, $i);
+
+					imagesetpixel($src_img, $left, $i, $cr);
+					imagesetpixel($src_img, $right, $i, $cl);
+
+					$left++;
+					$right--;
+				}
+			}
+		}
+		else
+		{
+			for ($i = 0; $i < $width; $i++)
+			{
+				$top = 0;
+				$bottom = $height - 1;
+
+				while ($top < $bottom)
+				{
+					$ct = imagecolorat($src_img, $i, $top);
+					$cb = imagecolorat($src_img, $i, $bottom);
+
+					imagesetpixel($src_img, $i, $top, $cb);
+					imagesetpixel($src_img, $i, $bottom, $ct);
+
+					$top++;
+					$bottom--;
+				}
+			}
+		}
+
+		// Show the image
+		if ($this->dynamic_output === TRUE)
+		{
+			$this->image_display_gd($src_img);
+		}
+		elseif ( ! $this->image_save_gd($src_img)) // ... or save it
+		{
+			return FALSE;
+		}
+
+		// Kill the file handles
+		imagedestroy($src_img);
+
+		chmod($this->full_dst_path, $this->file_permissions);
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Image Watermark
+	 *
+	 * This is a wrapper function that chooses the type
+	 * of watermarking based on the specified preference.
+	 *
+	 * @return	bool
+	 */
+	public function watermark()
+	{
+		return ($this->wm_type === 'overlay') ? $this->overlay_watermark() : $this->text_watermark();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Watermark - Graphic Version
+	 *
+	 * @return	bool
+	 */
+	public function overlay_watermark()
+	{
+		if ( ! function_exists('imagecolortransparent'))
+		{
+			$this->set_error('imglib_gd_required');
+			return FALSE;
+		}
+
+		// Fetch source image properties
+		$this->get_image_properties();
+
+		// Fetch watermark image properties
+		$props		= $this->get_image_properties($this->wm_overlay_path, TRUE);
+		$wm_img_type	= $props['image_type'];
+		$wm_width	= $props['width'];
+		$wm_height	= $props['height'];
+
+		// Create two image resources
+		$wm_img  = $this->image_create_gd($this->wm_overlay_path, $wm_img_type);
+		$src_img = $this->image_create_gd($this->full_src_path);
+
+		// Reverse the offset if necessary
+		// When the image is positioned at the bottom
+		// we don't want the vertical offset to push it
+		// further down. We want the reverse, so we'll
+		// invert the offset. Same with the horizontal
+		// offset when the image is at the right
+
+		$this->wm_vrt_alignment = strtoupper($this->wm_vrt_alignment[0]);
+		$this->wm_hor_alignment = strtoupper($this->wm_hor_alignment[0]);
+
+		if ($this->wm_vrt_alignment === 'B')
+			$this->wm_vrt_offset = $this->wm_vrt_offset * -1;
+
+		if ($this->wm_hor_alignment === 'R')
+			$this->wm_hor_offset = $this->wm_hor_offset * -1;
+
+		// Set the base x and y axis values
+		$x_axis = $this->wm_hor_offset + $this->wm_padding;
+		$y_axis = $this->wm_vrt_offset + $this->wm_padding;
+
+		// Set the vertical position
+		if ($this->wm_vrt_alignment === 'M')
+		{
+			$y_axis += ($this->orig_height / 2) - ($wm_height / 2);
+		}
+		elseif ($this->wm_vrt_alignment === 'B')
+		{
+			$y_axis += $this->orig_height - $wm_height;
+		}
+
+		// Set the horizontal position
+		if ($this->wm_hor_alignment === 'C')
+		{
+			$x_axis += ($this->orig_width / 2) - ($wm_width / 2);
+		}
+		elseif ($this->wm_hor_alignment === 'R')
+		{
+			$x_axis += $this->orig_width - $wm_width;
+		}
+
+		// Build the finalized image
+		if ($wm_img_type === 3 && function_exists('imagealphablending'))
+		{
+			@imagealphablending($src_img, TRUE);
+		}
+
+		// Set RGB values for text and shadow
+		$rgba = imagecolorat($wm_img, $this->wm_x_transp, $this->wm_y_transp);
+		$alpha = ($rgba & 0x7F000000) >> 24;
+
+		// make a best guess as to whether we're dealing with an image with alpha transparency or no/binary transparency
+		if ($alpha > 0)
+		{
+			// copy the image directly, the image's alpha transparency being the sole determinant of blending
+			imagecopy($src_img, $wm_img, $x_axis, $y_axis, 0, 0, $wm_width, $wm_height);
+		}
+		else
+		{
+			// set our RGB value from above to be transparent and merge the images with the specified opacity
+			imagecolortransparent($wm_img, imagecolorat($wm_img, $this->wm_x_transp, $this->wm_y_transp));
+			imagecopymerge($src_img, $wm_img, $x_axis, $y_axis, 0, 0, $wm_width, $wm_height, $this->wm_opacity);
+		}
+
+		// We can preserve transparency for PNG images
+		if ($this->image_type === 3)
+		{
+			imagealphablending($src_img, FALSE);
+			imagesavealpha($src_img, TRUE);
+		}
+
+		// Output the image
+		if ($this->dynamic_output === TRUE)
+		{
+			$this->image_display_gd($src_img);
+		}
+		elseif ( ! $this->image_save_gd($src_img)) // ... or save it
+		{
+			return FALSE;
+		}
+
+		imagedestroy($src_img);
+		imagedestroy($wm_img);
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Watermark - Text Version
+	 *
+	 * @return	bool
+	 */
+	public function text_watermark()
+	{
+		if ( ! ($src_img = $this->image_create_gd()))
+		{
+			return FALSE;
+		}
+
+		if ($this->wm_use_truetype === TRUE && ! file_exists($this->wm_font_path))
+		{
+			$this->set_error('imglib_missing_font');
+			return FALSE;
+		}
+
+		// Fetch source image properties
+		$this->get_image_properties();
+
+		// Reverse the vertical offset
+		// When the image is positioned at the bottom
+		// we don't want the vertical offset to push it
+		// further down. We want the reverse, so we'll
+		// invert the offset. Note: The horizontal
+		// offset flips itself automatically
+
+		if ($this->wm_vrt_alignment === 'B')
+		{
+			$this->wm_vrt_offset = $this->wm_vrt_offset * -1;
+		}
+
+		if ($this->wm_hor_alignment === 'R')
+		{
+			$this->wm_hor_offset = $this->wm_hor_offset * -1;
+		}
+
+		// Set font width and height
+		// These are calculated differently depending on
+		// whether we are using the true type font or not
+		if ($this->wm_use_truetype === TRUE)
+		{
+			if (empty($this->wm_font_size))
+			{
+				$this->wm_font_size = 17;
+			}
+
+			if (function_exists('imagettfbbox'))
+			{
+				$temp = imagettfbbox($this->wm_font_size, 0, $this->wm_font_path, $this->wm_text);
+				$temp = $temp[2] - $temp[0];
+
+				$fontwidth = $temp / strlen($this->wm_text);
+			}
+			else
+			{
+				$fontwidth = $this->wm_font_size - ($this->wm_font_size / 4);
+			}
+
+			$fontheight = $this->wm_font_size;
+			$this->wm_vrt_offset += $this->wm_font_size;
+		}
+		else
+		{
+			$fontwidth  = imagefontwidth($this->wm_font_size);
+			$fontheight = imagefontheight($this->wm_font_size);
+		}
+
+		// Set base X and Y axis values
+		$x_axis = $this->wm_hor_offset + $this->wm_padding;
+		$y_axis = $this->wm_vrt_offset + $this->wm_padding;
+
+		if ($this->wm_use_drop_shadow === FALSE)
+		{
+			$this->wm_shadow_distance = 0;
+		}
+
+		$this->wm_vrt_alignment = strtoupper($this->wm_vrt_alignment[0]);
+		$this->wm_hor_alignment = strtoupper($this->wm_hor_alignment[0]);
+
+		// Set vertical alignment
+		if ($this->wm_vrt_alignment === 'M')
+		{
+			$y_axis += ($this->orig_height / 2) + ($fontheight / 2);
+		}
+		elseif ($this->wm_vrt_alignment === 'B')
+		{
+			$y_axis += $this->orig_height - $fontheight - $this->wm_shadow_distance - ($fontheight / 2);
+		}
+
+		// Set horizontal alignment
+		if ($this->wm_hor_alignment === 'R')
+		{
+			$x_axis += $this->orig_width - ($fontwidth * strlen($this->wm_text)) - $this->wm_shadow_distance;
+		}
+		elseif ($this->wm_hor_alignment === 'C')
+		{
+			$x_axis += floor(($this->orig_width - ($fontwidth * strlen($this->wm_text))) / 2);
+		}
+
+		if ($this->wm_use_drop_shadow)
+		{
+			// Offset from text
+			$x_shad = $x_axis + $this->wm_shadow_distance;
+			$y_shad = $y_axis + $this->wm_shadow_distance;
+
+			/* Set RGB values for shadow
+			 *
+			 * First character is #, so we don't really need it.
+			 * Get the rest of the string and split it into 2-length
+			 * hex values:
+			 */
+			$drp_color = str_split(substr($this->wm_shadow_color, 1, 6), 2);
+			$drp_color = imagecolorclosest($src_img, hexdec($drp_color[0]), hexdec($drp_color[1]), hexdec($drp_color[2]));
+
+			// Add the shadow to the source image
+			if ($this->wm_use_truetype)
+			{
+				imagettftext($src_img, $this->wm_font_size, 0, $x_shad, $y_shad, $drp_color, $this->wm_font_path, $this->wm_text);
+			}
+			else
+			{
+				imagestring($src_img, $this->wm_font_size, $x_shad, $y_shad, $this->wm_text, $drp_color);
+			}
+		}
+
+		/* Set RGB values for text
+		 *
+		 * First character is #, so we don't really need it.
+		 * Get the rest of the string and split it into 2-length
+		 * hex values:
+		 */
+		$txt_color = str_split(substr($this->wm_font_color, 1, 6), 2);
+		$txt_color = imagecolorclosest($src_img, hexdec($txt_color[0]), hexdec($txt_color[1]), hexdec($txt_color[2]));
+
+		// Add the text to the source image
+		if ($this->wm_use_truetype)
+		{
+			imagettftext($src_img, $this->wm_font_size, 0, $x_axis, $y_axis, $txt_color, $this->wm_font_path, $this->wm_text);
+		}
+		else
+		{
+			imagestring($src_img, $this->wm_font_size, $x_axis, $y_axis, $this->wm_text, $txt_color);
+		}
+
+		// We can preserve transparency for PNG images
+		if ($this->image_type === 3)
+		{
+			imagealphablending($src_img, FALSE);
+			imagesavealpha($src_img, TRUE);
+		}
+
+		// Output the final image
+		if ($this->dynamic_output === TRUE)
+		{
+			$this->image_display_gd($src_img);
+		}
+		else
+		{
+			$this->image_save_gd($src_img);
+		}
+
+		imagedestroy($src_img);
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create Image - GD
+	 *
+	 * This simply creates an image resource handle
+	 * based on the type of image being processed
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	resource
+	 */
+	public function image_create_gd($path = '', $image_type = '')
+	{
+		if ($path === '')
+		{
+			$path = $this->full_src_path;
+		}
+
+		if ($image_type === '')
+		{
+			$image_type = $this->image_type;
+		}
+
+		switch ($image_type)
+		{
+			case 1:
+				if ( ! function_exists('imagecreatefromgif'))
+				{
+					$this->set_error(array('imglib_unsupported_imagecreate', 'imglib_gif_not_supported'));
+					return FALSE;
+				}
+
+				return imagecreatefromgif($path);
+			case 2:
+				if ( ! function_exists('imagecreatefromjpeg'))
+				{
+					$this->set_error(array('imglib_unsupported_imagecreate', 'imglib_jpg_not_supported'));
+					return FALSE;
+				}
+
+				return imagecreatefromjpeg($path);
+			case 3:
+				if ( ! function_exists('imagecreatefrompng'))
+				{
+					$this->set_error(array('imglib_unsupported_imagecreate', 'imglib_png_not_supported'));
+					return FALSE;
+				}
+
+				return imagecreatefrompng($path);
+			default:
+				$this->set_error(array('imglib_unsupported_imagecreate'));
+				return FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Write image file to disk - GD
+	 *
+	 * Takes an image resource as input and writes the file
+	 * to the specified destination
+	 *
+	 * @param	resource
+	 * @return	bool
+	 */
+	public function image_save_gd($resource)
+	{
+		switch ($this->image_type)
+		{
+			case 1:
+				if ( ! function_exists('imagegif'))
+				{
+					$this->set_error(array('imglib_unsupported_imagecreate', 'imglib_gif_not_supported'));
+					return FALSE;
+				}
+
+				if ( ! @imagegif($resource, $this->full_dst_path))
+				{
+					$this->set_error('imglib_save_failed');
+					return FALSE;
+				}
+			break;
+			case 2:
+				if ( ! function_exists('imagejpeg'))
+				{
+					$this->set_error(array('imglib_unsupported_imagecreate', 'imglib_jpg_not_supported'));
+					return FALSE;
+				}
+
+				if ( ! @imagejpeg($resource, $this->full_dst_path, $this->quality))
+				{
+					$this->set_error('imglib_save_failed');
+					return FALSE;
+				}
+			break;
+			case 3:
+				if ( ! function_exists('imagepng'))
+				{
+					$this->set_error(array('imglib_unsupported_imagecreate', 'imglib_png_not_supported'));
+					return FALSE;
+				}
+
+				if ( ! @imagepng($resource, $this->full_dst_path))
+				{
+					$this->set_error('imglib_save_failed');
+					return FALSE;
+				}
+			break;
+			default:
+				$this->set_error(array('imglib_unsupported_imagecreate'));
+				return FALSE;
+			break;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Dynamically outputs an image
+	 *
+	 * @param	resource
+	 * @return	void
+	 */
+	public function image_display_gd($resource)
+	{
+		header('Content-Disposition: filename='.$this->source_image.';');
+		header('Content-Type: '.$this->mime_type);
+		header('Content-Transfer-Encoding: binary');
+		header('Last-Modified: '.gmdate('D, d M Y H:i:s', time()).' GMT');
+
+		switch ($this->image_type)
+		{
+			case 1	:	imagegif($resource);
+				break;
+			case 2	:	imagejpeg($resource, NULL, $this->quality);
+				break;
+			case 3	:	imagepng($resource);
+				break;
+			default:	echo 'Unable to display the image';
+				break;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Re-proportion Image Width/Height
+	 *
+	 * When creating thumbs, the desired width/height
+	 * can end up warping the image due to an incorrect
+	 * ratio between the full-sized image and the thumb.
+	 *
+	 * This function lets us re-proportion the width/height
+	 * if users choose to maintain the aspect ratio when resizing.
+	 *
+	 * @return	void
+	 */
+	public function image_reproportion()
+	{
+		if (($this->width === 0 && $this->height === 0) OR $this->orig_width === 0 OR $this->orig_height === 0
+			OR ( ! ctype_digit((string) $this->width) && ! ctype_digit((string) $this->height))
+			OR ! ctype_digit((string) $this->orig_width) OR ! ctype_digit((string) $this->orig_height))
+		{
+			return;
+		}
+
+		// Sanitize
+		$this->width = (int) $this->width;
+		$this->height = (int) $this->height;
+
+		if ($this->master_dim !== 'width' && $this->master_dim !== 'height')
+		{
+			if ($this->width > 0 && $this->height > 0)
+			{
+				$this->master_dim = ((($this->orig_height/$this->orig_width) - ($this->height/$this->width)) < 0)
+							? 'width' : 'height';
+			}
+			else
+			{
+				$this->master_dim = ($this->height === 0) ? 'width' : 'height';
+			}
+		}
+		elseif (($this->master_dim === 'width' && $this->width === 0)
+			OR ($this->master_dim === 'height' && $this->height === 0))
+		{
+			return;
+		}
+
+		if ($this->master_dim === 'width')
+		{
+			$this->height = (int) ceil($this->width*$this->orig_height/$this->orig_width);
+		}
+		else
+		{
+			$this->width = (int) ceil($this->orig_width*$this->height/$this->orig_height);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get image properties
+	 *
+	 * A helper function that gets info about the file
+	 *
+	 * @param	string
+	 * @param	bool
+	 * @return	mixed
+	 */
+	public function get_image_properties($path = '', $return = FALSE)
+	{
+		// For now we require GD but we should
+		// find a way to determine this using IM or NetPBM
+
+		if ($path === '')
+		{
+			$path = $this->full_src_path;
+		}
+
+		if ( ! file_exists($path))
+		{
+			$this->set_error('imglib_invalid_path');
+			return FALSE;
+		}
+
+		$vals = getimagesize($path);
+		$types = array(1 => 'gif', 2 => 'jpeg', 3 => 'png');
+		$mime = (isset($types[$vals[2]])) ? 'image/'.$types[$vals[2]] : 'image/jpg';
+
+		if ($return === TRUE)
+		{
+			return array(
+					'width' =>	$vals[0],
+					'height' =>	$vals[1],
+					'image_type' =>	$vals[2],
+					'size_str' =>	$vals[3],
+					'mime_type' =>	$mime
+				);
+		}
+
+		$this->orig_width	= $vals[0];
+		$this->orig_height	= $vals[1];
+		$this->image_type	= $vals[2];
+		$this->size_str		= $vals[3];
+		$this->mime_type	= $mime;
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Size calculator
+	 *
+	 * This function takes a known width x height and
+	 * recalculates it to a new size. Only one
+	 * new variable needs to be known
+	 *
+	 *	$props = array(
+	 *			'width'		=> $width,
+	 *			'height'	=> $height,
+	 *			'new_width'	=> 40,
+	 *			'new_height'	=> ''
+	 *		);
+	 *
+	 * @param	array
+	 * @return	array
+	 */
+	public function size_calculator($vals)
+	{
+		if ( ! is_array($vals))
+		{
+			return;
+		}
+
+		$allowed = array('new_width', 'new_height', 'width', 'height');
+
+		foreach ($allowed as $item)
+		{
+			if (empty($vals[$item]))
+			{
+				$vals[$item] = 0;
+			}
+		}
+
+		if ($vals['width'] === 0 OR $vals['height'] === 0)
+		{
+			return $vals;
+		}
+
+		if ($vals['new_width'] === 0)
+		{
+			$vals['new_width'] = ceil($vals['width']*$vals['new_height']/$vals['height']);
+		}
+		elseif ($vals['new_height'] === 0)
+		{
+			$vals['new_height'] = ceil($vals['new_width']*$vals['height']/$vals['width']);
+		}
+
+		return $vals;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Explode source_image
+	 *
+	 * This is a helper function that extracts the extension
+	 * from the source_image.  This function lets us deal with
+	 * source_images with multiple periods, like: my.cool.jpg
+	 * It returns an associative array with two elements:
+	 * $array['ext']  = '.jpg';
+	 * $array['name'] = 'my.cool';
+	 *
+	 * @param	array
+	 * @return	array
+	 */
+	public function explode_name($source_image)
+	{
+		$ext = strrchr($source_image, '.');
+		$name = ($ext === FALSE) ? $source_image : substr($source_image, 0, -strlen($ext));
+
+		return array('ext' => $ext, 'name' => $name);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is GD Installed?
+	 *
+	 * @return	bool
+	 */
+	public function gd_loaded()
+	{
+		if ( ! extension_loaded('gd'))
+		{
+			/* As it is stated in the PHP manual, dl() is not always available
+			 * and even if so - it could generate an E_WARNING message on failure
+			 */
+			return (function_exists('dl') && @dl('gd.so'));
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get GD version
+	 *
+	 * @return	mixed
+	 */
+	public function gd_version()
+	{
+		if (function_exists('gd_info'))
+		{
+			$gd_version = @gd_info();
+			return preg_replace('/\D/', '', $gd_version['GD Version']);
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set error message
+	 *
+	 * @param	string
+	 * @return	void
+	 */
+	public function set_error($msg)
+	{
+		$CI =& get_instance();
+		$CI->lang->load('imglib');
+
+		if (is_array($msg))
+		{
+			foreach ($msg as $val)
+			{
+				$msg = ($CI->lang->line($val) === FALSE) ? $val : $CI->lang->line($val);
+				$this->error_msg[] = $msg;
+				log_message('error', $msg);
+			}
+		}
+		else
+		{
+			$msg = ($CI->lang->line($msg) === FALSE) ? $msg : $CI->lang->line($msg);
+			$this->error_msg[] = $msg;
+			log_message('error', $msg);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show error messages
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	public function display_errors($open = '<p>', $close = '</p>')
+	{
+		return (count($this->error_msg) > 0) ? $open.implode($close.$open, $this->error_msg).$close : '';
+	}
+
+}
diff --git a/README/Javascript.php b/README/Javascript.php
new file mode 100644
index 0000000..4cc6286
--- /dev/null
+++ b/README/Javascript.php
@@ -0,0 +1,856 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Javascript Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Javascript
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/javascript.html
+ * @deprecated	3.0.0	This was never a good idea in the first place.
+ */
+class CI_Javascript {
+
+	/**
+	 * JavaScript location
+	 *
+	 * @var	string
+	 */
+	protected $_javascript_location = 'js';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params = array())
+	{
+		$defaults = array('js_library_driver' => 'jquery', 'autoload' => TRUE);
+
+		foreach ($defaults as $key => $val)
+		{
+			if (isset($params[$key]) && $params[$key] !== '')
+			{
+				$defaults[$key] = $params[$key];
+			}
+		}
+
+		extract($defaults);
+
+		$this->CI =& get_instance();
+
+		// load the requested js library
+		$this->CI->load->library('Javascript/'.$js_library_driver, array('autoload' => $autoload));
+		// make js to refer to current library
+		$this->js =& $this->CI->$js_library_driver;
+
+		log_message('info', 'Javascript Class Initialized and loaded. Driver used: '.$js_library_driver);
+	}
+
+	// --------------------------------------------------------------------
+	// Event Code
+	// --------------------------------------------------------------------
+
+	/**
+	 * Blur
+	 *
+	 * Outputs a javascript library blur event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function blur($element = 'this', $js = '')
+	{
+		return $this->js->_blur($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Change
+	 *
+	 * Outputs a javascript library change event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function change($element = 'this', $js = '')
+	{
+		return $this->js->_change($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Click
+	 *
+	 * Outputs a javascript library click event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @param	bool	whether or not to return false
+	 * @return	string
+	 */
+	public function click($element = 'this', $js = '', $ret_false = TRUE)
+	{
+		return $this->js->_click($element, $js, $ret_false);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Double Click
+	 *
+	 * Outputs a javascript library dblclick event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function dblclick($element = 'this', $js = '')
+	{
+		return $this->js->_dblclick($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Outputs a javascript library error event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function error($element = 'this', $js = '')
+	{
+		return $this->js->_error($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Focus
+	 *
+	 * Outputs a javascript library focus event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function focus($element = 'this', $js = '')
+	{
+		return $this->js->_focus($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Hover
+	 *
+	 * Outputs a javascript library hover event
+	 *
+	 * @param	string	- element
+	 * @param	string	- Javascript code for mouse over
+	 * @param	string	- Javascript code for mouse out
+	 * @return	string
+	 */
+	public function hover($element = 'this', $over = '', $out = '')
+	{
+		return $this->js->_hover($element, $over, $out);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Keydown
+	 *
+	 * Outputs a javascript library keydown event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function keydown($element = 'this', $js = '')
+	{
+		return $this->js->_keydown($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Keyup
+	 *
+	 * Outputs a javascript library keydown event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function keyup($element = 'this', $js = '')
+	{
+		return $this->js->_keyup($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Load
+	 *
+	 * Outputs a javascript library load event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function load($element = 'this', $js = '')
+	{
+		return $this->js->_load($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mousedown
+	 *
+	 * Outputs a javascript library mousedown event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function mousedown($element = 'this', $js = '')
+	{
+		return $this->js->_mousedown($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mouse Out
+	 *
+	 * Outputs a javascript library mouseout event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function mouseout($element = 'this', $js = '')
+	{
+		return $this->js->_mouseout($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mouse Over
+	 *
+	 * Outputs a javascript library mouseover event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function mouseover($element = 'this', $js = '')
+	{
+		return $this->js->_mouseover($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mouseup
+	 *
+	 * Outputs a javascript library mouseup event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function mouseup($element = 'this', $js = '')
+	{
+		return $this->js->_mouseup($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Output
+	 *
+	 * Outputs the called javascript to the screen
+	 *
+	 * @param	string	The code to output
+	 * @return	string
+	 */
+	public function output($js)
+	{
+		return $this->js->_output($js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Ready
+	 *
+	 * Outputs a javascript library mouseup event
+	 *
+	 * @param	string	$js	Code to execute
+	 * @return	string
+	 */
+	public function ready($js)
+	{
+		return $this->js->_document_ready($js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Resize
+	 *
+	 * Outputs a javascript library resize event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function resize($element = 'this', $js = '')
+	{
+		return $this->js->_resize($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Scroll
+	 *
+	 * Outputs a javascript library scroll event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function scroll($element = 'this', $js = '')
+	{
+		return $this->js->_scroll($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Unload
+	 *
+	 * Outputs a javascript library unload event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	public function unload($element = 'this', $js = '')
+	{
+		return $this->js->_unload($element, $js);
+	}
+
+	// --------------------------------------------------------------------
+	// Effects
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Class
+	 *
+	 * Outputs a javascript library addClass event
+	 *
+	 * @param	string	- element
+	 * @param	string	- Class to add
+	 * @return	string
+	 */
+	public function addClass($element = 'this', $class = '')
+	{
+		return $this->js->_addClass($element, $class);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Animate
+	 *
+	 * Outputs a javascript library animate event
+	 *
+	 * @param	string	$element = 'this'
+	 * @param	array	$params = array()
+	 * @param	mixed	$speed			'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	$extra
+	 * @return	string
+	 */
+	public function animate($element = 'this', $params = array(), $speed = '', $extra = '')
+	{
+		return $this->js->_animate($element, $params, $speed, $extra);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fade In
+	 *
+	 * Outputs a javascript library hide event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	public function fadeIn($element = 'this', $speed = '', $callback = '')
+	{
+		return $this->js->_fadeIn($element, $speed, $callback);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fade Out
+	 *
+	 * Outputs a javascript library hide event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	public function fadeOut($element = 'this', $speed = '', $callback = '')
+	{
+		return $this->js->_fadeOut($element, $speed, $callback);
+	}
+	// --------------------------------------------------------------------
+
+	/**
+	 * Slide Up
+	 *
+	 * Outputs a javascript library slideUp event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	public function slideUp($element = 'this', $speed = '', $callback = '')
+	{
+		return $this->js->_slideUp($element, $speed, $callback);
+
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Remove Class
+	 *
+	 * Outputs a javascript library removeClass event
+	 *
+	 * @param	string	- element
+	 * @param	string	- Class to add
+	 * @return	string
+	 */
+	public function removeClass($element = 'this', $class = '')
+	{
+		return $this->js->_removeClass($element, $class);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Slide Down
+	 *
+	 * Outputs a javascript library slideDown event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	public function slideDown($element = 'this', $speed = '', $callback = '')
+	{
+		return $this->js->_slideDown($element, $speed, $callback);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Slide Toggle
+	 *
+	 * Outputs a javascript library slideToggle event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	public function slideToggle($element = 'this', $speed = '', $callback = '')
+	{
+		return $this->js->_slideToggle($element, $speed, $callback);
+
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Hide
+	 *
+	 * Outputs a javascript library hide action
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	public function hide($element = 'this', $speed = '', $callback = '')
+	{
+		return $this->js->_hide($element, $speed, $callback);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Toggle
+	 *
+	 * Outputs a javascript library toggle event
+	 *
+	 * @param	string	- element
+	 * @return	string
+	 */
+	public function toggle($element = 'this')
+	{
+		return $this->js->_toggle($element);
+
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Toggle Class
+	 *
+	 * Outputs a javascript library toggle class event
+	 *
+	 * @param	string	$element = 'this'
+	 * @param	string	$class = ''
+	 * @return	string
+	 */
+	public function toggleClass($element = 'this', $class = '')
+	{
+		return $this->js->_toggleClass($element, $class);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show
+	 *
+	 * Outputs a javascript library show event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	public function show($element = 'this', $speed = '', $callback = '')
+	{
+		return $this->js->_show($element, $speed, $callback);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile
+	 *
+	 * gather together all script needing to be output
+	 *
+	 * @param	string	$view_var
+	 * @param	bool	$script_tags
+	 * @return	string
+	 */
+	public function compile($view_var = 'script_foot', $script_tags = TRUE)
+	{
+		$this->js->_compile($view_var, $script_tags);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Clear Compile
+	 *
+	 * Clears any previous javascript collected for output
+	 *
+	 * @return	void
+	 */
+	public function clear_compile()
+	{
+		$this->js->_clear_compile();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * External
+	 *
+	 * Outputs a <script> tag with the source as an external js file
+	 *
+	 * @param	string	$external_file
+	 * @param	bool	$relative
+	 * @return	string
+	 */
+	public function external($external_file = '', $relative = FALSE)
+	{
+		if ($external_file !== '')
+		{
+			$this->_javascript_location = $external_file;
+		}
+		elseif ($this->CI->config->item('javascript_location') !== '')
+		{
+			$this->_javascript_location = $this->CI->config->item('javascript_location');
+		}
+
+		if ($relative === TRUE OR strpos($external_file, 'http://') === 0 OR strpos($external_file, 'https://') === 0)
+		{
+			$str = $this->_open_script($external_file);
+		}
+		elseif (strpos($this->_javascript_location, 'http://') !== FALSE)
+		{
+			$str = $this->_open_script($this->_javascript_location.$external_file);
+		}
+		else
+		{
+			$str = $this->_open_script($this->CI->config->slash_item('base_url').$this->_javascript_location.$external_file);
+		}
+
+		return $str.$this->_close_script();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Inline
+	 *
+	 * Outputs a <script> tag
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	bool	If a CDATA section should be added
+	 * @return	string
+	 */
+	public function inline($script, $cdata = TRUE)
+	{
+		return $this->_open_script()
+			. ($cdata ? "\n// <![CDATA[\n".$script."\n// ]]>\n" : "\n".$script."\n")
+			. $this->_close_script();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Open Script
+	 *
+	 * Outputs an opening <script>
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _open_script($src = '')
+	{
+		return '<script type="text/javascript" charset="'.strtolower($this->CI->config->item('charset')).'"'
+			.($src === '' ? '>' : ' src="'.$src.'">');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close Script
+	 *
+	 * Outputs an closing </script>
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _close_script($extra = "\n")
+	{
+		return '</script>'.$extra;
+	}
+
+	// --------------------------------------------------------------------
+	// AJAX-Y STUFF - still a testbed
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update
+	 *
+	 * Outputs a javascript library slideDown event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	public function update($element = 'this', $speed = '', $callback = '')
+	{
+		return $this->js->_updater($element, $speed, $callback);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate JSON
+	 *
+	 * Can be passed a database result or associative array and returns a JSON formatted string
+	 *
+	 * @param	mixed	result set or array
+	 * @param	bool	match array types (defaults to objects)
+	 * @return	string	a json formatted string
+	 */
+	public function generate_json($result = NULL, $match_array_type = FALSE)
+	{
+		// JSON data can optionally be passed to this function
+		// either as a database result object or an array, or a user supplied array
+		if ($result !== NULL)
+		{
+			if (is_object($result))
+			{
+				$json_result = is_callable(array($result, 'result_array')) ? $result->result_array() : (array) $result;
+			}
+			elseif (is_array($result))
+			{
+				$json_result = $result;
+			}
+			else
+			{
+				return $this->_prep_args($result);
+			}
+		}
+		else
+		{
+			return 'null';
+		}
+
+		$json = array();
+		$_is_assoc = TRUE;
+
+		if ( ! is_array($json_result) && empty($json_result))
+		{
+			show_error('Generate JSON Failed - Illegal key, value pair.');
+		}
+		elseif ($match_array_type)
+		{
+			$_is_assoc = $this->_is_associative_array($json_result);
+		}
+
+		foreach ($json_result as $k => $v)
+		{
+			if ($_is_assoc)
+			{
+				$json[] = $this->_prep_args($k, TRUE).':'.$this->generate_json($v, $match_array_type);
+			}
+			else
+			{
+				$json[] = $this->generate_json($v, $match_array_type);
+			}
+		}
+
+		$json = implode(',', $json);
+
+		return $_is_assoc ? '{'.$json.'}' : '['.$json.']';
+
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is associative array
+	 *
+	 * Checks for an associative array
+	 *
+	 * @param	array
+	 * @return	bool
+	 */
+	protected function _is_associative_array($arr)
+	{
+		foreach (array_keys($arr) as $key => $val)
+		{
+			if ($key !== $val)
+			{
+				return TRUE;
+			}
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep Args
+	 *
+	 * Ensures a standard json value and escapes values
+	 *
+	 * @param	mixed	$result
+	 * @param	bool	$is_key = FALSE
+	 * @return	string
+	 */
+	protected function _prep_args($result, $is_key = FALSE)
+	{
+		if ($result === NULL)
+		{
+			return 'null';
+		}
+		elseif (is_bool($result))
+		{
+			return ($result === TRUE) ? 'true' : 'false';
+		}
+		elseif (is_string($result) OR $is_key)
+		{
+			return '"'.str_replace(array('\\', "\t", "\n", "\r", '"', '/'), array('\\\\', '\\t', '\\n', "\\r", '\"', '\/'), $result).'"';
+		}
+		elseif (is_scalar($result))
+		{
+			return $result;
+		}
+	}
+
+}
diff --git a/README/Javascript/Jquery.php b/README/Javascript/Jquery.php
new file mode 100644
index 0000000..11f2d23
--- /dev/null
+++ b/README/Javascript/Jquery.php
@@ -0,0 +1,1076 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Jquery Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Loader
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/javascript.html
+ */
+class CI_Jquery extends CI_Javascript {
+
+	/**
+	 * JavaScript directory location
+	 *
+	 * @var	string
+	 */
+	protected $_javascript_folder = 'js';
+
+	/**
+	 * JQuery code for load
+	 *
+	 * @var	array
+	 */
+	public $jquery_code_for_load = array();
+
+	/**
+	 * JQuery code for compile
+	 *
+	 * @var	array
+	 */
+	public $jquery_code_for_compile = array();
+
+	/**
+	 * JQuery corner active flag
+	 *
+	 * @var	bool
+	 */
+	public $jquery_corner_active = FALSE;
+
+	/**
+	 * JQuery table sorter active flag
+	 *
+	 * @var	bool
+	 */
+	public $jquery_table_sorter_active = FALSE;
+
+	/**
+	 * JQuery table sorter pager active
+	 *
+	 * @var	bool
+	 */
+	public $jquery_table_sorter_pager_active = FALSE;
+
+	/**
+	 * JQuery AJAX image
+	 *
+	 * @var	string
+	 */
+	public $jquery_ajax_img = '';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		$this->CI =& get_instance();
+		extract($params);
+
+		if ($autoload === TRUE)
+		{
+			$this->script();
+		}
+
+		log_message('info', 'Jquery Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+	// Event Code
+	// --------------------------------------------------------------------
+
+	/**
+	 * Blur
+	 *
+	 * Outputs a jQuery blur event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _blur($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'blur');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Change
+	 *
+	 * Outputs a jQuery change event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _change($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'change');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Click
+	 *
+	 * Outputs a jQuery click event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @param	bool	whether or not to return false
+	 * @return	string
+	 */
+	protected function _click($element = 'this', $js = '', $ret_false = TRUE)
+	{
+		is_array($js) OR $js = array($js);
+
+		if ($ret_false)
+		{
+			$js[] = 'return false;';
+		}
+
+		return $this->_add_event($element, $js, 'click');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Double Click
+	 *
+	 * Outputs a jQuery dblclick event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _dblclick($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'dblclick');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Outputs a jQuery error event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _error($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'error');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Focus
+	 *
+	 * Outputs a jQuery focus event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _focus($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'focus');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Hover
+	 *
+	 * Outputs a jQuery hover event
+	 *
+	 * @param	string	- element
+	 * @param	string	- Javascript code for mouse over
+	 * @param	string	- Javascript code for mouse out
+	 * @return	string
+	 */
+	protected function _hover($element = 'this', $over = '', $out = '')
+	{
+		$event = "\n\t$(".$this->_prep_element($element).").hover(\n\t\tfunction()\n\t\t{\n\t\t\t{$over}\n\t\t}, \n\t\tfunction()\n\t\t{\n\t\t\t{$out}\n\t\t});\n";
+
+		$this->jquery_code_for_compile[] = $event;
+
+		return $event;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Keydown
+	 *
+	 * Outputs a jQuery keydown event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _keydown($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'keydown');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Keyup
+	 *
+	 * Outputs a jQuery keydown event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _keyup($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'keyup');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Load
+	 *
+	 * Outputs a jQuery load event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _load($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'load');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mousedown
+	 *
+	 * Outputs a jQuery mousedown event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _mousedown($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'mousedown');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mouse Out
+	 *
+	 * Outputs a jQuery mouseout event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _mouseout($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'mouseout');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mouse Over
+	 *
+	 * Outputs a jQuery mouseover event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _mouseover($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'mouseover');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Mouseup
+	 *
+	 * Outputs a jQuery mouseup event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _mouseup($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'mouseup');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Output
+	 *
+	 * Outputs script directly
+	 *
+	 * @param	array	$array_js = array()
+	 * @return	void
+	 */
+	protected function _output($array_js = array())
+	{
+		if ( ! is_array($array_js))
+		{
+			$array_js = array($array_js);
+		}
+
+		foreach ($array_js as $js)
+		{
+			$this->jquery_code_for_compile[] = "\t".$js."\n";
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Resize
+	 *
+	 * Outputs a jQuery resize event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _resize($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'resize');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Scroll
+	 *
+	 * Outputs a jQuery scroll event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _scroll($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'scroll');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Unload
+	 *
+	 * Outputs a jQuery unload event
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @return	string
+	 */
+	protected function _unload($element = 'this', $js = '')
+	{
+		return $this->_add_event($element, $js, 'unload');
+	}
+
+	// --------------------------------------------------------------------
+	// Effects
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Class
+	 *
+	 * Outputs a jQuery addClass event
+	 *
+	 * @param	string	$element
+	 * @param	string	$class
+	 * @return	string
+	 */
+	protected function _addClass($element = 'this', $class = '')
+	{
+		$element = $this->_prep_element($element);
+		return '$('.$element.').addClass("'.$class.'");';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Animate
+	 *
+	 * Outputs a jQuery animate event
+	 *
+	 * @param	string	$element
+	 * @param	array	$params
+	 * @param	string	$speed	'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	$extra
+	 * @return	string
+	 */
+	protected function _animate($element = 'this', $params = array(), $speed = '', $extra = '')
+	{
+		$element = $this->_prep_element($element);
+		$speed = $this->_validate_speed($speed);
+
+		$animations = "\t\t\t";
+
+		foreach ($params as $param => $value)
+		{
+			$animations .= $param.": '".$value."', ";
+		}
+
+		$animations = substr($animations, 0, -2); // remove the last ", "
+
+		if ($speed !== '')
+		{
+			$speed = ', '.$speed;
+		}
+
+		if ($extra !== '')
+		{
+			$extra = ', '.$extra;
+		}
+
+		return "$({$element}).animate({\n$animations\n\t\t}".$speed.$extra.');';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fade In
+	 *
+	 * Outputs a jQuery hide event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	protected function _fadeIn($element = 'this', $speed = '', $callback = '')
+	{
+		$element = $this->_prep_element($element);
+		$speed = $this->_validate_speed($speed);
+
+		if ($callback !== '')
+		{
+			$callback = ", function(){\n{$callback}\n}";
+		}
+
+		return "$({$element}).fadeIn({$speed}{$callback});";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fade Out
+	 *
+	 * Outputs a jQuery hide event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	protected function _fadeOut($element = 'this', $speed = '', $callback = '')
+	{
+		$element = $this->_prep_element($element);
+		$speed = $this->_validate_speed($speed);
+
+		if ($callback !== '')
+		{
+			$callback = ", function(){\n{$callback}\n}";
+		}
+
+		return '$('.$element.').fadeOut('.$speed.$callback.');';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Hide
+	 *
+	 * Outputs a jQuery hide action
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	protected function _hide($element = 'this', $speed = '', $callback = '')
+	{
+		$element = $this->_prep_element($element);
+		$speed = $this->_validate_speed($speed);
+
+		if ($callback !== '')
+		{
+			$callback = ", function(){\n{$callback}\n}";
+		}
+
+		return "$({$element}).hide({$speed}{$callback});";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Remove Class
+	 *
+	 * Outputs a jQuery remove class event
+	 *
+	 * @param	string	$element
+	 * @param	string	$class
+	 * @return	string
+	 */
+	protected function _removeClass($element = 'this', $class = '')
+	{
+		$element = $this->_prep_element($element);
+		return '$('.$element.').removeClass("'.$class.'");';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Slide Up
+	 *
+	 * Outputs a jQuery slideUp event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	protected function _slideUp($element = 'this', $speed = '', $callback = '')
+	{
+		$element = $this->_prep_element($element);
+		$speed = $this->_validate_speed($speed);
+
+		if ($callback !== '')
+		{
+			$callback = ", function(){\n{$callback}\n}";
+		}
+
+		return '$('.$element.').slideUp('.$speed.$callback.');';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Slide Down
+	 *
+	 * Outputs a jQuery slideDown event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	protected function _slideDown($element = 'this', $speed = '', $callback = '')
+	{
+		$element = $this->_prep_element($element);
+		$speed = $this->_validate_speed($speed);
+
+		if ($callback !== '')
+		{
+			$callback = ", function(){\n{$callback}\n}";
+		}
+
+		return '$('.$element.').slideDown('.$speed.$callback.');';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Slide Toggle
+	 *
+	 * Outputs a jQuery slideToggle event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	protected function _slideToggle($element = 'this', $speed = '', $callback = '')
+	{
+		$element = $this->_prep_element($element);
+		$speed = $this->_validate_speed($speed);
+
+		if ($callback !== '')
+		{
+			$callback = ", function(){\n{$callback}\n}";
+		}
+
+		return '$('.$element.').slideToggle('.$speed.$callback.');';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Toggle
+	 *
+	 * Outputs a jQuery toggle event
+	 *
+	 * @param	string	- element
+	 * @return	string
+	 */
+	protected function _toggle($element = 'this')
+	{
+		$element = $this->_prep_element($element);
+		return '$('.$element.').toggle();';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Toggle Class
+	 *
+	 * Outputs a jQuery toggle class event
+	 *
+	 * @param	string	$element
+	 * @param	string	$class
+	 * @return	string
+	 */
+	protected function _toggleClass($element = 'this', $class = '')
+	{
+		$element = $this->_prep_element($element);
+		return '$('.$element.').toggleClass("'.$class.'");';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show
+	 *
+	 * Outputs a jQuery show event
+	 *
+	 * @param	string	- element
+	 * @param	string	- One of 'slow', 'normal', 'fast', or time in milliseconds
+	 * @param	string	- Javascript callback function
+	 * @return	string
+	 */
+	protected function _show($element = 'this', $speed = '', $callback = '')
+	{
+		$element = $this->_prep_element($element);
+		$speed = $this->_validate_speed($speed);
+
+		if ($callback !== '')
+		{
+			$callback = ", function(){\n{$callback}\n}";
+		}
+
+		return '$('.$element.').show('.$speed.$callback.');';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Updater
+	 *
+	 * An Ajax call that populates the designated DOM node with
+	 * returned content
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	the controller to run the call against
+	 * @param	string	optional parameters
+	 * @return	string
+	 */
+
+	protected function _updater($container = 'this', $controller = '', $options = '')
+	{
+		$container = $this->_prep_element($container);
+		$controller = (strpos('://', $controller) === FALSE) ? $controller : $this->CI->config->site_url($controller);
+
+		// ajaxStart and ajaxStop are better choices here... but this is a stop gap
+		if ($this->CI->config->item('javascript_ajax_img') === '')
+		{
+			$loading_notifier = 'Loading...';
+		}
+		else
+		{
+			$loading_notifier = '<img src="'.$this->CI->config->slash_item('base_url').$this->CI->config->item('javascript_ajax_img').'" alt="Loading" />';
+		}
+
+		$updater = '$('.$container.").empty();\n" // anything that was in... get it out
+			."\t\t$(".$container.').prepend("'.$loading_notifier."\");\n"; // to replace with an image
+
+		$request_options = '';
+		if ($options !== '')
+		{
+			$request_options .= ', {'
+					.(is_array($options) ? "'".implode("', '", $options)."'" : "'".str_replace(':', "':'", $options)."'")
+					.'}';
+		}
+
+		return $updater."\t\t$($container).load('$controller'$request_options);";
+	}
+
+	// --------------------------------------------------------------------
+	// Pre-written handy stuff
+	// --------------------------------------------------------------------
+
+	/**
+	 * Zebra tables
+	 *
+	 * @param	string	$class
+	 * @param	string	$odd
+	 * @param	string	$hover
+	 * @return	string
+	 */
+	protected function _zebraTables($class = '', $odd = 'odd', $hover = '')
+	{
+		$class = ($class !== '') ? '.'.$class : '';
+		$zebra = "\t\$(\"table{$class} tbody tr:nth-child(even)\").addClass(\"{$odd}\");";
+
+		$this->jquery_code_for_compile[] = $zebra;
+
+		if ($hover !== '')
+		{
+			$hover = $this->hover("table{$class} tbody tr", "$(this).addClass('hover');", "$(this).removeClass('hover');");
+		}
+
+		return $zebra;
+	}
+
+	// --------------------------------------------------------------------
+	// Plugins
+	// --------------------------------------------------------------------
+
+	/**
+	 * Corner Plugin
+	 *
+	 * @link	http://www.malsup.com/jquery/corner/
+	 * @param	string	$element
+	 * @param	string	$corner_style
+	 * @return	string
+	 */
+	public function corner($element = '', $corner_style = '')
+	{
+		// may want to make this configurable down the road
+		$corner_location = '/plugins/jquery.corner.js';
+
+		if ($corner_style !== '')
+		{
+			$corner_style = '"'.$corner_style.'"';
+		}
+
+		return '$('.$this->_prep_element($element).').corner('.$corner_style.');';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Modal window
+	 *
+	 * Load a thickbox modal window
+	 *
+	 * @param	string	$src
+	 * @param	bool	$relative
+	 * @return	void
+	 */
+	public function modal($src, $relative = FALSE)
+	{
+		$this->jquery_code_for_load[] = $this->external($src, $relative);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Effect
+	 *
+	 * Load an Effect library
+	 *
+	 * @param	string	$src
+	 * @param	bool	$relative
+	 * @return	void
+	 */
+	public function effect($src, $relative = FALSE)
+	{
+		$this->jquery_code_for_load[] = $this->external($src, $relative);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Plugin
+	 *
+	 * Load a plugin library
+	 *
+	 * @param	string	$src
+	 * @param	bool	$relative
+	 * @return	void
+	 */
+	public function plugin($src, $relative = FALSE)
+	{
+		$this->jquery_code_for_load[] = $this->external($src, $relative);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * UI
+	 *
+	 * Load a user interface library
+	 *
+	 * @param	string	$src
+	 * @param	bool	$relative
+	 * @return	void
+	 */
+	public function ui($src, $relative = FALSE)
+	{
+		$this->jquery_code_for_load[] = $this->external($src, $relative);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Sortable
+	 *
+	 * Creates a jQuery sortable
+	 *
+	 * @param	string	$element
+	 * @param	array	$options
+	 * @return	string
+	 */
+	public function sortable($element, $options = array())
+	{
+		if (count($options) > 0)
+		{
+			$sort_options = array();
+			foreach ($options as $k=>$v)
+			{
+				$sort_options[] = "\n\t\t".$k.': '.$v;
+			}
+			$sort_options = implode(',', $sort_options);
+		}
+		else
+		{
+			$sort_options = '';
+		}
+
+		return '$('.$this->_prep_element($element).').sortable({'.$sort_options."\n\t});";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Table Sorter Plugin
+	 *
+	 * @param	string	table name
+	 * @param	string	plugin location
+	 * @return	string
+	 */
+	public function tablesorter($table = '', $options = '')
+	{
+		$this->jquery_code_for_compile[] = "\t$(".$this->_prep_element($table).').tablesorter('.$options.");\n";
+	}
+
+	// --------------------------------------------------------------------
+	// Class functions
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Event
+	 *
+	 * Constructs the syntax for an event, and adds to into the array for compilation
+	 *
+	 * @param	string	The element to attach the event to
+	 * @param	string	The code to execute
+	 * @param	string	The event to pass
+	 * @return	string
+	 */
+	protected function _add_event($element, $js, $event)
+	{
+		if (is_array($js))
+		{
+			$js = implode("\n\t\t", $js);
+		}
+
+		$event = "\n\t$(".$this->_prep_element($element).').'.$event."(function(){\n\t\t{$js}\n\t});\n";
+		$this->jquery_code_for_compile[] = $event;
+		return $event;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile
+	 *
+	 * As events are specified, they are stored in an array
+	 * This function compiles them all for output on a page
+	 *
+	 * @param	string	$view_var
+	 * @param	bool	$script_tags
+	 * @return	void
+	 */
+	protected function _compile($view_var = 'script_foot', $script_tags = TRUE)
+	{
+		// External references
+		$external_scripts = implode('', $this->jquery_code_for_load);
+		$this->CI->load->vars(array('library_src' => $external_scripts));
+
+		if (count($this->jquery_code_for_compile) === 0)
+		{
+			// no inline references, let's just return
+			return;
+		}
+
+		// Inline references
+		$script = '$(document).ready(function() {'."\n"
+			.implode('', $this->jquery_code_for_compile)
+			.'});';
+
+		$output = ($script_tags === FALSE) ? $script : $this->inline($script);
+
+		$this->CI->load->vars(array($view_var => $output));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Clear Compile
+	 *
+	 * Clears the array of script events collected for output
+	 *
+	 * @return	void
+	 */
+	protected function _clear_compile()
+	{
+		$this->jquery_code_for_compile = array();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Document Ready
+	 *
+	 * A wrapper for writing document.ready()
+	 *
+	 * @param	array	$js
+	 * @return	void
+	 */
+	protected function _document_ready($js)
+	{
+		is_array($js) OR $js = array($js);
+
+		foreach ($js as $script)
+		{
+			$this->jquery_code_for_compile[] = $script;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Script Tag
+	 *
+	 * Outputs the script tag that loads the jquery.js file into an HTML document
+	 *
+	 * @param	string	$library_src
+	 * @param	bool	$relative
+	 * @return	string
+	 */
+	public function script($library_src = '', $relative = FALSE)
+	{
+		$library_src = $this->external($library_src, $relative);
+		$this->jquery_code_for_load[] = $library_src;
+		return $library_src;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep Element
+	 *
+	 * Puts HTML element in quotes for use in jQuery code
+	 * unless the supplied element is the Javascript 'this'
+	 * object, in which case no quotes are added
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _prep_element($element)
+	{
+		if ($element !== 'this')
+		{
+			$element = '"'.$element.'"';
+		}
+
+		return $element;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate Speed
+	 *
+	 * Ensures the speed parameter is valid for jQuery
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _validate_speed($speed)
+	{
+		if (in_array($speed, array('slow', 'normal', 'fast')))
+		{
+			return '"'.$speed.'"';
+		}
+		elseif (preg_match('/[^0-9]/', $speed))
+		{
+			return '';
+		}
+
+		return $speed;
+	}
+
+}
diff --git a/README/Javascript/index.html b/README/Javascript/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/README/Javascript/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>403 Forbidden</title>
+</head>
+<body>
+
+<p>Directory access is forbidden.</p>
+
+</body>
+</html>
diff --git a/README/Migration.php b/README/Migration.php
new file mode 100644
index 0000000..45a3cbb
--- /dev/null
+++ b/README/Migration.php
@@ -0,0 +1,446 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Migration Class
+ *
+ * All migrations should implement this, forces up() and down() and gives
+ * access to the CI super-global.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		Reactor Engineers
+ * @link
+ */
+class CI_Migration {
+
+	/**
+	 * Whether the library is enabled
+	 *
+	 * @var bool
+	 */
+	protected $_migration_enabled = FALSE;
+
+	/**
+	 * Migration numbering type
+	 *
+	 * @var	bool
+	 */
+	protected $_migration_type = 'sequential';
+
+	/**
+	 * Path to migration classes
+	 *
+	 * @var string
+	 */
+	protected $_migration_path = NULL;
+
+	/**
+	 * Current migration version
+	 *
+	 * @var mixed
+	 */
+	protected $_migration_version = 0;
+
+	/**
+	 * Database table with migration info
+	 *
+	 * @var string
+	 */
+	protected $_migration_table = 'migrations';
+
+	/**
+	 * Whether to automatically run migrations
+	 *
+	 * @var	bool
+	 */
+	protected $_migration_auto_latest = FALSE;
+
+	/**
+	 * Migration basename regex
+	 *
+	 * @var bool
+	 */
+	protected $_migration_regex = NULL;
+
+	/**
+	 * Error message
+	 *
+	 * @var string
+	 */
+	protected $_error_string = '';
+
+	/**
+	 * Initialize Migration Class
+	 *
+	 * @param	array	$config
+	 * @return	void
+	 */
+	public function __construct($config = array())
+	{
+		// Only run this constructor on main library load
+		if ( ! in_array(get_class($this), array('CI_Migration', config_item('subclass_prefix').'Migration'), TRUE))
+		{
+			return;
+		}
+
+		foreach ($config as $key => $val)
+		{
+			$this->{'_'.$key} = $val;
+		}
+
+		log_message('info', 'Migrations Class Initialized');
+
+		// Are they trying to use migrations while it is disabled?
+		if ($this->_migration_enabled !== TRUE)
+		{
+			show_error('Migrations has been loaded but is disabled or set up incorrectly.');
+		}
+
+		// If not set, set it
+		$this->_migration_path !== '' OR $this->_migration_path = APPPATH.'migrations/';
+
+		// Add trailing slash if not set
+		$this->_migration_path = rtrim($this->_migration_path, '/').'/';
+
+		// Load migration language
+		$this->lang->load('migration');
+
+		// They'll probably be using dbforge
+		$this->load->dbforge();
+
+		// Make sure the migration table name was set.
+		if (empty($this->_migration_table))
+		{
+			show_error('Migrations configuration file (migration.php) must have "migration_table" set.');
+		}
+
+		// Migration basename regex
+		$this->_migration_regex = ($this->_migration_type === 'timestamp')
+			? '/^\d{14}_(\w+)$/'
+			: '/^\d{3}_(\w+)$/';
+
+		// Make sure a valid migration numbering type was set.
+		if ( ! in_array($this->_migration_type, array('sequential', 'timestamp')))
+		{
+			show_error('An invalid migration numbering type was specified: '.$this->_migration_type);
+		}
+
+		// If the migrations table is missing, make it
+		if ( ! $this->db->table_exists($this->_migration_table))
+		{
+			$this->dbforge->add_field(array(
+				'version' => array('type' => 'BIGINT', 'constraint' => 20),
+			));
+
+			$this->dbforge->create_table($this->_migration_table, TRUE);
+
+			$this->db->insert($this->_migration_table, array('version' => 0));
+		}
+
+		// Do we auto migrate to the latest migration?
+		if ($this->_migration_auto_latest === TRUE && ! $this->latest())
+		{
+			show_error($this->error_string());
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Migrate to a schema version
+	 *
+	 * Calls each migration step required to get to the schema version of
+	 * choice
+	 *
+	 * @param	string	$target_version	Target schema version
+	 * @return	mixed	TRUE if no migrations are found, current version string on success, FALSE on failure
+	 */
+	public function version($target_version)
+	{
+		// Note: We use strings, so that timestamp versions work on 32-bit systems
+		$current_version = $this->_get_version();
+
+		if ($this->_migration_type === 'sequential')
+		{
+			$target_version = sprintf('%03d', $target_version);
+		}
+		else
+		{
+			$target_version = (string) $target_version;
+		}
+
+		$migrations = $this->find_migrations();
+
+		if ($target_version > 0 && ! isset($migrations[$target_version]))
+		{
+			$this->_error_string = sprintf($this->lang->line('migration_not_found'), $target_version);
+			return FALSE;
+		}
+
+		if ($target_version > $current_version)
+		{
+			// Moving Up
+			$method = 'up';
+		}
+		else
+		{
+			// Moving Down, apply in reverse order
+			$method = 'down';
+			krsort($migrations);
+		}
+
+		if (empty($migrations))
+		{
+			return TRUE;
+		}
+
+		$previous = FALSE;
+
+		// Validate all available migrations, and run the ones within our target range
+		foreach ($migrations as $number => $file)
+		{
+			// Check for sequence gaps
+			if ($this->_migration_type === 'sequential' && $previous !== FALSE && abs($number - $previous) > 1)
+			{
+				$this->_error_string = sprintf($this->lang->line('migration_sequence_gap'), $number);
+				return FALSE;
+			}
+
+			include_once($file);
+			$class = 'Migration_'.ucfirst(strtolower($this->_get_migration_name(basename($file, '.php'))));
+
+			// Validate the migration file structure
+			if ( ! class_exists($class, FALSE))
+			{
+				$this->_error_string = sprintf($this->lang->line('migration_class_doesnt_exist'), $class);
+				return FALSE;
+			}
+
+			$previous = $number;
+
+			// Run migrations that are inside the target range
+			if (
+				($method === 'up'   && $number > $current_version && $number <= $target_version) OR
+				($method === 'down' && $number <= $current_version && $number > $target_version)
+			)
+			{
+				$instance = new $class();
+				if ( ! is_callable(array($instance, $method)))
+				{
+					$this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class);
+					return FALSE;
+				}
+
+				log_message('debug', 'Migrating '.$method.' from version '.$current_version.' to version '.$number);
+				call_user_func(array($instance, $method));
+				$current_version = $number;
+				$this->_update_version($current_version);
+			}
+		}
+
+		// This is necessary when moving down, since the the last migration applied
+		// will be the down() method for the next migration up from the target
+		if ($current_version <> $target_version)
+		{
+			$current_version = $target_version;
+			$this->_update_version($current_version);
+		}
+
+		log_message('debug', 'Finished migrating to '.$current_version);
+
+		return $current_version;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Sets the schema to the latest migration
+	 *
+	 * @return	mixed	Current version string on success, FALSE on failure
+	 */
+	public function latest()
+	{
+		$migrations = $this->find_migrations();
+
+		if (empty($migrations))
+		{
+			$this->_error_string = $this->lang->line('migration_none_found');
+			return FALSE;
+		}
+
+		$last_migration = basename(end($migrations));
+
+		// Calculate the last migration step from existing migration
+		// filenames and proceed to the standard version migration
+		return $this->version($this->_get_migration_number($last_migration));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Sets the schema to the migration version set in config
+	 *
+	 * @return	mixed	TRUE if no migrations are found, current version string on success, FALSE on failure
+	 */
+	public function current()
+	{
+		return $this->version($this->_migration_version);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error string
+	 *
+	 * @return	string	Error message returned as a string
+	 */
+	public function error_string()
+	{
+		return $this->_error_string;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Retrieves list of available migration scripts
+	 *
+	 * @return	array	list of migration file paths sorted by version
+	 */
+	public function find_migrations()
+	{
+		$migrations = array();
+
+		// Load all *_*.php files in the migrations path
+		foreach (glob($this->_migration_path.'*_*.php') as $file)
+		{
+			$name = basename($file, '.php');
+
+			// Filter out non-migration files
+			if (preg_match($this->_migration_regex, $name))
+			{
+				$number = $this->_get_migration_number($name);
+
+				// There cannot be duplicate migration numbers
+				if (isset($migrations[$number]))
+				{
+					$this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $number);
+					show_error($this->_error_string);
+				}
+
+				$migrations[$number] = $file;
+			}
+		}
+
+		ksort($migrations);
+		return $migrations;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Extracts the migration number from a filename
+	 *
+	 * @param	string	$migration
+	 * @return	string	Numeric portion of a migration filename
+	 */
+	protected function _get_migration_number($migration)
+	{
+		return sscanf($migration, '%[0-9]+', $number)
+			? $number : '0';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Extracts the migration class name from a filename
+	 *
+	 * @param	string	$migration
+	 * @return	string	text portion of a migration filename
+	 */
+	protected function _get_migration_name($migration)
+	{
+		$parts = explode('_', $migration);
+		array_shift($parts);
+		return implode('_', $parts);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Retrieves current schema version
+	 *
+	 * @return	string	Current migration version
+	 */
+	protected function _get_version()
+	{
+		$row = $this->db->select('version')->get($this->_migration_table)->row();
+		return $row ? $row->version : '0';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Stores the current schema version
+	 *
+	 * @param	string	$migration	Migration reached
+	 * @return	void
+	 */
+	protected function _update_version($migration)
+	{
+		$this->db->update($this->_migration_table, array(
+			'version' => $migration
+		));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Enable the use of CI super-global
+	 *
+	 * @param	string	$var
+	 * @return	mixed
+	 */
+	public function __get($var)
+	{
+		return get_instance()->$var;
+	}
+
+}
diff --git a/README/Pagination.php b/README/Pagination.php
new file mode 100644
index 0000000..4d18998
--- /dev/null
+++ b/README/Pagination.php
@@ -0,0 +1,697 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Pagination Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Pagination
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/pagination.html
+ */
+class CI_Pagination {
+
+	/**
+	 * Base URL
+	 *
+	 * The page that we're linking to
+	 *
+	 * @var	string
+	 */
+	protected $base_url		= '';
+
+	/**
+	 * Prefix
+	 *
+	 * @var	string
+	 */
+	protected $prefix = '';
+
+	/**
+	 * Suffix
+	 *
+	 * @var	string
+	 */
+	protected $suffix = '';
+
+	/**
+	 * Total number of items
+	 *
+	 * @var	int
+	 */
+	protected $total_rows = 0;
+
+	/**
+	 * Number of links to show
+	 *
+	 * Relates to "digit" type links shown before/after
+	 * the currently viewed page.
+	 *
+	 * @var	int
+	 */
+	protected $num_links = 2;
+
+	/**
+	 * Items per page
+	 *
+	 * @var	int
+	 */
+	public $per_page = 10;
+
+	/**
+	 * Current page
+	 *
+	 * @var	int
+	 */
+	public $cur_page = 0;
+
+	/**
+	 * Use page numbers flag
+	 *
+	 * Whether to use actual page numbers instead of an offset
+	 *
+	 * @var	bool
+	 */
+	protected $use_page_numbers = FALSE;
+
+	/**
+	 * First link
+	 *
+	 * @var	string
+	 */
+	protected $first_link = '&lsaquo; First';
+
+	/**
+	 * Next link
+	 *
+	 * @var	string
+	 */
+	protected $next_link = '&gt;';
+
+	/**
+	 * Previous link
+	 *
+	 * @var	string
+	 */
+	protected $prev_link = '&lt;';
+
+	/**
+	 * Last link
+	 *
+	 * @var	string
+	 */
+	protected $last_link = 'Last &rsaquo;';
+
+	/**
+	 * URI Segment
+	 *
+	 * @var	int
+	 */
+	protected $uri_segment = 0;
+
+	/**
+	 * Full tag open
+	 *
+	 * @var	string
+	 */
+	protected $full_tag_open = '';
+
+	/**
+	 * Full tag close
+	 *
+	 * @var	string
+	 */
+	protected $full_tag_close = '';
+
+	/**
+	 * First tag open
+	 *
+	 * @var	string
+	 */
+	protected $first_tag_open = '';
+
+	/**
+	 * First tag close
+	 *
+	 * @var	string
+	 */
+	protected $first_tag_close = '';
+
+	/**
+	 * Last tag open
+	 *
+	 * @var	string
+	 */
+	protected $last_tag_open = '';
+
+	/**
+	 * Last tag close
+	 *
+	 * @var	string
+	 */
+	protected $last_tag_close = '';
+
+	/**
+	 * First URL
+	 *
+	 * An alternative URL for the first page
+	 *
+	 * @var	string
+	 */
+	protected $first_url = '';
+
+	/**
+	 * Current tag open
+	 *
+	 * @var	string
+	 */
+	protected $cur_tag_open = '<strong>';
+
+	/**
+	 * Current tag close
+	 *
+	 * @var	string
+	 */
+	protected $cur_tag_close = '</strong>';
+
+	/**
+	 * Next tag open
+	 *
+	 * @var	string
+	 */
+	protected $next_tag_open = '';
+
+	/**
+	 * Next tag close
+	 *
+	 * @var	string
+	 */
+	protected $next_tag_close = '';
+
+	/**
+	 * Previous tag open
+	 *
+	 * @var	string
+	 */
+	protected $prev_tag_open = '';
+
+	/**
+	 * Previous tag close
+	 *
+	 * @var	string
+	 */
+	protected $prev_tag_close = '';
+
+	/**
+	 * Number tag open
+	 *
+	 * @var	string
+	 */
+	protected $num_tag_open = '';
+
+	/**
+	 * Number tag close
+	 *
+	 * @var	string
+	 */
+	protected $num_tag_close = '';
+
+	/**
+	 * Page query string flag
+	 *
+	 * @var	bool
+	 */
+	protected $page_query_string = FALSE;
+
+	/**
+	 * Query string segment
+	 *
+	 * @var	string
+	 */
+	protected $query_string_segment = 'per_page';
+
+	/**
+	 * Display pages flag
+	 *
+	 * @var	bool
+	 */
+	protected $display_pages = TRUE;
+
+	/**
+	 * Attributes
+	 *
+	 * @var	string
+	 */
+	protected $_attributes = '';
+
+	/**
+	 * Link types
+	 *
+	 * "rel" attribute
+	 *
+	 * @see	CI_Pagination::_attr_rel()
+	 * @var	array
+	 */
+	protected $_link_types = array();
+
+	/**
+	 * Reuse query string flag
+	 *
+	 * @var	bool
+	 */
+	protected $reuse_query_string = FALSE;
+
+	/**
+	 * Use global URL suffix flag
+	 *
+	 * @var	bool
+	 */
+	protected $use_global_url_suffix = FALSE;
+
+	/**
+	 * Data page attribute
+	 *
+	 * @var	string
+	 */
+	protected $data_page_attr = 'data-ci-pagination-page';
+
+	/**
+	 * CI Singleton
+	 *
+	 * @var	object
+	 */
+	protected $CI;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	array	$params	Initialization parameters
+	 * @return	void
+	 */
+	public function __construct($params = array())
+	{
+		$this->CI =& get_instance();
+		$this->CI->load->language('pagination');
+		foreach (array('first_link', 'next_link', 'prev_link', 'last_link') as $key)
+		{
+			if (($val = $this->CI->lang->line('pagination_'.$key)) !== FALSE)
+			{
+				$this->$key = $val;
+			}
+		}
+
+		$this->initialize($params);
+		log_message('info', 'Pagination Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize Preferences
+	 *
+	 * @param	array	$params	Initialization parameters
+	 * @return	CI_Pagination
+	 */
+	public function initialize(array $params = array())
+	{
+		isset($params['attributes']) OR $params['attributes'] = array();
+		if (is_array($params['attributes']))
+		{
+			$this->_parse_attributes($params['attributes']);
+			unset($params['attributes']);
+		}
+
+		// Deprecated legacy support for the anchor_class option
+		// Should be removed in CI 3.1+
+		if (isset($params['anchor_class']))
+		{
+			empty($params['anchor_class']) OR $attributes['class'] = $params['anchor_class'];
+			unset($params['anchor_class']);
+		}
+
+		foreach ($params as $key => $val)
+		{
+			if (property_exists($this, $key))
+			{
+				$this->$key = $val;
+			}
+		}
+
+		if ($this->CI->config->item('enable_query_strings') === TRUE)
+		{
+			$this->page_query_string = TRUE;
+		}
+
+		if ($this->use_global_url_suffix === TRUE)
+		{
+			$this->suffix = $this->CI->config->item('url_suffix');
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate the pagination links
+	 *
+	 * @return	string
+	 */
+	public function create_links()
+	{
+		// If our item count or per-page total is zero there is no need to continue.
+		// Note: DO NOT change the operator to === here!
+		if ($this->total_rows == 0 OR $this->per_page == 0)
+		{
+			return '';
+		}
+
+		// Calculate the total number of pages
+		$num_pages = (int) ceil($this->total_rows / $this->per_page);
+
+		// Is there only one page? Hm... nothing more to do here then.
+		if ($num_pages === 1)
+		{
+			return '';
+		}
+
+		// Check the user defined number of links.
+		$this->num_links = (int) $this->num_links;
+
+		if ($this->num_links < 0)
+		{
+			show_error('Your number of links must be a non-negative number.');
+		}
+
+		// Keep any existing query string items.
+		// Note: Has nothing to do with any other query string option.
+		if ($this->reuse_query_string === TRUE)
+		{
+			$get = $this->CI->input->get();
+
+			// Unset the controll, method, old-school routing options
+			unset($get['c'], $get['m'], $get[$this->query_string_segment]);
+		}
+		else
+		{
+			$get = array();
+		}
+
+		// Put together our base and first URLs.
+		// Note: DO NOT append to the properties as that would break successive calls
+		$base_url = trim($this->base_url);
+		$first_url = $this->first_url;
+
+		$query_string = '';
+		$query_string_sep = (strpos($base_url, '?') === FALSE) ? '?' : '&amp;';
+
+		// Are we using query strings?
+		if ($this->page_query_string === TRUE)
+		{
+			// If a custom first_url hasn't been specified, we'll create one from
+			// the base_url, but without the page item.
+			if ($first_url === '')
+			{
+				$first_url = $base_url;
+
+				// If we saved any GET items earlier, make sure they're appended.
+				if ( ! empty($get))
+				{
+					$first_url .= $query_string_sep.http_build_query($get);
+				}
+			}
+
+			// Add the page segment to the end of the query string, where the
+			// page number will be appended.
+			$base_url .= $query_string_sep.http_build_query(array_merge($get, array($this->query_string_segment => '')));
+		}
+		else
+		{
+			// Standard segment mode.
+			// Generate our saved query string to append later after the page number.
+			if ( ! empty($get))
+			{
+				$query_string = $query_string_sep.http_build_query($get);
+				$this->suffix .= $query_string;
+			}
+
+			// Does the base_url have the query string in it?
+			// If we're supposed to save it, remove it so we can append it later.
+			if ($this->reuse_query_string === TRUE && ($base_query_pos = strpos($base_url, '?')) !== FALSE)
+			{
+				$base_url = substr($base_url, 0, $base_query_pos);
+			}
+
+			if ($first_url === '')
+			{
+				$first_url = $base_url.$query_string;
+			}
+
+			$base_url = rtrim($base_url, '/').'/';
+		}
+
+		// Determine the current page number.
+		$base_page = ($this->use_page_numbers) ? 1 : 0;
+
+		// Are we using query strings?
+		if ($this->page_query_string === TRUE)
+		{
+			$this->cur_page = $this->CI->input->get($this->query_string_segment);
+		}
+		else
+		{
+			// Default to the last segment number if one hasn't been defined.
+			if ($this->uri_segment === 0)
+			{
+				$this->uri_segment = count($this->CI->uri->segment_array());
+			}
+
+			$this->cur_page = $this->CI->uri->segment($this->uri_segment);
+
+			// Remove any specified prefix/suffix from the segment.
+			if ($this->prefix !== '' OR $this->suffix !== '')
+			{
+				$this->cur_page = str_replace(array($this->prefix, $this->suffix), '', $this->cur_page);
+			}
+		}
+
+		// If something isn't quite right, back to the default base page.
+		if ( ! ctype_digit($this->cur_page) OR ($this->use_page_numbers && (int) $this->cur_page === 0))
+		{
+			$this->cur_page = $base_page;
+		}
+		else
+		{
+			// Make sure we're using integers for comparisons later.
+			$this->cur_page = (int) $this->cur_page;
+		}
+
+		// Is the page number beyond the result range?
+		// If so, we show the last page.
+		if ($this->use_page_numbers)
+		{
+			if ($this->cur_page > $num_pages)
+			{
+				$this->cur_page = $num_pages;
+			}
+		}
+		elseif ($this->cur_page > $this->total_rows)
+		{
+			$this->cur_page = ($num_pages - 1) * $this->per_page;
+		}
+
+		$uri_page_number = $this->cur_page;
+
+		// If we're using offset instead of page numbers, convert it
+		// to a page number, so we can generate the surrounding number links.
+		if ( ! $this->use_page_numbers)
+		{
+			$this->cur_page = (int) floor(($this->cur_page/$this->per_page) + 1);
+		}
+
+		// Calculate the start and end numbers. These determine
+		// which number to start and end the digit links with.
+		$start	= (($this->cur_page - $this->num_links) > 0) ? $this->cur_page - ($this->num_links - 1) : 1;
+		$end	= (($this->cur_page + $this->num_links) < $num_pages) ? $this->cur_page + $this->num_links : $num_pages;
+
+		// And here we go...
+		$output = '';
+
+		// Render the "First" link.
+		if ($this->first_link !== FALSE && $this->cur_page > ($this->num_links + 1 + ! $this->num_links))
+		{
+			// Take the general parameters, and squeeze this pagination-page attr in for JS frameworks.
+			$attributes = sprintf('%s %s="%d"', $this->_attributes, $this->data_page_attr, 1);
+
+			$output .= $this->first_tag_open.'<a href="'.$first_url.'"'.$attributes.$this->_attr_rel('start').'>'
+				.$this->first_link.'</a>'.$this->first_tag_close;
+		}
+
+		// Render the "Previous" link.
+		if ($this->prev_link !== FALSE && $this->cur_page !== 1)
+		{
+			$i = ($this->use_page_numbers) ? $uri_page_number - 1 : $uri_page_number - $this->per_page;
+
+			$attributes = sprintf('%s %s="%d"', $this->_attributes, $this->data_page_attr, ($this->cur_page - 1));
+
+			if ($i === $base_page)
+			{
+				// First page
+				$output .= $this->prev_tag_open.'<a href="'.$first_url.'"'.$attributes.$this->_attr_rel('prev').'>'
+					.$this->prev_link.'</a>'.$this->prev_tag_close;
+			}
+			else
+			{
+				$append = $this->prefix.$i.$this->suffix;
+				$output .= $this->prev_tag_open.'<a href="'.$base_url.$append.'"'.$attributes.$this->_attr_rel('prev').'>'
+					.$this->prev_link.'</a>'.$this->prev_tag_close;
+			}
+
+		}
+
+		// Render the pages
+		if ($this->display_pages !== FALSE)
+		{
+			// Write the digit links
+			for ($loop = $start - 1; $loop <= $end; $loop++)
+			{
+				$i = ($this->use_page_numbers) ? $loop : ($loop * $this->per_page) - $this->per_page;
+
+				$attributes = sprintf('%s %s="%d"', $this->_attributes, $this->data_page_attr, $loop);
+
+				if ($i >= $base_page)
+				{
+					if ($this->cur_page === $loop)
+					{
+						// Current page
+						$output .= $this->cur_tag_open.$loop.$this->cur_tag_close;
+					}
+					elseif ($i === $base_page)
+					{
+						// First page
+						$output .= $this->num_tag_open.'<a href="'.$first_url.'"'.$attributes.$this->_attr_rel('start').'>'
+							.$loop.'</a>'.$this->num_tag_close;
+					}
+					else
+					{
+						$append = $this->prefix.$i.$this->suffix;
+						$output .= $this->num_tag_open.'<a href="'.$base_url.$append.'"'.$attributes.'>'
+							.$loop.'</a>'.$this->num_tag_close;
+					}
+				}
+			}
+		}
+
+		// Render the "next" link
+		if ($this->next_link !== FALSE && $this->cur_page < $num_pages)
+		{
+			$i = ($this->use_page_numbers) ? $this->cur_page + 1 : $this->cur_page * $this->per_page;
+
+			$attributes = sprintf('%s %s="%d"', $this->_attributes, $this->data_page_attr, $this->cur_page + 1);
+
+			$output .= $this->next_tag_open.'<a href="'.$base_url.$this->prefix.$i.$this->suffix.'"'.$attributes
+				.$this->_attr_rel('next').'>'.$this->next_link.'</a>'.$this->next_tag_close;
+		}
+
+		// Render the "Last" link
+		if ($this->last_link !== FALSE && ($this->cur_page + $this->num_links + ! $this->num_links) < $num_pages)
+		{
+			$i = ($this->use_page_numbers) ? $num_pages : ($num_pages * $this->per_page) - $this->per_page;
+
+			$attributes = sprintf('%s %s="%d"', $this->_attributes, $this->data_page_attr, $num_pages);
+
+			$output .= $this->last_tag_open.'<a href="'.$base_url.$this->prefix.$i.$this->suffix.'"'.$attributes.'>'
+				.$this->last_link.'</a>'.$this->last_tag_close;
+		}
+
+		// Kill double slashes. Note: Sometimes we can end up with a double slash
+		// in the penultimate link so we'll kill all double slashes.
+		$output = preg_replace('#([^:"])//+#', '\\1/', $output);
+
+		// Add the wrapper HTML if exists
+		return $this->full_tag_open.$output.$this->full_tag_close;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse attributes
+	 *
+	 * @param	array	$attributes
+	 * @return	void
+	 */
+	protected function _parse_attributes($attributes)
+	{
+		isset($attributes['rel']) OR $attributes['rel'] = TRUE;
+		$this->_link_types = ($attributes['rel'])
+			? array('start' => 'start', 'prev' => 'prev', 'next' => 'next')
+			: array();
+		unset($attributes['rel']);
+
+		$this->_attributes = '';
+		foreach ($attributes as $key => $value)
+		{
+			$this->_attributes .= ' '.$key.'="'.$value.'"';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add "rel" attribute
+	 *
+	 * @link	http://www.w3.org/TR/html5/links.html#linkTypes
+	 * @param	string	$type
+	 * @return	string
+	 */
+	protected function _attr_rel($type)
+	{
+		if (isset($this->_link_types[$type]))
+		{
+			unset($this->_link_types[$type]);
+			return ' rel="'.$type.'"';
+		}
+
+		return '';
+	}
+
+}
diff --git a/README/Parser.php b/README/Parser.php
new file mode 100644
index 0000000..57981af
--- /dev/null
+++ b/README/Parser.php
@@ -0,0 +1,248 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Parser Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Parser
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/parser.html
+ */
+class CI_Parser {
+
+	/**
+	 * Left delimiter character for pseudo vars
+	 *
+	 * @var string
+	 */
+	public $l_delim = '{';
+
+	/**
+	 * Right delimiter character for pseudo vars
+	 *
+	 * @var string
+	 */
+	public $r_delim = '}';
+
+	/**
+	 * Reference to CodeIgniter instance
+	 *
+	 * @var object
+	 */
+	protected $CI;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$this->CI =& get_instance();
+		log_message('info', 'Parser Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse a template
+	 *
+	 * Parses pseudo-variables contained in the specified template view,
+	 * replacing them with the data in the second param
+	 *
+	 * @param	string
+	 * @param	array
+	 * @param	bool
+	 * @return	string
+	 */
+	public function parse($template, $data, $return = FALSE)
+	{
+		$template = $this->CI->load->view($template, $data, TRUE);
+
+		return $this->_parse($template, $data, $return);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse a String
+	 *
+	 * Parses pseudo-variables contained in the specified string,
+	 * replacing them with the data in the second param
+	 *
+	 * @param	string
+	 * @param	array
+	 * @param	bool
+	 * @return	string
+	 */
+	public function parse_string($template, $data, $return = FALSE)
+	{
+		return $this->_parse($template, $data, $return);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse a template
+	 *
+	 * Parses pseudo-variables contained in the specified template,
+	 * replacing them with the data in the second param
+	 *
+	 * @param	string
+	 * @param	array
+	 * @param	bool
+	 * @return	string
+	 */
+	protected function _parse($template, $data, $return = FALSE)
+	{
+		if ($template === '')
+		{
+			return FALSE;
+		}
+
+		$replace = array();
+		foreach ($data as $key => $val)
+		{
+			$replace = array_merge(
+				$replace,
+				is_array($val)
+					? $this->_parse_pair($key, $val, $template)
+					: $this->_parse_single($key, (string) $val, $template)
+			);
+		}
+
+		unset($data);
+		$template = strtr($template, $replace);
+
+		if ($return === FALSE)
+		{
+			$this->CI->output->append_output($template);
+		}
+
+		return $template;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the left/right variable delimiters
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	void
+	 */
+	public function set_delimiters($l = '{', $r = '}')
+	{
+		$this->l_delim = $l;
+		$this->r_delim = $r;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse a single key/value
+	 *
+	 * @param	string
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _parse_single($key, $val, $string)
+	{
+		return array($this->l_delim.$key.$this->r_delim => (string) $val);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse a tag pair
+	 *
+	 * Parses tag pairs: {some_tag} string... {/some_tag}
+	 *
+	 * @param	string
+	 * @param	array
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _parse_pair($variable, $data, $string)
+	{
+		$replace = array();
+		preg_match_all(
+			'#'.preg_quote($this->l_delim.$variable.$this->r_delim).'(.+?)'.preg_quote($this->l_delim.'/'.$variable.$this->r_delim).'#s',
+			$string,
+			$matches,
+			PREG_SET_ORDER
+		);
+
+		foreach ($matches as $match)
+		{
+			$str = '';
+			foreach ($data as $row)
+			{
+				$temp = array();
+				foreach ($row as $key => $val)
+				{
+					if (is_array($val))
+					{
+						$pair = $this->_parse_pair($key, $val, $match[1]);
+						if ( ! empty($pair))
+						{
+							$temp = array_merge($temp, $pair);
+						}
+
+						continue;
+					}
+
+					$temp[$this->l_delim.$key.$this->r_delim] = $val;
+				}
+
+				$str .= strtr($match[1], $temp);
+			}
+
+			$replace[$match[0]] = $str;
+		}
+
+		return $replace;
+	}
+
+}
diff --git a/README/Profiler.php b/README/Profiler.php
new file mode 100644
index 0000000..1e464d8
--- /dev/null
+++ b/README/Profiler.php
@@ -0,0 +1,570 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Profiler Class
+ *
+ * This class enables you to display benchmark, query, and other data
+ * in order to help with debugging and optimization.
+ *
+ * Note: At some point it would be good to move all the HTML in this class
+ * into a set of template files in order to allow customization.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/general/profiling.html
+ */
+class CI_Profiler {
+
+	/**
+	 * List of profiler sections available to show
+	 *
+	 * @var array
+	 */
+	protected $_available_sections = array(
+		'benchmarks',
+		'get',
+		'memory_usage',
+		'post',
+		'uri_string',
+		'controller_info',
+		'queries',
+		'http_headers',
+		'session_data',
+		'config'
+	);
+
+	/**
+	 * Number of queries to show before making the additional queries togglable
+	 *
+	 * @var int
+	 */
+	protected $_query_toggle_count = 25;
+
+	/**
+	 * Reference to the CodeIgniter singleton
+	 *
+	 * @var object
+	 */
+	protected $CI;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Initialize Profiler
+	 *
+	 * @param	array	$config	Parameters
+	 */
+	public function __construct($config = array())
+	{
+		$this->CI =& get_instance();
+		$this->CI->load->language('profiler');
+
+		// default all sections to display
+		foreach ($this->_available_sections as $section)
+		{
+			if ( ! isset($config[$section]))
+			{
+				$this->_compile_{$section} = TRUE;
+			}
+		}
+
+		$this->set_sections($config);
+		log_message('info', 'Profiler Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Sections
+	 *
+	 * Sets the private _compile_* properties to enable/disable Profiler sections
+	 *
+	 * @param	mixed	$config
+	 * @return	void
+	 */
+	public function set_sections($config)
+	{
+		if (isset($config['query_toggle_count']))
+		{
+			$this->_query_toggle_count = (int) $config['query_toggle_count'];
+			unset($config['query_toggle_count']);
+		}
+
+		foreach ($config as $method => $enable)
+		{
+			if (in_array($method, $this->_available_sections))
+			{
+				$this->_compile_{$method} = ($enable !== FALSE);
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Auto Profiler
+	 *
+	 * This function cycles through the entire array of mark points and
+	 * matches any two points that are named identically (ending in "_start"
+	 * and "_end" respectively).  It then compiles the execution times for
+	 * all points and returns it as an array
+	 *
+	 * @return	array
+	 */
+	protected function _compile_benchmarks()
+	{
+		$profile = array();
+		foreach ($this->CI->benchmark->marker as $key => $val)
+		{
+			// We match the "end" marker so that the list ends
+			// up in the order that it was defined
+			if (preg_match('/(.+?)_end$/i', $key, $match)
+				&& isset($this->CI->benchmark->marker[$match[1].'_end'], $this->CI->benchmark->marker[$match[1].'_start']))
+			{
+				$profile[$match[1]] = $this->CI->benchmark->elapsed_time($match[1].'_start', $key);
+			}
+		}
+
+		// Build a table containing the profile data.
+		// Note: At some point we should turn this into a template that can
+		// be modified. We also might want to make this data available to be logged
+
+		$output = "\n\n"
+			.'<fieldset id="ci_profiler_benchmarks" style="border:1px solid #900;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+			."\n"
+			.'<legend style="color:#900;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_benchmarks')."&nbsp;&nbsp;</legend>"
+			."\n\n\n<table style=\"width:100%;\">\n";
+
+		foreach ($profile as $key => $val)
+		{
+			$key = ucwords(str_replace(array('_', '-'), ' ', $key));
+			$output .= '<tr><td style="padding:5px;width:50%;color:#000;font-weight:bold;background-color:#ddd;">'
+					.$key.'&nbsp;&nbsp;</td><td style="padding:5px;width:50%;color:#900;font-weight:normal;background-color:#ddd;">'
+					.$val."</td></tr>\n";
+		}
+
+		return $output."</table>\n</fieldset>";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile Queries
+	 *
+	 * @return	string
+	 */
+	protected function _compile_queries()
+	{
+		$dbs = array();
+
+		// Let's determine which databases are currently connected to
+		foreach (get_object_vars($this->CI) as $name => $cobject)
+		{
+			if (is_object($cobject))
+			{
+				if ($cobject instanceof CI_DB)
+				{
+					$dbs[get_class($this->CI).':$'.$name] = $cobject;
+				}
+				elseif ($cobject instanceof CI_Model)
+				{
+					foreach (get_object_vars($cobject) as $mname => $mobject)
+					{
+						if ($mobject instanceof CI_DB)
+						{
+							$dbs[get_class($cobject).':$'.$mname] = $mobject;
+						}
+					}
+				}
+			}
+		}
+
+		if (count($dbs) === 0)
+		{
+			return "\n\n"
+				.'<fieldset id="ci_profiler_queries" style="border:1px solid #0000FF;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+				."\n"
+				.'<legend style="color:#0000FF;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_queries').'&nbsp;&nbsp;</legend>'
+				."\n\n\n<table style=\"border:none; width:100%;\">\n"
+				.'<tr><td style="width:100%;color:#0000FF;font-weight:normal;background-color:#eee;padding:5px;">'
+				.$this->CI->lang->line('profiler_no_db')
+				."</td></tr>\n</table>\n</fieldset>";
+		}
+
+		// Load the text helper so we can highlight the SQL
+		$this->CI->load->helper('text');
+
+		// Key words we want bolded
+		$highlight = array('SELECT', 'DISTINCT', 'FROM', 'WHERE', 'AND', 'LEFT&nbsp;JOIN', 'ORDER&nbsp;BY', 'GROUP&nbsp;BY', 'LIMIT', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'OR&nbsp;', 'HAVING', 'OFFSET', 'NOT&nbsp;IN', 'IN', 'LIKE', 'NOT&nbsp;LIKE', 'COUNT', 'MAX', 'MIN', 'ON', 'AS', 'AVG', 'SUM', '(', ')');
+
+		$output  = "\n\n";
+		$count = 0;
+
+		foreach ($dbs as $name => $db)
+		{
+			$hide_queries = (count($db->queries) > $this->_query_toggle_count) ? ' display:none' : '';
+			$total_time = number_format(array_sum($db->query_times), 4).' '.$this->CI->lang->line('profiler_seconds');
+
+			$show_hide_js = '(<span style="cursor: pointer;" onclick="var s=document.getElementById(\'ci_profiler_queries_db_'.$count.'\').style;s.display=s.display==\'none\'?\'\':\'none\';this.innerHTML=this.innerHTML==\''.$this->CI->lang->line('profiler_section_hide').'\'?\''.$this->CI->lang->line('profiler_section_show').'\':\''.$this->CI->lang->line('profiler_section_hide').'\';">'.$this->CI->lang->line('profiler_section_hide').'</span>)';
+
+			if ($hide_queries !== '')
+			{
+				$show_hide_js = '(<span style="cursor: pointer;" onclick="var s=document.getElementById(\'ci_profiler_queries_db_'.$count.'\').style;s.display=s.display==\'none\'?\'\':\'none\';this.innerHTML=this.innerHTML==\''.$this->CI->lang->line('profiler_section_show').'\'?\''.$this->CI->lang->line('profiler_section_hide').'\':\''.$this->CI->lang->line('profiler_section_show').'\';">'.$this->CI->lang->line('profiler_section_show').'</span>)';
+			}
+
+			$output .= '<fieldset style="border:1px solid #0000FF;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+				."\n"
+				.'<legend style="color:#0000FF;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_database')
+				.':&nbsp; '.$db->database.' ('.$name.')&nbsp;&nbsp;&nbsp;'.$this->CI->lang->line('profiler_queries')
+				.': '.count($db->queries).' ('.$total_time.')&nbsp;&nbsp;'.$show_hide_js."</legend>\n\n\n"
+				.'<table style="width:100%;'.$hide_queries.'" id="ci_profiler_queries_db_'.$count."\">\n";
+
+			if (count($db->queries) === 0)
+			{
+				$output .= '<tr><td style="width:100%;color:#0000FF;font-weight:normal;background-color:#eee;padding:5px;">'
+						.$this->CI->lang->line('profiler_no_queries')."</td></tr>\n";
+			}
+			else
+			{
+				foreach ($db->queries as $key => $val)
+				{
+					$time = number_format($db->query_times[$key], 4);
+					$val = highlight_code($val);
+
+					foreach ($highlight as $bold)
+					{
+						$val = str_replace($bold, '<strong>'.$bold.'</strong>', $val);
+					}
+
+					$output .= '<tr><td style="padding:5px;vertical-align:top;width:1%;color:#900;font-weight:normal;background-color:#ddd;">'
+							.$time.'&nbsp;&nbsp;</td><td style="padding:5px;color:#000;font-weight:normal;background-color:#ddd;">'
+							.$val."</td></tr>\n";
+				}
+			}
+
+			$output .= "</table>\n</fieldset>";
+			$count++;
+		}
+
+		return $output;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile $_GET Data
+	 *
+	 * @return	string
+	 */
+	protected function _compile_get()
+	{
+		$output = "\n\n"
+			.'<fieldset id="ci_profiler_get" style="border:1px solid #cd6e00;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+			."\n"
+			.'<legend style="color:#cd6e00;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_get_data')."&nbsp;&nbsp;</legend>\n";
+
+		if (count($_GET) === 0)
+		{
+			$output .= '<div style="color:#cd6e00;font-weight:normal;padding:4px 0 4px 0;">'.$this->CI->lang->line('profiler_no_get').'</div>';
+		}
+		else
+		{
+			$output .= "\n\n<table style=\"width:100%;border:none;\">\n";
+
+			foreach ($_GET as $key => $val)
+			{
+				is_int($key) OR $key = "'".$key."'";
+
+				$output .= '<tr><td style="width:50%;color:#000;background-color:#ddd;padding:5px;">&#36;_GET['
+					.$key.']&nbsp;&nbsp; </td><td style="width:50%;padding:5px;color:#cd6e00;font-weight:normal;background-color:#ddd;">'
+					.((is_array($val) OR is_object($val)) ? '<pre>'.htmlspecialchars(stripslashes(print_r($val, TRUE))).'</pre>' : htmlspecialchars(stripslashes($val)))
+					."</td></tr>\n";
+			}
+
+			$output .= "</table>\n";
+		}
+
+		return $output.'</fieldset>';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile $_POST Data
+	 *
+	 * @return	string
+	 */
+	protected function _compile_post()
+	{
+		$output = "\n\n"
+			.'<fieldset id="ci_profiler_post" style="border:1px solid #009900;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+			."\n"
+			.'<legend style="color:#009900;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_post_data')."&nbsp;&nbsp;</legend>\n";
+
+		if (count($_POST) === 0 && count($_FILES) === 0)
+		{
+			$output .= '<div style="color:#009900;font-weight:normal;padding:4px 0 4px 0;">'.$this->CI->lang->line('profiler_no_post').'</div>';
+		}
+		else
+		{
+			$output .= "\n\n<table style=\"width:100%;\">\n";
+
+			foreach ($_POST as $key => $val)
+			{
+				is_int($key) OR $key = "'".$key."'";
+
+				$output .= '<tr><td style="width:50%;padding:5px;color:#000;background-color:#ddd;">&#36;_POST['
+					.$key.']&nbsp;&nbsp; </td><td style="width:50%;padding:5px;color:#009900;font-weight:normal;background-color:#ddd;">';
+
+				if (is_array($val) OR is_object($val))
+				{
+					$output .= '<pre>'.htmlspecialchars(stripslashes(print_r($val, TRUE))).'</pre>';
+				}
+				else
+				{
+					$output .= htmlspecialchars(stripslashes($val));
+				}
+
+				$output .= "</td></tr>\n";
+			}
+
+			foreach ($_FILES as $key => $val)
+			{
+				is_int($key) OR $key = "'".$key."'";
+
+				$output .= '<tr><td style="width:50%;padding:5px;color:#000;background-color:#ddd;">&#36;_FILES['
+					.$key.']&nbsp;&nbsp; </td><td style="width:50%;padding:5px;color:#009900;font-weight:normal;background-color:#ddd;">';
+
+				if (is_array($val) OR is_object($val))
+				{
+					$output .= '<pre>'.htmlspecialchars(stripslashes(print_r($val, TRUE))).'</pre>';
+				}
+
+				$output .= "</td></tr>\n";
+			}
+
+			$output .= "</table>\n";
+		}
+
+		return $output.'</fieldset>';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show query string
+	 *
+	 * @return	string
+	 */
+	protected function _compile_uri_string()
+	{
+		return "\n\n"
+			.'<fieldset id="ci_profiler_uri_string" style="border:1px solid #000;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+			."\n"
+			.'<legend style="color:#000;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_uri_string')."&nbsp;&nbsp;</legend>\n"
+			.'<div style="color:#000;font-weight:normal;padding:4px 0 4px 0;">'
+			.($this->CI->uri->uri_string === '' ? $this->CI->lang->line('profiler_no_uri') : $this->CI->uri->uri_string)
+			.'</div></fieldset>';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show the controller and function that were called
+	 *
+	 * @return	string
+	 */
+	protected function _compile_controller_info()
+	{
+		return "\n\n"
+			.'<fieldset id="ci_profiler_controller_info" style="border:1px solid #995300;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+			."\n"
+			.'<legend style="color:#995300;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_controller_info')."&nbsp;&nbsp;</legend>\n"
+			.'<div style="color:#995300;font-weight:normal;padding:4px 0 4px 0;">'.$this->CI->router->class.'/'.$this->CI->router->method
+			.'</div></fieldset>';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile memory usage
+	 *
+	 * Display total used memory
+	 *
+	 * @return	string
+	 */
+	protected function _compile_memory_usage()
+	{
+		return "\n\n"
+			.'<fieldset id="ci_profiler_memory_usage" style="border:1px solid #5a0099;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+			."\n"
+			.'<legend style="color:#5a0099;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_memory_usage')."&nbsp;&nbsp;</legend>\n"
+			.'<div style="color:#5a0099;font-weight:normal;padding:4px 0 4px 0;">'
+			.(($usage = memory_get_usage()) != '' ? number_format($usage).' bytes' : $this->CI->lang->line('profiler_no_memory'))
+			.'</div></fieldset>';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile header information
+	 *
+	 * Lists HTTP headers
+	 *
+	 * @return	string
+	 */
+	protected function _compile_http_headers()
+	{
+		$output = "\n\n"
+			.'<fieldset id="ci_profiler_http_headers" style="border:1px solid #000;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+			."\n"
+			.'<legend style="color:#000;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_headers')
+			.'&nbsp;&nbsp;(<span style="cursor: pointer;" onclick="var s=document.getElementById(\'ci_profiler_httpheaders_table\').style;s.display=s.display==\'none\'?\'\':\'none\';this.innerHTML=this.innerHTML==\''.$this->CI->lang->line('profiler_section_show').'\'?\''.$this->CI->lang->line('profiler_section_hide').'\':\''.$this->CI->lang->line('profiler_section_show').'\';">'.$this->CI->lang->line('profiler_section_show')."</span>)</legend>\n\n\n"
+			.'<table style="width:100%;display:none;" id="ci_profiler_httpheaders_table">'."\n";
+
+		foreach (array('HTTP_ACCEPT', 'HTTP_USER_AGENT', 'HTTP_CONNECTION', 'SERVER_PORT', 'SERVER_NAME', 'REMOTE_ADDR', 'SERVER_SOFTWARE', 'HTTP_ACCEPT_LANGUAGE', 'SCRIPT_NAME', 'REQUEST_METHOD',' HTTP_HOST', 'REMOTE_HOST', 'CONTENT_TYPE', 'SERVER_PROTOCOL', 'QUERY_STRING', 'HTTP_ACCEPT_ENCODING', 'HTTP_X_FORWARDED_FOR', 'HTTP_DNT') as $header)
+		{
+			$val = isset($_SERVER[$header]) ? $_SERVER[$header] : '';
+			$output .= '<tr><td style="vertical-align:top;width:50%;padding:5px;color:#900;background-color:#ddd;">'
+				.$header.'&nbsp;&nbsp;</td><td style="width:50%;padding:5px;color:#000;background-color:#ddd;">'.$val."</td></tr>\n";
+		}
+
+		return $output."</table>\n</fieldset>";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile config information
+	 *
+	 * Lists developer config variables
+	 *
+	 * @return	string
+	 */
+	protected function _compile_config()
+	{
+		$output = "\n\n"
+			.'<fieldset id="ci_profiler_config" style="border:1px solid #000;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+			."\n"
+			.'<legend style="color:#000;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_config').'&nbsp;&nbsp;(<span style="cursor: pointer;" onclick="var s=document.getElementById(\'ci_profiler_config_table\').style;s.display=s.display==\'none\'?\'\':\'none\';this.innerHTML=this.innerHTML==\''.$this->CI->lang->line('profiler_section_show').'\'?\''.$this->CI->lang->line('profiler_section_hide').'\':\''.$this->CI->lang->line('profiler_section_show').'\';">'.$this->CI->lang->line('profiler_section_show')."</span>)</legend>\n\n\n"
+			.'<table style="width:100%;display:none;" id="ci_profiler_config_table">'."\n";
+
+		foreach ($this->CI->config->config as $config => $val)
+		{
+			if (is_array($val) OR is_object($val))
+			{
+				$val = print_r($val, TRUE);
+			}
+
+			$output .= '<tr><td style="padding:5px;vertical-align:top;color:#900;background-color:#ddd;">'
+				.$config.'&nbsp;&nbsp;</td><td style="padding:5px;color:#000;background-color:#ddd;">'.htmlspecialchars($val)."</td></tr>\n";
+		}
+
+		return $output."</table>\n</fieldset>";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile session userdata
+	 *
+	 * @return 	string
+	 */
+	protected function _compile_session_data()
+	{
+		if ( ! isset($this->CI->session))
+		{
+			return;
+		}
+
+		$output = '<fieldset id="ci_profiler_csession" style="border:1px solid #000;padding:6px 10px 10px 10px;margin:20px 0 20px 0;background-color:#eee;">'
+			.'<legend style="color:#000;">&nbsp;&nbsp;'.$this->CI->lang->line('profiler_session_data').'&nbsp;&nbsp;(<span style="cursor: pointer;" onclick="var s=document.getElementById(\'ci_profiler_session_data\').style;s.display=s.display==\'none\'?\'\':\'none\';this.innerHTML=this.innerHTML==\''.$this->CI->lang->line('profiler_section_show').'\'?\''.$this->CI->lang->line('profiler_section_hide').'\':\''.$this->CI->lang->line('profiler_section_show').'\';">'.$this->CI->lang->line('profiler_section_show').'</span>)</legend>'
+			.'<table style="width:100%;display:none;" id="ci_profiler_session_data">';
+
+		foreach ($this->CI->session->userdata() as $key => $val)
+		{
+			if (is_array($val) OR is_object($val))
+			{
+				$val = print_r($val, TRUE);
+			}
+
+			$output .= '<tr><td style="padding:5px;vertical-align:top;color:#900;background-color:#ddd;">'
+				.$key.'&nbsp;&nbsp;</td><td style="padding:5px;color:#000;background-color:#ddd;">'.htmlspecialchars($val)."</td></tr>\n";
+		}
+
+		return $output."</table>\n</fieldset>";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Run the Profiler
+	 *
+	 * @return	string
+	 */
+	public function run()
+	{
+		$output = '<div id="codeigniter_profiler" style="clear:both;background-color:#fff;padding:10px;">';
+		$fields_displayed = 0;
+
+		foreach ($this->_available_sections as $section)
+		{
+			if ($this->_compile_{$section} !== FALSE)
+			{
+				$func = '_compile_'.$section;
+				$output .= $this->{$func}();
+				$fields_displayed++;
+			}
+		}
+
+		if ($fields_displayed === 0)
+		{
+			$output .= '<p style="border:1px solid #5a0099;padding:10px;margin:20px 0;background-color:#eee;">'
+				.$this->CI->lang->line('profiler_no_profiles').'</p>';
+		}
+
+		return $output.'</div>';
+	}
+
+}
diff --git a/README/README.php b/README/README.php
new file mode 100644
index 0000000..ed47ea6
--- /dev/null
+++ b/README/README.php
@@ -0,0 +1,1026 @@
+<?php
+
+$content='Talk about your understanding of closures
+
+Using closures designed primarily to private methods and variables. Closure advantage of global variables are avoided contamination, the disadvantage is the closure will be permanent memory, the memory usage increases, improper use is likely to cause a memory leak.
+
+Closures have three characteristics:
+
+1. Function 2. The internal function nested function can refer to external parameters and variables 3. The parameters and variables will not be garbage collection mechanism Recycling
+You talk about the shortcomings of Cookie
+
+cookie Although persistent client data to provide a convenient, sharing the burden of server storage, but there are still many limitations. Generates up to 20 under each specific domain: the first cookie
+
+1.IE6 or earlier up to 20 cookie 
+2.IE7 later version and last may have 50 cookie. 
+3.Firefox up to 50 cookie 
+4.chrome and Safari does not do hard limit
+IE and Opera will clean up the least recently used cookie , Firefox will randomly to clean cookie .
+
+cookie maximum of approximately 4096 bytes, for compatibility, generally can not exceed 4095 bytes.
+
+IE can provide a way to store persistent user data, called UserData , from IE5.0 has supported. Each data up to 128K, under each domain up to 1M. The persistent data in the cache, if not cleaned the cache, it will always exist.
+
+Advantages: high scalability and availability
+
+1. Good programming, control the size of the session cookie stored in the object. 
+2. encryption and secure transmission technology (SSL), to reduce the possibility of cookie to crack. 
+3. Only non-sensitive data is stored in a cookie, even if stolen will not have significant losses. 
+4. Control cookie lifetime, so it will not always be effective. Theft are likely to get an expired cookie.
+Disadvantages:
+
+1.`Cookie` limit the number and length. Each domain can have a maximum of 20 cookie, each cookie length can not exceed 4KB, otherwise it will be truncated. 
+
+2. The security issues. If the cookie is intercepted, that person can get all the session information. Even if encryption is also unhelpful, because Interceptor does not need to know the meaning of the cookie, cookie forwarding as long as he can achieve their goals. 
+
+3. Some state can not be stored in the client. For example, in order to prevent duplicate submission form, we need to save a counter on the server side. If we put this counter is stored in the client, then it will not achieve any effect.
+Browser local storage
+
+In later versions of the browser, JS provides sessionStorage and globalStorage . In HTML5 provides in localStorage to replace globalStorage .
+
+html5 The Web Storage includes two storage methods: sessionStorage and localStorage .
+
+sessionStorage data stored locally for a session (session) in which the data only in the same session of the page to access data, and when the end of the session also will be destroyed. So sessionStorage not a persistent local storage, just session-level storage.
+
+And localStorage for persistent local storage, unless the initiative to remove the data, or the data is never out of date.
+
+the difference between web storage and a cookie
+
+Web Storage concept and cookie similar, except that it is designed for higher-capacity storage. Cookie size is limited, and every time you request a new page when the Cookie will be sent in the past, so that virtually waste bandwidth, another cookie also need to specify the scope, not cross-domain calls.
+
+In addition, Web Storage has setItem, getItem, removeItem, clear methods, unlike the cookie front-end developers need their own package setCookie, getCookie .
+
+But the cookie is not indispensable: cookie 's role is to interact with the server as HTTP part of the specification exists, and Web Storage simply to the local "store" raw data
+
+Browser support in addition to IE7 and below are not supported, other standard browsers fully support (ie and FF need to run a web server in), it is worth mentioning that IE always good things, such as IE7, IE6 in the userData fact is javascript local storage solutions. By simple code package can be unified to all the browsers support Web storage .
+
+localStorage and sessionStorage have the same method of operation, for example setItem, getItem and removeItem etc.
+
+cookie and session distinction:
+
+1, cookie data is stored on the customer's browser, session data on the server. 
+ 2, cookie is not very safe, others can be analyzed at a local store and COOKIE COOKIE cheat 
+    should be used taking into account the security session. 
+ 3, session will be within a certain time saved on the server. When accessing the increase, it would be more occupy the performance of your server 
+     consider to reduce server performance, you should use COOKIE. 
+ 4, the data stored in a single cookie can not exceed 4K, many browsers are limited to a maximum of one site to save 20 cookie. 
+ 5, so personal recommendations: 
+    the login information and other important information is stored as SESSION 
+    additional information, if necessary, it can be placed in COOKIE
+CSS-related issues
+
+display: none and visibility: hidden difference?
+
+display: none to hide the corresponding elements in the document layout is no longer space allocated to it, it is the element of each side will be closed, 
+on when he never existed. 
+
+visibility: hidden hides the corresponding elements, but still retain the original layout of the space in the document.
+The difference in the link andimport CSS is?
+
+(1) link belong to HTML tags, and CSSimport is provided; 
+(2) the page is loaded, link will also be loaded, but referencesimport CSS will wait until the page is finished loading reloading; 
+(3) Import only to recognize in IE5 or more, while link is HTML tag, no compatibility issues; 
+style right (4) linkimport way of weight above the weight of heavy.
+Similarities and Differences between absolute and float attributes: position
+
+A: common: 
+inline element set `float` and` absolute` property allows elements from the document flow, and can set its width and height. 
+
+B: Different point: 
+float will still occupy the position, position overrides other elements in the document flow.
+Introduce the box-sizing property?
+
+box-sizing property is primarily used to control element analytical model box model. The default value is content-Box .
+
+content-Box : Let element to maintain the standard W3C box model. Width / height of the element / height is determined by the border + padding + content width, set width / height attribute refers to the content portion of the width / height
+
+border-Box : Let element to maintain the traditional box model IE (IE6 and IE6 ~ 7 the following versions of quirks mode). Set width / height attribute refers to the border + padding + content
+
+Under standard browser, according to W3C box model specification analysis, once the border or modify the elements within distance, it will affect the size of the box element, you have to recalculate the box the size of the elements, thus affecting the layout of the entire page.
+
+CSS selectors are there? Which attributes can be inherited? Priority algorithm calculated? Added CSS3 pseudo-classes are those?
+
+1.id selector (# myid) 
+2. Class selector (.myclassname) 
+3. Tag selectors (div, h1, P) 
+4. adjacent selector (h1 + P) 
+5. child selector (ul> li ) 
+6. descendant selectors (li a) 
+7. wildcard selector (*) 
+8. attribute selector (a [rel = "external"]) 
+9. pseudo-class selectors (a: hover, li: nth-child)
+Inheritable style: font-size font-family color, text-indent;
+
+Non Inherited Styles: border padding margin width height;
+
+Priority under the principle of proximity, with weights whichever the case recently defined style;
+
+Loading last loaded positioning styles to prevail;
+
+Priority:
+! Important> ID> class> tag   
+
+Important priority higher than inline, inline but higher than the id
+Added CSS3 pseudo-class examples:
+p: first-of-type select each <p> element belongs to the first element of its parent <p> element. 
+p: last-of-type select each <p> element belongs to its parent last <p> element. 
+p: only-of-type select each <p> element belongs to its parent element only <p> elements. 
+p: only-child selects each <p> element belongs to its parent element only child element. 
+p: nth-child (2) Select each <p> element belongs to the parent element of the second sub-element. 
+: Enabled: disabled control form control disabled. 
+: Checked radio button or checkbox is selected.
+position values, relative and absolute are positioned with respect to whom?
+
+absolute 
+        generate absolute element positioned relative to the recent level is not static positioned parent element to be positioned. 
+
+fixed (old IE does not support) 
+    to generate absolute element positioned relative to the browser window positioning. 
+
+relative 
+    generating element relative positioning, relative to its position in the general stream positioning. 
+
+static default. No positioning element occurs in the normal stream
+CSS3 What's new?
+
+CSS3 rounded corners (border-radius), shadow (box-shadow), 
+add special effects to the text (text-shadow,), a linear gradient (gradient), rotation (transform) 
+transform: Rotate (9deg) Scale (0.85,0.90) translate (0px, -30px) skew (-9deg, 0deg); // rotation, scaling, positioning, tilt 
+adds more CSS selectors more background rgba 
+unique introduced in CSS3 pseudo-elements :: selection. 
+Media queries , multi-column layout 
+border-image
+XML and JSON difference?
+
+(1) The data volume terms. 
+XML JSON relative terms, the volume of data is small, the speed of delivery faster. 
+(2) aspects of data exchange. 
+JSON and JavaScript interaction more convenient and easier to parse processing, better data interaction. 
+(3) The data described aspects. 
+JSON description of data than XML poor. 
+(4) The transmission speed. 
+JSON's speed is much faster than XML.
+BFC norms of understanding?
+
+      BFC, block-level formatting context, a BFC created a new box is an independent layout, style, child elements inside the box will not affect the outside elements. In the same BFC two adjoining block-level box in the vertical direction (the direction of a relationship and layout) of margin will collapse from happening. 
+    (A concept W3C CSS 2.1 specification, which determines how the elements in the layout of its contents, and its relationship with other elements and interactions.)
+Under explain CSS sprites, and how you want to use it in a page or site.
+
+CSS Sprites in fact, the number of pages in the background image into an image file, and then use the CSS "background-image", "background- repeat", "background-position" a combination of background positioning, background-position can be used Digital can accurately locate the position of the background image. This can reduce the cost of many pictures request because the request takes a relatively long; although requests can be complicated, but there are limits, are six general browser. For the future, there is no need to do so, and because of `http2`.
+html part
+
+Talk about your understanding of the semantic?
+
+1, removed or lost when allowing page style showing clear structure 
+2, is conducive to SEO: search engines and to establish good communication helps crawlers to crawl more useful information: reptiles rely on labels to determine the context and the individual right keyword weight; 
+3, easy to resolve other devices (such as a screen reader, blind readers, mobile devices) in meaningful ways to render web pages; 
+4, easy to team development and maintenance, more readable semantic, is under Important Trends step right of the page, follow the W3C standards team follow this standard, the difference can be reduced.
+Doctype role? Strict mode and mixed mode how to distinguish? They mean?
+
+(1), <! DOCTYPE> declaration in the document in the front, in the <html> tag before. Tells the browser to render the document in which mode. 
+
+(2) strict mode layout and JS mode of operation is based on the highest standards of the browser supports running. 
+
+(3), in promiscuous mode, page backward compatible with liberal display. Simulate the behavior of older browsers do not work to prevent the site. 
+
+(4), DOCTYPE does not exist or incorrectly formatted document will lead to promiscuous mode rendering.   
+Do you know how many document types Doctype?
+
+The label can be declared DTD three types, respectively strict version, interim version and an HTML-based documentation framework. 
+ HTML 4.01 specifies three document types: Strict, Transitional, and Frameset. 
+ XHTML 1.0 specifies three XML document types: Strict, Transitional, and Frameset. 
+Standards (standard) mode (that is, in strict rendering mode) complies with the latest standards for rendering Web pages, while Quirks 
+ (inclusive) mode (that is, loose rendering mode or compatibility mode) is used to render conventional web browser designed.
+HTML and XHTML-- What is the difference between the two
+
+Difference: 
+1. All tags must have a corresponding end tag 
+element and attribute names 2. All tags must be lowercase 
+3. All the XML tags must be properly nested 
+4. All attributes must be quoted. " "enclosed 
+5. Put all the <and & special symbols with the coded representation 
+6. assign a value to all of the properties 
+in the comment 7. Do not manipulation "-" 
+8. The image must have caption
+Common compatibility problems?
+
+* Png24 bit image appears on iE6 browser background, the solution is made ​​PNG8. It can refer to a script processing. 
+
+* The browser's default margin and padding. The solution is to add a global * {margin: 0; padding: 0;} to unify. 
+
+* IE6 bilateral Distance bug: After the block attribute labels float, there are rampant margin circumstances, in ie6 show larger margin than the settings. 
+
+* Double ie generated from the float (IE6 bilateral distance problem: In IE6, if the element is set to float, while setting the margin-left or margin-right, margin value will double.) 
+  #box {float: left; width: 10px; margin: 0 0 0 100px;} 
+
+ Under such circumstances IE will produce 20px distance, the solution is to add --_ display in the label style float control: inline; be converted to inline property. (_ This symbol identifies only ie6) 
+
+* gradual recognition of the way from the local population gradually excluded. 
+
+  First, the clever use of "\ 9" This marks the IE's browser separated out from all the circumstances. 
+  Then, again using the "+" to IE8 and IE7, IE6 separated, so that IE8 has been independently identified. 
+
+  CSS 
+      .bb { 
+       background-color: # f1ee18; / * All identification * / 
+      .background-color: # 00deff \ 9; / * IE6,7,8 identification * / 
+      + background-color: # a200ff; / * IE6, 7 Identification * / 
+      _background-color: # 1e0bd1; / * IE6 recognition * / 
+      } 
+
+* under IE, you can use the method to get the general properties for custom properties, 
+   you can also use getAttribute () Gets the custom attributes; 
+   under Firefox, only Use getAttribute () Gets custom attributes. 
+   Workaround: Uniform for custom properties through getAttribute (). 
+
+* Under IE, event object has x, y attributes, but no pageX, pageY property; 
+  under Firefox, event object has pageX, pageY property, but there is no x, y properties. 
+
+* Solution :( conditional comments) drawback is that in IE browser may add additional HTTP requests. 
+
+* Under Chrome Chinese interface will be smaller than default to force the 12px 12px text display, 
+  by the addition of CSS properties -webkit-text-size-adjust: none; resolved. 
+
+* After hover style is not accessible by hyperlink is clicked appears visited The Hyperlink style does not have hover and active the solution is to change the order of CSS properties: 
+LVHA: a: link {} a: visited {} a: hover {} a: Active {} 
+
+* weird modalities: Leakage write DTD statement, Firefox will still be in accordance with the standard model to resolve the page, but IE will trigger quirks mode. To avoid quirks mode gives us unnecessary trouble, it is best to develop the good habit of writing DTD declaration. You can now use [html5] (http://www.w3.org/TR/html5/single-page.html) recommended notation: `<DOCTYPE html>` 
+
+* top and bottom margin overlap problem 
+ie and ff are present, adjacent both margin-left and margin-right does not coincide div, but margin-top and margin-bottom but it will coincide occur. 
+Solution, to develop good coding habits, while using margin-top or while using margin-bottom. 
+* Ie6 png image format support is not good for (quote a script processing)
+Floating and explain how it works? Clear float tips
+
+Floating elements from the document flow, does not occupy space. Floats across the border to stay its borders contain or floating elements. 
+
+1. Clear float with an empty label. 
+   This method is to add an empty label definition css clear in all floating behind the label: both the disadvantages is to increase the meaningless labels. 
+2. Use the overflow. 
+   To a parent tag contains floating elements add css property overflow: auto; zoom: 1; zoom: 1 for compatibility with IE6. 
+3. Use after pseudo remove floating objects. 
+   This method applies only to non-IE browsers. Specific wording may refer to the following examples. Use should pay attention to the following points. First, the method must be set to clear the height of the floating element's pseudo-objects: 0, otherwise the element will be much higher than the actual number of pixels;
+Problems caused by floating elements and solutions?
+
+Problems caused by the floating elements: 
+
+(1) height of the parent element can not be softened, peer influence and elements of the parent element 
+(2) and non-floating elements floating element siblings (inline elements) will follow thereafter 
+(3) if not the first element of float, then that element is also required before the float, otherwise it will affect the structure of the page displayed
+Workaround: use CSS in Clear: Both ; attribute to clear floating elements 2,3 resolve the problem, the problem 1, add the following styles to parent element Add clearfix style:
+
+.clearfix: after {content:; display ".": Block; height: 0; Clear: Both; visibility: hidden;} 
+.clearfix {display: inline-Block;} / * for IE / Mac * /
+Clear float several ways:
+
+1, additional labeling, <div style = "clear: both;"> </ div> (disadvantage: But this approach would cause additional HTML tag structure looks simple enough) 
+2, use after pseudo-class 
+
+#parent: after { 
+    content:; "." 
+    height: 0; 
+    visibility: hidden; 
+    display: Block; 
+    Clear: Both; 
+    } 
+
+3, floating external element 
+4, is set to `hidden`` overflow` or auto
+IE 8 the following versions of the browser box model What is the difference
+
+The following box model IE8 browser's width and height defined elements does not include padding and border
+DOM manipulation - how to add, remove, move, copy, create, and find nodes.
+
+(1) Create a new node 
+
+      createDocumentFragment () // Create a DOM fragment 
+
+      createElement () // create a specific element 
+
+      createTextNode () // Create a text node 
+
+(2) to add, remove, replace, insert 
+
+      appendChild () 
+
+      removeChild ( ) 
+
+      replaceChild () 
+
+      insertBefore () // before the existing child node into a new child node 
+
+(3) Find 
+
+      getElementsByTagName () // by tag name 
+
+      getElementsByName () // by the value of the Name property of the element (IE fault tolerance Strong, will be an array, including name id equal value) 
+
+      getElementById () // by elemental Id, uniqueness
+What's new html5, removing those elements? How to deal with a new label HTML5 browser compatibility problems? How to distinguish between HTML and HTML5?
+
+* HTML5 now is not a subset of SGML, mainly to increase with respect to the image, location, storage, multi-tasking and other features. 
+
+* Drag release (Drag and drop) API 
+  semantic content better label (header, nav, footer, aside, article, section) 
+  audio, video, API (audio, video) 
+  canvas (Canvas) API 
+  geography (Geolocation) API 
+  Local localStorage offline storage for long term storage of data, the browser is closed after data is not lost; 
+  sessionStorage of data after the browser is closed automatically delete 
+
+  form controls, calendar, date, time, email, url, search   
+  WebWorker new technologies, websocket, Geolocation 
+
+* Remove The elements of 
+
+the element of pure performance: basefont, big, center, font, s, strike, tt, u; 
+
+adversely affect the availability of the elements of: frame, frameset, noframes; 
+
+support HTML5 new label: 
+
+* IE8 / IE7 / IE6 support through document.createElement method of producing labels, 
+  can use this feature to allow these browsers support HTML5 new label, 
+
+  after the browser to support the new label, you also need to add tags default style: 
+
+* Of course, the best way is the direct use of mature framework, use the most is html5shim framework 
+   <-! [if lt IE 9]> 
+   <Script> src = "http://html5shim.googlecode.com/svn/trunk/html5.js" </ Script> 
+   <[ENDIF]! -> 
+How to distinguish: DOCTYPE declaration \ new structural elements \ functional elements
+iframe advantages and disadvantages?
+
+: 1.` <iframe> `merit 
+
+    solve slow to load third-party content such as icons and advertisements of loading problems 
+    Security sandbox 
+    parallel load script 
+
+shortcomings 2.` <iframe> `are: 
+
+
+    * iframes clog Onload event of the main page; 
+
+    * Instant content is empty, it takes time to load 
+    * no semantics
+How to communicate within the browser between multiple tabs?
+
+Call localstorge, cookies and other local storage
+The difference between threads and processes
+
+A program has at least one process, a process has at least one thread. 
+divided scale smaller than the thread process, making multi-threaded program concurrency high. 
+In addition, the process has a separate memory unit in the implementation process, and multiple threads shared memory, thereby greatly enhancing the operational efficiency of the program. 
+Threads in the implementation process and the process is still a difference. Each has a separate thread running entry, exit sequence and procedure execution order. But the thread is not able to perform independent, it must exist according to the application, providing multiple threads of execution is controlled by the application. 
+From a logical point of view, the significance of a multi-threaded application, there are a plurality of the operative part can execute simultaneously. But the operating system is not seen as multiple threads multiple independent applications to achieve scheduling and management and resource allocation process. This is an important distinction between processes and threads.
+How do you document and optimize resource site?
+
+Expect solutions include: 
+ file merge 
+ file Minimize / file compression 
+ using CDN hosted 
+ cache usage (multiple domain names to provide caching) 
+ Other
+Please name three reduce page load time.
+
+1. Optimize image 
+ 2. Select the image format (GIF: color offered fewer colors used in some of the less demanding areas) 
+ 3. Optimize CSS (compression merge css, such as margin-top, margin-left ... ) 
+ 4. slash site (eg www.campr.com/ directory, will determine the "directory what file type or directory.) 
+ 5. indicate the height and width (if the browser does not find these two parameters, It needs to calculate the size of one side while downloading pictures, if the picture a lot, browsers need to constantly adjust the page. This will not only affect the speed, but also affect browsing experience. 
+When the browser know the height and width parameters, even if the picture is temporarily unable to display the page will make pictures of space, and then continue to load the contents back thus loading time faster browsing experience is better.) 
+
+6. To reduce http requests (merge files, merge pictures).
+Which tools you use to test the performance of the code?
+
+Profiler, JSPerf (http://jsperf.com/nexttick-vs-setzerotimeout-vs-settimeout), Dromaeo
+What is FOUC (free style content flicker)? How do you avoid FOUC?
+
+FOUC - Flash Of Unstyled Content document style flashing 
+ <style type = "text / css" media = "all"> @ import "../ fouc.css"; </ style> 
+import CSS file and references that is causing the problem the culprit. IE will first load the DOM entire HTML document, and then go to import external CSS files, so the page DOM has finished loading content into CSS import is completed there will be a time in the middle on the page is not the style, the length of this period of time with network speed, computer speed have a relationship. 
+ Workaround surprisingly simple, as long as between the <head> add a <link> or <script> element on it.
+null and undefined difference?
+
+null is a "no," the object representation when converted to a value of 0; undefined is a indicating "no," the original value when converted value NaN .
+
+When the variable declaration has not been initialized, the default variable is undefined . null object has not been used to indicate the presence, used to represent the function attempts to return an object that does not exist.
+
+undefined means "missing value", that is where it should have a value, but not yet defined. Typical usage is:
+
+(1) variable is declared, but no assignment, it means undefined. 
+
+(2) When you call the function, the parameters should be provided not provided, this parameter is equal to undefined. 
+
+(3) The object has no assignment of property, which is undefined. 
+
+(4) function does not return a value, the default returns undefined.
+null means "no object", ie where there should be no value. Typical usage is:
+
+(1) as a function of the parameters, it indicates that the function of the parameter is not an object. 
+
+(2) as the end point of the object's prototype chain.
+What specific new operator did it?
+
+   1. Create an empty object, and this variable refers to the object, but also inherited the prototype of the function. 
+   2, attributes and methods are added to this reference object. 
+   3, the newly created object referenced by this, and the last implicit return this. 
+
+var obj = {}; 
+obj .__ proto__ = Base.prototype; 
+Base.call (obj);
+js lazy loading options?
+
+defer and async, dynamically created DOM mode (create script, is inserted into the DOM, after loading is completed callBack), asynchronous load demand js
+How to solve cross-border issues?
+
+    jsonp, document.domain + iframe, window.name, window.postMessage proxy server settings on the page, 
+
+JSONP principle is dynamically inserted script tag
+For details, see: Detailed js cross-border issues
+
+and innerHTML of difference documen.write
+
+document.write only redraw the entire page 
+
+innerHTML can redraw part of a page
+.call () and .apply () and the difference between the role?
+
+Role: dynamically changing operating environment of a method of a class. See the difference: JavaScript study summary (iv) function function section
+
+What action will cause a memory leak?
+
+A memory leak means any object is no longer owned or persists after you need it. 
+Garbage collector periodically scans the object, and calculate the number of references to other objects of each object. If an object reference number is 0 (no references to other objects through the object), or only a reference to the object is circular, then the object's memory can be recovered. 
+
+The first parameter setTimeout function using strings instead of words, can cause a memory leak. 
+Closures, console log, cycling (at two object references to each other and each other to retain, it will generate a cycle)
+See: Detailed js variables, scopes and memory
+
+JavaScript variable declarations in scope and upgrade?
+
+See: Detailed JavaScript function mode
+
+How to determine the current script to run in the browser or node environment?
+
+Global objects by judging whether the window, if not for the window, the current script is not running in the browser
+Other questions?
+
+Have you ever had more difficult technical problem? How did you solve it?
+
+Lists and other browsers are not the same characteristics IE?
+
+What is graceful degradation and progressive enhancement?
+
+Graceful degradation: Web sites can work in all modern browsers, if you are using an older browser, the code checks to confirm whether they can work properly. Due to the unique IE box model layout issues, for different versions of the IE hack practiced graceful degradation, and increase candidate solutions for those who can not support the browser, so that the legacy browser to experience some form but not downgrade completely ineffective. 
+
+Progressive enhancement: from the basic functions are supported by all browsers start, gradually add that only a modern browser supported features, additional styles to the page on the basis of sound and features of the browser. When the browser support, they will be presented automatically and play a role.
+See: CSS study summarized (a)
+
+WEB application from the server to the client Data initiative to push those ways?
+
+Javascript data push
+
+Commet: HTTP long connection based server push technology
+
+Push program based WebSocket
+
+SSE (Server-Send Event): new ways to push data server
+Front-end interface for this post is how engineers understood? It is the prospect of what would happen?
+
+The front is the most close to the user's programmers are closer than the back-end database, product manager, operations, safety. 
+    1, to achieve interactive interface 
+    2, to enhance the user experience 
+    3, with Node.js, the front end can achieve some of the things the service side of 
+
+
+the front end is closest to the user's programmers, the ability of the front end is to make the product evolved from 90 points to 100 points, even better 
+
+ involved in the project, to complete the implementation of high-quality renderings fast, accurate to 1px; 
+
+ , UI design, product manager of communication with team members; 
+
+ good page structure, page reconstruction and user experience; 
+
+ processing hack, compatible, write beautiful code format; 
+
+ optimized for the server, embracing the latest front-end technology.
+What are your ways to optimize performance?
+
+( For more details, please see Yahoo 14 performance optimization principle ).
+
+  (1) reduce the number of requests http: CSS Sprites, JS, CSS source code compression, image size control is appropriate; page Gzip, CDN hosting, data caching, image server. 
+
+  (2) front-end template JS + data, reduce wasted bandwidth due HTML tags caused the front end with a variable holds AJAX request, before each operation a local variable, not a request to reduce the number of requests 
+
+  (3) instead of using innerHTML DOM operations, reducing the number of operations DOM and optimize javascript performance. 
+
+  (4) When the style you want to set a lot of time to set className instead of directly operating style. 
+
+  (5) less global variables, DOM node cache lookup results. Reducing the IO read operations. 
+
+  (6) avoid the use of CSS Expression (css expression), also known as Dynamic properties (dynamic properties). 
+
+  (7) Pictures preloaded style sheet on top, plus a timestamp at the bottom of the script.
+Details: http://segmentfault.com/blog/trigkit4/1190000000691919
+
+A page is loaded from the URL to the page display input is completed, the process what happened?
+
+    Divided into four steps: 
+    (1) when sending a URL request, regardless of whether the URL is the URL of each resource on the Web page URL, or Web page, the browser will open a thread to handle the request, while in remote DNS Start a DNS query on the server. This enables the browser to get requests the corresponding IP address. 
+    (2), the browser and the remote Web server through TCP three-way handshake negotiation to establish a TCP / IP connection. The handshake includes a synchronous packet, one sync - reply message and a reply message, these three packets transmitted between the browser and the server. The handshake first attempts by the client to establish communication, then the server response and to accept the request of the client, the last issue of the request has been accepted by the client packets. 
+    (3) Once the TCP / IP connection is established, the browser sends an HTTP GET request to a remote server over the connection. Remote server to find the resources and the use of HTTP response back to the resource, HTTP response status value of 200 represents a correct response. 
+    (4) In this case, Web servers provide resources and services, the client begins to download resources. 
+
+After the request returns, they entered the front-end module of our attention 
+simply, the browser will parse the HTML generated DOM Tree, secondly generated according to CSS CSS Rule Tree, and javascript and can manipulate DOM DOM API
+Details: What happened to the process from entering the URL in the browser receives?
+
+Usually how to manage your project?
+
+Advance team must determine a good global style (globe.css), coding mode (utf-8) and the like; 
+
+        coding habits must be consistent (for example, are based on the inherited type of writing, all written in a single line style); 
+
+        to prepare people dimension styles, each module They are promptly labeled (labeled key style local calls); 
+
+        annotate pages (page modules such as start and end); 
+
+        CSS with HTML parallel sub-folder storage, had named Unity (such as style.css); 
+
+        JS points in folders named JS function subject to the English translation. 
+
+        Images.png png8 format pictures with an integrated use of integrated and easy to use as possible future management
+Most popular talk recently something, right? Which sites frequented?
+
+Node.js, Mongodb, npm, MVVM, MEAN, three.js, React. 
+Website: w3cfuns, sf, hacknews, CSDN, Mu classes, blog garden, InfoQ, w3cplus etc.
+Several ways to create a javascript object
+
+1, factory mode 
+2, the constructor mode 
+3, the prototype mode 
+4, mixing the constructor and prototype mode 
+5, the dynamic prototype model 
+6, the parasitic constructor mode 
+7, sound constructor mode
+6 ways inherited javascript
+
+1, the prototype inheritance chain 
+2, to borrow the constructor inherited 
+3, a combination of inheritance (Prototype + loan structure) 
+4, prototype inherited, 
+5, Parasitic succession 
+6, parasitic combined inheritance
+Details: Detailed JavaScript inheritance
+
+ajax process
+
+(1) create XMLHttpRequest object, the object is to create an asynchronous call. 
+
+(2) create a new HTTP request, and specify the method of the HTTP request, URL and authentication information. 
+
+(3) Set the response function HTTP request status changes. 
+
+(4) sends an HTTP request. 
+
+(5) to obtain the asynchronous call returns the data. 
+
+(6) using JavaScript and DOM implementations partial refresh.
+Details: JavaScript study summary (vii) Ajax and Http Status Word
+
+Asynchronous loading and delayed loading
+
+1. asynchronous loading of programs: dynamically inserted script tag 
+2. js code to get through ajax, then performed by eval 
+adding defer the 3.script tag or async attribute 
+4. Create and insert the iframe, it asynchronous execution JS 
+5. delay loading : Some js code is not initialized when the page immediately need, and only later in certain circumstances require.
+Front-end security issues?
+
+sql injection principle
+
+It is through the SQL commands into Web form submit or enter the domain name or page request query string, and ultimately achieve deceive server to execute malicious SQL commands.
+
+In general the following points:
+
+1. Never trust user input, to check for user input, you can use regular expressions, or limit the length of the single and double "-" in conversion. 
+2. Never use dynamic assembly SQL, you can use parameterized SQL or stored procedures directly access data query. 
+3. Never use administrator privileges database connection, use limited individual permissions for each application database connection. 
+4. Do not put the confidential information stored in clear text, please encryption or hash out passwords and sensitive information.
+XSS principle and Prevention
+
+Xss (cross-site scripting) attack means that an attacker to insert malicious Web page in html tags or javascript code. For example: the attacker in the forums to put a seemingly secure link, the user clicks cheat, steal private information in the cookie; or attacker in the forum, adding a malicious form, when the user submits the form, yet the information transfer to the attacker's server, rather than trust the site that the user originally thought.
+
+XSS prevention methods
+
+1. Code in the right place and the user input variables need to be carefully check the length and "<", ">", ";", "'" and other characters do filter; followed before any of the contents must be written pages encode , Avoid accidentally html tag to get out. This is a good level, at least you can block more than half of XSS attacks. 
+2. Avoid direct cookie disclosure of user privacy, such as Email , password, and so on. 3. By cookie and systems ip binding to reduce cookie dangerous leak after. Thus the attacker obtained cookie no practical value, it is impossible brought replay. 
+4. Try to use GET rather than POST submission form
+
+XSS and CSRF What's the difference?
+
+XSS is to obtain information, other users do not need to know in advance the code page and the data packet. CSRF is to replace the user completes a specified action, you need to know the code page to other users and data packets.
+
+To complete a CSRF attack, the victim must be followed by the completion of two steps:
+
+　　1. Log trusted site A, and generates Cookie locally. 2. Under no circumstances logout A visiting dangerous sites B.
+
+CSRF defense
+
+1. The service side of the CSRF ways like a lot, but the general idea is the same, that is, an increase in the Clients page pseudo-random numbers. 2. Use the verification code
+
+ie chrome versions and how many resources can be downloaded in parallel
+
+IE6 two concurrent, six concurrent iE7 after the upgrade version is also after 6 
+
+Firefox, chrome is 6
+javascript how to achieve inside the inheritance, how to avoid the prototype chain above object sharing
+
+With mixed mode constructor and prototype chain to achieve inheritance, avoid sharing objects can refer to the classic extend () function, and many have a front frame of the package, it is to use an empty function as an intermediate variable
+grunt, YUI compressor and google clojure used for code compression usage.
+
+YUI Compressor is a tool compress JS and CSS files used, the use of Java development. 
+
+Usage: 
+
+// compress JS 
+Java -jar YUICompressor-2.4.2.jar --type JS --charset UTF-8 -v src.js> packed.js 
+// compress CSS 
+Java -jar YUICompressor-2.4.2.jar --type css --charset utf-8 -v src.css> packed.css
+For more information: you need to master the front-end code performance optimization tools
+
+Flash, Ajax respective advantages and disadvantages, in use how to choose?
+
+1, Flash ajax contrast 
+Flash for handling multimedia, vector graphics, to access the machine; on the CSS, inadequate handling text, not easy to be searched. 
+Ajax for CSS, text support is very good, support search; multimedia, vector graphics, inadequate machine access. 
+Common: with no server refresh deliver the message, the user is offline and online, manipulate DOM
+Please explain the JavaScript same origin policy.
+
+Concept: homologous strategy is client-side script (especially Javascript ) important safety metrics. It first from Netscape Navigator2.0 , its purpose is to prevent a document or script loaded from several different sources.
+
+Here's same-origin policy refers to: protocol name, the same port, same origin policy is a security protocol. Refers to a script read-only attribute window and documents from the same source.
+
+Why have homologous limit?
+
+We illustrate: For example, a hacker program, he used Iframe the real bank login page insert to his page, when you use a real user name, password, he can page through Javascript read your form the input in the content, so the user name, password easily at hand.
+
+What is a "use strict";? Advantages and disadvantages of using it are what?
+
+ECMAscript 5 added a second operating modes: "strict mode" (strict mode). As the name suggests, this model allows Javascript to run under more stringent conditions.
+
+The purpose of the "strict mode", mainly in the following:
+
+- Eliminate some unreasonable Javascript syntax, not rigorous place, reducing the number of bizarre behavior; 
+- eliminate some code running unsafe place to ensure the safety of code to run; 
+- improve compiler efficiency, increase the operating speed; 
+- for the future of new Javascript version pave the way.
+NOTE: After testing IE6,7,8,9 strict mode is not supported.
+
+Cons: Now site JS will compress files with the strict mode, while others do not. Then these documents could have been strict mode is merge after the string on to the middle of the file, not only does not indicate strict mode, but a waste of bytes after compression.
+
+The difference between GET and POST, and when to use POST?
+
+    GET: generally used for information access, use URL parameters to pass on information to send the numbers are limited, in the 2000 general character 
+    POST: Usually used to modify resources on the server, there is no restriction on the transmitted information. 
+
+    GET requires the use of Request.QueryString to get the value of a variable, while the POST method to get the value of a variable through Request.Form, 
+    ie Get through the address bar to traditional values, while Post by submitting the form to traditional values. 
+
+However, in the following cases, please use the POST request: 
+You can not use a cache file (a file or database on the update server) 
+to send large amounts of data (POST does not limit the amount of data) to the server 
+when a user sends an unknown character input, POST than GET more stable and more reliable
+Where there will be css obstruction, where there will be js blocked?
+
+js of blocking characteristic: all browsers to download JS time, will stop all other activities, such as downloading other resources, presentation content and so on. Until JS download, parse, after finished resumes parallel downloading other resources and render content. In order to improve the user experience, a new generation of browsers support concurrent download JS , but JS download still blocking other resources to download (such as Image, css files, etc.).
+
+Because the browser in order to prevent JS modify DOM tree, you need to rebuild the DOM tree of the case, so it will block other downloads and rendering.
+
+Embed JS will block all content presentation, and external JS will only clog the subsequent content display, 2 ways to download later resources will be blocked. That style does not block external loading external script, but will block the implementation of an external script.
+
+CSS how can clog loaded? CSS could have been downloaded in parallel, in what circumstances would appear blocked loaded (in the test observation, IE6 under CSS are blocking load)
+
+When CSS followed by embedding JS when the CSS will block behind resources situation. When to embed JS into CSS front, it does not appear the case of obstruction.
+
+Fundamental reason: because the browser will remain html in css and js order, the stylesheet must be loaded before the embedding of JS execution, analytical end. The embedding of JS can block behind resource loading, it will appear above the CSS case blocking the download.
+
+Embed JS should be placed on what position?
+
+   1, on the bottom, although still on the bottom will block all presented, but will not block resources. 
+
+   2, if the embedded JS on the head, please send embed CSS JS on the head. 
+
+   3. Use defer (only supports IE) 
+
+   4, do not call a function of time in the long run embedded JS, if must use, can be used to call `setTimeout`
+Javascript blocking load specific ways
+
+The script at the bottom. <Link> or on the head , and to ensure js before loading, to load the page properly displayed. <Script> tag on the </ body> ago.
+A set of scripts : Because each <script> tag download blocking page resolution process, so limiting the page <script> total can also improve performance. Suitable for inline scripts and external scripts.
+
+Non-blocking script : such as the page finishes loading, reloading js code. That is, in window.onload issued after the event to start the download code. (1) defer attributes: support IE4 and fierfox3.5 later browser (2) dynamic script elements: Document Object Model (DOM) allows you to use js dynamically create HTML almost the entire contents of the document. Code is as follows:
+
+
+
+<Script> 
+var Script = document.createElement ("Script"); 
+script.type = "text / JavaScript"; 
+script.src = "file.js"; 
+document.getElementsByTagName ("head") [0] .appendChild (Script ); 
+</ Script>
+This technique focuses on: Wherever start downloading, file downloading and running the amount will not block other page processing. Even in the head inside (except http link for downloading the file).
+
+Closure-related issues?
+
+For more information: Detailed js Closures
+
+js event handler problem?
+
+For details, see: JavaScript study summary (ix) event Comments
+
+eval is doing what?
+
+Its function is to parse the string corresponding to the JS code and run; 
+you should avoid using eval, unsafe, very time consuming properties (2 times, once parsed into js statement, once executed).
+JavaScript prototype, the prototype chain? What are the characteristics?
+
+* Prototype object is an ordinary object is an object that comes with implicit __proto__ property, prototypes may also have its own prototype, if the prototype of a prototype object is not null, then we call the prototype chain. 
+* Prototype chain used by some inheritance and shared properties of objects of the (limited) object chain.
+Event mechanism event, IE and Firefox What is the difference? How to stop the bubble?
+
+1. We operate in a web page (some operations corresponding to multiple events). For example: When we click on a button will generate an event. JavaScript can be detected behavior.  
+ 2. Event handling mechanism: IE is the event bubbling, firefox supports both event model, that is: type of event capture and bubbling type events. ; 
+ 3. ev.stopPropagation (); Note that the old method ie ev.cancelBubble = true;
+ajax What is? ajax interaction model? synchronous and asynchronous difference? How to solve cross-border issues?
+
+For details, see: JavaScript study summary (vii) Ajax and Http Status Word
+
+1. Adoption of the asynchronous model to enhance the user experience 
+
+  2. Optimize the transmission between the browser and the server, reducing unnecessary data back and forth, reducing the bandwidth 
+
+  portion 3. Ajax running on the client, undertaken originally borne by the server work, reducing the server load large amount of users under. 
+
+  What is the biggest characteristic 2. Ajax is. 
+
+  Ajax enables dynamic does not refresh (partial refresh) 
+  readyState property state can have five values: 0 = uninitialized 1 = Start 2 = send, 3 = reception, 4 = complete 
+
+ajax shortcomings 
+
+  1, ajax do not support the browser back button. 
+
+  2, AJAX security issues exposed the details of the server to interact with. 
+
+  3, support for the search engines is relatively weak. 
+
+  4, undermining the exception mechanism of the program. 
+
+  5, it is not easy to debug. 
+
+Cross-domain: Setting proxy page on jsonp, iframe, window.name, window.postMessage, server
+Depth cloning js object
+
+  function clone (Obj) {    
+        var buf;    
+        if (Obj instanceof Array) {    
+            buf = []; // create an empty array 
+            var i = Obj.length;    
+            while (I -) {    
+                buf [i] = clone (Obj [i]);    
+            }    
+            return buf;    
+        } else if (Obj instanceof Object) {    
+            buf = {}; // create an empty object 
+            for (var k in Obj) {// add a new property to the object 
+                buf [k] = clone (Obj [k]);    
+            }    
+            return buf;    
+        } else {    
+            return Obj;    
+        }    
+    }  
+The difference between AMD and CMD norms?
+
+For more information: Detailed JavaScript Modular Development
+
+Site reconstruction of understanding?
+
+Reconstruction Site: without changing the external behavior of the premise, to simplify the structure, add readability, while the front end of the site to maintain consistent behavior. That is in the UI does not change the situation, to optimize the site, in the expansion while maintaining a consistent UI. 
+
+Reconstruction of the traditional web sites is usually: 
+
+table (table) layout to DIV + CSS 
+front end to make the site compatible with modern browsers (for irregularities in the CSS, such as IE6 valid) 
+for the mobile platform optimized 
+for ships SEO optimized 
+site reconstruction deeper aspects that should be considered 
+
+to reduce the coupling between codes 
+allow the code to maintain flexibility 
+in strict accordance with specification code 
+extensible API designed 
+to replace the old framework, language (eg VB) 
+enhance the user experience 
+generally speaking for optimized for speed is also included in the reconstruction of the 
+
+compressed JS, CSS, image and other front-end resources (usually be resolved by the server) 
+performance optimization program (such as data read and write) 
+using CDN to accelerate resource loading 
+for JS DOM optimized 
+HTTP server File Cache
+How to obtain the UA?
+
+<Script> 
+    whatBrowser function () {   
+        document.Browser.Name.value = navigator.appName;   
+        document.Browser.Version.value = navigator.appVersion;   
+        document.Browser.Code.value = navigator.appCodeName;   
+        document.Browser.Agent. value = navigator.userAgent;   
+    }   
+</ Script>
+js array to weight
+
+The following are three ways to go heavy array:
+
+Array.prototype.unique1 = function () { 
+  var N = []; // a new temporary array 
+  for (var i = 0; i <this.length; i ++) // traverse the current array 
+  { 
+    // If the current array first i have saved into a temporary array, skip, 
+    // otherwise the current term push to the temporary array inside 
+    if (n.indexOf (this [i]) == -1) n.push (this [i]); 
+  } 
+  return N; 
+} 
+
+Array.prototype.unique2 = function () 
+{ 
+    var N = {}, r = []; // N is a hash table, r is a temporary array 
+    for (var i = 0; i <this.length; i ++) // traverse the current array 
+    { 
+        if (N [this [i]]) // If the hash table does not have the current item! 
+        { 
+            N [this [i]] = true; // stored in the hash table 
+            r.push (this [i]); // the current term push the current array to a temporary array inside 
+        } 
+    } 
+    return r; 
+} 
+
+Array.prototype.unique3 = function () 
+{ 
+    var N = [this [0]]; // results array 
+    for ( var i = 1; i <this.length; i ++) // iterate from the start of the second term 
+    { 
+        // if the current array of item i in the current array position is not the first occurrence of i, 
+        it represents the i // entries are repeated, ignored. Otherwise, the result is stored in the array 
+        if (this.indexOf (this [i]) == i) n.push (this [i]); 
+    } 
+    return N; 
+}
+HTTP status code
+
+After 100 Continue to continue, generally when sending post request, sent the http header the server will return this message, to confirm, specific parameter information after sending 
+200 OK normal return information 
+201 Created request was successful and the server created a new resource 
+202 Accepted The server has accepted the request, but not yet processed 
+301 Moved Permanently The requested page has been permanently moved to a new location. 
+302 Found temporary redirect. 
+303 See Other temporary redirection, and always use a GET request a new URI. 
+304 Not Modified since the last request, the requested page has not been modified. 
+
+Format 400 Bad Request Server can not understand the request, the client should not attempt to use the same content again initiated the request. 
+401 Unauthorized Unauthorized request. 
+403 Forbidden Forbidden. 
+404 Not Found can not find how to match resources with URI. 
+
+500 Internal Server Error The most common server-side error. 
+503 Service Unavailable server is temporarily unable to process the request (may be overloaded or maintenance).
+js operations to get and set cookie
+
+// Create the cookie 
+function setCookie (name, value, expires, path, domain, secure) { 
+    var cookieText = encodeURIComponent (name) + '=' + encodeURIComponent (value); 
+    if (expires instanceof Date) { 
+        cookieText + = '; expires = '+ expires; 
+    } 
+    if (path) { 
+        cookieText + = '; expires =' + expires; 
+    } 
+    if (domain) { 
+        cookieText + = '; domain =' + domain; 
+    } 
+    if (secure) { 
+        cookieText + = '; secure '; 
+    } 
+    document.cookie = cookieText; 
+} 
+
+// get the cookie 
+function getCookie (name) { 
+    var cookieName = encodeURIComponent (name) + '='; 
+    var cookieStart = document.cookie.indexOf (cookieName); 
+    var CookieValue = null; 
+    if (cookieStart> -1) { 
+        var cookieEnd = document.cookie.indexOf (';', cookieStart); 
+        if (cookieEnd == -1) { 
+            cookieEnd = document.cookie.length; 
+        } 
+        CookieValue = decodeURIComponent (document.cookie. substring (cookieStart + cookieName.length, cookieEnd)); 
+    } 
+    return CookieValue; 
+} 
+
+// delete the cookie 
+function unsetCookie (name) { 
+    document.cookie = name + "=; expires =" + new Date (0); 
+}
+Talk about the three-way handshake TCP transport policy
+
+In order to reach the target accurately at the data, TCP uses a three-way handshake protocol strategy. After using the TCP protocol to send out packets, TCP will not ignore the situation after transfer, it will be delivered to the other party to confirm the success. Handshake using TCP flags: SYN and ACK. 
+Data with the sender first sends a SYN packet to the other flag. After the receiver receives, SYN / ACK packet backhaul flags to show communicate with a confirmation message. Packet, representing the last, the sender return pass with ACK flag "handshake" End 
+If at some stage during the handshake inexplicably interrupted, TCP protocol will be in the same order to send the same packet again.
+Talk about your understanding of the Promise
+
+In accordance with Promise / A + defined, Promise has four states:
+
+pending: the initial state, the non-fulfilled or rejected. 
+Fulfilled: the success of the operation. 
+rejected: the failed operation. 
+settled: Promise has been fulfilled or rejected, and not pending
+In addition, Fulfilled and rejected together collectively settled .
+
+Promise objects used for delayed (deferred) and asynchronous (asynchronous) calculations.
+
+Promise of constructor
+Construct a Promise , the most basic usage is as follows:
+
+var = new Promise Promise (function (resolve, reject) { 
+    if (...) {// Succeed 
+        resolve (result); 
+    } else {// fails 
+        reject (Error (errMessage)); 
+    } 
+});
+Promise instance has then method (with a then object method, commonly referred to thenable ). Its use is as follows:
+
+promise.then (onFulfilled, onRejected)
+Receives two function as an argument, one fulfilled is called when one rejected when they were called, received parameter is the future, onFulfilled correspond resolve , onRejected correspond to reject .
+
+Javascript garbage collection method
+
+Clear mark (mark and sweep)
+
+This is the JavaScript garbage collection the most common way, when the variable into the execution environment, such as a function to declare a variable, the garbage collector will be marked as "into the environment", when leaving the environment variables (ending function execution) to labeled "leave the environment."
+
+Garbage collector to run when all variables are stored in memory plus mark, and then remove the environment variables and variables (closures) is referenced by an environment variable, there are still marked after this is done is to delete variables
+
+Reference count (reference counting)
+
+In the low version IE memory leaks occur frequently, many times, because it uses reference counting for garbage collection. Policy reference count is the number used by the track record of each value, when you declare a variable number of citations and a reference type assigned to the variable when this value is increased by one, if the value of the variable into another is worth quoting this number by 1, when citations this value becomes zero, no variables described in use, this value can not be accessed, so its footprint can be recovered, so the garbage collector in Running time to clean out the reference number of the space occupied by a value of 0.
+
+In IE though JavaScript objects marked clearing the way for garbage collection, but the BOM and DOM objects is through reference counting garbage, that is as far as BOM and DOM circular reference problem occurs.
+
+Talk about performance optimization problem
+
+Code level: Avoid using css expression, avoid the use of advanced selectors, wildcard selector. Cache use: Cache Ajax, use CDN, use external js and css file to the cache, add the Expires header, the server configuration Etag, reducing the number of requests and other DNS lookup: merge styles and scripts, using css image sprites, beyond the initial first screen Image resource demand loading, lazy loading static resources. Request Bandwidth: compressed file, open GZIP,
+
+Mobile end performance optimization
+
+Try to use css3 animation, open hardware acceleration. Appropriate use of touch event instead of click event. Avoid using css3 gradient shading effects. Use as few box-shadow and Gradients . box-shadow and gradients are often performance killer page
+What is Etag?
+
+Browser download components when they will be stored in the browser cache. If you need to get the same components, the browser cache time again check the components, if already expired, the browser sends a conditional GET request to the server, the server determines the cache still valid, it sends a 304 response, telling the browser can be reused cache component.
+
+Then the server is based on what the judge cache is still valid? The answer is, there are two ways, one is ETag mentioned earlier, another is based on the Last-Modified
+
+Expires and Cache-Control
+
+Expires requires the client and server strict clock synchronization. HTTP1.1 introduction of Cache-Control to overcome the limitations of Expires header. If the max-age and Expires simultaneously, the max-age have a higher priority.
+
+Cache-Control: no-cache, Private, max-age = 0 
+ETag: abcde 
+Expires: Thu, 15 Apr 2014 20:00:00 GMT 
+Pragma: Private 
+Last-Modified: // now $ RFC1123 format
+The difference between stacks and queues?
+
+Stack insert and delete operations are carried out at one end, and queue operation is carried out at both ends. 
+FIFO queue, stack advanced out after. 
+Stack only allowed in the tail end of the insert and delete, and queue only allowed in the tail end of the insert, delete the header end
+The difference between stack and heap?
+
+Stack area (stack) - assigned automatically by the compiler release, storage function parameters, local variables and so on. 
+Heap (heap) - general distribution release by the programmer, if the programmer does not release, may be recovered by the OS when the program ends. 
+Heap (data structures): heap can be seen as a tree, such as: heap sort; 
+stack (data structures): An advanced after the data structure.
+How much do you know about Http 2.0?
+
+HTTP / 2 introduced a "server push (serverpush)" concept, which allows the server before the client needs the data to take the initiative to send data to the client cache, thereby improving performance. HTTP / 2 encryption to provide more support for HTTP / 2 using a multiplexing technology that allows multiple messages simultaneously on a cross connection. It adds header compression (header compression), so even very small requests, which request and response header will only occupy a small percentage of bandwidth.
+Status API Training Shop Blog About Pricing
+© 2015 GitHub , Inc. Terms Privacy Security Contact Help';
+?>
diff --git a/README/Session/Session.php b/README/Session/Session.php
new file mode 100644
index 0000000..05a470d
--- /dev/null
+++ b/README/Session/Session.php
@@ -0,0 +1,890 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 2.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Session Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author		Andrey Andreev
+ * @link		http://codeigniter.com/user_guide/libraries/sessions.html
+ */
+class CI_Session {
+
+	/**
+	 * Userdata array
+	 *
+	 * Just a reference to $_SESSION, for BC purposes.
+	 */
+	public $userdata;
+
+	protected $_driver = 'files';
+	protected $_config;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(array $params = array())
+	{
+		// No sessions under CLI
+		if (is_cli())
+		{
+			log_message('debug', 'Session: Initialization under CLI aborted.');
+			return;
+		}
+		elseif ((bool) ini_get('session.auto_start'))
+		{
+			log_message('error', 'Session: session.auto_start is enabled in php.ini. Aborting.');
+			return;
+		}
+		elseif ( ! empty($params['driver']))
+		{
+			$this->_driver = $params['driver'];
+			unset($params['driver']);
+		}
+		elseif ($driver = config_item('sess_driver'))
+		{
+			$this->_driver = $driver;
+		}
+		// Note: BC workaround
+		elseif (config_item('sess_use_database'))
+		{
+			$this->_driver = 'database';
+		}
+
+		$class = $this->_ci_load_classes($this->_driver);
+
+		// Configuration ...
+		$this->_configure($params);
+
+		$class = new $class($this->_config);
+		if ($class instanceof SessionHandlerInterface)
+		{
+			if (is_php('5.4'))
+			{
+				session_set_save_handler($class, TRUE);
+			}
+			else
+			{
+				session_set_save_handler(
+					array($class, 'open'),
+					array($class, 'close'),
+					array($class, 'read'),
+					array($class, 'write'),
+					array($class, 'destroy'),
+					array($class, 'gc')
+				);
+
+				register_shutdown_function('session_write_close');
+			}
+		}
+		else
+		{
+			log_message('error', "Session: Driver '".$this->_driver."' doesn't implement SessionHandlerInterface. Aborting.");
+			return;
+		}
+
+		// Sanitize the cookie, because apparently PHP doesn't do that for userspace handlers
+		if (isset($_COOKIE[$this->_config['cookie_name']])
+			&& (
+				! is_string($_COOKIE[$this->_config['cookie_name']])
+				OR ! preg_match('/^[0-9a-f]{40}$/', $_COOKIE[$this->_config['cookie_name']])
+			)
+		)
+		{
+			unset($_COOKIE[$this->_config['cookie_name']]);
+		}
+
+		session_start();
+
+		// Is session ID auto-regeneration configured? (ignoring ajax requests)
+		if ((empty($_SERVER['HTTP_X_REQUESTED_WITH']) OR strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest')
+			&& ($regenerate_time = config_item('sess_time_to_update')) > 0
+		)
+		{
+			if ( ! isset($_SESSION['__ci_last_regenerate']))
+			{
+				$_SESSION['__ci_last_regenerate'] = time();
+			}
+			elseif ($_SESSION['__ci_last_regenerate'] < (time() - $regenerate_time))
+			{
+				$this->sess_regenerate((bool) config_item('sess_regenerate_destroy'));
+			}
+		}
+		// Another work-around ... PHP doesn't seem to send the session cookie
+		// unless it is being currently created or regenerated
+		elseif (isset($_COOKIE[$this->_config['cookie_name']]) && $_COOKIE[$this->_config['cookie_name']] === session_id())
+		{
+			setcookie(
+				$this->_config['cookie_name'],
+				session_id(),
+				(empty($this->_config['cookie_lifetime']) ? 0 : time() + $this->_config['cookie_lifetime']),
+				$this->_config['cookie_path'],
+				$this->_config['cookie_domain'],
+				$this->_config['cookie_secure'],
+				TRUE
+			);
+		}
+
+		$this->_ci_init_vars();
+
+		log_message('info', "Session: Class initialized using '".$this->_driver."' driver.");
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * CI Load Classes
+	 *
+	 * An internal method to load all possible dependency and extension
+	 * classes. It kind of emulates the CI_Driver library, but is
+	 * self-sufficient.
+	 *
+	 * @param	string	$driver	Driver name
+	 * @return	string	Driver class name
+	 */
+	protected function _ci_load_classes($driver)
+	{
+		// PHP 5.4 compatibility
+		interface_exists('SessionHandlerInterface', FALSE) OR require_once(BASEPATH.'libraries/Session/SessionHandlerInterface.php');
+
+		$prefix = config_item('subclass_prefix');
+
+		if ( ! class_exists('CI_Session_driver', FALSE))
+		{
+			require_once(
+				file_exists(APPPATH.'libraries/Session/Session_driver.php')
+					? APPPATH.'libraries/Session/Session_driver.php'
+					: BASEPATH.'libraries/Session/Session_driver.php'
+			);
+
+			if (file_exists($file_path = APPPATH.'libraries/Session/'.$prefix.'Session_driver.php'))
+			{
+				require_once($file_path);
+			}
+		}
+
+		$class = 'Session_'.$driver.'_driver';
+
+		// Allow custom drivers without the CI_ or MY_ prefix
+		if ( ! class_exists($class, FALSE) && file_exists($file_path = APPPATH.'libraries/Session/drivers/'.$class.'.php'))
+		{
+			require_once($file_path);
+			if (class_exists($class, FALSE))
+			{
+				return $class;
+			}
+		}
+
+		if ( ! class_exists('CI_'.$class, FALSE))
+		{
+			if (file_exists($file_path = APPPATH.'libraries/Session/drivers/'.$class.'.php') OR file_exists($file_path = BASEPATH.'libraries/Session/drivers/'.$class.'.php'))
+			{
+				require_once($file_path);
+			}
+
+			if ( ! class_exists('CI_'.$class, FALSE) && ! class_exists($class, FALSE))
+			{
+				throw new UnexpectedValueException("Session: Configured driver '".$driver."' was not found. Aborting.");
+			}
+		}
+
+		if ( ! class_exists($prefix.$class) && file_exists($file_path = APPPATH.'libraries/Session/drivers/'.$prefix.$class.'.php'))
+		{
+			require_once($file_path);
+			if (class_exists($prefix.$class, FALSE))
+			{
+				return $prefix.$class;
+			}
+			else
+			{
+				log_message('debug', 'Session: '.$prefix.$class.".php found but it doesn't declare class ".$prefix.$class.'.');
+			}
+		}
+
+		return 'CI_'.$class;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Configuration
+	 *
+	 * Handle input parameters and configuration defaults
+	 *
+	 * @param	array	&$params	Input parameters
+	 * @return	void
+	 */
+	protected function _configure(&$params)
+	{
+		$expiration = config_item('sess_expiration');
+
+		if (isset($params['cookie_lifetime']))
+		{
+			$params['cookie_lifetime'] = (int) $params['cookie_lifetime'];
+		}
+		else
+		{
+			$params['cookie_lifetime'] = ( ! isset($expiration) && config_item('sess_expire_on_close'))
+				? 0 : (int) $expiration;
+		}
+
+		isset($params['cookie_name']) OR $params['cookie_name'] = config_item('sess_cookie_name');
+		if (empty($params['cookie_name']))
+		{
+			$params['cookie_name'] = ini_get('session.name');
+		}
+		else
+		{
+			ini_set('session.name', $params['cookie_name']);
+		}
+
+		isset($params['cookie_path']) OR $params['cookie_path'] = config_item('cookie_path');
+		isset($params['cookie_domain']) OR $params['cookie_domain'] = config_item('cookie_domain');
+		isset($params['cookie_secure']) OR $params['cookie_secure'] = (bool) config_item('cookie_secure');
+
+		session_set_cookie_params(
+			$params['cookie_lifetime'],
+			$params['cookie_path'],
+			$params['cookie_domain'],
+			$params['cookie_secure'],
+			TRUE // HttpOnly; Yes, this is intentional and not configurable for security reasons
+		);
+
+		if (empty($expiration))
+		{
+			$params['expiration'] = (int) ini_get('session.gc_maxlifetime');
+		}
+		else
+		{
+			$params['expiration'] = (int) $expiration;
+			ini_set('session.gc_maxlifetime', $expiration);
+		}
+
+		$params['match_ip'] = (bool) (isset($params['match_ip']) ? $params['match_ip'] : config_item('sess_match_ip'));
+
+		isset($params['save_path']) OR $params['save_path'] = config_item('sess_save_path');
+
+		$this->_config = $params;
+
+		// Security is king
+		ini_set('session.use_trans_sid', 0);
+		ini_set('session.use_strict_mode', 1);
+		ini_set('session.use_cookies', 1);
+		ini_set('session.use_only_cookies', 1);
+		ini_set('session.hash_function', 1);
+		ini_set('session.hash_bits_per_character', 4);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Handle temporary variables
+	 *
+	 * Clears old "flash" data, marks the new one for deletion and handles
+	 * "temp" data deletion.
+	 *
+	 * @return	void
+	 */
+	protected function _ci_init_vars()
+	{
+		if ( ! empty($_SESSION['__ci_vars']))
+		{
+			$current_time = time();
+
+			foreach ($_SESSION['__ci_vars'] as $key => &$value)
+			{
+				if ($value === 'new')
+				{
+					$_SESSION['__ci_vars'][$key] = 'old';
+				}
+				// Hacky, but 'old' will (implicitly) always be less than time() ;)
+				// DO NOT move this above the 'new' check!
+				elseif ($value < $current_time)
+				{
+					unset($_SESSION[$key], $_SESSION['__ci_vars'][$key]);
+				}
+			}
+
+			if (empty($_SESSION['__ci_vars']))
+			{
+				unset($_SESSION['__ci_vars']);
+			}
+		}
+
+		$this->userdata =& $_SESSION;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Mark as flash
+	 *
+	 * @param	mixed	$key	Session data key(s)
+	 * @return	bool
+	 */
+	public function mark_as_flash($key)
+	{
+		if (is_array($key))
+		{
+			for ($i = 0, $c = count($key); $i < $c; $i++)
+			{
+				if ( ! isset($_SESSION[$key[$i]]))
+				{
+					return FALSE;
+				}
+			}
+
+			$new = array_fill_keys($key, 'new');
+
+			$_SESSION['__ci_vars'] = isset($_SESSION['__ci_vars'])
+				? array_merge($_SESSION['__ci_vars'], $new)
+				: $new;
+
+			return TRUE;
+		}
+
+		if ( ! isset($_SESSION[$key]))
+		{
+			return FALSE;
+		}
+
+		$_SESSION['__ci_vars'][$key] = 'new';
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get flash keys
+	 *
+	 * @return	array
+	 */
+	public function get_flash_keys()
+	{
+		if ( ! isset($_SESSION['__ci_vars']))
+		{
+			return array();
+		}
+
+		$keys = array();
+		foreach (array_keys($_SESSION['__ci_vars']) as $key)
+		{
+			is_int($_SESSION['__ci_vars'][$key]) OR $keys[] = $key;
+		}
+
+		return $keys;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Unmark flash
+	 *
+	 * @param	mixed	$key	Session data key(s)
+	 * @return	void
+	 */
+	public function unmark_flash($key)
+	{
+		if (empty($_SESSION['__ci_vars']))
+		{
+			return;
+		}
+
+		is_array($key) OR $key = array($key);
+
+		foreach ($key as $k)
+		{
+			if (isset($_SESSION['__ci_vars'][$k]) && ! is_int($_SESSION['__ci_vars'][$k]))
+			{
+				unset($_SESSION['__ci_vars'][$k]);
+			}
+		}
+
+		if (empty($_SESSION['__ci_vars']))
+		{
+			unset($_SESSION['__ci_vars']);
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Mark as temp
+	 *
+	 * @param	mixed	$key	Session data key(s)
+	 * @param	int	$ttl	Time-to-live in seconds
+	 * @return	bool
+	 */
+	public function mark_as_temp($key, $ttl = 300)
+	{
+		$ttl += time();
+
+		if (is_array($key))
+		{
+			$temp = array();
+
+			foreach ($key as $k => $v)
+			{
+				// Do we have a key => ttl pair, or just a key?
+				if (is_int($k))
+				{
+					$k = $v;
+					$v = $ttl;
+				}
+				else
+				{
+					$v += time();
+				}
+
+				if ( ! isset($_SESSION[$k]))
+				{
+					return FALSE;
+				}
+
+				$temp[$k] = $v;
+			}
+
+			$_SESSION['__ci_vars'] = isset($_SESSION['__ci_vars'])
+				? array_merge($_SESSION['__ci_vars'], $temp)
+				: $temp;
+
+			return TRUE;
+		}
+
+		if ( ! isset($_SESSION[$key]))
+		{
+			return FALSE;
+		}
+
+		$_SESSION['__ci_vars'][$key] = $ttl;
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get temp keys
+	 *
+	 * @return	array
+	 */
+	public function get_temp_keys()
+	{
+		if ( ! isset($_SESSION['__ci_vars']))
+		{
+			return array();
+		}
+
+		$keys = array();
+		foreach (array_keys($_SESSION['__ci_vars']) as $key)
+		{
+			is_int($_SESSION['__ci_vars'][$key]) && $keys[] = $key;
+		}
+
+		return $keys;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Unmark flash
+	 *
+	 * @param	mixed	$key	Session data key(s)
+	 * @return	void
+	 */
+	public function unmark_temp($key)
+	{
+		if (empty($_SESSION['__ci_vars']))
+		{
+			return;
+		}
+
+		is_array($key) OR $key = array($key);
+
+		foreach ($key as $k)
+		{
+			if (isset($_SESSION['__ci_vars'][$k]) && is_int($_SESSION['__ci_vars'][$k]))
+			{
+				unset($_SESSION['__ci_vars'][$k]);
+			}
+		}
+
+		if (empty($_SESSION['__ci_vars']))
+		{
+			unset($_SESSION['__ci_vars']);
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * __get()
+	 *
+	 * @param	string	$key	'session_id' or a session data key
+	 * @return	mixed
+	 */
+	public function __get($key)
+	{
+		// Note: Keep this order the same, just in case somebody wants to
+		//       use 'session_id' as a session data key, for whatever reason
+		if (isset($_SESSION[$key]))
+		{
+			return $_SESSION[$key];
+		}
+		elseif ($key === 'session_id')
+		{
+			return session_id();
+		}
+
+		return NULL;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * __set()
+	 *
+	 * @param	string	$key	Session data key
+	 * @param	mixed	$value	Session data value
+	 * @return	void
+	 */
+	public function __set($key, $value)
+	{
+		$_SESSION[$key] = $value;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Session destroy
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @return	void
+	 */
+	public function sess_destroy()
+	{
+		session_destroy();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Session regenerate
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	bool	$destroy	Destroy old session data flag
+	 * @return	void
+	 */
+	public function sess_regenerate($destroy = FALSE)
+	{
+		$_SESSION['__ci_last_regenerate'] = time();
+		session_regenerate_id($destroy);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get userdata reference
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @returns	array
+	 */
+	public function &get_userdata()
+	{
+		return $_SESSION;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Userdata (fetch)
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	string	$key	Session data key
+	 * @return	mixed	Session data value or NULL if not found
+	 */
+	public function userdata($key = NULL)
+	{
+		if (isset($key))
+		{
+			return isset($_SESSION[$key]) ? $_SESSION[$key] : NULL;
+		}
+		elseif (empty($_SESSION))
+		{
+			return array();
+		}
+
+		$userdata = array();
+		$_exclude = array_merge(
+			array('__ci_vars'),
+			$this->get_flash_keys(),
+			$this->get_temp_keys()
+		);
+
+		foreach (array_keys($_SESSION) as $key)
+		{
+			if ( ! in_array($key, $_exclude, TRUE))
+			{
+				$userdata[$key] = $_SESSION[$key];
+			}
+		}
+
+		return $userdata;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Set userdata
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	mixed	$data	Session data key or an associative array
+	 * @param	mixed	$value	Value to store
+	 * @return	void
+	 */
+	public function set_userdata($data, $value = NULL)
+	{
+		if (is_array($data))
+		{
+			foreach ($data as $key => &$value)
+			{
+				$_SESSION[$key] = $value;
+			}
+
+			return;
+		}
+
+		$_SESSION[$data] = $value;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Unset userdata
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	mixed	$data	Session data key(s)
+	 * @return	void
+	 */
+	public function unset_userdata($key)
+	{
+		if (is_array($key))
+		{
+			foreach ($key as $k)
+			{
+				unset($_SESSION[$k]);
+			}
+
+			return;
+		}
+
+		unset($_SESSION[$key]);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * All userdata (fetch)
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @return	array	$_SESSION, excluding flash data items
+	 */
+	public function all_userdata()
+	{
+		return $this->userdata();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Has userdata
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	string	$key	Session data key
+	 * @return	bool
+	 */
+	public function has_userdata($key)
+	{
+		return isset($_SESSION[$key]);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Flashdata (fetch)
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	string	$key	Session data key
+	 * @return	mixed	Session data value or NULL if not found
+	 */
+	public function flashdata($key = NULL)
+	{
+		if (isset($key))
+		{
+			return (isset($_SESSION['__ci_vars'], $_SESSION['__ci_vars'][$key], $_SESSION[$key]) && ! is_int($_SESSION['__ci_vars'][$key]))
+				? $_SESSION[$key]
+				: NULL;
+		}
+
+		$flashdata = array();
+
+		if ( ! empty($_SESSION['__ci_vars']))
+		{
+			foreach ($_SESSION['__ci_vars'] as $key => &$value)
+			{
+				is_int($value) OR $flashdata[$key] = $_SESSION[$key];
+			}
+		}
+
+		return $flashdata;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Set flashdata
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	mixed	$data	Session data key or an associative array
+	 * @param	mixed	$value	Value to store
+	 * @return	void
+	 */
+	public function set_flashdata($data, $value = NULL)
+	{
+		$this->set_userdata($data, $value);
+		$this->mark_as_flash(is_array($data) ? array_keys($data) : $data);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Keep flashdata
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	mixed	$key	Session data key(s)
+	 * @return	void
+	 */
+	public function keep_flashdata($key)
+	{
+		$this->mark_as_flash($key);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Temp data (fetch)
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	string	$key	Session data key
+	 * @return	mixed	Session data value or NULL if not found
+	 */
+	public function tempdata($key = NULL)
+	{
+		if (isset($key))
+		{
+			return (isset($_SESSION['__ci_vars'], $_SESSION['__ci_vars'][$key], $_SESSION[$key]) && is_int($_SESSION['__ci_vars'][$key]))
+				? $_SESSION[$key]
+				: NULL;
+		}
+
+		$tempdata = array();
+
+		if ( ! empty($_SESSION['__ci_vars']))
+		{
+			foreach ($_SESSION['__ci_vars'] as $key => &$value)
+			{
+				is_int($value) && $tempdata[$key] = $_SESSION[$key];
+			}
+		}
+
+		return $tempdata;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Set tempdata
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	mixed	$data	Session data key or an associative array of items
+	 * @param	mixed	$value	Value to store
+	 * @param	int	$ttl	Time-to-live in seconds
+	 * @return	void
+	 */
+	public function set_tempdata($data, $value = NULL, $ttl = 300)
+	{
+		$this->set_userdata($data, $value);
+		$this->mark_as_temp(is_array($data) ? array_keys($data) : $data, $ttl);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Unset tempdata
+	 *
+	 * Legacy CI_Session compatibility method
+	 *
+	 * @param	mixed	$data	Session data key(s)
+	 * @return	void
+	 */
+	public function unset_tempdata($key)
+	{
+		$this->unmark_temp($key);
+	}
+
+}
diff --git a/README/Session/SessionHandlerInterface.php b/README/Session/SessionHandlerInterface.php
new file mode 100644
index 0000000..9dab5ac
--- /dev/null
+++ b/README/Session/SessionHandlerInterface.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SessionHandlerInterface
+ *
+ * PHP 5.4 compatibility interface
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	http://codeigniter.com/user_guide/libraries/sessions.html
+ */
+interface SessionHandlerInterface {
+
+	public function open($save_path, $name);
+	public function close();
+	public function read($session_id);
+	public function write($session_id, $session_data);
+	public function destroy($session_id);
+	public function gc($maxlifetime);
+}
diff --git a/README/Session/Session_driver.php b/README/Session/Session_driver.php
new file mode 100644
index 0000000..47376da
--- /dev/null
+++ b/README/Session/Session_driver.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Session Driver Class
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	http://codeigniter.com/user_guide/libraries/sessions.html
+ */
+abstract class CI_Session_driver implements SessionHandlerInterface {
+
+	protected $_config;
+
+	/**
+	 * Data fingerprint
+	 *
+	 * @var	bool
+	 */
+	protected $_fingerprint;
+
+	/**
+	 * Lock placeholder
+	 *
+	 * @var	mixed
+	 */
+	protected $_lock = FALSE;
+
+	/**
+	 * Read session ID
+	 *
+	 * Used to detect session_regenerate_id() calls because PHP only calls
+	 * write() after regenerating the ID.
+	 *
+	 * @var	string
+	 */
+	protected $_session_id;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(&$params)
+	{
+		$this->_config =& $params;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cookie destroy
+	 *
+	 * Internal method to force removal of a cookie by the client
+	 * when session_destroy() is called.
+	 *
+	 * @return	bool
+	 */
+	protected function _cookie_destroy()
+	{
+		return setcookie(
+			$this->_config['cookie_name'],
+			NULL,
+			1,
+			$this->_config['cookie_path'],
+			$this->_config['cookie_domain'],
+			$this->_config['cookie_secure'],
+			TRUE
+		);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get lock
+	 *
+	 * A dummy method allowing drivers with no locking functionality
+	 * (databases other than PostgreSQL and MySQL) to act as if they
+	 * do acquire a lock.
+	 *
+	 * @param	string	$session_id
+	 * @return	bool
+	 */
+	protected function _get_lock($session_id)
+	{
+		$this->_lock = TRUE;
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Release lock
+	 *
+	 * @return	bool
+	 */
+	protected function _release_lock()
+	{
+		if ($this->_lock)
+		{
+			$this->_lock = FALSE;
+		}
+
+		return TRUE;
+	}
+
+}
diff --git a/README/Session/drivers/Session_database_driver.php b/README/Session/drivers/Session_database_driver.php
new file mode 100644
index 0000000..1d01c29
--- /dev/null
+++ b/README/Session/drivers/Session_database_driver.php
@@ -0,0 +1,389 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Session Database Driver
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	http://codeigniter.com/user_guide/libraries/sessions.html
+ */
+class CI_Session_database_driver extends CI_Session_driver implements SessionHandlerInterface {
+
+	/**
+	 * DB object
+	 *
+	 * @var	object
+	 */
+	protected $_db;
+
+	/**
+	 * Row exists flag
+	 *
+	 * @var	bool
+	 */
+	protected $_row_exists = FALSE;
+
+	/**
+	 * Lock "driver" flag
+	 *
+	 * @var	string
+	 */
+	protected $_platform;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(&$params)
+	{
+		parent::__construct($params);
+
+		$CI =& get_instance();
+		isset($CI->db) OR $CI->load->database();
+		$this->_db = $CI->db;
+
+		if ( ! $this->_db instanceof CI_DB_query_builder)
+		{
+			throw new Exception('Query Builder not enabled for the configured database. Aborting.');
+		}
+		elseif ($this->_db->pconnect)
+		{
+			throw new Exception('Configured database connection is persistent. Aborting.');
+		}
+		elseif ($this->_db->cache_on)
+		{
+			throw new Exception('Configured database connection has cache enabled. Aborting.');
+		}
+
+		$db_driver = $this->_db->dbdriver.(empty($this->_db->subdriver) ? '' : '_'.$this->_db->subdriver);
+		if (strpos($db_driver, 'mysql') !== FALSE)
+		{
+			$this->_platform = 'mysql';
+		}
+		elseif (in_array($db_driver, array('postgre', 'pdo_pgsql'), TRUE))
+		{
+			$this->_platform = 'postgre';
+		}
+
+		// Note: BC work-around for the old 'sess_table_name' setting, should be removed in the future.
+		isset($this->_config['save_path']) OR $this->_config['save_path'] = config_item('sess_table_name');
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Open
+	 *
+	 * Initializes the database connection
+	 *
+	 * @param	string	$save_path	Table name
+	 * @param	string	$name		Session cookie name, unused
+	 * @return	bool
+	 */
+	public function open($save_path, $name)
+	{
+		return empty($this->_db->conn_id)
+			? (bool) $this->_db->db_connect()
+			: TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Read
+	 *
+	 * Reads session data and acquires a lock
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	string	Serialized session data
+	 */
+	public function read($session_id)
+	{
+		if ($this->_get_lock($session_id) !== FALSE)
+		{
+			// Needed by write() to detect session_regenerate_id() calls
+			$this->_session_id = $session_id;
+
+			$this->_db
+				->select('data')
+				->from($this->_config['save_path'])
+				->where('id', $session_id);
+
+			if ($this->_config['match_ip'])
+			{
+				$this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
+			}
+
+			if (($result = $this->_db->get()->row()) === NULL)
+			{
+				$this->_fingerprint = md5('');
+				return '';
+			}
+
+			// PostgreSQL's variant of a BLOB datatype is Bytea, which is a
+			// PITA to work with, so we use base64-encoded data in a TEXT
+			// field instead.
+			$result = ($this->_platform === 'postgre')
+				? base64_decode(rtrim($result->data))
+				: $result->data;
+
+			$this->_fingerprint = md5($result);
+			$this->_row_exists = TRUE;
+			return $result;
+		}
+
+		$this->_fingerprint = md5('');
+		return '';
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Write
+	 *
+	 * Writes (create / update) session data
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @param	string	$session_data	Serialized session data
+	 * @return	bool
+	 */
+	public function write($session_id, $session_data)
+	{
+		// Was the ID regenerated?
+		if ($session_id !== $this->_session_id)
+		{
+			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
+			{
+				return FALSE;
+			}
+
+			$this->_row_exists = FALSE;
+			$this->_session_id = $session_id;
+		}
+		elseif ($this->_lock === FALSE)
+		{
+			return FALSE;
+		}
+
+		if ($this->_row_exists === FALSE)
+		{
+			$insert_data = array(
+				'id' => $session_id,
+				'ip_address' => $_SERVER['REMOTE_ADDR'],
+				'timestamp' => time(),
+				'data' => ($this->_platform === 'postgre' ? base64_encode($session_data) : $session_data)
+			);
+
+			if ($this->_db->insert($this->_config['save_path'], $insert_data))
+			{
+				$this->_fingerprint = md5($session_data);
+				return $this->_row_exists = TRUE;
+			}
+
+			return FALSE;
+		}
+
+		$this->_db->where('id', $session_id);
+		if ($this->_config['match_ip'])
+		{
+			$this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
+		}
+
+		$update_data = array('timestamp' => time());
+		if ($this->_fingerprint !== md5($session_data))
+		{
+			$update_data['data'] = ($this->_platform === 'postgre')
+				? base64_encode($session_data)
+				: $session_data;
+		}
+
+		if ($this->_db->update($this->_config['save_path'], $update_data))
+		{
+			$this->_fingerprint = md5($session_data);
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Close
+	 *
+	 * Releases locks
+	 *
+	 * @return	bool
+	 */
+	public function close()
+	{
+		return ($this->_lock)
+			? $this->_release_lock()
+			: TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Destroy
+	 *
+	 * Destroys the current session.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	public function destroy($session_id)
+	{
+		if ($this->_lock)
+		{
+			$this->_db->where('id', $session_id);
+			if ($this->_config['match_ip'])
+			{
+				$this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
+			}
+
+			return $this->_db->delete($this->_config['save_path'])
+				? ($this->close() && $this->_cookie_destroy())
+				: FALSE;
+		}
+
+		return ($this->close() && $this->_cookie_destroy());
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Garbage Collector
+	 *
+	 * Deletes expired sessions
+	 *
+	 * @param	int 	$maxlifetime	Maximum lifetime of sessions
+	 * @return	bool
+	 */
+	public function gc($maxlifetime)
+	{
+		return $this->_db->delete($this->_config['save_path'], 'timestamp < '.(time() - $maxlifetime));
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get lock
+	 *
+	 * Acquires a lock, depending on the underlying platform.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	protected function _get_lock($session_id)
+	{
+		if ($this->_platform === 'mysql')
+		{
+			$arg = $session_id.($this->_config['match_ip'] ? '_'.$_SERVER['REMOTE_ADDR'] : '');
+			if ($this->_db->query("SELECT GET_LOCK('".$arg."', 300) AS ci_session_lock")->row()->ci_session_lock)
+			{
+				$this->_lock = $arg;
+				return TRUE;
+			}
+
+			return FALSE;
+		}
+		elseif ($this->_platform === 'postgre')
+		{
+			$arg = "hashtext('".$session_id."')".($this->_config['match_ip'] ? ", hashtext('".$_SERVER['REMOTE_ADDR']."')" : '');
+			if ($this->_db->simple_query('SELECT pg_advisory_lock('.$arg.')'))
+			{
+				$this->_lock = $arg;
+				return TRUE;
+			}
+
+			return FALSE;
+		}
+
+		return parent::_get_lock($session_id);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Release lock
+	 *
+	 * Releases a previously acquired lock
+	 *
+	 * @return	bool
+	 */
+	protected function _release_lock()
+	{
+		if ( ! $this->_lock)
+		{
+			return TRUE;
+		}
+
+		if ($this->_platform === 'mysql')
+		{
+			if ($this->_db->query("SELECT RELEASE_LOCK('".$this->_lock."') AS ci_session_lock")->row()->ci_session_lock)
+			{
+				$this->_lock = FALSE;
+				return TRUE;
+			}
+
+			return FALSE;
+		}
+		elseif ($this->_platform === 'postgre')
+		{
+			if ($this->_db->simple_query('SELECT pg_advisory_unlock('.$this->_lock.')'))
+			{
+				$this->_lock = FALSE;
+				return TRUE;
+			}
+
+			return FALSE;
+		}
+
+		return parent::_release_lock();
+	}
+
+}
diff --git a/README/Session/drivers/Session_files_driver.php b/README/Session/drivers/Session_files_driver.php
new file mode 100644
index 0000000..45da91c
--- /dev/null
+++ b/README/Session/drivers/Session_files_driver.php
@@ -0,0 +1,362 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+*/
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Session Files Driver
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	http://codeigniter.com/user_guide/libraries/sessions.html
+ */
+class CI_Session_files_driver extends CI_Session_driver implements SessionHandlerInterface {
+
+	/**
+	 * Save path
+	 *
+	 * @var	string
+	 */
+	protected $_save_path;
+
+	/**
+	 * File handle
+	 *
+	 * @var	resource
+	 */
+	protected $_file_handle;
+
+	/**
+	 * File name
+	 *
+	 * @var	resource
+	 */
+	protected $_file_path;
+
+	/**
+	 * File new flag
+	 *
+	 * @var	bool
+	 */
+	protected $_file_new;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(&$params)
+	{
+		parent::__construct($params);
+
+		if (isset($this->_config['save_path']))
+		{
+			$this->_config['save_path'] = rtrim($this->_config['save_path'], '/\\');
+			ini_set('session.save_path', $this->_config['save_path']);
+		}
+		else
+		{
+			$this->_config['save_path'] = rtrim(ini_get('session.save_path'), '/\\');
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Open
+	 *
+	 * Sanitizes the save_path directory.
+	 *
+	 * @param	string	$save_path	Path to session files' directory
+	 * @param	string	$name		Session cookie name
+	 * @return	bool
+	 */
+	public function open($save_path, $name)
+	{
+		if ( ! is_dir($save_path))
+		{
+			if ( ! mkdir($save_path, 0700, TRUE))
+			{
+				throw new Exception("Session: Configured save path '".$this->_config['save_path']."' is not a directory, doesn't exist or cannot be created.");
+			}
+		}
+		elseif ( ! is_writable($save_path))
+		{
+			throw new Exception("Session: Configured save path '".$this->_config['save_path']."' is not writable by the PHP process.");
+		}
+
+		$this->_config['save_path'] = $save_path;
+		$this->_file_path = $this->_config['save_path'].DIRECTORY_SEPARATOR
+			.$name // we'll use the session cookie name as a prefix to avoid collisions
+			.($this->_config['match_ip'] ? md5($_SERVER['REMOTE_ADDR']) : '');
+
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Read
+	 *
+	 * Reads session data and acquires a lock
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	string	Serialized session data
+	 */
+	public function read($session_id)
+	{
+		// This might seem weird, but PHP 5.6 introduces session_reset(),
+		// which re-reads session data
+		if ($this->_file_handle === NULL)
+		{
+			// Just using fopen() with 'c+b' mode would be perfect, but it is only
+			// available since PHP 5.2.6 and we have to set permissions for new files,
+			// so we'd have to hack around this ...
+			if (($this->_file_new = ! file_exists($this->_file_path.$session_id)) === TRUE)
+			{
+				if (($this->_file_handle = fopen($this->_file_path.$session_id, 'w+b')) === FALSE)
+				{
+					log_message('error', "Session: File '".$this->_file_path.$session_id."' doesn't exist and cannot be created.");
+					return FALSE;
+				}
+			}
+			elseif (($this->_file_handle = fopen($this->_file_path.$session_id, 'r+b')) === FALSE)
+			{
+				log_message('error', "Session: Unable to open file '".$this->_file_path.$session_id."'.");
+				return FALSE;
+			}
+
+			if (flock($this->_file_handle, LOCK_EX) === FALSE)
+			{
+				log_message('error', "Session: Unable to obtain lock for file '".$this->_file_path.$session_id."'.");
+				fclose($this->_file_handle);
+				$this->_file_handle = NULL;
+				return FALSE;
+			}
+
+			// Needed by write() to detect session_regenerate_id() calls
+			$this->_session_id = $session_id;
+
+			if ($this->_file_new)
+			{
+				chmod($this->_file_path.$session_id, 0600);
+				$this->_fingerprint = md5('');
+				return '';
+			}
+		}
+		else
+		{
+			rewind($this->_file_handle);
+		}
+
+		$session_data = '';
+		for ($read = 0, $length = filesize($this->_file_path.$session_id); $read < $length; $read += strlen($buffer))
+		{
+			if (($buffer = fread($this->_file_handle, $length - $read)) === FALSE)
+			{
+				break;
+			}
+
+			$session_data .= $buffer;
+		}
+
+		$this->_fingerprint = md5($session_data);
+		return $session_data;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Write
+	 *
+	 * Writes (create / update) session data
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @param	string	$session_data	Serialized session data
+	 * @return	bool
+	 */
+	public function write($session_id, $session_data)
+	{
+		// If the two IDs don't match, we have a session_regenerate_id() call
+		// and we need to close the old handle and open a new one
+		if ($session_id !== $this->_session_id && ( ! $this->close() OR $this->read($session_id) === FALSE))
+		{
+			return FALSE;
+		}
+
+		if ( ! is_resource($this->_file_handle))
+		{
+			return FALSE;
+		}
+		elseif ($this->_fingerprint === md5($session_data))
+		{
+			return ($this->_file_new)
+				? TRUE
+				: touch($this->_file_path.$session_id);
+		}
+
+		if ( ! $this->_file_new)
+		{
+			ftruncate($this->_file_handle, 0);
+			rewind($this->_file_handle);
+		}
+
+		if (($length = strlen($session_data)) > 0)
+		{
+			for ($written = 0; $written < $length; $written += $result)
+			{
+				if (($result = fwrite($this->_file_handle, substr($session_data, $written))) === FALSE)
+				{
+					break;
+				}
+			}
+
+			if ( ! is_int($result))
+			{
+				$this->_fingerprint = md5(substr($session_data, 0, $written));
+				log_message('error', 'Session: Unable to write data.');
+				return FALSE;
+			}
+		}
+
+		$this->_fingerprint = md5($session_data);
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Close
+	 *
+	 * Releases locks and closes file descriptor.
+	 *
+	 * @return	bool
+	 */
+	public function close()
+	{
+		if (is_resource($this->_file_handle))
+		{
+			flock($this->_file_handle, LOCK_UN);
+			fclose($this->_file_handle);
+
+			$this->_file_handle = $this->_file_new = $this->_session_id = NULL;
+			return TRUE;
+		}
+
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Destroy
+	 *
+	 * Destroys the current session.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	public function destroy($session_id)
+	{
+		if ($this->close())
+		{
+			return file_exists($this->_file_path.$session_id)
+				? (unlink($this->_file_path.$session_id) && $this->_cookie_destroy())
+				: TRUE;
+		}
+		elseif ($this->_file_path !== NULL)
+		{
+			clearstatcache();
+			return file_exists($this->_file_path.$session_id)
+				? (unlink($this->_file_path.$session_id) && $this->_cookie_destroy())
+				: TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Garbage Collector
+	 *
+	 * Deletes expired sessions
+	 *
+	 * @param	int 	$maxlifetime	Maximum lifetime of sessions
+	 * @return	bool
+	 */
+	public function gc($maxlifetime)
+	{
+		if ( ! is_dir($this->_config['save_path']) OR ($directory = opendir($this->_config['save_path'])) === FALSE)
+		{
+			log_message('debug', "Session: Garbage collector couldn't list files under directory '".$this->_config['save_path']."'.");
+			return FALSE;
+		}
+
+		$ts = time() - $maxlifetime;
+
+		$pattern = sprintf(
+			'/^%s[0-9a-f]{%d}$/',
+			preg_quote($this->_config['cookie_name'], '/'),
+			($this->_config['match_ip'] === TRUE ? 72 : 40)
+		);
+
+		while (($file = readdir($directory)) !== FALSE)
+		{
+			// If the filename doesn't match this pattern, it's either not a session file or is not ours
+			if ( ! preg_match($pattern, $file)
+				OR ! is_file($this->_config['save_path'].DIRECTORY_SEPARATOR.$file)
+				OR ($mtime = filemtime($this->_config['save_path'].DIRECTORY_SEPARATOR.$file)) === FALSE
+				OR $mtime > $ts)
+			{
+				continue;
+			}
+
+			unlink($this->_config['save_path'].DIRECTORY_SEPARATOR.$file);
+		}
+
+		closedir($directory);
+
+		return TRUE;
+	}
+
+}
diff --git a/README/Session/drivers/Session_memcached_driver.php b/README/Session/drivers/Session_memcached_driver.php
new file mode 100644
index 0000000..97b8605
--- /dev/null
+++ b/README/Session/drivers/Session_memcached_driver.php
@@ -0,0 +1,363 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Session Memcached Driver
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	http://codeigniter.com/user_guide/libraries/sessions.html
+ */
+class CI_Session_memcached_driver extends CI_Session_driver implements SessionHandlerInterface {
+
+	/**
+	 * Memcached instance
+	 *
+	 * @var	Memcached
+	 */
+	protected $_memcached;
+
+	/**
+	 * Key prefix
+	 *
+	 * @var	string
+	 */
+	protected $_key_prefix = 'ci_session:';
+
+	/**
+	 * Lock key
+	 *
+	 * @var	string
+	 */
+	protected $_lock_key;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(&$params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->_config['save_path']))
+		{
+			log_message('error', 'Session: No Memcached save path configured.');
+		}
+
+		if ($this->_config['match_ip'] === TRUE)
+		{
+			$this->_key_prefix .= $_SERVER['REMOTE_ADDR'].':';
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Open
+	 *
+	 * Sanitizes save_path and initializes connections.
+	 *
+	 * @param	string	$save_path	Server path(s)
+	 * @param	string	$name		Session cookie name, unused
+	 * @return	bool
+	 */
+	public function open($save_path, $name)
+	{
+		$this->_memcached = new Memcached();
+		$this->_memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, TRUE); // required for touch() usage
+		$server_list = array();
+		foreach ($this->_memcached->getServerList() as $server)
+		{
+			$server_list[] = $server['host'].':'.$server['port'];
+		}
+
+		if ( ! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->_config['save_path'], $matches, PREG_SET_ORDER))
+		{
+			$this->_memcached = NULL;
+			log_message('error', 'Session: Invalid Memcached save path format: '.$this->_config['save_path']);
+			return FALSE;
+		}
+
+		foreach ($matches as $match)
+		{
+			// If Memcached already has this server (or if the port is invalid), skip it
+			if (in_array($match[1].':'.$match[2], $server_list, TRUE))
+			{
+				log_message('debug', 'Session: Memcached server pool already has '.$match[1].':'.$match[2]);
+				continue;
+			}
+
+			if ( ! $this->_memcached->addServer($match[1], $match[2], isset($match[3]) ? $match[3] : 0))
+			{
+				log_message('error', 'Could not add '.$match[1].':'.$match[2].' to Memcached server pool.');
+			}
+			else
+			{
+				$server_list[] = $match[1].':'.$match[2];
+			}
+		}
+
+		if (empty($server_list))
+		{
+			log_message('error', 'Session: Memcached server pool is empty.');
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Read
+	 *
+	 * Reads session data and acquires a lock
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	string	Serialized session data
+	 */
+	public function read($session_id)
+	{
+		if (isset($this->_memcached) && $this->_get_lock($session_id))
+		{
+			// Needed by write() to detect session_regenerate_id() calls
+			$this->_session_id = $session_id;
+
+			$session_data = (string) $this->_memcached->get($this->_key_prefix.$session_id);
+			$this->_fingerprint = md5($session_data);
+			return $session_data;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Write
+	 *
+	 * Writes (create / update) session data
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @param	string	$session_data	Serialized session data
+	 * @return	bool
+	 */
+	public function write($session_id, $session_data)
+	{
+		if ( ! isset($this->_memcached))
+		{
+			return FALSE;
+		}
+		// Was the ID regenerated?
+		elseif ($session_id !== $this->_session_id)
+		{
+			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
+			{
+				return FALSE;
+			}
+
+			$this->_fingerprint = md5('');
+			$this->_session_id = $session_id;
+		}
+
+		if (isset($this->_lock_key))
+		{
+			$this->_memcached->replace($this->_lock_key, time(), 300);
+			if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
+			{
+				if ($this->_memcached->set($this->_key_prefix.$session_id, $session_data, $this->_config['expiration']))
+				{
+					$this->_fingerprint = $fingerprint;
+					return TRUE;
+				}
+
+				return FALSE;
+			}
+
+			return $this->_memcached->touch($this->_key_prefix.$session_id, $this->_config['expiration']);
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Close
+	 *
+	 * Releases locks and closes connection.
+	 *
+	 * @return	bool
+	 */
+	public function close()
+	{
+		if (isset($this->_memcached))
+		{
+			isset($this->_lock_key) && $this->_memcached->delete($this->_lock_key);
+			if ( ! $this->_memcached->quit())
+			{
+				return FALSE;
+			}
+
+			$this->_memcached = NULL;
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Destroy
+	 *
+	 * Destroys the current session.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	public function destroy($session_id)
+	{
+		if (isset($this->_memcached, $this->_lock_key))
+		{
+			$this->_memcached->delete($this->_key_prefix.$session_id);
+			return $this->_cookie_destroy();
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Garbage Collector
+	 *
+	 * Deletes expired sessions
+	 *
+	 * @param	int 	$maxlifetime	Maximum lifetime of sessions
+	 * @return	bool
+	 */
+	public function gc($maxlifetime)
+	{
+		// Not necessary, Memcached takes care of that.
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get lock
+	 *
+	 * Acquires an (emulated) lock.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	protected function _get_lock($session_id)
+	{
+		if (isset($this->_lock_key))
+		{
+			return $this->_memcached->replace($this->_lock_key, time(), 300);
+		}
+
+		// 30 attempts to obtain a lock, in case another request already has it
+		$lock_key = $this->_key_prefix.$session_id.':lock';
+		$attempt = 0;
+		do
+		{
+			if ($this->_memcached->get($lock_key))
+			{
+				sleep(1);
+				continue;
+			}
+
+			if ( ! $this->_memcached->set($lock_key, time(), 300))
+			{
+				log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
+				return FALSE;
+			}
+
+			$this->_lock_key = $lock_key;
+			break;
+		}
+		while (++$attempt < 30);
+
+		if ($attempt === 30)
+		{
+			log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 30 attempts, aborting.');
+			return FALSE;
+		}
+
+		$this->_lock = TRUE;
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Release lock
+	 *
+	 * Releases a previously acquired lock
+	 *
+	 * @return	bool
+	 */
+	protected function _release_lock()
+	{
+		if (isset($this->_memcached, $this->_lock_key) && $this->_lock)
+		{
+			if ( ! $this->_memcached->delete($this->_lock_key) && $this->_memcached->getResultCode() !== Memcached::RES_NOTFOUND)
+			{
+				log_message('error', 'Session: Error while trying to free lock for '.$this->_lock_key);
+				return FALSE;
+			}
+
+			$this->_lock_key = NULL;
+			$this->_lock = FALSE;
+		}
+
+		return TRUE;
+	}
+
+}
diff --git a/README/Session/drivers/Session_redis_driver.php b/README/Session/drivers/Session_redis_driver.php
new file mode 100644
index 0000000..b098cc4
--- /dev/null
+++ b/README/Session/drivers/Session_redis_driver.php
@@ -0,0 +1,381 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Session Redis Driver
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	http://codeigniter.com/user_guide/libraries/sessions.html
+ */
+class CI_Session_redis_driver extends CI_Session_driver implements SessionHandlerInterface {
+
+	/**
+	 * phpRedis instance
+	 *
+	 * @var	resource
+	 */
+	protected $_redis;
+
+	/**
+	 * Key prefix
+	 *
+	 * @var	string
+	 */
+	protected $_key_prefix = 'ci_session:';
+
+	/**
+	 * Lock key
+	 *
+	 * @var	string
+	 */
+	protected $_lock_key;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(&$params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->_config['save_path']))
+		{
+			log_message('error', 'Session: No Redis save path configured.');
+		}
+		elseif (preg_match('#(?:tcp://)?([^:?]+)(?:\:(\d+))?(\?.+)?#', $this->_config['save_path'], $matches))
+		{
+			isset($matches[3]) OR $matches[3] = ''; // Just to avoid undefined index notices below
+			$this->_config['save_path'] = array(
+				'host' => $matches[1],
+				'port' => empty($matches[2]) ? NULL : $matches[2],
+				'password' => preg_match('#auth=([^\s&]+)#', $matches[3], $match) ? $match[1] : NULL,
+				'database' => preg_match('#database=(\d+)#', $matches[3], $match) ? (int) $match[1] : NULL,
+				'timeout' => preg_match('#timeout=(\d+\.\d+)#', $matches[3], $match) ? (float) $match[1] : NULL
+			);
+
+			preg_match('#prefix=([^\s&]+)#', $matches[3], $match) && $this->_key_prefix = $match[1];
+		}
+		else
+		{
+			log_message('error', 'Session: Invalid Redis save path format: '.$this->_config['save_path']);
+		}
+
+		if ($this->_config['match_ip'] === TRUE)
+		{
+			$this->_key_prefix .= $_SERVER['REMOTE_ADDR'].':';
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Open
+	 *
+	 * Sanitizes save_path and initializes connection.
+	 *
+	 * @param	string	$save_path	Server path
+	 * @param	string	$name		Session cookie name, unused
+	 * @return	bool
+	 */
+	public function open($save_path, $name)
+	{
+		if (empty($this->_config['save_path']))
+		{
+			return FALSE;
+		}
+
+		$redis = new Redis();
+		if ( ! $redis->connect($this->_config['save_path']['host'], $this->_config['save_path']['port'], $this->_config['save_path']['timeout']))
+		{
+			log_message('error', 'Session: Unable to connect to Redis with the configured settings.');
+		}
+		elseif (isset($this->_config['save_path']['password']) && ! $redis->auth($this->_config['save_path']['password']))
+		{
+			log_message('error', 'Session: Unable to authenticate to Redis instance.');
+		}
+		elseif (isset($this->_config['save_path']['database']) && ! $redis->select($this->_config['save_path']['database']))
+		{
+			log_message('error', 'Session: Unable to select Redis database with index '.$this->_config['save_path']['database']);
+		}
+		else
+		{
+			$this->_redis = $redis;
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Read
+	 *
+	 * Reads session data and acquires a lock
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	string	Serialized session data
+	 */
+	public function read($session_id)
+	{
+		if (isset($this->_redis) && $this->_get_lock($session_id))
+		{
+			// Needed by write() to detect session_regenerate_id() calls
+			$this->_session_id = $session_id;
+
+			$session_data = (string) $this->_redis->get($this->_key_prefix.$session_id);
+			$this->_fingerprint = md5($session_data);
+			return $session_data;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Write
+	 *
+	 * Writes (create / update) session data
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @param	string	$session_data	Serialized session data
+	 * @return	bool
+	 */
+	public function write($session_id, $session_data)
+	{
+		if ( ! isset($this->_redis))
+		{
+			return FALSE;
+		}
+		// Was the ID regenerated?
+		elseif ($session_id !== $this->_session_id)
+		{
+			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
+			{
+				return FALSE;
+			}
+
+			$this->_fingerprint = md5('');
+			$this->_session_id = $session_id;
+		}
+
+		if (isset($this->_lock_key))
+		{
+			$this->_redis->setTimeout($this->_lock_key, 300);
+			if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
+			{
+				if ($this->_redis->set($this->_key_prefix.$session_id, $session_data, $this->_config['expiration']))
+				{
+					$this->_fingerprint = $fingerprint;
+					return TRUE;
+				}
+
+				return FALSE;
+			}
+
+			return $this->_redis->setTimeout($this->_key_prefix.$session_id, $this->_config['expiration']);
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Close
+	 *
+	 * Releases locks and closes connection.
+	 *
+	 * @return	bool
+	 */
+	public function close()
+	{
+		if (isset($this->_redis))
+		{
+			try {
+				if ($this->_redis->ping() === '+PONG')
+				{
+					isset($this->_lock_key) && $this->_redis->delete($this->_lock_key);
+					if ( ! $this->_redis->close())
+					{
+						return FALSE;
+					}
+				}
+			}
+			catch (RedisException $e)
+			{
+				log_message('error', 'Session: Got RedisException on close(): '.$e->getMessage());
+			}
+
+			$this->_redis = NULL;
+			return TRUE;
+		}
+
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Destroy
+	 *
+	 * Destroys the current session.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	public function destroy($session_id)
+	{
+		if (isset($this->_redis, $this->_lock_key))
+		{
+			if (($result = $this->_redis->delete($this->_key_prefix.$session_id)) !== 1)
+			{
+				log_message('debug', 'Session: Redis::delete() expected to return 1, got '.var_export($result, TRUE).' instead.');
+			}
+
+			return $this->_cookie_destroy();
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Garbage Collector
+	 *
+	 * Deletes expired sessions
+	 *
+	 * @param	int 	$maxlifetime	Maximum lifetime of sessions
+	 * @return	bool
+	 */
+	public function gc($maxlifetime)
+	{
+		// Not necessary, Redis takes care of that.
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get lock
+	 *
+	 * Acquires an (emulated) lock.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	protected function _get_lock($session_id)
+	{
+		if (isset($this->_lock_key))
+		{
+			return $this->_redis->setTimeout($this->_lock_key, 300);
+		}
+
+		// 30 attempts to obtain a lock, in case another request already has it
+		$lock_key = $this->_key_prefix.$session_id.':lock';
+		$attempt = 0;
+		do
+		{
+			if (($ttl = $this->_redis->ttl($lock_key)) > 0)
+			{
+				sleep(1);
+				continue;
+			}
+
+			if ( ! $this->_redis->setex($lock_key, 300, time()))
+			{
+				log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
+				return FALSE;
+			}
+
+			$this->_lock_key = $lock_key;
+			break;
+		}
+		while (++$attempt < 30);
+
+		if ($attempt === 30)
+		{
+			log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 30 attempts, aborting.');
+			return FALSE;
+		}
+		elseif ($ttl === -1)
+		{
+			log_message('debug', 'Session: Lock for '.$this->_key_prefix.$session_id.' had no TTL, overriding.');
+		}
+
+		$this->_lock = TRUE;
+		return TRUE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Release lock
+	 *
+	 * Releases a previously acquired lock
+	 *
+	 * @return	bool
+	 */
+	protected function _release_lock()
+	{
+		if (isset($this->_redis, $this->_lock_key) && $this->_lock)
+		{
+			if ( ! $this->_redis->delete($this->_lock_key))
+			{
+				log_message('error', 'Session: Error while trying to free lock for '.$this->_lock_key);
+				return FALSE;
+			}
+
+			$this->_lock_key = NULL;
+			$this->_lock = FALSE;
+		}
+
+		return TRUE;
+	}
+
+}
diff --git a/README/Session/drivers/index.html b/README/Session/drivers/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/README/Session/drivers/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>403 Forbidden</title>
+</head>
+<body>
+
+<p>Directory access is forbidden.</p>
+
+</body>
+</html>
diff --git a/README/Session/index.html b/README/Session/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/README/Session/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>403 Forbidden</title>
+</head>
+<body>
+
+<p>Directory access is forbidden.</p>
+
+</body>
+</html>
diff --git a/README/Table.php b/README/Table.php
new file mode 100644
index 0000000..2d98230
--- /dev/null
+++ b/README/Table.php
@@ -0,0 +1,537 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.3.1
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * HTML Table Generating Class
+ *
+ * Lets you create tables manually or from database result objects, or arrays.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	HTML Tables
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/table.html
+ */
+class CI_Table {
+
+	/**
+	 * Data for table rows
+	 *
+	 * @var array
+	 */
+	public $rows		= array();
+
+	/**
+	 * Data for table heading
+	 *
+	 * @var array
+	 */
+	public $heading		= array();
+
+	/**
+	 * Whether or not to automatically create the table header
+	 *
+	 * @var bool
+	 */
+	public $auto_heading	= TRUE;
+
+	/**
+	 * Table caption
+	 *
+	 * @var string
+	 */
+	public $caption		= NULL;
+
+	/**
+	 * Table layout template
+	 *
+	 * @var array
+	 */
+	public $template	= NULL;
+
+	/**
+	 * Newline setting
+	 *
+	 * @var string
+	 */
+	public $newline		= "\n";
+
+	/**
+	 * Contents of empty cells
+	 *
+	 * @var string
+	 */
+	public $empty_cells	= '';
+
+	/**
+	 * Callback for custom table layout
+	 *
+	 * @var function
+	 */
+	public $function	= NULL;
+
+	/**
+	 * Set the template from the table config file if it exists
+	 *
+	 * @param	array	$config	(default: array())
+	 * @return	void
+	 */
+	public function __construct($config = array())
+	{
+		// initialize config
+		foreach ($config as $key => $val)
+		{
+			$this->template[$key] = $val;
+		}
+
+		log_message('info', 'Table Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the template
+	 *
+	 * @param	array	$template
+	 * @return	bool
+	 */
+	public function set_template($template)
+	{
+		if ( ! is_array($template))
+		{
+			return FALSE;
+		}
+
+		$this->template = $template;
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the table heading
+	 *
+	 * Can be passed as an array or discreet params
+	 *
+	 * @param	mixed
+	 * @return	CI_Table
+	 */
+	public function set_heading($args = array())
+	{
+		$this->heading = $this->_prep_args(func_get_args());
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set columns. Takes a one-dimensional array as input and creates
+	 * a multi-dimensional array with a depth equal to the number of
+	 * columns. This allows a single array with many elements to be
+	 * displayed in a table that has a fixed column count.
+	 *
+	 * @param	array	$array
+	 * @param	int	$col_limit
+	 * @return	array
+	 */
+	public function make_columns($array = array(), $col_limit = 0)
+	{
+		if ( ! is_array($array) OR count($array) === 0 OR ! is_int($col_limit))
+		{
+			return FALSE;
+		}
+
+		// Turn off the auto-heading feature since it's doubtful we
+		// will want headings from a one-dimensional array
+		$this->auto_heading = FALSE;
+
+		if ($col_limit === 0)
+		{
+			return $array;
+		}
+
+		$new = array();
+		do
+		{
+			$temp = array_splice($array, 0, $col_limit);
+
+			if (count($temp) < $col_limit)
+			{
+				for ($i = count($temp); $i < $col_limit; $i++)
+				{
+					$temp[] = '&nbsp;';
+				}
+			}
+
+			$new[] = $temp;
+		}
+		while (count($array) > 0);
+
+		return $new;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set "empty" cells
+	 *
+	 * Can be passed as an array or discreet params
+	 *
+	 * @param	mixed	$value
+	 * @return	CI_Table
+	 */
+	public function set_empty($value)
+	{
+		$this->empty_cells = $value;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add a table row
+	 *
+	 * Can be passed as an array or discreet params
+	 *
+	 * @param	mixed
+	 * @return	CI_Table
+	 */
+	public function add_row($args = array())
+	{
+		$this->rows[] = $this->_prep_args(func_get_args());
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep Args
+	 *
+	 * Ensures a standard associative array format for all cell data
+	 *
+	 * @param	array
+	 * @return	array
+	 */
+	protected function _prep_args($args)
+	{
+		// If there is no $args[0], skip this and treat as an associative array
+		// This can happen if there is only a single key, for example this is passed to table->generate
+		// array(array('foo'=>'bar'))
+		if (isset($args[0]) && count($args) === 1 && is_array($args[0]) && ! isset($args[0]['data']))
+		{
+			$args = $args[0];
+		}
+
+		foreach ($args as $key => $val)
+		{
+			is_array($val) OR $args[$key] = array('data' => $val);
+		}
+
+		return $args;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add a table caption
+	 *
+	 * @param	string	$caption
+	 * @return	CI_Table
+	 */
+	public function set_caption($caption)
+	{
+		$this->caption = $caption;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate the table
+	 *
+	 * @param	mixed	$table_data
+	 * @return	string
+	 */
+	public function generate($table_data = NULL)
+	{
+		// The table data can optionally be passed to this function
+		// either as a database result object or an array
+		if ( ! empty($table_data))
+		{
+			if ($table_data instanceof CI_DB_result)
+			{
+				$this->_set_from_db_result($table_data);
+			}
+			elseif (is_array($table_data))
+			{
+				$this->_set_from_array($table_data);
+			}
+		}
+
+		// Is there anything to display? No? Smite them!
+		if (empty($this->heading) && empty($this->rows))
+		{
+			return 'Undefined table data';
+		}
+
+		// Compile and validate the template date
+		$this->_compile_template();
+
+		// Validate a possibly existing custom cell manipulation function
+		if (isset($this->function) && ! is_callable($this->function))
+		{
+			$this->function = NULL;
+		}
+
+		// Build the table!
+
+		$out = $this->template['table_open'].$this->newline;
+
+		// Add any caption here
+		if ($this->caption)
+		{
+			$out .= '<caption>'.$this->caption.'</caption>'.$this->newline;
+		}
+
+		// Is there a table heading to display?
+		if ( ! empty($this->heading))
+		{
+			$out .= $this->template['thead_open'].$this->newline.$this->template['heading_row_start'].$this->newline;
+
+			foreach ($this->heading as $heading)
+			{
+				$temp = $this->template['heading_cell_start'];
+
+				foreach ($heading as $key => $val)
+				{
+					if ($key !== 'data')
+					{
+						$temp = str_replace('<th', '<th '.$key.'="'.$val.'"', $temp);
+					}
+				}
+
+				$out .= $temp.(isset($heading['data']) ? $heading['data'] : '').$this->template['heading_cell_end'];
+			}
+
+			$out .= $this->template['heading_row_end'].$this->newline.$this->template['thead_close'].$this->newline;
+		}
+
+		// Build the table rows
+		if ( ! empty($this->rows))
+		{
+			$out .= $this->template['tbody_open'].$this->newline;
+
+			$i = 1;
+			foreach ($this->rows as $row)
+			{
+				if ( ! is_array($row))
+				{
+					break;
+				}
+
+				// We use modulus to alternate the row colors
+				$name = fmod($i++, 2) ? '' : 'alt_';
+
+				$out .= $this->template['row_'.$name.'start'].$this->newline;
+
+				foreach ($row as $cell)
+				{
+					$temp = $this->template['cell_'.$name.'start'];
+
+					foreach ($cell as $key => $val)
+					{
+						if ($key !== 'data')
+						{
+							$temp = str_replace('<td', '<td '.$key.'="'.$val.'"', $temp);
+						}
+					}
+
+					$cell = isset($cell['data']) ? $cell['data'] : '';
+					$out .= $temp;
+
+					if ($cell === '' OR $cell === NULL)
+					{
+						$out .= $this->empty_cells;
+					}
+					elseif (isset($this->function))
+					{
+						$out .= call_user_func($this->function, $cell);
+					}
+					else
+					{
+						$out .= $cell;
+					}
+
+					$out .= $this->template['cell_'.$name.'end'];
+				}
+
+				$out .= $this->template['row_'.$name.'end'].$this->newline;
+			}
+
+			$out .= $this->template['tbody_close'].$this->newline;
+		}
+
+		$out .= $this->template['table_close'];
+
+		// Clear table class properties before generating the table
+		$this->clear();
+
+		return $out;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Clears the table arrays.  Useful if multiple tables are being generated
+	 *
+	 * @return	CI_Table
+	 */
+	public function clear()
+	{
+		$this->rows = array();
+		$this->heading = array();
+		$this->auto_heading = TRUE;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set table data from a database result object
+	 *
+	 * @param	CI_DB_result	$db_result	Database result object
+	 * @return	void
+	 */
+	protected function _set_from_db_result($object)
+	{
+		// First generate the headings from the table column names
+		if ($this->auto_heading === TRUE && empty($this->heading))
+		{
+			$this->heading = $this->_prep_args($object->list_fields());
+		}
+
+		foreach ($object->result_array() as $row)
+		{
+			$this->rows[] = $this->_prep_args($row);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set table data from an array
+	 *
+	 * @param	array	$data
+	 * @return	void
+	 */
+	protected function _set_from_array($data)
+	{
+		if ($this->auto_heading === TRUE && empty($this->heading))
+		{
+			$this->heading = $this->_prep_args(array_shift($data));
+		}
+
+		foreach ($data as &$row)
+		{
+			$this->rows[] = $this->_prep_args($row);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile Template
+	 *
+	 * @return	void
+	 */
+	protected function _compile_template()
+	{
+		if ($this->template === NULL)
+		{
+			$this->template = $this->_default_template();
+			return;
+		}
+
+		$this->temp = $this->_default_template();
+		foreach (array('table_open', 'thead_open', 'thead_close', 'heading_row_start', 'heading_row_end', 'heading_cell_start', 'heading_cell_end', 'tbody_open', 'tbody_close', 'row_start', 'row_end', 'cell_start', 'cell_end', 'row_alt_start', 'row_alt_end', 'cell_alt_start', 'cell_alt_end', 'table_close') as $val)
+		{
+			if ( ! isset($this->template[$val]))
+			{
+				$this->template[$val] = $this->temp[$val];
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Default Template
+	 *
+	 * @return	array
+	 */
+	protected function _default_template()
+	{
+		return array(
+			'table_open'		=> '<table border="0" cellpadding="4" cellspacing="0">',
+
+			'thead_open'		=> '<thead>',
+			'thead_close'		=> '</thead>',
+
+			'heading_row_start'	=> '<tr>',
+			'heading_row_end'	=> '</tr>',
+			'heading_cell_start'	=> '<th>',
+			'heading_cell_end'	=> '</th>',
+
+			'tbody_open'		=> '<tbody>',
+			'tbody_close'		=> '</tbody>',
+
+			'row_start'		=> '<tr>',
+			'row_end'		=> '</tr>',
+			'cell_start'		=> '<td>',
+			'cell_end'		=> '</td>',
+
+			'row_alt_start'		=> '<tr>',
+			'row_alt_end'		=> '</tr>',
+			'cell_alt_start'	=> '<td>',
+			'cell_alt_end'		=> '</td>',
+
+			'table_close'		=> '</table>'
+		);
+	}
+
+}
diff --git a/README/Trackback.php b/README/Trackback.php
new file mode 100644
index 0000000..23bdbbd
--- /dev/null
+++ b/README/Trackback.php
@@ -0,0 +1,556 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Trackback Class
+ *
+ * Trackback Sending/Receiving Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Trackbacks
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/trackback.html
+ */
+class CI_Trackback {
+
+	/**
+	 * Character set
+	 *
+	 * @var	string
+	 */
+	public $charset = 'UTF-8';
+
+	/**
+	 * Trackback data
+	 *
+	 * @var	array
+	 */
+	public $data = array(
+		'url' => '',
+		'title' => '',
+		'excerpt' => '',
+		'blog_name' => '',
+		'charset' => ''
+	);
+
+	/**
+	 * Convert ASCII flag
+	 *
+	 * Whether to convert high-ASCII and MS Word
+	 * characters to HTML entities.
+	 *
+	 * @var	bool
+	 */
+	public $convert_ascii = TRUE;
+
+	/**
+	 * Response
+	 *
+	 * @var	string
+	 */
+	public $response = '';
+
+	/**
+	 * Error messages list
+	 *
+	 * @var	string[]
+	 */
+	public $error_msg = array();
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		log_message('info', 'Trackback Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send Trackback
+	 *
+	 * @param	array
+	 * @return	bool
+	 */
+	public function send($tb_data)
+	{
+		if ( ! is_array($tb_data))
+		{
+			$this->set_error('The send() method must be passed an array');
+			return FALSE;
+		}
+
+		// Pre-process the Trackback Data
+		foreach (array('url', 'title', 'excerpt', 'blog_name', 'ping_url') as $item)
+		{
+			if ( ! isset($tb_data[$item]))
+			{
+				$this->set_error('Required item missing: '.$item);
+				return FALSE;
+			}
+
+			switch ($item)
+			{
+				case 'ping_url':
+					$$item = $this->extract_urls($tb_data[$item]);
+					break;
+				case 'excerpt':
+					$$item = $this->limit_characters($this->convert_xml(strip_tags(stripslashes($tb_data[$item]))));
+					break;
+				case 'url':
+					$$item = str_replace('&#45;', '-', $this->convert_xml(strip_tags(stripslashes($tb_data[$item]))));
+					break;
+				default:
+					$$item = $this->convert_xml(strip_tags(stripslashes($tb_data[$item])));
+					break;
+			}
+
+			// Convert High ASCII Characters
+			if ($this->convert_ascii === TRUE && in_array($item, array('excerpt', 'title', 'blog_name'), TRUE))
+			{
+				$$item = $this->convert_ascii($$item);
+			}
+		}
+
+		// Build the Trackback data string
+		$charset = isset($tb_data['charset']) ? $tb_data['charset'] : $this->charset;
+
+		$data = 'url='.rawurlencode($url).'&title='.rawurlencode($title).'&blog_name='.rawurlencode($blog_name)
+			.'&excerpt='.rawurlencode($excerpt).'&charset='.rawurlencode($charset);
+
+		// Send Trackback(s)
+		$return = TRUE;
+		if (count($ping_url) > 0)
+		{
+			foreach ($ping_url as $url)
+			{
+				if ($this->process($url, $data) === FALSE)
+				{
+					$return = FALSE;
+				}
+			}
+		}
+
+		return $return;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Receive Trackback  Data
+	 *
+	 * This function simply validates the incoming TB data.
+	 * It returns FALSE on failure and TRUE on success.
+	 * If the data is valid it is set to the $this->data array
+	 * so that it can be inserted into a database.
+	 *
+	 * @return	bool
+	 */
+	public function receive()
+	{
+		foreach (array('url', 'title', 'blog_name', 'excerpt') as $val)
+		{
+			if (empty($_POST[$val]))
+			{
+				$this->set_error('The following required POST variable is missing: '.$val);
+				return FALSE;
+			}
+
+			$this->data['charset'] = isset($_POST['charset']) ? strtoupper(trim($_POST['charset'])) : 'auto';
+
+			if ($val !== 'url' && MB_ENABLED === TRUE)
+			{
+				if (MB_ENABLED === TRUE)
+				{
+					$_POST[$val] = mb_convert_encoding($_POST[$val], $this->charset, $this->data['charset']);
+				}
+				elseif (ICONV_ENABLED === TRUE)
+				{
+					$_POST[$val] = @iconv($this->data['charset'], $this->charset.'//IGNORE', $_POST[$val]);
+				}
+			}
+
+			$_POST[$val] = ($val !== 'url') ? $this->convert_xml(strip_tags($_POST[$val])) : strip_tags($_POST[$val]);
+
+			if ($val === 'excerpt')
+			{
+				$_POST['excerpt'] = $this->limit_characters($_POST['excerpt']);
+			}
+
+			$this->data[$val] = $_POST[$val];
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send Trackback Error Message
+	 *
+	 * Allows custom errors to be set. By default it
+	 * sends the "incomplete information" error, as that's
+	 * the most common one.
+	 *
+	 * @param	string
+	 * @return	void
+	 */
+	public function send_error($message = 'Incomplete Information')
+	{
+		exit('<?xml version="1.0" encoding="utf-8"?'.">\n<response>\n<error>1</error>\n<message>".$message."</message>\n</response>");
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send Trackback Success Message
+	 *
+	 * This should be called when a trackback has been
+	 * successfully received and inserted.
+	 *
+	 * @return	void
+	 */
+	public function send_success()
+	{
+		exit('<?xml version="1.0" encoding="utf-8"?'.">\n<response>\n<error>0</error>\n</response>");
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch a particular item
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function data($item)
+	{
+		return isset($this->data[$item]) ? $this->data[$item] : '';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process Trackback
+	 *
+	 * Opens a socket connection and passes the data to
+	 * the server. Returns TRUE on success, FALSE on failure
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	bool
+	 */
+	public function process($url, $data)
+	{
+		$target = parse_url($url);
+
+		// Open the socket
+		if ( ! $fp = @fsockopen($target['host'], 80))
+		{
+			$this->set_error('Invalid Connection: '.$url);
+			return FALSE;
+		}
+
+		// Build the path
+		$path = isset($target['path']) ? $target['path'] : $url;
+		empty($target['query']) OR $path .= '?'.$target['query'];
+
+		// Add the Trackback ID to the data string
+		if ($id = $this->get_id($url))
+		{
+			$data = 'tb_id='.$id.'&'.$data;
+		}
+
+		// Transfer the data
+		fputs($fp, 'POST '.$path." HTTP/1.0\r\n");
+		fputs($fp, 'Host: '.$target['host']."\r\n");
+		fputs($fp, "Content-type: application/x-www-form-urlencoded\r\n");
+		fputs($fp, 'Content-length: '.strlen($data)."\r\n");
+		fputs($fp, "Connection: close\r\n\r\n");
+		fputs($fp, $data);
+
+		// Was it successful?
+
+		$this->response = '';
+		while ( ! feof($fp))
+		{
+			$this->response .= fgets($fp, 128);
+		}
+		@fclose($fp);
+
+		if (stripos($this->response, '<error>0</error>') === FALSE)
+		{
+			$message = preg_match('/<message>(.*?)<\/message>/is', $this->response, $match)
+				? trim($match[1])
+				: 'An unknown error was encountered';
+			$this->set_error($message);
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Extract Trackback URLs
+	 *
+	 * This function lets multiple trackbacks be sent.
+	 * It takes a string of URLs (separated by comma or
+	 * space) and puts each URL into an array
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function extract_urls($urls)
+	{
+		// Remove the pesky white space and replace with a comma, then replace doubles.
+		$urls = str_replace(',,', ',', preg_replace('/\s*(\S+)\s*/', '\\1,', $urls));
+
+		// Break into an array via commas and remove duplicates
+		$urls = array_unique(preg_split('/[,]/', rtrim($urls, ',')));
+
+		array_walk($urls, array($this, 'validate_url'));
+		return $urls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate URL
+	 *
+	 * Simply adds "http://" if missing
+	 *
+	 * @param	string
+	 * @return	void
+	 */
+	public function validate_url(&$url)
+	{
+		$url = trim($url);
+
+		if (strpos($url, 'http') !== 0)
+		{
+			$url = 'http://'.$url;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Find the Trackback URL's ID
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function get_id($url)
+	{
+		$tb_id = '';
+
+		if (strpos($url, '?') !== FALSE)
+		{
+			$tb_array = explode('/', $url);
+			$tb_end   = $tb_array[count($tb_array)-1];
+
+			if ( ! is_numeric($tb_end))
+			{
+				$tb_end  = $tb_array[count($tb_array)-2];
+			}
+
+			$tb_array = explode('=', $tb_end);
+			$tb_id	= $tb_array[count($tb_array)-1];
+		}
+		else
+		{
+			$url = rtrim($url, '/');
+
+			$tb_array = explode('/', $url);
+			$tb_id	= $tb_array[count($tb_array)-1];
+
+			if ( ! is_numeric($tb_id))
+			{
+				$tb_id = $tb_array[count($tb_array)-2];
+			}
+		}
+
+		return ctype_digit((string) $tb_id) ? $tb_id : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Convert Reserved XML characters to Entities
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function convert_xml($str)
+	{
+		$temp = '__TEMP_AMPERSANDS__';
+
+		$str = preg_replace(array('/&#(\d+);/', '/&(\w+);/'), $temp.'\\1;', $str);
+
+		$str = str_replace(array('&', '<', '>', '"', "'", '-'),
+					array('&amp;', '&lt;', '&gt;', '&quot;', '&#39;', '&#45;'),
+					$str);
+
+		return preg_replace(array('/'.$temp.'(\d+);/', '/'.$temp.'(\w+);/'), array('&#\\1;', '&\\1;'), $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Character limiter
+	 *
+	 * Limits the string based on the character count. Will preserve complete words.
+	 *
+	 * @param	string
+	 * @param	int
+	 * @param	string
+	 * @return	string
+	 */
+	public function limit_characters($str, $n = 500, $end_char = '&#8230;')
+	{
+		if (strlen($str) < $n)
+		{
+			return $str;
+		}
+
+		$str = preg_replace('/\s+/', ' ', str_replace(array("\r\n", "\r", "\n"), ' ', $str));
+
+		if (strlen($str) <= $n)
+		{
+			return $str;
+		}
+
+		$out = '';
+		foreach (explode(' ', trim($str)) as $val)
+		{
+			$out .= $val.' ';
+			if (strlen($out) >= $n)
+			{
+				return rtrim($out).$end_char;
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * High ASCII to Entities
+	 *
+	 * Converts Hight ascii text and MS Word special chars
+	 * to character entities
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function convert_ascii($str)
+	{
+		$count	= 1;
+		$out	= '';
+		$temp	= array();
+
+		for ($i = 0, $s = strlen($str); $i < $s; $i++)
+		{
+			$ordinal = ord($str[$i]);
+
+			if ($ordinal < 128)
+			{
+				$out .= $str[$i];
+			}
+			else
+			{
+				if (count($temp) === 0)
+				{
+					$count = ($ordinal < 224) ? 2 : 3;
+				}
+
+				$temp[] = $ordinal;
+
+				if (count($temp) === $count)
+				{
+					$number = ($count === 3)
+						? (($temp[0] % 16) * 4096) + (($temp[1] % 64) * 64) + ($temp[2] % 64)
+						: (($temp[0] % 32) * 64) + ($temp[1] % 64);
+
+					$out .= '&#'.$number.';';
+					$count = 1;
+					$temp = array();
+				}
+			}
+		}
+
+		return $out;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set error message
+	 *
+	 * @param	string
+	 * @return	void
+	 */
+	public function set_error($msg)
+	{
+		log_message('error', $msg);
+		$this->error_msg[] = $msg;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show error messages
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	public function display_errors($open = '<p>', $close = '</p>')
+	{
+		return (count($this->error_msg) > 0) ? $open.implode($close.$open, $this->error_msg).$close : '';
+	}
+
+}
diff --git a/README/Typography.php b/README/Typography.php
new file mode 100644
index 0000000..3b6cb16
--- /dev/null
+++ b/README/Typography.php
@@ -0,0 +1,424 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Typography Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/typography.html
+ */
+class CI_Typography {
+
+	/**
+	 * Block level elements that should not be wrapped inside <p> tags
+	 *
+	 * @var string
+	 */
+	public $block_elements = 'address|blockquote|div|dl|fieldset|form|h\d|hr|noscript|object|ol|p|pre|script|table|ul';
+
+	/**
+	 * Elements that should not have <p> and <br /> tags within them.
+	 *
+	 * @var string
+	 */
+	public $skip_elements	= 'p|pre|ol|ul|dl|object|table|h\d';
+
+	/**
+	 * Tags we want the parser to completely ignore when splitting the string.
+	 *
+	 * @var string
+	 */
+	public $inline_elements = 'a|abbr|acronym|b|bdo|big|br|button|cite|code|del|dfn|em|i|img|ins|input|label|map|kbd|q|samp|select|small|span|strong|sub|sup|textarea|tt|var';
+
+	/**
+	 * array of block level elements that require inner content to be within another block level element
+	 *
+	 * @var array
+	 */
+	public $inner_block_required = array('blockquote');
+
+	/**
+	 * the last block element parsed
+	 *
+	 * @var string
+	 */
+	public $last_block_element = '';
+
+	/**
+	 * whether or not to protect quotes within { curly braces }
+	 *
+	 * @var bool
+	 */
+	public $protect_braced_quotes = FALSE;
+
+	/**
+	 * Auto Typography
+	 *
+	 * This function converts text, making it typographically correct:
+	 *	- Converts double spaces into paragraphs.
+	 *	- Converts single line breaks into <br /> tags
+	 *	- Converts single and double quotes into correctly facing curly quote entities.
+	 *	- Converts three dots into ellipsis.
+	 *	- Converts double dashes into em-dashes.
+	 *  - Converts two spaces into entities
+	 *
+	 * @param	string
+	 * @param	bool	whether to reduce more then two consecutive newlines to two
+	 * @return	string
+	 */
+	public function auto_typography($str, $reduce_linebreaks = FALSE)
+	{
+		if ($str === '')
+		{
+			return '';
+		}
+
+		// Standardize Newlines to make matching easier
+		if (strpos($str, "\r") !== FALSE)
+		{
+			$str = str_replace(array("\r\n", "\r"), "\n", $str);
+		}
+
+		// Reduce line breaks.  If there are more than two consecutive linebreaks
+		// we'll compress them down to a maximum of two since there's no benefit to more.
+		if ($reduce_linebreaks === TRUE)
+		{
+			$str = preg_replace("/\n\n+/", "\n\n", $str);
+		}
+
+		// HTML comment tags don't conform to patterns of normal tags, so pull them out separately, only if needed
+		$html_comments = array();
+		if (strpos($str, '<!--') !== FALSE && preg_match_all('#(<!\-\-.*?\-\->)#s', $str, $matches))
+		{
+			for ($i = 0, $total = count($matches[0]); $i < $total; $i++)
+			{
+				$html_comments[] = $matches[0][$i];
+				$str = str_replace($matches[0][$i], '{@HC'.$i.'}', $str);
+			}
+		}
+
+		// match and yank <pre> tags if they exist.  It's cheaper to do this separately since most content will
+		// not contain <pre> tags, and it keeps the PCRE patterns below simpler and faster
+		if (strpos($str, '<pre') !== FALSE)
+		{
+			$str = preg_replace_callback('#<pre.*?>.*?</pre>#si', array($this, '_protect_characters'), $str);
+		}
+
+		// Convert quotes within tags to temporary markers.
+		$str = preg_replace_callback('#<.+?>#si', array($this, '_protect_characters'), $str);
+
+		// Do the same with braces if necessary
+		if ($this->protect_braced_quotes === TRUE)
+		{
+			$str = preg_replace_callback('#\{.+?\}#si', array($this, '_protect_characters'), $str);
+		}
+
+		// Convert "ignore" tags to temporary marker.  The parser splits out the string at every tag
+		// it encounters.  Certain inline tags, like image tags, links, span tags, etc. will be
+		// adversely affected if they are split out so we'll convert the opening bracket < temporarily to: {@TAG}
+		$str = preg_replace('#<(/*)('.$this->inline_elements.')([ >])#i', '{@TAG}\\1\\2\\3', $str);
+
+		/* Split the string at every tag. This expression creates an array with this prototype:
+		 *
+		 *	[array]
+		 *	{
+		 *		[0] = <opening tag>
+		 *		[1] = Content...
+		 *		[2] = <closing tag>
+		 *		Etc...
+		 *	}
+		 */
+		$chunks = preg_split('/(<(?:[^<>]+(?:"[^"]*"|\'[^\']*\')?)+>)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
+
+		// Build our finalized string.  We cycle through the array, skipping tags, and processing the contained text
+		$str = '';
+		$process = TRUE;
+
+		for ($i = 0, $c = count($chunks) - 1; $i <= $c; $i++)
+		{
+			// Are we dealing with a tag? If so, we'll skip the processing for this cycle.
+			// Well also set the "process" flag which allows us to skip <pre> tags and a few other things.
+			if (preg_match('#<(/*)('.$this->block_elements.').*?>#', $chunks[$i], $match))
+			{
+				if (preg_match('#'.$this->skip_elements.'#', $match[2]))
+				{
+					$process = ($match[1] === '/');
+				}
+
+				if ($match[1] === '')
+				{
+					$this->last_block_element = $match[2];
+				}
+
+				$str .= $chunks[$i];
+				continue;
+			}
+
+			if ($process === FALSE)
+			{
+				$str .= $chunks[$i];
+				continue;
+			}
+
+			//  Force a newline to make sure end tags get processed by _format_newlines()
+			if ($i === $c)
+			{
+				$chunks[$i] .= "\n";
+			}
+
+			//  Convert Newlines into <p> and <br /> tags
+			$str .= $this->_format_newlines($chunks[$i]);
+		}
+
+		// No opening block level tag? Add it if needed.
+		if ( ! preg_match('/^\s*<(?:'.$this->block_elements.')/i', $str))
+		{
+			$str = preg_replace('/^(.*?)<('.$this->block_elements.')/i', '<p>$1</p><$2', $str);
+		}
+
+		// Convert quotes, elipsis, em-dashes, non-breaking spaces, and ampersands
+		$str = $this->format_characters($str);
+
+		// restore HTML comments
+		for ($i = 0, $total = count($html_comments); $i < $total; $i++)
+		{
+			// remove surrounding paragraph tags, but only if there's an opening paragraph tag
+			// otherwise HTML comments at the ends of paragraphs will have the closing tag removed
+			// if '<p>{@HC1}' then replace <p>{@HC1}</p> with the comment, else replace only {@HC1} with the comment
+			$str = preg_replace('#(?(?=<p>\{@HC'.$i.'\})<p>\{@HC'.$i.'\}(\s*</p>)|\{@HC'.$i.'\})#s', $html_comments[$i], $str);
+		}
+
+		// Final clean up
+		$table = array(
+
+						// If the user submitted their own paragraph tags within the text
+						// we will retain them instead of using our tags.
+						'/(<p[^>*?]>)<p>/'	=> '$1', // <?php BBEdit syntax coloring bug fix
+
+						// Reduce multiple instances of opening/closing paragraph tags to a single one
+						'#(</p>)+#'			=> '</p>',
+						'/(<p>\W*<p>)+/'	=> '<p>',
+
+						// Clean up stray paragraph tags that appear before block level elements
+						'#<p></p><('.$this->block_elements.')#'	=> '<$1',
+
+						// Clean up stray non-breaking spaces preceeding block elements
+						'#(&nbsp;\s*)+<('.$this->block_elements.')#'	=> '  <$2',
+
+						// Replace the temporary markers we added earlier
+						'/\{@TAG\}/'		=> '<',
+						'/\{@DQ\}/'			=> '"',
+						'/\{@SQ\}/'			=> "'",
+						'/\{@DD\}/'			=> '--',
+						'/\{@NBS\}/'		=> '  ',
+
+						// An unintended consequence of the _format_newlines function is that
+						// some of the newlines get truncated, resulting in <p> tags
+						// starting immediately after <block> tags on the same line.
+						// This forces a newline after such occurrences, which looks much nicer.
+						"/><p>\n/"			=> ">\n<p>",
+
+						// Similarly, there might be cases where a closing </block> will follow
+						// a closing </p> tag, so we'll correct it by adding a newline in between
+						'#</p></#'			=> "</p>\n</"
+						);
+
+		// Do we need to reduce empty lines?
+		if ($reduce_linebreaks === TRUE)
+		{
+			$table['#<p>\n*</p>#'] = '';
+		}
+		else
+		{
+			// If we have empty paragraph tags we add a non-breaking space
+			// otherwise most browsers won't treat them as true paragraphs
+			$table['#<p></p>#'] = '<p>&nbsp;</p>';
+		}
+
+		return preg_replace(array_keys($table), $table, $str);
+
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Format Characters
+	 *
+	 * This function mainly converts double and single quotes
+	 * to curly entities, but it also converts em-dashes,
+	 * double spaces, and ampersands
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function format_characters($str)
+	{
+		static $table;
+
+		if ( ! isset($table))
+		{
+			$table = array(
+							// nested smart quotes, opening and closing
+							// note that rules for grammar (English) allow only for two levels deep
+							// and that single quotes are _supposed_ to always be on the outside
+							// but we'll accommodate both
+							// Note that in all cases, whitespace is the primary determining factor
+							// on which direction to curl, with non-word characters like punctuation
+							// being a secondary factor only after whitespace is addressed.
+							'/\'"(\s|$)/'					=> '&#8217;&#8221;$1',
+							'/(^|\s|<p>)\'"/'				=> '$1&#8216;&#8220;',
+							'/\'"(\W)/'						=> '&#8217;&#8221;$1',
+							'/(\W)\'"/'						=> '$1&#8216;&#8220;',
+							'/"\'(\s|$)/'					=> '&#8221;&#8217;$1',
+							'/(^|\s|<p>)"\'/'				=> '$1&#8220;&#8216;',
+							'/"\'(\W)/'						=> '&#8221;&#8217;$1',
+							'/(\W)"\'/'						=> '$1&#8220;&#8216;',
+
+							// single quote smart quotes
+							'/\'(\s|$)/'					=> '&#8217;$1',
+							'/(^|\s|<p>)\'/'				=> '$1&#8216;',
+							'/\'(\W)/'						=> '&#8217;$1',
+							'/(\W)\'/'						=> '$1&#8216;',
+
+							// double quote smart quotes
+							'/"(\s|$)/'						=> '&#8221;$1',
+							'/(^|\s|<p>)"/'					=> '$1&#8220;',
+							'/"(\W)/'						=> '&#8221;$1',
+							'/(\W)"/'						=> '$1&#8220;',
+
+							// apostrophes
+							"/(\w)'(\w)/"					=> '$1&#8217;$2',
+
+							// Em dash and ellipses dots
+							'/\s?\-\-\s?/'					=> '&#8212;',
+							'/(\w)\.{3}/'					=> '$1&#8230;',
+
+							// double space after sentences
+							'/(\W)  /'						=> '$1&nbsp; ',
+
+							// ampersands, if not a character entity
+							'/&(?!#?[a-zA-Z0-9]{2,};)/'		=> '&amp;'
+						);
+		}
+
+		return preg_replace(array_keys($table), $table, $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Format Newlines
+	 *
+	 * Converts newline characters into either <p> tags or <br />
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _format_newlines($str)
+	{
+		if ($str === '' OR (strpos($str, "\n") === FALSE && ! in_array($this->last_block_element, $this->inner_block_required)))
+		{
+			return $str;
+		}
+
+		// Convert two consecutive newlines to paragraphs
+		$str = str_replace("\n\n", "</p>\n\n<p>", $str);
+
+		// Convert single spaces to <br /> tags
+		$str = preg_replace("/([^\n])(\n)([^\n])/", '\\1<br />\\2\\3', $str);
+
+		// Wrap the whole enchilada in enclosing paragraphs
+		if ($str !== "\n")
+		{
+			// We trim off the right-side new line so that the closing </p> tag
+			// will be positioned immediately following the string, matching
+			// the behavior of the opening <p> tag
+			$str =  '<p>'.rtrim($str).'</p>';
+		}
+
+		// Remove empty paragraphs if they are on the first line, as this
+		// is a potential unintended consequence of the previous code
+		return preg_replace('/<p><\/p>(.*)/', '\\1', $str, 1);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Protect Characters
+	 *
+	 * Protects special characters from being formatted later
+	 * We don't want quotes converted within tags so we'll temporarily convert them to {@DQ} and {@SQ}
+	 * and we don't want double dashes converted to emdash entities, so they are marked with {@DD}
+	 * likewise double spaces are converted to {@NBS} to prevent entity conversion
+	 *
+	 * @param	array
+	 * @return	string
+	 */
+	protected function _protect_characters($match)
+	{
+		return str_replace(array("'",'"','--','  '), array('{@SQ}', '{@DQ}', '{@DD}', '{@NBS}'), $match[0]);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Convert newlines to HTML line breaks except within PRE tags
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	public function nl2br_except_pre($str)
+	{
+		$newstr = '';
+		for ($ex = explode('pre>', $str), $ct = count($ex), $i = 0; $i < $ct; $i++)
+		{
+			$newstr .= (($i % 2) === 0) ? nl2br($ex[$i]) : $ex[$i];
+			if ($ct - 1 !== $i)
+			{
+				$newstr .= 'pre>';
+			}
+		}
+
+		return $newstr;
+	}
+
+}
diff --git a/README/Unit_test.php b/README/Unit_test.php
new file mode 100644
index 0000000..3f986f3
--- /dev/null
+++ b/README/Unit_test.php
@@ -0,0 +1,407 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.3.1
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Unit Testing Class
+ *
+ * Simple testing class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	UnitTesting
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/unit_testing.html
+ */
+class CI_Unit_test {
+
+	/**
+	 * Active flag
+	 *
+	 * @var	bool
+	 */
+	public $active = TRUE;
+
+	/**
+	 * Test results
+	 *
+	 * @var	array
+	 */
+	public $results = array();
+
+	/**
+	 * Strict comparison flag
+	 *
+	 * Whether to use === or == when comparing
+	 *
+	 * @var	bool
+	 */
+	public $strict = FALSE;
+
+	/**
+	 * Template
+	 *
+	 * @var	string
+	 */
+	protected $_template = NULL;
+
+	/**
+	 * Template rows
+	 *
+	 * @var	string
+	 */
+	protected $_template_rows = NULL;
+
+	/**
+	 * List of visible test items
+	 *
+	 * @var	array
+	 */
+	protected $_test_items_visible	= array(
+		'test_name',
+		'test_datatype',
+		'res_datatype',
+		'result',
+		'file',
+		'line',
+		'notes'
+	);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		log_message('info', 'Unit Testing Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Run the tests
+	 *
+	 * Runs the supplied tests
+	 *
+	 * @param	array	$items
+	 * @return	void
+	 */
+	public function set_test_items($items)
+	{
+		if ( ! empty($items) && is_array($items))
+		{
+			$this->_test_items_visible = $items;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Run the tests
+	 *
+	 * Runs the supplied tests
+	 *
+	 * @param	mixed	$test
+	 * @param	mixed	$expected
+	 * @param	string	$test_name
+	 * @param	string	$notes
+	 * @return	string
+	 */
+	public function run($test, $expected = TRUE, $test_name = 'undefined', $notes = '')
+	{
+		if ($this->active === FALSE)
+		{
+			return FALSE;
+		}
+
+		if (in_array($expected, array('is_object', 'is_string', 'is_bool', 'is_true', 'is_false', 'is_int', 'is_numeric', 'is_float', 'is_double', 'is_array', 'is_null', 'is_resource'), TRUE))
+		{
+			$expected = str_replace('is_double', 'is_float', $expected);
+			$result = $expected($test);
+			$extype = str_replace(array('true', 'false'), 'bool', str_replace('is_', '', $expected));
+		}
+		else
+		{
+			$result = ($this->strict === TRUE) ? ($test === $expected) : ($test == $expected);
+			$extype = gettype($expected);
+		}
+
+		$back = $this->_backtrace();
+
+		$report = array (
+			'test_name'     => $test_name,
+			'test_datatype' => gettype($test),
+			'res_datatype'  => $extype,
+			'result'        => ($result === TRUE) ? 'passed' : 'failed',
+			'file'          => $back['file'],
+			'line'          => $back['line'],
+			'notes'         => $notes
+		);
+
+		$this->results[] = $report;
+
+		return $this->report($this->result(array($report)));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate a report
+	 *
+	 * Displays a table with the test data
+	 *
+	 * @param	array	 $result
+	 * @return	string
+	 */
+	public function report($result = array())
+	{
+		if (count($result) === 0)
+		{
+			$result = $this->result();
+		}
+
+		$CI =& get_instance();
+		$CI->load->language('unit_test');
+
+		$this->_parse_template();
+
+		$r = '';
+		foreach ($result as $res)
+		{
+			$table = '';
+
+			foreach ($res as $key => $val)
+			{
+				if ($key === $CI->lang->line('ut_result'))
+				{
+					if ($val === $CI->lang->line('ut_passed'))
+					{
+						$val = '<span style="color: #0C0;">'.$val.'</span>';
+					}
+					elseif ($val === $CI->lang->line('ut_failed'))
+					{
+						$val = '<span style="color: #C00;">'.$val.'</span>';
+					}
+				}
+
+				$table .= str_replace(array('{item}', '{result}'), array($key, $val), $this->_template_rows);
+			}
+
+			$r .= str_replace('{rows}', $table, $this->_template);
+		}
+
+		return $r;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Use strict comparison
+	 *
+	 * Causes the evaluation to use === rather than ==
+	 *
+	 * @param	bool	$state
+	 * @return	void
+	 */
+	public function use_strict($state = TRUE)
+	{
+		$this->strict = (bool) $state;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Make Unit testing active
+	 *
+	 * Enables/disables unit testing
+	 *
+	 * @param	bool
+	 * @return	void
+	 */
+	public function active($state = TRUE)
+	{
+		$this->active = (bool) $state;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result Array
+	 *
+	 * Returns the raw result data
+	 *
+	 * @param	array	$results
+	 * @return	array
+	 */
+	public function result($results = array())
+	{
+		$CI =& get_instance();
+		$CI->load->language('unit_test');
+
+		if (count($results) === 0)
+		{
+			$results = $this->results;
+		}
+
+		$retval = array();
+		foreach ($results as $result)
+		{
+			$temp = array();
+			foreach ($result as $key => $val)
+			{
+				if ( ! in_array($key, $this->_test_items_visible))
+				{
+					continue;
+				}
+				elseif (in_array($key, array('test_name', 'test_datatype', 'test_res_datatype', 'result'), TRUE))
+				{
+					if (FALSE !== ($line = $CI->lang->line(strtolower('ut_'.$val), FALSE)))
+					{
+						$val = $line;
+					}
+				}
+
+				$temp[$CI->lang->line('ut_'.$key, FALSE)] = $val;
+			}
+
+			$retval[] = $temp;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the template
+	 *
+	 * This lets us set the template to be used to display results
+	 *
+	 * @param	string
+	 * @return	void
+	 */
+	public function set_template($template)
+	{
+		$this->_template = $template;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate a backtrace
+	 *
+	 * This lets us show file names and line numbers
+	 *
+	 * @return	array
+	 */
+	protected function _backtrace()
+	{
+		$back = debug_backtrace();
+		return array(
+			'file' => (isset($back[1]['file']) ? $back[1]['file'] : ''),
+			'line' => (isset($back[1]['line']) ? $back[1]['line'] : '')
+		);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Default Template
+	 *
+	 * @return	string
+	 */
+	protected function _default_template()
+	{
+		$this->_template = "\n".'<table style="width:100%; font-size:small; margin:10px 0; border-collapse:collapse; border:1px solid #CCC;">{rows}'."\n</table>";
+
+		$this->_template_rows = "\n\t<tr>\n\t\t".'<th style="text-align: left; border-bottom:1px solid #CCC;">{item}</th>'
+					."\n\t\t".'<td style="border-bottom:1px solid #CCC;">{result}</td>'."\n\t</tr>";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse Template
+	 *
+	 * Harvests the data within the template {pseudo-variables}
+	 *
+	 * @return	void
+	 */
+	protected function _parse_template()
+	{
+		if ($this->_template_rows !== NULL)
+		{
+			return;
+		}
+
+		if ($this->_template === NULL OR ! preg_match('/\{rows\}(.*?)\{\/rows\}/si', $this->_template, $match))
+		{
+			$this->_default_template();
+			return;
+		}
+
+		$this->_template_rows = $match[1];
+		$this->_template = str_replace($match[0], '{rows}', $this->_template);
+	}
+
+}
+
+/**
+ * Helper function to test boolean TRUE
+ *
+ * @param	mixed	$test
+ * @return	bool
+ */
+function is_true($test)
+{
+	return ($test === TRUE);
+}
+
+/**
+ * Helper function to test boolean FALSE
+ *
+ * @param	mixed	$test
+ * @return	bool
+ */
+function is_false($test)
+{
+	return ($test === FALSE);
+}
diff --git a/README/Upload.php b/README/Upload.php
new file mode 100644
index 0000000..8a2dec7
--- /dev/null
+++ b/README/Upload.php
@@ -0,0 +1,1308 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * File Uploading Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Uploads
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/file_uploading.html
+ */
+class CI_Upload {
+
+	/**
+	 * Maximum file size
+	 *
+	 * @var	int
+	 */
+	public $max_size = 0;
+
+	/**
+	 * Maximum image width
+	 *
+	 * @var	int
+	 */
+	public $max_width = 0;
+
+	/**
+	 * Maximum image height
+	 *
+	 * @var	int
+	 */
+	public $max_height = 0;
+
+	/**
+	 * Minimum image width
+	 *
+	 * @var	int
+	 */
+	public $min_width = 0;
+
+	/**
+	 * Minimum image height
+	 *
+	 * @var	int
+	 */
+	public $min_height = 0;
+
+	/**
+	 * Maximum filename length
+	 *
+	 * @var	int
+	 */
+	public $max_filename = 0;
+
+	/**
+	 * Maximum duplicate filename increment ID
+	 *
+	 * @var	int
+	 */
+	public $max_filename_increment = 100;
+
+	/**
+	 * Allowed file types
+	 *
+	 * @var	string
+	 */
+	public $allowed_types = '';
+
+	/**
+	 * Temporary filename
+	 *
+	 * @var	string
+	 */
+	public $file_temp = '';
+
+	/**
+	 * Filename
+	 *
+	 * @var	string
+	 */
+	public $file_name = '';
+
+	/**
+	 * Original filename
+	 *
+	 * @var	string
+	 */
+	public $orig_name = '';
+
+	/**
+	 * File type
+	 *
+	 * @var	string
+	 */
+	public $file_type = '';
+
+	/**
+	 * File size
+	 *
+	 * @var	int
+	 */
+	public $file_size = NULL;
+
+	/**
+	 * Filename extension
+	 *
+	 * @var	string
+	 */
+	public $file_ext = '';
+
+	/**
+	 * Force filename extension to lowercase
+	 *
+	 * @var	string
+	 */
+	public $file_ext_tolower = FALSE;
+
+	/**
+	 * Upload path
+	 *
+	 * @var	string
+	 */
+	public $upload_path = '';
+
+	/**
+	 * Overwrite flag
+	 *
+	 * @var	bool
+	 */
+	public $overwrite = FALSE;
+
+	/**
+	 * Obfuscate filename flag
+	 *
+	 * @var	bool
+	 */
+	public $encrypt_name = FALSE;
+
+	/**
+	 * Is image flag
+	 *
+	 * @var	bool
+	 */
+	public $is_image = FALSE;
+
+	/**
+	 * Image width
+	 *
+	 * @var	int
+	 */
+	public $image_width = NULL;
+
+	/**
+	 * Image height
+	 *
+	 * @var	int
+	 */
+	public $image_height = NULL;
+
+	/**
+	 * Image type
+	 *
+	 * @var	string
+	 */
+	public $image_type = '';
+
+	/**
+	 * Image size string
+	 *
+	 * @var	string
+	 */
+	public $image_size_str = '';
+
+	/**
+	 * Error messages list
+	 *
+	 * @var	array
+	 */
+	public $error_msg = array();
+
+	/**
+	 * Remove spaces flag
+	 *
+	 * @var	bool
+	 */
+	public $remove_spaces = TRUE;
+
+	/**
+	 * MIME detection flag
+	 *
+	 * @var	bool
+	 */
+	public $detect_mime = TRUE;
+
+	/**
+	 * XSS filter flag
+	 *
+	 * @var	bool
+	 */
+	public $xss_clean = FALSE;
+
+	/**
+	 * Apache mod_mime fix flag
+	 *
+	 * @var	bool
+	 */
+	public $mod_mime_fix = TRUE;
+
+	/**
+	 * Temporary filename prefix
+	 *
+	 * @var	string
+	 */
+	public $temp_prefix = 'temp_file_';
+
+	/**
+	 * Filename sent by the client
+	 *
+	 * @var	bool
+	 */
+	public $client_name = '';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Filename override
+	 *
+	 * @var	string
+	 */
+	protected $_file_name_override = '';
+
+	/**
+	 * MIME types list
+	 *
+	 * @var	array
+	 */
+	protected $_mimes = array();
+
+	/**
+	 * CI Singleton
+	 *
+	 * @var	object
+	 */
+	protected $_CI;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	array	$props
+	 * @return	void
+	 */
+	public function __construct($config = array())
+	{
+		empty($config) OR $this->initialize($config, FALSE);
+
+		$this->_mimes =& get_mimes();
+		$this->_CI =& get_instance();
+
+		log_message('info', 'Upload Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize preferences
+	 *
+	 * @param	array	$config
+	 * @param	bool	$reset
+	 * @return	CI_Upload
+	 */
+	public function initialize(array $config = array(), $reset = TRUE)
+	{
+		$reflection = new ReflectionClass($this);
+
+		if ($reset === TRUE)
+		{
+			$defaults = $reflection->getDefaultProperties();
+			foreach (array_keys($defaults) as $key)
+			{
+				if ($key[0] === '_')
+				{
+					continue;
+				}
+
+				if (isset($config[$key]))
+				{
+					if ($reflection->hasMethod('set_'.$key))
+					{
+						$this->{'set_'.$key}($config[$key]);
+					}
+					else
+					{
+						$this->$key = $config[$key];
+					}
+				}
+				else
+				{
+					$this->$key = $defaults[$key];
+				}
+			}
+		}
+		else
+		{
+			foreach ($config as $key => &$value)
+			{
+				if ($key[0] !== '_' && $reflection->hasProperty($key))
+				{
+					if ($reflection->hasMethod('set_'.$key))
+					{
+						$this->{'set_'.$key}($value);
+					}
+					else
+					{
+						$this->$key = $value;
+					}
+				}
+			}
+		}
+
+		// if a file_name was provided in the config, use it instead of the user input
+		// supplied file name for all uploads until initialized again
+		$this->_file_name_override = $this->file_name;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Perform the file upload
+	 *
+	 * @param	string	$field
+	 * @return	bool
+	 */
+	public function do_upload($field = 'userfile')
+	{
+		// Is $_FILES[$field] set? If not, no reason to continue.
+		if (isset($_FILES[$field]))
+		{
+			$_file = $_FILES[$field];
+		}
+		// Does the field name contain array notation?
+		elseif (($c = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $field, $matches)) > 1)
+		{
+			$_file = $_FILES;
+			for ($i = 0; $i < $c; $i++)
+			{
+				// We can't track numeric iterations, only full field names are accepted
+				if (($field = trim($matches[0][$i], '[]')) === '' OR ! isset($_file[$field]))
+				{
+					$_file = NULL;
+					break;
+				}
+
+				$_file = $_file[$field];
+			}
+		}
+
+		if ( ! isset($_file))
+		{
+			$this->set_error('upload_no_file_selected', 'debug');
+			return FALSE;
+		}
+
+		// Is the upload path valid?
+		if ( ! $this->validate_upload_path())
+		{
+			// errors will already be set by validate_upload_path() so just return FALSE
+			return FALSE;
+		}
+
+		// Was the file able to be uploaded? If not, determine the reason why.
+		if ( ! is_uploaded_file($_file['tmp_name']))
+		{
+			$error = isset($_file['error']) ? $_file['error'] : 4;
+
+			switch ($error)
+			{
+				case UPLOAD_ERR_INI_SIZE:
+					$this->set_error('upload_file_exceeds_limit', 'info');
+					break;
+				case UPLOAD_ERR_FORM_SIZE:
+					$this->set_error('upload_file_exceeds_form_limit', 'info');
+					break;
+				case UPLOAD_ERR_PARTIAL:
+					$this->set_error('upload_file_partial', 'debug');
+					break;
+				case UPLOAD_ERR_NO_FILE:
+					$this->set_error('upload_no_file_selected', 'debug');
+					break;
+				case UPLOAD_ERR_NO_TMP_DIR:
+					$this->set_error('upload_no_temp_directory', 'error');
+					break;
+				case UPLOAD_ERR_CANT_WRITE:
+					$this->set_error('upload_unable_to_write_file', 'error');
+					break;
+				case UPLOAD_ERR_EXTENSION:
+					$this->set_error('upload_stopped_by_extension', 'debug');
+					break;
+				default:
+					$this->set_error('upload_no_file_selected', 'debug');
+					break;
+			}
+
+			return FALSE;
+		}
+
+		// Set the uploaded data as class variables
+		$this->file_temp = $_file['tmp_name'];
+		$this->file_size = $_file['size'];
+
+		// Skip MIME type detection?
+		if ($this->detect_mime !== FALSE)
+		{
+			$this->_file_mime_type($_file);
+		}
+
+		$this->file_type = preg_replace('/^(.+?);.*$/', '\\1', $this->file_type);
+		$this->file_type = strtolower(trim(stripslashes($this->file_type), '"'));
+		$this->file_name = $this->_prep_filename($_file['name']);
+		$this->file_ext	 = $this->get_extension($this->file_name);
+		$this->client_name = $this->file_name;
+
+		// Is the file type allowed to be uploaded?
+		if ( ! $this->is_allowed_filetype())
+		{
+			$this->set_error('upload_invalid_filetype', 'debug');
+			return FALSE;
+		}
+
+		// if we're overriding, let's now make sure the new name and type is allowed
+		if ($this->_file_name_override !== '')
+		{
+			$this->file_name = $this->_prep_filename($this->_file_name_override);
+
+			// If no extension was provided in the file_name config item, use the uploaded one
+			if (strpos($this->_file_name_override, '.') === FALSE)
+			{
+				$this->file_name .= $this->file_ext;
+			}
+			else
+			{
+				// An extension was provided, let's have it!
+				$this->file_ext	= $this->get_extension($this->_file_name_override);
+			}
+
+			if ( ! $this->is_allowed_filetype(TRUE))
+			{
+				$this->set_error('upload_invalid_filetype', 'debug');
+				return FALSE;
+			}
+		}
+
+		// Convert the file size to kilobytes
+		if ($this->file_size > 0)
+		{
+			$this->file_size = round($this->file_size/1024, 2);
+		}
+
+		// Is the file size within the allowed maximum?
+		if ( ! $this->is_allowed_filesize())
+		{
+			$this->set_error('upload_invalid_filesize', 'info');
+			return FALSE;
+		}
+
+		// Are the image dimensions within the allowed size?
+		// Note: This can fail if the server has an open_basedir restriction.
+		if ( ! $this->is_allowed_dimensions())
+		{
+			$this->set_error('upload_invalid_dimensions', 'info');
+			return FALSE;
+		}
+
+		// Sanitize the file name for security
+		$this->file_name = $this->_CI->security->sanitize_filename($this->file_name);
+
+		// Truncate the file name if it's too long
+		if ($this->max_filename > 0)
+		{
+			$this->file_name = $this->limit_filename_length($this->file_name, $this->max_filename);
+		}
+
+		// Remove white spaces in the name
+		if ($this->remove_spaces === TRUE)
+		{
+			$this->file_name = preg_replace('/\s+/', '_', $this->file_name);
+		}
+
+		/*
+		 * Validate the file name
+		 * This function appends an number onto the end of
+		 * the file if one with the same name already exists.
+		 * If it returns false there was a problem.
+		 */
+		$this->orig_name = $this->file_name;
+		if (FALSE === ($this->file_name = $this->set_filename($this->upload_path, $this->file_name)))
+		{
+			return FALSE;
+		}
+
+		/*
+		 * Run the file through the XSS hacking filter
+		 * This helps prevent malicious code from being
+		 * embedded within a file. Scripts can easily
+		 * be disguised as images or other file types.
+		 */
+		if ($this->xss_clean && $this->do_xss_clean() === FALSE)
+		{
+			$this->set_error('upload_unable_to_write_file', 'error');
+			return FALSE;
+		}
+
+		/*
+		 * Move the file to the final destination
+		 * To deal with different server configurations
+		 * we'll attempt to use copy() first. If that fails
+		 * we'll use move_uploaded_file(). One of the two should
+		 * reliably work in most environments
+		 */
+		if ( ! @copy($this->file_temp, $this->upload_path.$this->file_name))
+		{
+			if ( ! @move_uploaded_file($this->file_temp, $this->upload_path.$this->file_name))
+			{
+				$this->set_error('upload_destination_error', 'error');
+				return FALSE;
+			}
+		}
+
+		/*
+		 * Set the finalized image dimensions
+		 * This sets the image width/height (assuming the
+		 * file was an image). We use this information
+		 * in the "data" function.
+		 */
+		$this->set_image_properties($this->upload_path.$this->file_name);
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Finalized Data Array
+	 *
+	 * Returns an associative array containing all of the information
+	 * related to the upload, allowing the developer easy access in one array.
+	 *
+	 * @param	string	$index
+	 * @return	mixed
+	 */
+	public function data($index = NULL)
+	{
+		$data = array(
+				'file_name'		=> $this->file_name,
+				'file_type'		=> $this->file_type,
+				'file_path'		=> $this->upload_path,
+				'full_path'		=> $this->upload_path.$this->file_name,
+				'raw_name'		=> str_replace($this->file_ext, '', $this->file_name),
+				'orig_name'		=> $this->orig_name,
+				'client_name'		=> $this->client_name,
+				'file_ext'		=> $this->file_ext,
+				'file_size'		=> $this->file_size,
+				'is_image'		=> $this->is_image(),
+				'image_width'		=> $this->image_width,
+				'image_height'		=> $this->image_height,
+				'image_type'		=> $this->image_type,
+				'image_size_str'	=> $this->image_size_str,
+			);
+
+		if ( ! empty($index))
+		{
+			return isset($data[$index]) ? $data[$index] : NULL;
+		}
+
+		return $data;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Upload Path
+	 *
+	 * @param	string	$path
+	 * @return	CI_Upload
+	 */
+	public function set_upload_path($path)
+	{
+		// Make sure it has a trailing slash
+		$this->upload_path = rtrim($path, '/').'/';
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the file name
+	 *
+	 * This function takes a filename/path as input and looks for the
+	 * existence of a file with the same name. If found, it will append a
+	 * number to the end of the filename to avoid overwriting a pre-existing file.
+	 *
+	 * @param	string	$path
+	 * @param	string	$filename
+	 * @return	string
+	 */
+	public function set_filename($path, $filename)
+	{
+		if ($this->encrypt_name === TRUE)
+		{
+			$filename = md5(uniqid(mt_rand())).$this->file_ext;
+		}
+
+		if ($this->overwrite === TRUE OR ! file_exists($path.$filename))
+		{
+			return $filename;
+		}
+
+		$filename = str_replace($this->file_ext, '', $filename);
+
+		$new_filename = '';
+		for ($i = 1; $i < $this->max_filename_increment; $i++)
+		{
+			if ( ! file_exists($path.$filename.$i.$this->file_ext))
+			{
+				$new_filename = $filename.$i.$this->file_ext;
+				break;
+			}
+		}
+
+		if ($new_filename === '')
+		{
+			$this->set_error('upload_bad_filename', 'debug');
+			return FALSE;
+		}
+		else
+		{
+			return $new_filename;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Maximum File Size
+	 *
+	 * @param	int	$n
+	 * @return	CI_Upload
+	 */
+	public function set_max_filesize($n)
+	{
+		$this->max_size = ($n < 0) ? 0 : (int) $n;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Maximum File Size
+	 *
+	 * An internal alias to set_max_filesize() to help with configuration
+	 * as initialize() will look for a set_<property_name>() method ...
+	 *
+	 * @param	int	$n
+	 * @return	CI_Upload
+	 */
+	protected function set_max_size($n)
+	{
+		return $this->set_max_filesize($n);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Maximum File Name Length
+	 *
+	 * @param	int	$n
+	 * @return	CI_Upload
+	 */
+	public function set_max_filename($n)
+	{
+		$this->max_filename = ($n < 0) ? 0 : (int) $n;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Maximum Image Width
+	 *
+	 * @param	int	$n
+	 * @return	CI_Upload
+	 */
+	public function set_max_width($n)
+	{
+		$this->max_width = ($n < 0) ? 0 : (int) $n;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Maximum Image Height
+	 *
+	 * @param	int	$n
+	 * @return	CI_Upload
+	 */
+	public function set_max_height($n)
+	{
+		$this->max_height = ($n < 0) ? 0 : (int) $n;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set minimum image width
+	 *
+	 * @param	int	$n
+	 * @return	CI_Upload
+	 */
+	public function set_min_width($n)
+	{
+		$this->min_width = ($n < 0) ? 0 : (int) $n;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set minimum image height
+	 *
+	 * @param	int	$n
+	 * @return	CI_Upload
+	 */
+	public function set_min_height($n)
+	{
+		$this->min_height = ($n < 0) ? 0 : (int) $n;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Allowed File Types
+	 *
+	 * @param	mixed	$types
+	 * @return	CI_Upload
+	 */
+	public function set_allowed_types($types)
+	{
+		$this->allowed_types = (is_array($types) OR $types === '*')
+			? $types
+			: explode('|', $types);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Image Properties
+	 *
+	 * Uses GD to determine the width/height/type of image
+	 *
+	 * @param	string	$path
+	 * @return	CI_Upload
+	 */
+	public function set_image_properties($path = '')
+	{
+		if ($this->is_image() && function_exists('getimagesize'))
+		{
+			if (FALSE !== ($D = @getimagesize($path)))
+			{
+				$types = array(1 => 'gif', 2 => 'jpeg', 3 => 'png');
+
+				$this->image_width	= $D[0];
+				$this->image_height	= $D[1];
+				$this->image_type	= isset($types[$D[2]]) ? $types[$D[2]] : 'unknown';
+				$this->image_size_str	= $D[3]; // string containing height and width
+			}
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set XSS Clean
+	 *
+	 * Enables the XSS flag so that the file that was uploaded
+	 * will be run through the XSS filter.
+	 *
+	 * @param	bool	$flag
+	 * @return	CI_Upload
+	 */
+	public function set_xss_clean($flag = FALSE)
+	{
+		$this->xss_clean = ($flag === TRUE);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate the image
+	 *
+	 * @return	bool
+	 */
+	public function is_image()
+	{
+		// IE will sometimes return odd mime-types during upload, so here we just standardize all
+		// jpegs or pngs to the same file type.
+
+		$png_mimes  = array('image/x-png');
+		$jpeg_mimes = array('image/jpg', 'image/jpe', 'image/jpeg', 'image/pjpeg');
+
+		if (in_array($this->file_type, $png_mimes))
+		{
+			$this->file_type = 'image/png';
+		}
+		elseif (in_array($this->file_type, $jpeg_mimes))
+		{
+			$this->file_type = 'image/jpeg';
+		}
+
+		$img_mimes = array('image/gif',	'image/jpeg', 'image/png');
+
+		return in_array($this->file_type, $img_mimes, TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Verify that the filetype is allowed
+	 *
+	 * @param	bool	$ignore_mime
+	 * @return	bool
+	 */
+	public function is_allowed_filetype($ignore_mime = FALSE)
+	{
+		if ($this->allowed_types === '*')
+		{
+			return TRUE;
+		}
+
+		if (empty($this->allowed_types) OR ! is_array($this->allowed_types))
+		{
+			$this->set_error('upload_no_file_types', 'debug');
+			return FALSE;
+		}
+
+		$ext = strtolower(ltrim($this->file_ext, '.'));
+
+		if ( ! in_array($ext, $this->allowed_types, TRUE))
+		{
+			return FALSE;
+		}
+
+		// Images get some additional checks
+		if (in_array($ext, array('gif', 'jpg', 'jpeg', 'jpe', 'png'), TRUE) && @getimagesize($this->file_temp) === FALSE)
+		{
+			return FALSE;
+		}
+
+		if ($ignore_mime === TRUE)
+		{
+			return TRUE;
+		}
+
+		if (isset($this->_mimes[$ext]))
+		{
+			return is_array($this->_mimes[$ext])
+				? in_array($this->file_type, $this->_mimes[$ext], TRUE)
+				: ($this->_mimes[$ext] === $this->file_type);
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Verify that the file is within the allowed size
+	 *
+	 * @return	bool
+	 */
+	public function is_allowed_filesize()
+	{
+		return ($this->max_size === 0 OR $this->max_size > $this->file_size);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Verify that the image is within the allowed width/height
+	 *
+	 * @return	bool
+	 */
+	public function is_allowed_dimensions()
+	{
+		if ( ! $this->is_image())
+		{
+			return TRUE;
+		}
+
+		if (function_exists('getimagesize'))
+		{
+			$D = @getimagesize($this->file_temp);
+
+			if ($this->max_width > 0 && $D[0] > $this->max_width)
+			{
+				return FALSE;
+			}
+
+			if ($this->max_height > 0 && $D[1] > $this->max_height)
+			{
+				return FALSE;
+			}
+
+			if ($this->min_width > 0 && $D[0] < $this->min_width)
+			{
+				return FALSE;
+			}
+
+			if ($this->min_height > 0 && $D[1] < $this->min_height)
+			{
+				return FALSE;
+			}
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate Upload Path
+	 *
+	 * Verifies that it is a valid upload path with proper permissions.
+	 *
+	 * @return	bool
+	 */
+	public function validate_upload_path()
+	{
+		if ($this->upload_path === '')
+		{
+			$this->set_error('upload_no_filepath', 'error');
+			return FALSE;
+		}
+
+		if (realpath($this->upload_path) !== FALSE)
+		{
+			$this->upload_path = str_replace('\\', '/', realpath($this->upload_path));
+		}
+
+		if ( ! is_dir($this->upload_path))
+		{
+			$this->set_error('upload_no_filepath', 'error');
+			return FALSE;
+		}
+
+		if ( ! is_really_writable($this->upload_path))
+		{
+			$this->set_error('upload_not_writable', 'error');
+			return FALSE;
+		}
+
+		$this->upload_path = preg_replace('/(.+?)\/*$/', '\\1/',  $this->upload_path);
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Extract the file extension
+	 *
+	 * @param	string	$filename
+	 * @return	string
+	 */
+	public function get_extension($filename)
+	{
+		$x = explode('.', $filename);
+
+		if (count($x) === 1)
+		{
+			return '';
+		}
+
+		$ext = ($this->file_ext_tolower) ? strtolower(end($x)) : end($x);
+		return '.'.$ext;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Limit the File Name Length
+	 *
+	 * @param	string	$filename
+	 * @param	int	$length
+	 * @return	string
+	 */
+	public function limit_filename_length($filename, $length)
+	{
+		if (strlen($filename) < $length)
+		{
+			return $filename;
+		}
+
+		$ext = '';
+		if (strpos($filename, '.') !== FALSE)
+		{
+			$parts		= explode('.', $filename);
+			$ext		= '.'.array_pop($parts);
+			$filename	= implode('.', $parts);
+		}
+
+		return substr($filename, 0, ($length - strlen($ext))).$ext;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Runs the file through the XSS clean function
+	 *
+	 * This prevents people from embedding malicious code in their files.
+	 * I'm not sure that it won't negatively affect certain files in unexpected ways,
+	 * but so far I haven't found that it causes trouble.
+	 *
+	 * @return	string
+	 */
+	public function do_xss_clean()
+	{
+		$file = $this->file_temp;
+
+		if (filesize($file) == 0)
+		{
+			return FALSE;
+		}
+
+		if (memory_get_usage() && ($memory_limit = ini_get('memory_limit')))
+		{
+			$memory_limit *= 1024 * 1024;
+
+			// There was a bug/behavioural change in PHP 5.2, where numbers over one million get output
+			// into scientific notation. number_format() ensures this number is an integer
+			// http://bugs.php.net/bug.php?id=43053
+
+			$memory_limit = number_format(ceil(filesize($file) + $memory_limit), 0, '.', '');
+
+			ini_set('memory_limit', $memory_limit); // When an integer is used, the value is measured in bytes. - PHP.net
+		}
+
+		// If the file being uploaded is an image, then we should have no problem with XSS attacks (in theory), but
+		// IE can be fooled into mime-type detecting a malformed image as an html file, thus executing an XSS attack on anyone
+		// using IE who looks at the image. It does this by inspecting the first 255 bytes of an image. To get around this
+		// CI will itself look at the first 255 bytes of an image to determine its relative safety. This can save a lot of
+		// processor power and time if it is actually a clean image, as it will be in nearly all instances _except_ an
+		// attempted XSS attack.
+
+		if (function_exists('getimagesize') && @getimagesize($file) !== FALSE)
+		{
+			if (($file = @fopen($file, 'rb')) === FALSE) // "b" to force binary
+			{
+				return FALSE; // Couldn't open the file, return FALSE
+			}
+
+			$opening_bytes = fread($file, 256);
+			fclose($file);
+
+			// These are known to throw IE into mime-type detection chaos
+			// <a, <body, <head, <html, <img, <plaintext, <pre, <script, <table, <title
+			// title is basically just in SVG, but we filter it anyhow
+
+			// if it's an image or no "triggers" detected in the first 256 bytes - we're good
+			return ! preg_match('/<(a|body|head|html|img|plaintext|pre|script|table|title)[\s>]/i', $opening_bytes);
+		}
+
+		if (($data = @file_get_contents($file)) === FALSE)
+		{
+			return FALSE;
+		}
+
+		return $this->_CI->security->xss_clean($data, TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set an error message
+	 *
+	 * @param	string	$msg
+	 * @return	CI_Upload
+	 */
+	public function set_error($msg, $log_level = 'error')
+	{
+		$this->_CI->lang->load('upload');
+
+		is_array($msg) OR $msg = array($msg);
+		foreach ($msg as $val)
+		{
+			$msg = ($this->_CI->lang->line($val) === FALSE) ? $val : $this->_CI->lang->line($val);
+			$this->error_msg[] = $msg;
+			log_message($log_level, $msg);
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Display the error message
+	 *
+	 * @param	string	$open
+	 * @param	string	$close
+	 * @return	string
+	 */
+	public function display_errors($open = '<p>', $close = '</p>')
+	{
+		return (count($this->error_msg) > 0) ? $open.implode($close.$open, $this->error_msg).$close : '';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep Filename
+	 *
+	 * Prevents possible script execution from Apache's handling
+	 * of files' multiple extensions.
+	 *
+	 * @link	http://httpd.apache.org/docs/1.3/mod/mod_mime.html#multipleext
+	 *
+	 * @param	string	$filename
+	 * @return	string
+	 */
+	protected function _prep_filename($filename)
+	{
+		if ($this->mod_mime_fix === FALSE OR $this->allowed_types === '*' OR ($ext_pos = strrpos($filename, '.')) === FALSE)
+		{
+			return $filename;
+		}
+
+		$ext = substr($filename, $ext_pos);
+		$filename = substr($filename, 0, $ext_pos);
+		return str_replace('.', '_', $filename).$ext;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * File MIME type
+	 *
+	 * Detects the (actual) MIME type of the uploaded file, if possible.
+	 * The input array is expected to be $_FILES[$field]
+	 *
+	 * @param	array	$file
+	 * @return	void
+	 */
+	protected function _file_mime_type($file)
+	{
+		// We'll need this to validate the MIME info string (e.g. text/plain; charset=us-ascii)
+		$regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/';
+
+		/* Fileinfo extension - most reliable method
+		 *
+		 * Unfortunately, prior to PHP 5.3 - it's only available as a PECL extension and the
+		 * more convenient FILEINFO_MIME_TYPE flag doesn't exist.
+		 */
+		if (function_exists('finfo_file'))
+		{
+			$finfo = @finfo_open(FILEINFO_MIME);
+			if (is_resource($finfo)) // It is possible that a FALSE value is returned, if there is no magic MIME database file found on the system
+			{
+				$mime = @finfo_file($finfo, $file['tmp_name']);
+				finfo_close($finfo);
+
+				/* According to the comments section of the PHP manual page,
+				 * it is possible that this function returns an empty string
+				 * for some files (e.g. if they don't exist in the magic MIME database)
+				 */
+				if (is_string($mime) && preg_match($regexp, $mime, $matches))
+				{
+					$this->file_type = $matches[1];
+					return;
+				}
+			}
+		}
+
+		/* This is an ugly hack, but UNIX-type systems provide a "native" way to detect the file type,
+		 * which is still more secure than depending on the value of $_FILES[$field]['type'], and as it
+		 * was reported in issue #750 (https://github.com/EllisLab/CodeIgniter/issues/750) - it's better
+		 * than mime_content_type() as well, hence the attempts to try calling the command line with
+		 * three different functions.
+		 *
+		 * Notes:
+		 *	- the DIRECTORY_SEPARATOR comparison ensures that we're not on a Windows system
+		 *	- many system admins would disable the exec(), shell_exec(), popen() and similar functions
+		 *	  due to security concerns, hence the function_usable() checks
+		 */
+		if (DIRECTORY_SEPARATOR !== '\\')
+		{
+			$cmd = function_exists('escapeshellarg')
+				? 'file --brief --mime '.escapeshellarg($file['tmp_name']).' 2>&1'
+				: 'file --brief --mime '.$file['tmp_name'].' 2>&1';
+
+			if (function_usable('exec'))
+			{
+				/* This might look confusing, as $mime is being populated with all of the output when set in the second parameter.
+				 * However, we only need the last line, which is the actual return value of exec(), and as such - it overwrites
+				 * anything that could already be set for $mime previously. This effectively makes the second parameter a dummy
+				 * value, which is only put to allow us to get the return status code.
+				 */
+				$mime = @exec($cmd, $mime, $return_status);
+				if ($return_status === 0 && is_string($mime) && preg_match($regexp, $mime, $matches))
+				{
+					$this->file_type = $matches[1];
+					return;
+				}
+			}
+
+			if ( ! ini_get('safe_mode') && function_usable('shell_exec'))
+			{
+				$mime = @shell_exec($cmd);
+				if (strlen($mime) > 0)
+				{
+					$mime = explode("\n", trim($mime));
+					if (preg_match($regexp, $mime[(count($mime) - 1)], $matches))
+					{
+						$this->file_type = $matches[1];
+						return;
+					}
+				}
+			}
+
+			if (function_usable('popen'))
+			{
+				$proc = @popen($cmd, 'r');
+				if (is_resource($proc))
+				{
+					$mime = @fread($proc, 512);
+					@pclose($proc);
+					if ($mime !== FALSE)
+					{
+						$mime = explode("\n", trim($mime));
+						if (preg_match($regexp, $mime[(count($mime) - 1)], $matches))
+						{
+							$this->file_type = $matches[1];
+							return;
+						}
+					}
+				}
+			}
+		}
+
+		// Fall back to the deprecated mime_content_type(), if available (still better than $_FILES[$field]['type'])
+		if (function_exists('mime_content_type'))
+		{
+			$this->file_type = @mime_content_type($file['tmp_name']);
+			if (strlen($this->file_type) > 0) // It's possible that mime_content_type() returns FALSE or an empty string
+			{
+				return;
+			}
+		}
+
+		$this->file_type = $file['type'];
+	}
+
+}
diff --git a/README/User_agent.php b/README/User_agent.php
new file mode 100644
index 0000000..53d932a
--- /dev/null
+++ b/README/User_agent.php
@@ -0,0 +1,683 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * User Agent Class
+ *
+ * Identifies the platform, browser, robot, or mobile device of the browsing agent
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	User Agent
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/user_agent.html
+ */
+class CI_User_agent {
+
+	/**
+	 * Current user-agent
+	 *
+	 * @var string
+	 */
+	public $agent = NULL;
+
+	/**
+	 * Flag for if the user-agent belongs to a browser
+	 *
+	 * @var bool
+	 */
+	public $is_browser = FALSE;
+
+	/**
+	 * Flag for if the user-agent is a robot
+	 *
+	 * @var bool
+	 */
+	public $is_robot = FALSE;
+
+	/**
+	 * Flag for if the user-agent is a mobile browser
+	 *
+	 * @var bool
+	 */
+	public $is_mobile = FALSE;
+
+	/**
+	 * Languages accepted by the current user agent
+	 *
+	 * @var array
+	 */
+	public $languages = array();
+
+	/**
+	 * Character sets accepted by the current user agent
+	 *
+	 * @var array
+	 */
+	public $charsets = array();
+
+	/**
+	 * List of platforms to compare against current user agent
+	 *
+	 * @var array
+	 */
+	public $platforms = array();
+
+	/**
+	 * List of browsers to compare against current user agent
+	 *
+	 * @var array
+	 */
+	public $browsers = array();
+
+	/**
+	 * List of mobile browsers to compare against current user agent
+	 *
+	 * @var array
+	 */
+	public $mobiles = array();
+
+	/**
+	 * List of robots to compare against current user agent
+	 *
+	 * @var array
+	 */
+	public $robots = array();
+
+	/**
+	 * Current user-agent platform
+	 *
+	 * @var string
+	 */
+	public $platform = '';
+
+	/**
+	 * Current user-agent browser
+	 *
+	 * @var string
+	 */
+	public $browser = '';
+
+	/**
+	 * Current user-agent version
+	 *
+	 * @var string
+	 */
+	public $version = '';
+
+	/**
+	 * Current user-agent mobile name
+	 *
+	 * @var string
+	 */
+	public $mobile = '';
+
+	/**
+	 * Current user-agent robot name
+	 *
+	 * @var string
+	 */
+	public $robot = '';
+
+	/**
+	 * HTTP Referer
+	 *
+	 * @var	mixed
+	 */
+	public $referer;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * Sets the User Agent and runs the compilation routine
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		if (isset($_SERVER['HTTP_USER_AGENT']))
+		{
+			$this->agent = trim($_SERVER['HTTP_USER_AGENT']);
+		}
+
+		if ($this->agent !== NULL && $this->_load_agent_file())
+		{
+			$this->_compile_data();
+		}
+
+		log_message('info', 'User Agent Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile the User Agent Data
+	 *
+	 * @return	bool
+	 */
+	protected function _load_agent_file()
+	{
+		if (($found = file_exists(APPPATH.'config/user_agents.php')))
+		{
+			include(APPPATH.'config/user_agents.php');
+		}
+
+		if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/user_agents.php'))
+		{
+			include(APPPATH.'config/'.ENVIRONMENT.'/user_agents.php');
+			$found = TRUE;
+		}
+
+		if ($found !== TRUE)
+		{
+			return FALSE;
+		}
+
+		$return = FALSE;
+
+		if (isset($platforms))
+		{
+			$this->platforms = $platforms;
+			unset($platforms);
+			$return = TRUE;
+		}
+
+		if (isset($browsers))
+		{
+			$this->browsers = $browsers;
+			unset($browsers);
+			$return = TRUE;
+		}
+
+		if (isset($mobiles))
+		{
+			$this->mobiles = $mobiles;
+			unset($mobiles);
+			$return = TRUE;
+		}
+
+		if (isset($robots))
+		{
+			$this->robots = $robots;
+			unset($robots);
+			$return = TRUE;
+		}
+
+		return $return;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile the User Agent Data
+	 *
+	 * @return	bool
+	 */
+	protected function _compile_data()
+	{
+		$this->_set_platform();
+
+		foreach (array('_set_robot', '_set_browser', '_set_mobile') as $function)
+		{
+			if ($this->$function() === TRUE)
+			{
+				break;
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the Platform
+	 *
+	 * @return	bool
+	 */
+	protected function _set_platform()
+	{
+		if (is_array($this->platforms) && count($this->platforms) > 0)
+		{
+			foreach ($this->platforms as $key => $val)
+			{
+				if (preg_match('|'.preg_quote($key).'|i', $this->agent))
+				{
+					$this->platform = $val;
+					return TRUE;
+				}
+			}
+		}
+
+		$this->platform = 'Unknown Platform';
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the Browser
+	 *
+	 * @return	bool
+	 */
+	protected function _set_browser()
+	{
+		if (is_array($this->browsers) && count($this->browsers) > 0)
+		{
+			foreach ($this->browsers as $key => $val)
+			{
+				if (preg_match('|'.$key.'.*?([0-9\.]+)|i', $this->agent, $match))
+				{
+					$this->is_browser = TRUE;
+					$this->version = $match[1];
+					$this->browser = $val;
+					$this->_set_mobile();
+					return TRUE;
+				}
+			}
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the Robot
+	 *
+	 * @return	bool
+	 */
+	protected function _set_robot()
+	{
+		if (is_array($this->robots) && count($this->robots) > 0)
+		{
+			foreach ($this->robots as $key => $val)
+			{
+				if (preg_match('|'.preg_quote($key).'|i', $this->agent))
+				{
+					$this->is_robot = TRUE;
+					$this->robot = $val;
+					$this->_set_mobile();
+					return TRUE;
+				}
+			}
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the Mobile Device
+	 *
+	 * @return	bool
+	 */
+	protected function _set_mobile()
+	{
+		if (is_array($this->mobiles) && count($this->mobiles) > 0)
+		{
+			foreach ($this->mobiles as $key => $val)
+			{
+				if (FALSE !== (stripos($this->agent, $key)))
+				{
+					$this->is_mobile = TRUE;
+					$this->mobile = $val;
+					return TRUE;
+				}
+			}
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the accepted languages
+	 *
+	 * @return	void
+	 */
+	protected function _set_languages()
+	{
+		if ((count($this->languages) === 0) && ! empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
+		{
+			$this->languages = explode(',', preg_replace('/(;\s?q=[0-9\.]+)|\s/i', '', strtolower(trim($_SERVER['HTTP_ACCEPT_LANGUAGE']))));
+		}
+
+		if (count($this->languages) === 0)
+		{
+			$this->languages = array('Undefined');
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set the accepted character sets
+	 *
+	 * @return	void
+	 */
+	protected function _set_charsets()
+	{
+		if ((count($this->charsets) === 0) && ! empty($_SERVER['HTTP_ACCEPT_CHARSET']))
+		{
+			$this->charsets = explode(',', preg_replace('/(;\s?q=.+)|\s/i', '', strtolower(trim($_SERVER['HTTP_ACCEPT_CHARSET']))));
+		}
+
+		if (count($this->charsets) === 0)
+		{
+			$this->charsets = array('Undefined');
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is Browser
+	 *
+	 * @param	string	$key
+	 * @return	bool
+	 */
+	public function is_browser($key = NULL)
+	{
+		if ( ! $this->is_browser)
+		{
+			return FALSE;
+		}
+
+		// No need to be specific, it's a browser
+		if ($key === NULL)
+		{
+			return TRUE;
+		}
+
+		// Check for a specific browser
+		return (isset($this->browsers[$key]) && $this->browser === $this->browsers[$key]);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is Robot
+	 *
+	 * @param	string	$key
+	 * @return	bool
+	 */
+	public function is_robot($key = NULL)
+	{
+		if ( ! $this->is_robot)
+		{
+			return FALSE;
+		}
+
+		// No need to be specific, it's a robot
+		if ($key === NULL)
+		{
+			return TRUE;
+		}
+
+		// Check for a specific robot
+		return (isset($this->robots[$key]) && $this->robot === $this->robots[$key]);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is Mobile
+	 *
+	 * @param	string	$key
+	 * @return	bool
+	 */
+	public function is_mobile($key = NULL)
+	{
+		if ( ! $this->is_mobile)
+		{
+			return FALSE;
+		}
+
+		// No need to be specific, it's a mobile
+		if ($key === NULL)
+		{
+			return TRUE;
+		}
+
+		// Check for a specific robot
+		return (isset($this->mobiles[$key]) && $this->mobile === $this->mobiles[$key]);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is this a referral from another site?
+	 *
+	 * @return	bool
+	 */
+	public function is_referral()
+	{
+		if ( ! isset($this->referer))
+		{
+			if (empty($_SERVER['HTTP_REFERER']))
+			{
+				$this->referer = FALSE;
+			}
+			else
+			{
+				$referer_host = @parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
+				$own_host = parse_url(config_item('base_url'), PHP_URL_HOST);
+
+				$this->referer = ($referer_host && $referer_host !== $own_host);
+			}
+		}
+
+		return $this->referer;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Agent String
+	 *
+	 * @return	string
+	 */
+	public function agent_string()
+	{
+		return $this->agent;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Platform
+	 *
+	 * @return	string
+	 */
+	public function platform()
+	{
+		return $this->platform;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Browser Name
+	 *
+	 * @return	string
+	 */
+	public function browser()
+	{
+		return $this->browser;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the Browser Version
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		return $this->version;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get The Robot Name
+	 *
+	 * @return	string
+	 */
+	public function robot()
+	{
+		return $this->robot;
+	}
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the Mobile Device
+	 *
+	 * @return	string
+	 */
+	public function mobile()
+	{
+		return $this->mobile;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the referrer
+	 *
+	 * @return	bool
+	 */
+	public function referrer()
+	{
+		return empty($_SERVER['HTTP_REFERER']) ? '' : trim($_SERVER['HTTP_REFERER']);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the accepted languages
+	 *
+	 * @return	array
+	 */
+	public function languages()
+	{
+		if (count($this->languages) === 0)
+		{
+			$this->_set_languages();
+		}
+
+		return $this->languages;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the accepted Character Sets
+	 *
+	 * @return	array
+	 */
+	public function charsets()
+	{
+		if (count($this->charsets) === 0)
+		{
+			$this->_set_charsets();
+		}
+
+		return $this->charsets;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Test for a particular language
+	 *
+	 * @param	string	$lang
+	 * @return	bool
+	 */
+	public function accept_lang($lang = 'en')
+	{
+		return in_array(strtolower($lang), $this->languages(), TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Test for a particular character set
+	 *
+	 * @param	string	$charset
+	 * @return	bool
+	 */
+	public function accept_charset($charset = 'utf-8')
+	{
+		return in_array(strtolower($charset), $this->charsets(), TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse a custom user-agent string
+	 *
+	 * @param	string	$string
+	 * @return	void
+	 */
+	public function parse($string)
+	{
+		// Reset values
+		$this->is_browser = FALSE;
+		$this->is_robot = FALSE;
+		$this->is_mobile = FALSE;
+		$this->browser = '';
+		$this->version = '';
+		$this->mobile = '';
+		$this->robot = '';
+
+		// Set the new user-agent string and parse it, unless empty
+		$this->agent = $string;
+
+		if ( ! empty($string))
+		{
+			$this->_compile_data();
+		}
+	}
+
+}
diff --git a/README/Xmlrpc.php b/README/Xmlrpc.php
new file mode 100644
index 0000000..55555f5
--- /dev/null
+++ b/README/Xmlrpc.php
@@ -0,0 +1,1923 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+if ( ! function_exists('xml_parser_create'))
+{
+	show_error('Your PHP installation does not support XML');
+}
+
+// ------------------------------------------------------------------------
+
+/**
+ * XML-RPC request handler class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	XML-RPC
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/xmlrpc.html
+ */
+class CI_Xmlrpc {
+
+	/**
+	 * Debug flag
+	 *
+	 * @var	bool
+	 */
+	public $debug		= FALSE;
+
+	/**
+	 * I4 data type
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcI4	= 'i4';
+
+	/**
+	 * Integer data type
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcInt	= 'int';
+
+	/**
+	 * Boolean data type
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcBoolean	= 'boolean';
+
+	/**
+	 * Double data type
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcDouble	= 'double';
+
+	/**
+	 * String data type
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcString	= 'string';
+
+	/**
+	 * DateTime format
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcDateTime	= 'dateTime.iso8601';
+
+	/**
+	 * Base64 data type
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcBase64	= 'base64';
+
+	/**
+	 * Array data type
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcArray	= 'array';
+
+	/**
+	 * Struct data type
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcStruct	= 'struct';
+
+	/**
+	 * Data types list
+	 *
+	 * @var	array
+	 */
+	public $xmlrpcTypes	= array();
+
+	/**
+	 * Valid parents list
+	 *
+	 * @var	array
+	 */
+	public $valid_parents	= array();
+
+	/**
+	 * Response error numbers list
+	 *
+	 * @var	array
+	 */
+	public $xmlrpcerr		= array();
+
+	/**
+	 * Response error messages list
+	 *
+	 * @var	string[]
+	 */
+	public $xmlrpcstr		= array();
+
+	/**
+	 * Encoding charset
+	 *
+	 * @var	string
+	 */
+	public $xmlrpc_defencoding	= 'UTF-8';
+
+	/**
+	 * XML-RPC client name
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcName		= 'XML-RPC for CodeIgniter';
+
+	/**
+	 * XML-RPC version
+	 *
+	 * @var	string
+	 */
+	public $xmlrpcVersion		= '1.1';
+
+	/**
+	 * Start of user errors
+	 *
+	 * @var	int
+	 */
+	public $xmlrpcerruser		= 800;
+
+	/**
+	 * Start of XML parse errors
+	 *
+	 * @var	int
+	 */
+	public $xmlrpcerrxml		= 100;
+
+	/**
+	 * Backslash replacement value
+	 *
+	 * @var	string
+	 */
+	public $xmlrpc_backslash	= '';
+
+	/**
+	 * XML-RPC Client object
+	 *
+	 * @var	object
+	 */
+	public $client;
+
+	/**
+	 * XML-RPC Method name
+	 *
+	 * @var	string
+	 */
+	public $method;
+
+	/**
+	 * XML-RPC Data
+	 *
+	 * @var	array
+	 */
+	public $data;
+
+	/**
+	 * XML-RPC Message
+	 *
+	 * @var	string
+	 */
+	public $message			= '';
+
+	/**
+	 * Request error message
+	 *
+	 * @var	string
+	 */
+	public $error			= '';
+
+	/**
+	 * XML-RPC result object
+	 *
+	 * @var	object
+	 */
+	public $result;
+
+	/**
+	 * XML-RPC Reponse
+	 *
+	 * @var	array
+	 */
+	public $response		= array(); // Response from remote server
+
+	/**
+	 * XSS Filter flag
+	 *
+	 * @var	bool
+	 */
+	public $xss_clean		= TRUE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * Initializes property default values
+	 *
+	 * @param	array	$config
+	 * @return	void
+	 */
+	public function __construct($config = array())
+	{
+		$this->xmlrpc_backslash = chr(92).chr(92);
+
+		// Types for info sent back and forth
+		$this->xmlrpcTypes = array(
+			$this->xmlrpcI4	 		=> '1',
+			$this->xmlrpcInt		=> '1',
+			$this->xmlrpcBoolean	=> '1',
+			$this->xmlrpcString		=> '1',
+			$this->xmlrpcDouble		=> '1',
+			$this->xmlrpcDateTime	=> '1',
+			$this->xmlrpcBase64		=> '1',
+			$this->xmlrpcArray		=> '2',
+			$this->xmlrpcStruct		=> '3'
+		);
+
+		// Array of Valid Parents for Various XML-RPC elements
+		$this->valid_parents = array('BOOLEAN' => array('VALUE'),
+			'I4'				=> array('VALUE'),
+			'INT'				=> array('VALUE'),
+			'STRING'			=> array('VALUE'),
+			'DOUBLE'			=> array('VALUE'),
+			'DATETIME.ISO8601'	=> array('VALUE'),
+			'BASE64'			=> array('VALUE'),
+			'ARRAY'			=> array('VALUE'),
+			'STRUCT'			=> array('VALUE'),
+			'PARAM'			=> array('PARAMS'),
+			'METHODNAME'		=> array('METHODCALL'),
+			'PARAMS'			=> array('METHODCALL', 'METHODRESPONSE'),
+			'MEMBER'			=> array('STRUCT'),
+			'NAME'				=> array('MEMBER'),
+			'DATA'				=> array('ARRAY'),
+			'FAULT'			=> array('METHODRESPONSE'),
+			'VALUE'			=> array('MEMBER', 'DATA', 'PARAM', 'FAULT')
+		);
+
+		// XML-RPC Responses
+		$this->xmlrpcerr['unknown_method'] = '1';
+		$this->xmlrpcstr['unknown_method'] = 'This is not a known method for this XML-RPC Server';
+		$this->xmlrpcerr['invalid_return'] = '2';
+		$this->xmlrpcstr['invalid_return'] = 'The XML data received was either invalid or not in the correct form for XML-RPC. Turn on debugging to examine the XML data further.';
+		$this->xmlrpcerr['incorrect_params'] = '3';
+		$this->xmlrpcstr['incorrect_params'] = 'Incorrect parameters were passed to method';
+		$this->xmlrpcerr['introspect_unknown'] = '4';
+		$this->xmlrpcstr['introspect_unknown'] = 'Cannot inspect signature for request: method unknown';
+		$this->xmlrpcerr['http_error'] = '5';
+		$this->xmlrpcstr['http_error'] = "Did not receive a '200 OK' response from remote server.";
+		$this->xmlrpcerr['no_data'] = '6';
+		$this->xmlrpcstr['no_data'] = 'No data received from server.';
+
+		$this->initialize($config);
+
+		log_message('info', 'XML-RPC Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize
+	 *
+	 * @param	array	$config
+	 * @return	void
+	 */
+	public function initialize($config = array())
+	{
+		if (count($config) > 0)
+		{
+			foreach ($config as $key => $val)
+			{
+				if (isset($this->$key))
+				{
+					$this->$key = $val;
+				}
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse server URL
+	 *
+	 * @param	string	$url
+	 * @param	int	$port
+	 * @param	string	$proxy
+	 * @param	int	$proxy_port
+	 * @return	void
+	 */
+	public function server($url, $port = 80, $proxy = FALSE, $proxy_port = 8080)
+	{
+		if (strpos($url, 'http') !== 0)
+		{
+			$url = 'http://'.$url;
+		}
+
+		$parts = parse_url($url);
+
+		if (isset($parts['user'], $parts['pass']))
+		{
+			$parts['host'] = $parts['user'].':'.$parts['pass'].'@'.$parts['host'];
+		}
+
+		$path = isset($parts['path']) ? $parts['path'] : '/';
+
+		if ( ! empty($parts['query']))
+		{
+			$path .= '?'.$parts['query'];
+		}
+
+		$this->client = new XML_RPC_Client($path, $parts['host'], $port, $proxy, $proxy_port);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Timeout
+	 *
+	 * @param	int	$seconds
+	 * @return	void
+	 */
+	public function timeout($seconds = 5)
+	{
+		if ($this->client !== NULL && is_int($seconds))
+		{
+			$this->client->timeout = $seconds;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Methods
+	 *
+	 * @param	string	$function	Method name
+	 * @return	void
+	 */
+	public function method($function)
+	{
+		$this->method = $function;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Take Array of Data and Create Objects
+	 *
+	 * @param	array	$incoming
+	 * @return	void
+	 */
+	public function request($incoming)
+	{
+		if ( ! is_array($incoming))
+		{
+			// Send Error
+			return;
+		}
+
+		$this->data = array();
+
+		foreach ($incoming as $key => $value)
+		{
+			$this->data[$key] = $this->values_parsing($value);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Debug
+	 *
+	 * @param	bool	$flag
+	 * @return	void
+	 */
+	public function set_debug($flag = TRUE)
+	{
+		$this->debug = ($flag === TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Values Parsing
+	 *
+	 * @param	mixed	$value
+	 * @return	object
+	 */
+	public function values_parsing($value)
+	{
+		if (is_array($value) && array_key_exists(0, $value))
+		{
+			if ( ! isset($value[1], $this->xmlrpcTypes[$value[1]]))
+			{
+				$temp = new XML_RPC_Values($value[0], (is_array($value[0]) ? 'array' : 'string'));
+			}
+			else
+			{
+				if (is_array($value[0]) && ($value[1] === 'struct' OR $value[1] === 'array'))
+				{
+					while (list($k) = each($value[0]))
+					{
+						$value[0][$k] = $this->values_parsing($value[0][$k]);
+					}
+				}
+
+				$temp = new XML_RPC_Values($value[0], $value[1]);
+			}
+		}
+		else
+		{
+			$temp = new XML_RPC_Values($value, 'string');
+		}
+
+		return $temp;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Sends XML-RPC Request
+	 *
+	 * @return	bool
+	 */
+	public function send_request()
+	{
+		$this->message = new XML_RPC_Message($this->method, $this->data);
+		$this->message->debug = $this->debug;
+
+		if ( ! $this->result = $this->client->send($this->message) OR ! is_object($this->result->val))
+		{
+			$this->error = $this->result->errstr;
+			return FALSE;
+		}
+
+		$this->response = $this->result->decode();
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns Error
+	 *
+	 * @return	string
+	 */
+	public function display_error()
+	{
+		return $this->error;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns Remote Server Response
+	 *
+	 * @return	string
+	 */
+	public function display_response()
+	{
+		return $this->response;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Sends an Error Message for Server Request
+	 *
+	 * @param	int	$number
+	 * @param	string	$message
+	 * @return	object
+	 */
+	public function send_error_message($number, $message)
+	{
+		return new XML_RPC_Response(0, $number, $message);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send Response for Server Request
+	 *
+	 * @param	array	$response
+	 * @return	object
+	 */
+	public function send_response($response)
+	{
+		// $response should be array of values, which will be parsed
+		// based on their data and type into a valid group of XML-RPC values
+		return new XML_RPC_Response($this->values_parsing($response));
+	}
+
+} // END XML_RPC Class
+
+/**
+ * XML-RPC Client class
+ *
+ * @category	XML-RPC
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/xmlrpc.html
+ */
+class XML_RPC_Client extends CI_Xmlrpc
+{
+	/**
+	 * Path
+	 *
+	 * @var	string
+	 */
+	public $path			= '';
+
+	/**
+	 * Server hostname
+	 *
+	 * @var	string
+	 */
+	public $server			= '';
+
+	/**
+	 * Server port
+	 *
+	 * @var	int
+	 */
+	public $port			= 80;
+
+	/**
+	 *
+	 * Server username
+	 *
+	 * @var	string
+	 */
+	public $username;
+
+	/**
+	 * Server password
+	 *
+	 * @var	string
+	 */
+	public $password;
+
+	/**
+	 * Proxy hostname
+	 *
+	 * @var	string
+	 */
+	public $proxy			= FALSE;
+
+	/**
+	 * Proxy port
+	 *
+	 * @var	int
+	 */
+	public $proxy_port		= 8080;
+
+	/**
+	 * Error number
+	 *
+	 * @var	string
+	 */
+	public $errno			= '';
+
+	/**
+	 * Error message
+	 *
+	 * @var	string
+	 */
+	public $errstring		= '';
+
+	/**
+	 * Timeout in seconds
+	 *
+	 * @var	int
+	 */
+	public $timeout		= 5;
+
+	/**
+	 * No Multicall flag
+	 *
+	 * @var	bool
+	 */
+	public $no_multicall	= FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	string	$path
+	 * @param	object	$server
+	 * @param	int	$port
+	 * @param	string	$proxy
+	 * @param	int	$proxy_port
+	 * @return	void
+	 */
+	public function __construct($path, $server, $port = 80, $proxy = FALSE, $proxy_port = 8080)
+	{
+		parent::__construct();
+
+		$url = parse_url('http://'.$server);
+
+		if (isset($url['user'], $url['pass']))
+		{
+			$this->username = $url['user'];
+			$this->password = $url['pass'];
+		}
+
+		$this->port = $port;
+		$this->server = $url['host'];
+		$this->path = $path;
+		$this->proxy = $proxy;
+		$this->proxy_port = $proxy_port;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send message
+	 *
+	 * @param	mixed	$msg
+	 * @return	object
+	 */
+	public function send($msg)
+	{
+		if (is_array($msg))
+		{
+			// Multi-call disabled
+			return new XML_RPC_Response(0, $this->xmlrpcerr['multicall_recursion'], $this->xmlrpcstr['multicall_recursion']);
+		}
+
+		return $this->sendPayload($msg);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send payload
+	 *
+	 * @param	object	$msg
+	 * @return	object
+	 */
+	public function sendPayload($msg)
+	{
+		if ($this->proxy === FALSE)
+		{
+			$server = $this->server;
+			$port = $this->port;
+		}
+		else
+		{
+			$server = $this->proxy;
+			$port = $this->proxy_port;
+		}
+
+		$fp = @fsockopen($server, $port, $this->errno, $this->errstring, $this->timeout);
+
+		if ( ! is_resource($fp))
+		{
+			error_log($this->xmlrpcstr['http_error']);
+			return new XML_RPC_Response(0, $this->xmlrpcerr['http_error'], $this->xmlrpcstr['http_error']);
+		}
+
+		if (empty($msg->payload))
+		{
+			// $msg = XML_RPC_Messages
+			$msg->createPayload();
+		}
+
+		$r = "\r\n";
+		$op = 'POST '.$this->path.' HTTP/1.0'.$r
+			.'Host: '.$this->server.$r
+			.'Content-Type: text/xml'.$r
+			.(isset($this->username, $this->password) ? 'Authorization: Basic '.base64_encode($this->username.':'.$this->password).$r : '')
+			.'User-Agent: '.$this->xmlrpcName.$r
+			.'Content-Length: '.strlen($msg->payload).$r.$r
+			.$msg->payload;
+
+		for ($written = $timestamp = 0, $length = strlen($op); $written < $length; $written += $result)
+		{
+			if (($result = fwrite($fp, substr($op, $written))) === FALSE)
+			{
+				break;
+			}
+			// See https://bugs.php.net/bug.php?id=39598 and http://php.net/manual/en/function.fwrite.php#96951
+			elseif ($result === 0)
+			{
+				if ($timestamp === 0)
+				{
+					$timestamp = time();
+				}
+				elseif ($timestamp < (time() - $this->timeout))
+				{
+					$result = FALSE;
+					break;
+				}
+
+				usleep(250000);
+				continue;
+			}
+			else
+			{
+				$timestamp = 0;
+			}
+		}
+
+		if ($result === FALSE)
+		{
+			error_log($this->xmlrpcstr['http_error']);
+			return new XML_RPC_Response(0, $this->xmlrpcerr['http_error'], $this->xmlrpcstr['http_error']);
+		}
+
+		$resp = $msg->parseResponse($fp);
+		fclose($fp);
+		return $resp;
+	}
+
+} // END XML_RPC_Client Class
+
+/**
+ * XML-RPC Response class
+ *
+ * @category	XML-RPC
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/xmlrpc.html
+ */
+class XML_RPC_Response
+{
+
+	/**
+	 * Value
+	 *
+	 * @var	mixed
+	 */
+	public $val		= 0;
+
+	/**
+	 * Error number
+	 *
+	 * @var	int
+	 */
+	public $errno		= 0;
+
+	/**
+	 * Error message
+	 *
+	 * @var	string
+	 */
+	public $errstr		= '';
+
+	/**
+	 * Headers list
+	 *
+	 * @var	array
+	 */
+	public $headers		= array();
+
+	/**
+	 * XSS Filter flag
+	 *
+	 * @var	bool
+	 */
+	public $xss_clean	= TRUE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	mixed	$val
+	 * @param	int	$code
+	 * @param	string	$fstr
+	 * @return	void
+	 */
+	public function __construct($val, $code = 0, $fstr = '')
+	{
+		if ($code !== 0)
+		{
+			// error
+			$this->errno = $code;
+			$this->errstr = htmlspecialchars($fstr,
+							(is_php('5.4') ? ENT_XML1 | ENT_NOQUOTES : ENT_NOQUOTES),
+							'UTF-8');
+		}
+		elseif ( ! is_object($val))
+		{
+			// programmer error, not an object
+			error_log("Invalid type '".gettype($val)."' (value: ".$val.') passed to XML_RPC_Response. Defaulting to empty value.');
+			$this->val = new XML_RPC_Values();
+		}
+		else
+		{
+			$this->val = $val;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fault code
+	 *
+	 * @return	int
+	 */
+	public function faultCode()
+	{
+		return $this->errno;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fault string
+	 *
+	 * @return	string
+	 */
+	public function faultString()
+	{
+		return $this->errstr;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Value
+	 *
+	 * @return	mixed
+	 */
+	public function value()
+	{
+		return $this->val;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prepare response
+	 *
+	 * @return	string	xml
+	 */
+	public function prepare_response()
+	{
+		return "<methodResponse>\n"
+			.($this->errno
+				? '<fault>
+	<value>
+		<struct>
+			<member>
+				<name>faultCode</name>
+				<value><int>'.$this->errno.'</int></value>
+			</member>
+			<member>
+				<name>faultString</name>
+				<value><string>'.$this->errstr.'</string></value>
+			</member>
+		</struct>
+	</value>
+</fault>'
+				: "<params>\n<param>\n".$this->val->serialize_class()."</param>\n</params>")
+			."\n</methodResponse>";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Decode
+	 *
+	 * @param	mixed	$array
+	 * @return	array
+	 */
+	public function decode($array = NULL)
+	{
+		$CI =& get_instance();
+
+		if (is_array($array))
+		{
+			while (list($key) = each($array))
+			{
+				if (is_array($array[$key]))
+				{
+					$array[$key] = $this->decode($array[$key]);
+				}
+				elseif ($this->xss_clean)
+				{
+					$array[$key] = $CI->security->xss_clean($array[$key]);
+				}
+			}
+
+			return $array;
+		}
+
+		$result = $this->xmlrpc_decoder($this->val);
+
+		if (is_array($result))
+		{
+			$result = $this->decode($result);
+		}
+		elseif ($this->xss_clean)
+		{
+			$result = $CI->security->xss_clean($result);
+		}
+
+		return $result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * XML-RPC Object to PHP Types
+	 *
+	 * @param	object
+	 * @return	array
+	 */
+	public function xmlrpc_decoder($xmlrpc_val)
+	{
+		$kind = $xmlrpc_val->kindOf();
+
+		if ($kind === 'scalar')
+		{
+			return $xmlrpc_val->scalarval();
+		}
+		elseif ($kind === 'array')
+		{
+			reset($xmlrpc_val->me);
+			$b = current($xmlrpc_val->me);
+			$arr = array();
+
+			for ($i = 0, $size = count($b); $i < $size; $i++)
+			{
+				$arr[] = $this->xmlrpc_decoder($xmlrpc_val->me['array'][$i]);
+			}
+			return $arr;
+		}
+		elseif ($kind === 'struct')
+		{
+			reset($xmlrpc_val->me['struct']);
+			$arr = array();
+
+			while (list($key,$value) = each($xmlrpc_val->me['struct']))
+			{
+				$arr[$key] = $this->xmlrpc_decoder($value);
+			}
+			return $arr;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ISO-8601 time to server or UTC time
+	 *
+	 * @param	string
+	 * @param	bool
+	 * @return	int	unix timestamp
+	 */
+	public function iso8601_decode($time, $utc = FALSE)
+	{
+		// Return a time in the localtime, or UTC
+		$t = 0;
+		if (preg_match('/([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/', $time, $regs))
+		{
+			$fnc = ($utc === TRUE) ? 'gmmktime' : 'mktime';
+			$t = $fnc($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]);
+		}
+		return $t;
+	}
+
+} // END XML_RPC_Response Class
+
+/**
+ * XML-RPC Message class
+ *
+ * @category	XML-RPC
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/xmlrpc.html
+ */
+class XML_RPC_Message extends CI_Xmlrpc
+{
+
+	/**
+	 * Payload
+	 *
+	 * @var	string
+	 */
+	public $payload;
+
+	/**
+	 * Method name
+	 *
+	 * @var	string
+	 */
+	public $method_name;
+
+	/**
+	 * Parameter list
+	 *
+	 * @var	array
+	 */
+	public $params		= array();
+
+	/**
+	 * XH?
+	 *
+	 * @var	array
+	 */
+	public $xh		= array();
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	string	$method
+	 * @param	array	$pars
+	 * @return	void
+	 */
+	public function __construct($method, $pars = FALSE)
+	{
+		parent::__construct();
+
+		$this->method_name = $method;
+		if (is_array($pars) && count($pars) > 0)
+		{
+			for ($i = 0, $c = count($pars); $i < $c; $i++)
+			{
+				// $pars[$i] = XML_RPC_Values
+				$this->params[] = $pars[$i];
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create Payload to Send
+	 *
+	 * @return	void
+	 */
+	public function createPayload()
+	{
+		$this->payload = '<?xml version="1.0"?'.">\r\n<methodCall>\r\n"
+				.'<methodName>'.$this->method_name."</methodName>\r\n"
+				."<params>\r\n";
+
+		for ($i = 0, $c = count($this->params); $i < $c; $i++)
+		{
+			// $p = XML_RPC_Values
+			$p = $this->params[$i];
+			$this->payload .= "<param>\r\n".$p->serialize_class()."</param>\r\n";
+		}
+
+		$this->payload .= "</params>\r\n</methodCall>\r\n";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse External XML-RPC Server's Response
+	 *
+	 * @param	resource
+	 * @return	object
+	 */
+	public function parseResponse($fp)
+	{
+		$data = '';
+
+		while ($datum = fread($fp, 4096))
+		{
+			$data .= $datum;
+		}
+
+		// Display HTTP content for debugging
+		if ($this->debug === TRUE)
+		{
+			echo "<pre>---DATA---\n".htmlspecialchars($data)."\n---END DATA---\n\n</pre>";
+		}
+
+		// Check for data
+		if ($data === '')
+		{
+			error_log($this->xmlrpcstr['no_data']);
+			return new XML_RPC_Response(0, $this->xmlrpcerr['no_data'], $this->xmlrpcstr['no_data']);
+		}
+
+		// Check for HTTP 200 Response
+		if (strpos($data, 'HTTP') === 0 && ! preg_match('/^HTTP\/[0-9\.]+ 200 /', $data))
+		{
+			$errstr = substr($data, 0, strpos($data, "\n")-1);
+			return new XML_RPC_Response(0, $this->xmlrpcerr['http_error'], $this->xmlrpcstr['http_error'].' ('.$errstr.')');
+		}
+
+		//-------------------------------------
+		// Create and Set Up XML Parser
+		//-------------------------------------
+
+		$parser = xml_parser_create($this->xmlrpc_defencoding);
+		$pname = (string) $parser;
+		$this->xh[$pname] = array(
+			'isf'		=> 0,
+			'ac'		=> '',
+			'headers'	=> array(),
+			'stack'		=> array(),
+			'valuestack'	=> array(),
+			'isf_reason'	=> 0
+		);
+
+		xml_set_object($parser, $this);
+		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, TRUE);
+		xml_set_element_handler($parser, 'open_tag', 'closing_tag');
+		xml_set_character_data_handler($parser, 'character_data');
+		//xml_set_default_handler($parser, 'default_handler');
+
+		// Get headers
+		$lines = explode("\r\n", $data);
+		while (($line = array_shift($lines)))
+		{
+			if (strlen($line) < 1)
+			{
+				break;
+			}
+			$this->xh[$pname]['headers'][] = $line;
+		}
+		$data = implode("\r\n", $lines);
+
+		// Parse XML data
+		if ( ! xml_parse($parser, $data, count($data)))
+		{
+			$errstr = sprintf('XML error: %s at line %d',
+						xml_error_string(xml_get_error_code($parser)),
+						xml_get_current_line_number($parser));
+
+			$r = new XML_RPC_Response(0, $this->xmlrpcerr['invalid_return'], $this->xmlrpcstr['invalid_return']);
+			xml_parser_free($parser);
+			return $r;
+		}
+		xml_parser_free($parser);
+
+		// Got ourselves some badness, it seems
+		if ($this->xh[$pname]['isf'] > 1)
+		{
+			if ($this->debug === TRUE)
+			{
+				echo "---Invalid Return---\n".$this->xh[$pname]['isf_reason']."---Invalid Return---\n\n";
+			}
+
+			return new XML_RPC_Response(0, $this->xmlrpcerr['invalid_return'], $this->xmlrpcstr['invalid_return'].' '.$this->xh[$pname]['isf_reason']);
+		}
+		elseif ( ! is_object($this->xh[$pname]['value']))
+		{
+			return new XML_RPC_Response(0, $this->xmlrpcerr['invalid_return'], $this->xmlrpcstr['invalid_return'].' '.$this->xh[$pname]['isf_reason']);
+		}
+
+		// Display XML content for debugging
+		if ($this->debug === TRUE)
+		{
+			echo '<pre>';
+
+			if (count($this->xh[$pname]['headers'] > 0))
+			{
+				echo "---HEADERS---\n";
+				foreach ($this->xh[$pname]['headers'] as $header)
+				{
+					echo $header."\n";
+				}
+				echo "---END HEADERS---\n\n";
+			}
+
+			echo "---DATA---\n".htmlspecialchars($data)."\n---END DATA---\n\n---PARSED---\n";
+			var_dump($this->xh[$pname]['value']);
+			echo "\n---END PARSED---</pre>";
+		}
+
+		// Send response
+		$v = $this->xh[$pname]['value'];
+		if ($this->xh[$pname]['isf'])
+		{
+			$errno_v = $v->me['struct']['faultCode'];
+			$errstr_v = $v->me['struct']['faultString'];
+			$errno = $errno_v->scalarval();
+
+			if ($errno === 0)
+			{
+				// FAULT returned, errno needs to reflect that
+				$errno = -1;
+			}
+
+			$r = new XML_RPC_Response($v, $errno, $errstr_v->scalarval());
+		}
+		else
+		{
+			$r = new XML_RPC_Response($v);
+		}
+
+		$r->headers = $this->xh[$pname]['headers'];
+		return $r;
+	}
+
+	// --------------------------------------------------------------------
+
+	// ------------------------------------
+	//  Begin Return Message Parsing section
+	// ------------------------------------
+
+	// quick explanation of components:
+	//   ac - used to accumulate values
+	//   isf - used to indicate a fault
+	//   lv - used to indicate "looking for a value": implements
+	//		the logic to allow values with no types to be strings
+	//   params - used to store parameters in method calls
+	//   method - used to store method name
+	//	 stack - array with parent tree of the xml element,
+	//			 used to validate the nesting of elements
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Start Element Handler
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	void
+	 */
+	public function open_tag($the_parser, $name)
+	{
+		$the_parser = (string) $the_parser;
+
+		// If invalid nesting, then return
+		if ($this->xh[$the_parser]['isf'] > 1) return;
+
+		// Evaluate and check for correct nesting of XML elements
+		if (count($this->xh[$the_parser]['stack']) === 0)
+		{
+			if ($name !== 'METHODRESPONSE' && $name !== 'METHODCALL')
+			{
+				$this->xh[$the_parser]['isf'] = 2;
+				$this->xh[$the_parser]['isf_reason'] = 'Top level XML-RPC element is missing';
+				return;
+			}
+		}
+		// not top level element: see if parent is OK
+		elseif ( ! in_array($this->xh[$the_parser]['stack'][0], $this->valid_parents[$name], TRUE))
+		{
+			$this->xh[$the_parser]['isf'] = 2;
+			$this->xh[$the_parser]['isf_reason'] = 'XML-RPC element '.$name.' cannot be child of '.$this->xh[$the_parser]['stack'][0];
+			return;
+		}
+
+		switch ($name)
+		{
+			case 'STRUCT':
+			case 'ARRAY':
+				// Creates array for child elements
+				$cur_val = array('value' => array(), 'type' => $name);
+				array_unshift($this->xh[$the_parser]['valuestack'], $cur_val);
+				break;
+			case 'METHODNAME':
+			case 'NAME':
+				$this->xh[$the_parser]['ac'] = '';
+				break;
+			case 'FAULT':
+				$this->xh[$the_parser]['isf'] = 1;
+				break;
+			case 'PARAM':
+				$this->xh[$the_parser]['value'] = NULL;
+				break;
+			case 'VALUE':
+				$this->xh[$the_parser]['vt'] = 'value';
+				$this->xh[$the_parser]['ac'] = '';
+				$this->xh[$the_parser]['lv'] = 1;
+				break;
+			case 'I4':
+			case 'INT':
+			case 'STRING':
+			case 'BOOLEAN':
+			case 'DOUBLE':
+			case 'DATETIME.ISO8601':
+			case 'BASE64':
+				if ($this->xh[$the_parser]['vt'] !== 'value')
+				{
+					//two data elements inside a value: an error occurred!
+					$this->xh[$the_parser]['isf'] = 2;
+					$this->xh[$the_parser]['isf_reason'] = 'There is a '.$name.' element following a '
+										.$this->xh[$the_parser]['vt'].' element inside a single value';
+					return;
+				}
+
+				$this->xh[$the_parser]['ac'] = '';
+				break;
+			case 'MEMBER':
+				// Set name of <member> to nothing to prevent errors later if no <name> is found
+				$this->xh[$the_parser]['valuestack'][0]['name'] = '';
+
+				// Set NULL value to check to see if value passed for this param/member
+				$this->xh[$the_parser]['value'] = NULL;
+				break;
+			case 'DATA':
+			case 'METHODCALL':
+			case 'METHODRESPONSE':
+			case 'PARAMS':
+				// valid elements that add little to processing
+				break;
+			default:
+				/// An Invalid Element is Found, so we have trouble
+				$this->xh[$the_parser]['isf'] = 2;
+				$this->xh[$the_parser]['isf_reason'] = 'Invalid XML-RPC element found: '.$name;
+				break;
+		}
+
+		// Add current element name to stack, to allow validation of nesting
+		array_unshift($this->xh[$the_parser]['stack'], $name);
+
+		$name === 'VALUE' OR $this->xh[$the_parser]['lv'] = 0;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * End Element Handler
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	void
+	 */
+	public function closing_tag($the_parser, $name)
+	{
+		$the_parser = (string) $the_parser;
+
+		if ($this->xh[$the_parser]['isf'] > 1) return;
+
+		// Remove current element from stack and set variable
+		// NOTE: If the XML validates, then we do not have to worry about
+		// the opening and closing of elements. Nesting is checked on the opening
+		// tag so we be safe there as well.
+
+		$curr_elem = array_shift($this->xh[$the_parser]['stack']);
+
+		switch ($name)
+		{
+			case 'STRUCT':
+			case 'ARRAY':
+				$cur_val = array_shift($this->xh[$the_parser]['valuestack']);
+				$this->xh[$the_parser]['value'] = isset($cur_val['values']) ? $cur_val['values'] : array();
+				$this->xh[$the_parser]['vt']	= strtolower($name);
+				break;
+			case 'NAME':
+				$this->xh[$the_parser]['valuestack'][0]['name'] = $this->xh[$the_parser]['ac'];
+				break;
+			case 'BOOLEAN':
+			case 'I4':
+			case 'INT':
+			case 'STRING':
+			case 'DOUBLE':
+			case 'DATETIME.ISO8601':
+			case 'BASE64':
+				$this->xh[$the_parser]['vt'] = strtolower($name);
+
+				if ($name === 'STRING')
+				{
+					$this->xh[$the_parser]['value'] = $this->xh[$the_parser]['ac'];
+				}
+				elseif ($name === 'DATETIME.ISO8601')
+				{
+					$this->xh[$the_parser]['vt']	= $this->xmlrpcDateTime;
+					$this->xh[$the_parser]['value'] = $this->xh[$the_parser]['ac'];
+				}
+				elseif ($name === 'BASE64')
+				{
+					$this->xh[$the_parser]['value'] = base64_decode($this->xh[$the_parser]['ac']);
+				}
+				elseif ($name === 'BOOLEAN')
+				{
+					// Translated BOOLEAN values to TRUE AND FALSE
+					$this->xh[$the_parser]['value'] = (bool) $this->xh[$the_parser]['ac'];
+				}
+				elseif ($name=='DOUBLE')
+				{
+					// we have a DOUBLE
+					// we must check that only 0123456789-.<space> are characters here
+					$this->xh[$the_parser]['value'] = preg_match('/^[+-]?[eE0-9\t \.]+$/', $this->xh[$the_parser]['ac'])
+										? (float) $this->xh[$the_parser]['ac']
+										: 'ERROR_NON_NUMERIC_FOUND';
+				}
+				else
+				{
+					// we have an I4/INT
+					// we must check that only 0123456789-<space> are characters here
+					$this->xh[$the_parser]['value'] = preg_match('/^[+-]?[0-9\t ]+$/', $this->xh[$the_parser]['ac'])
+										? (int) $this->xh[$the_parser]['ac']
+										: 'ERROR_NON_NUMERIC_FOUND';
+				}
+				$this->xh[$the_parser]['ac'] = '';
+				$this->xh[$the_parser]['lv'] = 3; // indicate we've found a value
+				break;
+			case 'VALUE':
+				// This if() detects if no scalar was inside <VALUE></VALUE>
+				if ($this->xh[$the_parser]['vt'] == 'value')
+				{
+					$this->xh[$the_parser]['value']	= $this->xh[$the_parser]['ac'];
+					$this->xh[$the_parser]['vt']	= $this->xmlrpcString;
+				}
+
+				// build the XML-RPC value out of the data received, and substitute it
+				$temp = new XML_RPC_Values($this->xh[$the_parser]['value'], $this->xh[$the_parser]['vt']);
+
+				if (count($this->xh[$the_parser]['valuestack']) && $this->xh[$the_parser]['valuestack'][0]['type'] === 'ARRAY')
+				{
+					// Array
+					$this->xh[$the_parser]['valuestack'][0]['values'][] = $temp;
+				}
+				else
+				{
+					// Struct
+					$this->xh[$the_parser]['value'] = $temp;
+				}
+				break;
+			case 'MEMBER':
+				$this->xh[$the_parser]['ac'] = '';
+
+				// If value add to array in the stack for the last element built
+				if ($this->xh[$the_parser]['value'])
+				{
+					$this->xh[$the_parser]['valuestack'][0]['values'][$this->xh[$the_parser]['valuestack'][0]['name']] = $this->xh[$the_parser]['value'];
+				}
+				break;
+			case 'DATA':
+				$this->xh[$the_parser]['ac'] = '';
+				break;
+			case 'PARAM':
+				if ($this->xh[$the_parser]['value'])
+				{
+					$this->xh[$the_parser]['params'][] = $this->xh[$the_parser]['value'];
+				}
+				break;
+			case 'METHODNAME':
+				$this->xh[$the_parser]['method'] = ltrim($this->xh[$the_parser]['ac']);
+				break;
+			case 'PARAMS':
+			case 'FAULT':
+			case 'METHODCALL':
+			case 'METHORESPONSE':
+				// We're all good kids with nuthin' to do
+				break;
+			default:
+				// End of an Invalid Element. Taken care of during the opening tag though
+				break;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse character data
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	void
+	 */
+	public function character_data($the_parser, $data)
+	{
+		$the_parser = (string) $the_parser;
+
+		if ($this->xh[$the_parser]['isf'] > 1) return; // XML Fault found already
+
+		// If a value has not been found
+		if ($this->xh[$the_parser]['lv'] !== 3)
+		{
+			if ($this->xh[$the_parser]['lv'] === 1)
+			{
+				$this->xh[$the_parser]['lv'] = 2; // Found a value
+			}
+
+			if ( ! isset($this->xh[$the_parser]['ac']))
+			{
+				$this->xh[$the_parser]['ac'] = '';
+			}
+
+			$this->xh[$the_parser]['ac'] .= $data;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add parameter
+	 *
+	 * @param	mixed
+	 * @return	void
+	 */
+	public function addParam($par)
+	{
+		$this->params[] = $par;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Output parameters
+	 *
+	 * @param	array	$array
+	 * @return	array
+	 */
+	public function output_parameters(array $array = array())
+	{
+		$CI =& get_instance();
+
+		if ( ! empty($array))
+		{
+			while (list($key) = each($array))
+			{
+				if (is_array($array[$key]))
+				{
+					$array[$key] = $this->output_parameters($array[$key]);
+				}
+				elseif ($key !== 'bits' && $this->xss_clean)
+				{
+					// 'bits' is for the MetaWeblog API image bits
+					// @todo - this needs to be made more general purpose
+					$array[$key] = $CI->security->xss_clean($array[$key]);
+				}
+			}
+
+			return $array;
+		}
+
+		$parameters = array();
+
+		for ($i = 0, $c = count($this->params); $i < $c; $i++)
+		{
+			$a_param = $this->decode_message($this->params[$i]);
+
+			if (is_array($a_param))
+			{
+				$parameters[] = $this->output_parameters($a_param);
+			}
+			else
+			{
+				$parameters[] = ($this->xss_clean) ? $CI->security->xss_clean($a_param) : $a_param;
+			}
+		}
+
+		return $parameters;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Decode message
+	 *
+	 * @param	object
+	 * @return	mixed
+	 */
+	public function decode_message($param)
+	{
+		$kind = $param->kindOf();
+
+		if ($kind === 'scalar')
+		{
+			return $param->scalarval();
+		}
+		elseif ($kind === 'array')
+		{
+			reset($param->me);
+			$b = current($param->me);
+			$arr = array();
+
+			for ($i = 0, $c = count($b); $i < $c; $i++)
+			{
+				$arr[] = $this->decode_message($param->me['array'][$i]);
+			}
+
+			return $arr;
+		}
+		elseif ($kind === 'struct')
+		{
+			reset($param->me['struct']);
+			$arr = array();
+
+			while (list($key,$value) = each($param->me['struct']))
+			{
+				$arr[$key] = $this->decode_message($value);
+			}
+
+			return $arr;
+		}
+	}
+
+} // END XML_RPC_Message Class
+
+/**
+ * XML-RPC Values class
+ *
+ * @category	XML-RPC
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/xmlrpc.html
+ */
+class XML_RPC_Values extends CI_Xmlrpc
+{
+	/**
+	 * Value data
+	 *
+	 * @var	array
+	 */
+	public $me	= array();
+
+	/**
+	 * Value type
+	 *
+	 * @var	int
+	 */
+	public $mytype	= 0;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	mixed	$val
+	 * @param	string	$type
+	 * @return	void
+	 */
+	public function __construct($val = -1, $type = '')
+	{
+		parent::__construct();
+
+		if ($val !== -1 OR $type !== '')
+		{
+			$type = $type === '' ? 'string' : $type;
+
+			if ($this->xmlrpcTypes[$type] == 1)
+			{
+				$this->addScalar($val, $type);
+			}
+			elseif ($this->xmlrpcTypes[$type] == 2)
+			{
+				$this->addArray($val);
+			}
+			elseif ($this->xmlrpcTypes[$type] == 3)
+			{
+				$this->addStruct($val);
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add scalar value
+	 *
+	 * @param	scalar
+	 * @param	string
+	 * @return	int
+	 */
+	public function addScalar($val, $type = 'string')
+	{
+		$typeof = $this->xmlrpcTypes[$type];
+
+		if ($this->mytype === 1)
+		{
+			echo '<strong>XML_RPC_Values</strong>: scalar can have only one value<br />';
+			return 0;
+		}
+
+		if ($typeof != 1)
+		{
+			echo '<strong>XML_RPC_Values</strong>: not a scalar type (${typeof})<br />';
+			return 0;
+		}
+
+		if ($type === $this->xmlrpcBoolean)
+		{
+			$val = (int) (strcasecmp($val, 'true') === 0 OR $val === 1 OR ($val === TRUE && strcasecmp($val, 'false')));
+		}
+
+		if ($this->mytype === 2)
+		{
+			// adding to an array here
+			$ar = $this->me['array'];
+			$ar[] = new XML_RPC_Values($val, $type);
+			$this->me['array'] = $ar;
+		}
+		else
+		{
+			// a scalar, so set the value and remember we're scalar
+			$this->me[$type] = $val;
+			$this->mytype = $typeof;
+		}
+
+		return 1;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add array value
+	 *
+	 * @param	array
+	 * @return	int
+	 */
+	public function addArray($vals)
+	{
+		if ($this->mytype !== 0)
+		{
+			echo '<strong>XML_RPC_Values</strong>: already initialized as a ['.$this->kindOf().']<br />';
+			return 0;
+		}
+
+		$this->mytype = $this->xmlrpcTypes['array'];
+		$this->me['array'] = $vals;
+		return 1;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add struct value
+	 *
+	 * @param	object
+	 * @return	int
+	 */
+	public function addStruct($vals)
+	{
+		if ($this->mytype !== 0)
+		{
+			echo '<strong>XML_RPC_Values</strong>: already initialized as a ['.$this->kindOf().']<br />';
+			return 0;
+		}
+		$this->mytype = $this->xmlrpcTypes['struct'];
+		$this->me['struct'] = $vals;
+		return 1;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get value type
+	 *
+	 * @return	string
+	 */
+	public function kindOf()
+	{
+		switch ($this->mytype)
+		{
+			case 3: return 'struct';
+			case 2: return 'array';
+			case 1: return 'scalar';
+			default: return 'undef';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Serialize data
+	 *
+	 * @param	string
+	 * @param	mixed
+	 * @return	string
+	 */
+	public function serializedata($typ, $val)
+	{
+		$rs = '';
+
+		switch ($this->xmlrpcTypes[$typ])
+		{
+			case 3:
+				// struct
+				$rs .= "<struct>\n";
+				reset($val);
+				while (list($key2, $val2) = each($val))
+				{
+					$rs .= "<member>\n<name>{$key2}</name>\n".$this->serializeval($val2)."</member>\n";
+				}
+				$rs .= '</struct>';
+				break;
+			case 2:
+				// array
+				$rs .= "<array>\n<data>\n";
+				for ($i = 0, $c = count($val); $i < $c; $i++)
+				{
+					$rs .= $this->serializeval($val[$i]);
+				}
+				$rs .= "</data>\n</array>\n";
+				break;
+			case 1:
+				// others
+				switch ($typ)
+				{
+					case $this->xmlrpcBase64:
+						$rs .= '<'.$typ.'>'.base64_encode( (string) $val).'</'.$typ.">\n";
+						break;
+					case $this->xmlrpcBoolean:
+						$rs .= '<'.$typ.'>'.( (bool) $val ? '1' : '0').'</'.$typ.">\n";
+						break;
+					case $this->xmlrpcString:
+						$rs .= '<'.$typ.'>'.htmlspecialchars( (string) $val).'</'.$typ.">\n";
+						break;
+					default:
+						$rs .= '<'.$typ.'>'.$val.'</'.$typ.">\n";
+						break;
+				}
+			default:
+				break;
+		}
+
+		return $rs;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Serialize class
+	 *
+	 * @return	string
+	 */
+	public function serialize_class()
+	{
+		return $this->serializeval($this);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Serialize value
+	 *
+	 * @param	object
+	 * @return	string
+	 */
+	public function serializeval($o)
+	{
+		$ar = $o->me;
+		reset($ar);
+
+		list($typ, $val) = each($ar);
+		return "<value>\n".$this->serializedata($typ, $val)."</value>\n";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Scalar value
+	 *
+	 * @return	mixed
+	 */
+	public function scalarval()
+	{
+		reset($this->me);
+		return current($this->me);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Encode time in ISO-8601 form.
+	 * Useful for sending time in XML-RPC
+	 *
+	 * @param	int	unix timestamp
+	 * @param	bool
+	 * @return	string
+	*/
+	public function iso8601_encode($time, $utc = FALSE)
+	{
+		return ($utc) ? strftime('%Y%m%dT%H:%i:%s', $time) : gmstrftime('%Y%m%dT%H:%i:%s', $time);
+	}
+
+} // END XML_RPC_Values Class
diff --git a/README/Xmlrpcs.php b/README/Xmlrpcs.php
new file mode 100644
index 0000000..00d1fec
--- /dev/null
+++ b/README/Xmlrpcs.php
@@ -0,0 +1,625 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+if ( ! function_exists('xml_parser_create'))
+{
+	show_error('Your PHP installation does not support XML');
+}
+
+if ( ! class_exists('CI_Xmlrpc', FALSE))
+{
+	show_error('You must load the Xmlrpc class before loading the Xmlrpcs class in order to create a server.');
+}
+
+// ------------------------------------------------------------------------
+
+/**
+ * XML-RPC server class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	XML-RPC
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/xmlrpc.html
+ */
+class CI_Xmlrpcs extends CI_Xmlrpc {
+
+	/**
+	 * Array of methods mapped to function names and signatures
+	 *
+	 * @var array
+	 */
+	public $methods = array();
+
+	/**
+	 * Debug Message
+	 *
+	 * @var string
+	 */
+	public $debug_msg = '';
+
+	/**
+	 * XML RPC Server methods
+	 *
+	 * @var array
+	 */
+	public $system_methods	= array();
+
+	/**
+	 * Configuration object
+	 *
+	 * @var object
+	 */
+	public $object = FALSE;
+
+	/**
+	 * Initialize XMLRPC class
+	 *
+	 * @param	array	$config
+	 * @return	void
+	 */
+	public function __construct($config = array())
+	{
+		parent::__construct();
+		$this->set_system_methods();
+
+		if (isset($config['functions']) && is_array($config['functions']))
+		{
+			$this->methods = array_merge($this->methods, $config['functions']);
+		}
+
+		log_message('info', 'XML-RPC Server Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize Prefs and Serve
+	 *
+	 * @param	mixed
+	 * @return	void
+	 */
+	public function initialize($config = array())
+	{
+		if (isset($config['functions']) && is_array($config['functions']))
+		{
+			$this->methods = array_merge($this->methods, $config['functions']);
+		}
+
+		if (isset($config['debug']))
+		{
+			$this->debug = $config['debug'];
+		}
+
+		if (isset($config['object']) && is_object($config['object']))
+		{
+			$this->object = $config['object'];
+		}
+
+		if (isset($config['xss_clean']))
+		{
+			$this->xss_clean = $config['xss_clean'];
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Setting of System Methods
+	 *
+	 * @return	void
+	 */
+	public function set_system_methods()
+	{
+		$this->methods = array(
+					'system.listMethods'	 => array(
+										'function' => 'this.listMethods',
+										'signature' => array(array($this->xmlrpcArray, $this->xmlrpcString), array($this->xmlrpcArray)),
+										'docstring' => 'Returns an array of available methods on this server'),
+					'system.methodHelp'	 => array(
+										'function' => 'this.methodHelp',
+										'signature' => array(array($this->xmlrpcString, $this->xmlrpcString)),
+										'docstring' => 'Returns a documentation string for the specified method'),
+					'system.methodSignature' => array(
+										'function' => 'this.methodSignature',
+										'signature' => array(array($this->xmlrpcArray, $this->xmlrpcString)),
+										'docstring' => 'Returns an array describing the return type and required parameters of a method'),
+					'system.multicall'	 => array(
+										'function' => 'this.multicall',
+										'signature' => array(array($this->xmlrpcArray, $this->xmlrpcArray)),
+										'docstring' => 'Combine multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details')
+				);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Main Server Function
+	 *
+	 * @return	void
+	 */
+	public function serve()
+	{
+		$r = $this->parseRequest();
+		$payload = '<?xml version="1.0" encoding="'.$this->xmlrpc_defencoding.'"?'.'>'."\n".$this->debug_msg.$r->prepare_response();
+
+		header('Content-Type: text/xml');
+		header('Content-Length: '.strlen($payload));
+		exit($payload);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Method to Class
+	 *
+	 * @param	string	method name
+	 * @param	string	function
+	 * @param	string	signature
+	 * @param	string	docstring
+	 * @return	void
+	 */
+	public function add_to_map($methodname, $function, $sig, $doc)
+	{
+		$this->methods[$methodname] = array(
+			'function'	=> $function,
+			'signature'	=> $sig,
+			'docstring'	=> $doc
+		);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse Server Request
+	 *
+	 * @param	string	data
+	 * @return	object	xmlrpc response
+	 */
+	public function parseRequest($data = '')
+	{
+		//-------------------------------------
+		//  Get Data
+		//-------------------------------------
+
+		if ($data === '')
+		{
+			$CI =& get_instance();
+			if ($CI->input->method() === 'post')
+			{
+				$data = $CI->input->raw_input_stream;
+			}
+		}
+
+		//-------------------------------------
+		//  Set up XML Parser
+		//-------------------------------------
+
+		$parser = xml_parser_create($this->xmlrpc_defencoding);
+		$parser_object = new XML_RPC_Message('filler');
+		$pname = (string) $parser;
+
+		$parser_object->xh[$pname] = array(
+			'isf' => 0,
+			'isf_reason' => '',
+			'params' => array(),
+			'stack' => array(),
+			'valuestack' => array(),
+			'method' => ''
+		);
+
+		xml_set_object($parser, $parser_object);
+		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, TRUE);
+		xml_set_element_handler($parser, 'open_tag', 'closing_tag');
+		xml_set_character_data_handler($parser, 'character_data');
+		//xml_set_default_handler($parser, 'default_handler');
+
+		//-------------------------------------
+		// PARSE + PROCESS XML DATA
+		//-------------------------------------
+
+		if ( ! xml_parse($parser, $data, 1))
+		{
+			// Return XML error as a faultCode
+			$r = new XML_RPC_Response(0,
+				$this->xmlrpcerrxml + xml_get_error_code($parser),
+				sprintf('XML error: %s at line %d',
+				xml_error_string(xml_get_error_code($parser)),
+				xml_get_current_line_number($parser)));
+			xml_parser_free($parser);
+		}
+		elseif ($parser_object->xh[$pname]['isf'])
+		{
+			return new XML_RPC_Response(0, $this->xmlrpcerr['invalid_return'], $this->xmlrpcstr['invalid_return']);
+		}
+		else
+		{
+			xml_parser_free($parser);
+
+			$m = new XML_RPC_Message($parser_object->xh[$pname]['method']);
+			$plist = '';
+
+			for ($i = 0, $c = count($parser_object->xh[$pname]['params']); $i < $c; $i++)
+			{
+				if ($this->debug === TRUE)
+				{
+					$plist .= $i.' - '.print_r(get_object_vars($parser_object->xh[$pname]['params'][$i]), TRUE).";\n";
+				}
+
+				$m->addParam($parser_object->xh[$pname]['params'][$i]);
+			}
+
+			if ($this->debug === TRUE)
+			{
+				echo "<pre>---PLIST---\n".$plist."\n---PLIST END---\n\n</pre>";
+			}
+
+			$r = $this->_execute($m);
+		}
+
+		//-------------------------------------
+		// SET DEBUGGING MESSAGE
+		//-------------------------------------
+
+		if ($this->debug === TRUE)
+		{
+			$this->debug_msg = "<!-- DEBUG INFO:\n\n".$plist."\n END DEBUG-->\n";
+		}
+
+		return $r;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Executes the Method
+	 *
+	 * @param	object
+	 * @return	mixed
+	 */
+	protected function _execute($m)
+	{
+		$methName = $m->method_name;
+
+		// Check to see if it is a system call
+		$system_call = (strpos($methName, 'system') === 0);
+
+		if ($this->xss_clean === FALSE)
+		{
+			$m->xss_clean = FALSE;
+		}
+
+		//-------------------------------------
+		// Valid Method
+		//-------------------------------------
+
+		if ( ! isset($this->methods[$methName]['function']))
+		{
+			return new XML_RPC_Response(0, $this->xmlrpcerr['unknown_method'], $this->xmlrpcstr['unknown_method']);
+		}
+
+		//-------------------------------------
+		// Check for Method (and Object)
+		//-------------------------------------
+
+		$method_parts = explode('.', $this->methods[$methName]['function']);
+		$objectCall = (isset($method_parts[1]) && $method_parts[1] !== '');
+
+		if ($system_call === TRUE)
+		{
+			if ( ! is_callable(array($this,$method_parts[1])))
+			{
+				return new XML_RPC_Response(0, $this->xmlrpcerr['unknown_method'], $this->xmlrpcstr['unknown_method']);
+			}
+		}
+		elseif (($objectCall && ! is_callable(array($method_parts[0], $method_parts[1])))
+			OR ( ! $objectCall && ! is_callable($this->methods[$methName]['function']))
+		)
+		{
+			return new XML_RPC_Response(0, $this->xmlrpcerr['unknown_method'], $this->xmlrpcstr['unknown_method']);
+		}
+
+		//-------------------------------------
+		// Checking Methods Signature
+		//-------------------------------------
+
+		if (isset($this->methods[$methName]['signature']))
+		{
+			$sig = $this->methods[$methName]['signature'];
+			for ($i = 0, $c = count($sig); $i < $c; $i++)
+			{
+				$current_sig = $sig[$i];
+
+				if (count($current_sig) === count($m->params)+1)
+				{
+					for ($n = 0, $mc = count($m->params); $n < $mc; $n++)
+					{
+						$p = $m->params[$n];
+						$pt = ($p->kindOf() === 'scalar') ? $p->scalarval() : $p->kindOf();
+
+						if ($pt !== $current_sig[$n+1])
+						{
+							$pno = $n+1;
+							$wanted = $current_sig[$n+1];
+
+							return new XML_RPC_Response(0,
+								$this->xmlrpcerr['incorrect_params'],
+								$this->xmlrpcstr['incorrect_params'] .
+								': Wanted '.$wanted.', got '.$pt.' at param '.$pno.')');
+						}
+					}
+				}
+			}
+		}
+
+		//-------------------------------------
+		// Calls the Function
+		//-------------------------------------
+
+		if ($objectCall === TRUE)
+		{
+			if ($method_parts[0] === 'this' && $system_call === TRUE)
+			{
+				return call_user_func(array($this, $method_parts[1]), $m);
+			}
+			elseif ($this->object === FALSE)
+			{
+				return get_instance()->$method_parts[1]($m);
+			}
+			else
+			{
+				return $this->object->$method_parts[1]($m);
+			}
+		}
+		else
+		{
+			return call_user_func($this->methods[$methName]['function'], $m);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Server Function: List Methods
+	 *
+	 * @param	mixed
+	 * @return	object
+	 */
+	public function listMethods($m)
+	{
+		$v = new XML_RPC_Values();
+		$output = array();
+
+		foreach ($this->methods as $key => $value)
+		{
+			$output[] = new XML_RPC_Values($key, 'string');
+		}
+
+		foreach ($this->system_methods as $key => $value)
+		{
+			$output[] = new XML_RPC_Values($key, 'string');
+		}
+
+		$v->addArray($output);
+		return new XML_RPC_Response($v);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Server Function: Return Signature for Method
+	 *
+	 * @param	mixed
+	 * @return	object
+	 */
+	public function methodSignature($m)
+	{
+		$parameters = $m->output_parameters();
+		$method_name = $parameters[0];
+
+		if (isset($this->methods[$method_name]))
+		{
+			if ($this->methods[$method_name]['signature'])
+			{
+				$sigs = array();
+				$signature = $this->methods[$method_name]['signature'];
+
+				for ($i = 0, $c = count($signature); $i < $c; $i++)
+				{
+					$cursig = array();
+					$inSig = $signature[$i];
+					for ($j = 0, $jc = count($inSig); $j < $jc; $j++)
+					{
+						$cursig[]= new XML_RPC_Values($inSig[$j], 'string');
+					}
+					$sigs[] = new XML_RPC_Values($cursig, 'array');
+				}
+
+				return new XML_RPC_Response(new XML_RPC_Values($sigs, 'array'));
+			}
+
+			return new XML_RPC_Response(new XML_RPC_Values('undef', 'string'));
+		}
+
+		return new XML_RPC_Response(0, $this->xmlrpcerr['introspect_unknown'], $this->xmlrpcstr['introspect_unknown']);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Server Function: Doc String for Method
+	 *
+	 * @param	mixed
+	 * @return	object
+	 */
+	public function methodHelp($m)
+	{
+		$parameters = $m->output_parameters();
+		$method_name = $parameters[0];
+
+		if (isset($this->methods[$method_name]))
+		{
+			$docstring = isset($this->methods[$method_name]['docstring']) ? $this->methods[$method_name]['docstring'] : '';
+
+			return new XML_RPC_Response(new XML_RPC_Values($docstring, 'string'));
+		}
+		else
+		{
+			return new XML_RPC_Response(0, $this->xmlrpcerr['introspect_unknown'], $this->xmlrpcstr['introspect_unknown']);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Server Function: Multi-call
+	 *
+	 * @param	mixed
+	 * @return	object
+	 */
+	public function multicall($m)
+	{
+		// Disabled
+		return new XML_RPC_Response(0, $this->xmlrpcerr['unknown_method'], $this->xmlrpcstr['unknown_method']);
+
+		$parameters = $m->output_parameters();
+		$calls = $parameters[0];
+
+		$result = array();
+
+		foreach ($calls as $value)
+		{
+			$m = new XML_RPC_Message($value[0]);
+			$plist = '';
+
+			for ($i = 0, $c = count($value[1]); $i < $c; $i++)
+			{
+				$m->addParam(new XML_RPC_Values($value[1][$i], 'string'));
+			}
+
+			$attempt = $this->_execute($m);
+
+			if ($attempt->faultCode() !== 0)
+			{
+				return $attempt;
+			}
+
+			$result[] = new XML_RPC_Values(array($attempt->value()), 'array');
+		}
+
+		return new XML_RPC_Response(new XML_RPC_Values($result, 'array'));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Multi-call Function: Error Handling
+	 *
+	 * @param	mixed
+	 * @return	object
+	 */
+	public function multicall_error($err)
+	{
+		$str = is_string($err) ? $this->xmlrpcstr["multicall_${err}"] : $err->faultString();
+		$code = is_string($err) ? $this->xmlrpcerr["multicall_${err}"] : $err->faultCode();
+
+		$struct['faultCode'] = new XML_RPC_Values($code, 'int');
+		$struct['faultString'] = new XML_RPC_Values($str, 'string');
+
+		return new XML_RPC_Values($struct, 'struct');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Multi-call Function: Processes method
+	 *
+	 * @param	mixed
+	 * @return	object
+	 */
+	public function do_multicall($call)
+	{
+		if ($call->kindOf() !== 'struct')
+		{
+			return $this->multicall_error('notstruct');
+		}
+		elseif ( ! $methName = $call->me['struct']['methodName'])
+		{
+			return $this->multicall_error('nomethod');
+		}
+
+		list($scalar_type, $scalar_value) = each($methName->me);
+		$scalar_type = $scalar_type === $this->xmlrpcI4 ? $this->xmlrpcInt : $scalar_type;
+
+		if ($methName->kindOf() !== 'scalar' OR $scalar_type !== 'string')
+		{
+			return $this->multicall_error('notstring');
+		}
+		elseif ($scalar_value === 'system.multicall')
+		{
+			return $this->multicall_error('recursion');
+		}
+		elseif ( ! $params = $call->me['struct']['params'])
+		{
+			return $this->multicall_error('noparams');
+		}
+		elseif ($params->kindOf() !== 'array')
+		{
+			return $this->multicall_error('notarray');
+		}
+
+		list($a, $b) = each($params->me);
+
+		$msg = new XML_RPC_Message($scalar_value);
+		for ($i = 0, $numParams = count($b); $i < $numParams; $i++)
+		{
+			$msg->params[] = $params->me['array'][$i];
+		}
+
+		$result = $this->_execute($msg);
+
+		if ($result->faultCode() !== 0)
+		{
+			return $this->multicall_error($result);
+		}
+
+		return new XML_RPC_Values(array($result->value()), 'array');
+	}
+
+}
diff --git a/README/Zip.php b/README/Zip.php
new file mode 100644
index 0000000..3e98ac5
--- /dev/null
+++ b/README/Zip.php
@@ -0,0 +1,484 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ *
+ * 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.
+ *
+ * @package	CodeIgniter
+ * @author	EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license	http://opensource.org/licenses/MIT	MIT License
+ * @link	http://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Zip Compression Class
+ *
+ * This class is based on a library I found at Zend:
+ * http://www.zend.com/codex.php?id=696&single=1
+ *
+ * The original library is a little rough around the edges so I
+ * refactored it and added several additional methods -- Rick Ellis
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Encryption
+ * @author		EllisLab Dev Team
+ * @link		http://codeigniter.com/user_guide/libraries/zip.html
+ */
+class CI_Zip {
+
+	/**
+	 * Zip data in string form
+	 *
+	 * @var string
+	 */
+	public $zipdata = '';
+
+	/**
+	 * Zip data for a directory in string form
+	 *
+	 * @var string
+	 */
+	public $directory = '';
+
+	/**
+	 * Number of files/folder in zip file
+	 *
+	 * @var int
+	 */
+	public $entries = 0;
+
+	/**
+	 * Number of files in zip
+	 *
+	 * @var int
+	 */
+	public $file_num = 0;
+
+	/**
+	 * relative offset of local header
+	 *
+	 * @var int
+	 */
+	public $offset = 0;
+
+	/**
+	 * Reference to time at init
+	 *
+	 * @var int
+	 */
+	public $now;
+
+	/**
+	 * The level of compression
+	 *
+	 * Ranges from 0 to 9, with 9 being the highest level.
+	 *
+	 * @var	int
+	 */
+	public $compression_level = 2;
+
+	/**
+	 * Initialize zip compression class
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$this->now = time();
+		log_message('info', 'Zip Compression Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Directory
+	 *
+	 * Lets you add a virtual directory into which you can place files.
+	 *
+	 * @param	mixed	$directory	the directory name. Can be string or array
+	 * @return	void
+	 */
+	public function add_dir($directory)
+	{
+		foreach ((array) $directory as $dir)
+		{
+			if ( ! preg_match('|.+/$|', $dir))
+			{
+				$dir .= '/';
+			}
+
+			$dir_time = $this->_get_mod_time($dir);
+			$this->_add_dir($dir, $dir_time['file_mtime'], $dir_time['file_mdate']);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get file/directory modification time
+	 *
+	 * If this is a newly created file/dir, we will set the time to 'now'
+	 *
+	 * @param	string	$dir	path to file
+	 * @return	array	filemtime/filemdate
+	 */
+	protected function _get_mod_time($dir)
+	{
+		// filemtime() may return false, but raises an error for non-existing files
+		$date = file_exists($dir) ? getdate(filemtime($dir)) : getdate($this->now);
+
+		return array(
+			'file_mtime' => ($date['hours'] << 11) + ($date['minutes'] << 5) + $date['seconds'] / 2,
+			'file_mdate' => (($date['year'] - 1980) << 9) + ($date['mon'] << 5) + $date['mday']
+		);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Directory
+	 *
+	 * @param	string	$dir	the directory name
+	 * @param	int	$file_mtime
+	 * @param	int	$file_mdate
+	 * @return	void
+	 */
+	protected function _add_dir($dir, $file_mtime, $file_mdate)
+	{
+		$dir = str_replace('\\', '/', $dir);
+
+		$this->zipdata .=
+			"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00"
+			.pack('v', $file_mtime)
+			.pack('v', $file_mdate)
+			.pack('V', 0) // crc32
+			.pack('V', 0) // compressed filesize
+			.pack('V', 0) // uncompressed filesize
+			.pack('v', strlen($dir)) // length of pathname
+			.pack('v', 0) // extra field length
+			.$dir
+			// below is "data descriptor" segment
+			.pack('V', 0) // crc32
+			.pack('V', 0) // compressed filesize
+			.pack('V', 0); // uncompressed filesize
+
+		$this->directory .=
+			"\x50\x4b\x01\x02\x00\x00\x0a\x00\x00\x00\x00\x00"
+			.pack('v', $file_mtime)
+			.pack('v', $file_mdate)
+			.pack('V',0) // crc32
+			.pack('V',0) // compressed filesize
+			.pack('V',0) // uncompressed filesize
+			.pack('v', strlen($dir)) // length of pathname
+			.pack('v', 0) // extra field length
+			.pack('v', 0) // file comment length
+			.pack('v', 0) // disk number start
+			.pack('v', 0) // internal file attributes
+			.pack('V', 16) // external file attributes - 'directory' bit set
+			.pack('V', $this->offset) // relative offset of local header
+			.$dir;
+
+		$this->offset = strlen($this->zipdata);
+		$this->entries++;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Data to Zip
+	 *
+	 * Lets you add files to the archive. If the path is included
+	 * in the filename it will be placed within a directory. Make
+	 * sure you use add_dir() first to create the folder.
+	 *
+	 * @param	mixed	$filepath	A single filepath or an array of file => data pairs
+	 * @param	string	$data		Single file contents
+	 * @return	void
+	 */
+	public function add_data($filepath, $data = NULL)
+	{
+		if (is_array($filepath))
+		{
+			foreach ($filepath as $path => $data)
+			{
+				$file_data = $this->_get_mod_time($path);
+				$this->_add_data($path, $data, $file_data['file_mtime'], $file_data['file_mdate']);
+			}
+		}
+		else
+		{
+			$file_data = $this->_get_mod_time($filepath);
+			$this->_add_data($filepath, $data, $file_data['file_mtime'], $file_data['file_mdate']);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Data to Zip
+	 *
+	 * @param	string	$filepath	the file name/path
+	 * @param	string	$data	the data to be encoded
+	 * @param	int	$file_mtime
+	 * @param	int	$file_mdate
+	 * @return	void
+	 */
+	protected function _add_data($filepath, $data, $file_mtime, $file_mdate)
+	{
+		$filepath = str_replace('\\', '/', $filepath);
+
+		$uncompressed_size = strlen($data);
+		$crc32  = crc32($data);
+		$gzdata = substr(gzcompress($data, $this->compression_level), 2, -4);
+		$compressed_size = strlen($gzdata);
+
+		$this->zipdata .=
+			"\x50\x4b\x03\x04\x14\x00\x00\x00\x08\x00"
+			.pack('v', $file_mtime)
+			.pack('v', $file_mdate)
+			.pack('V', $crc32)
+			.pack('V', $compressed_size)
+			.pack('V', $uncompressed_size)
+			.pack('v', strlen($filepath)) // length of filename
+			.pack('v', 0) // extra field length
+			.$filepath
+			.$gzdata; // "file data" segment
+
+		$this->directory .=
+			"\x50\x4b\x01\x02\x00\x00\x14\x00\x00\x00\x08\x00"
+			.pack('v', $file_mtime)
+			.pack('v', $file_mdate)
+			.pack('V', $crc32)
+			.pack('V', $compressed_size)
+			.pack('V', $uncompressed_size)
+			.pack('v', strlen($filepath)) // length of filename
+			.pack('v', 0) // extra field length
+			.pack('v', 0) // file comment length
+			.pack('v', 0) // disk number start
+			.pack('v', 0) // internal file attributes
+			.pack('V', 32) // external file attributes - 'archive' bit set
+			.pack('V', $this->offset) // relative offset of local header
+			.$filepath;
+
+		$this->offset = strlen($this->zipdata);
+		$this->entries++;
+		$this->file_num++;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Read the contents of a file and add it to the zip
+	 *
+	 * @param	string	$path
+	 * @param	bool	$archive_filepath
+	 * @return	bool
+	 */
+	public function read_file($path, $archive_filepath = FALSE)
+	{
+		if (file_exists($path) && FALSE !== ($data = file_get_contents($path)))
+		{
+			if (is_string($archive_filepath))
+			{
+				$name = str_replace('\\', '/', $archive_filepath);
+			}
+			else
+			{
+				$name = str_replace('\\', '/', $path);
+
+				if ($archive_filepath === FALSE)
+				{
+					$name = preg_replace('|.*/(.+)|', '\\1', $name);
+				}
+			}
+
+			$this->add_data($name, $data);
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Read a directory and add it to the zip.
+	 *
+	 * This function recursively reads a folder and everything it contains (including
+	 * sub-folders) and creates a zip based on it. Whatever directory structure
+	 * is in the original file path will be recreated in the zip file.
+	 *
+	 * @param	string	$path	path to source directory
+	 * @param	bool	$preserve_filepath
+	 * @param	string	$root_path
+	 * @return	bool
+	 */
+	public function read_dir($path, $preserve_filepath = TRUE, $root_path = NULL)
+	{
+		$path = rtrim($path, '/\\').DIRECTORY_SEPARATOR;
+		if ( ! $fp = @opendir($path))
+		{
+			return FALSE;
+		}
+
+		// Set the original directory root for child dir's to use as relative
+		if ($root_path === NULL)
+		{
+			$root_path = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, dirname($path)).DIRECTORY_SEPARATOR;
+		}
+
+		while (FALSE !== ($file = readdir($fp)))
+		{
+			if ($file[0] === '.')
+			{
+				continue;
+			}
+
+			if (is_dir($path.$file))
+			{
+				$this->read_dir($path.$file.DIRECTORY_SEPARATOR, $preserve_filepath, $root_path);
+			}
+			elseif (FALSE !== ($data = file_get_contents($path.$file)))
+			{
+				$name = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $path);
+				if ($preserve_filepath === FALSE)
+				{
+					$name = str_replace($root_path, '', $name);
+				}
+
+				$this->add_data($name.$file, $data);
+			}
+		}
+
+		closedir($fp);
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the Zip file
+	 *
+	 * @return	string	(binary encoded)
+	 */
+	public function get_zip()
+	{
+		// Is there any data to return?
+		if ($this->entries === 0)
+		{
+			return FALSE;
+		}
+
+		return $this->zipdata
+			.$this->directory."\x50\x4b\x05\x06\x00\x00\x00\x00"
+			.pack('v', $this->entries) // total # of entries "on this disk"
+			.pack('v', $this->entries) // total # of entries overall
+			.pack('V', strlen($this->directory)) // size of central dir
+			.pack('V', strlen($this->zipdata)) // offset to start of central dir
+			."\x00\x00"; // .zip file comment length
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Write File to the specified directory
+	 *
+	 * Lets you write a file
+	 *
+	 * @param	string	$filepath	the file name
+	 * @return	bool
+	 */
+	public function archive($filepath)
+	{
+		if ( ! ($fp = @fopen($filepath, 'w+b')))
+		{
+			return FALSE;
+		}
+
+		flock($fp, LOCK_EX);
+
+		for ($result = $written = 0, $data = $this->get_zip(), $length = strlen($data); $written < $length; $written += $result)
+		{
+			if (($result = fwrite($fp, substr($data, $written))) === FALSE)
+			{
+				break;
+			}
+		}
+
+		flock($fp, LOCK_UN);
+		fclose($fp);
+
+		return is_int($result);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Download
+	 *
+	 * @param	string	$filename	the file name
+	 * @return	void
+	 */
+	public function download($filename = 'backup.zip')
+	{
+		if ( ! preg_match('|.+?\.zip$|', $filename))
+		{
+			$filename .= '.zip';
+		}
+
+		get_instance()->load->helper('download');
+		$get_zip = $this->get_zip();
+		$zip_content =& $get_zip;
+
+		force_download($filename, $zip_content);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize Data
+	 *
+	 * Lets you clear current zip data. Useful if you need to create
+	 * multiple zips with different data.
+	 *
+	 * @return	CI_Zip
+	 */
+	public function clear_data()
+	{
+		$this->zipdata = '';
+		$this->directory = '';
+		$this->entries = 0;
+		$this->file_num = 0;
+		$this->offset = 0;
+		return $this;
+	}
+
+}
diff --git a/README/index.html b/README/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/README/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>403 Forbidden</title>
+</head>
+<body>
+
+<p>Directory access is forbidden.</p>
+
+</body>
+</html>
